diff --git a/.gitignore b/.gitignore index bf83f55..e0dd377 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ .bundle/ /output/ BASE_PATH -.DS_Store \ No newline at end of file +.DS_Store diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index eca07e4..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.1.2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d129c79..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: ruby -cache: bundler -install: bundle install --jobs 4 -before_script: cp BASE_PATH.example BASE_PATH -env: - - GUIDES_LANGUAGE=zh-CN -rvm: - - 2.1.2 -notifications: - email: false \ No newline at end of file diff --git a/BASE_PATH.example b/BASE_PATH.example deleted file mode 100644 index 18c0010..0000000 --- a/BASE_PATH.example +++ /dev/null @@ -1 +0,0 @@ -~/docs/rails-guides-translation-cn \ No newline at end of file diff --git a/CC-BY-SA.png b/CC-BY-SA.png deleted file mode 100644 index 8770732..0000000 Binary files a/CC-BY-SA.png and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3d68a43 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +## Rails 5.1.0 (April 27, 2017) ## + +* No changes. + +## Rails 5.0.1 (December 21, 2016) ## + +* No changes. + + +## Rails 5.0.1.rc2 (December 10, 2016) ## + +* No changes. + + +## Rails 5.0.1.rc1 (December 01, 2016) ## + +* No changes. + + +## Rails 5.0.0 (June 30, 2016) ## + +* Update example of passing a proc to `:message` option for validating records. + + This behavior was recently changed in [Pull Request #24199](https://github.com/rails/rails/pull/24119) to + pass the object being validated as first argument to the `:message` proc, + instead of the key of the field being validated. + + *Prathamesh Sonpatki* + +* Added new guide: Action Cable Overview. + + *David Kuhta* + +* Add code of conduct to contributing guide. + + *Jon Moss* + +* New section in Configuring: Configuring Active Job. + + *Eliot Sykes* + +* New section in Active Record Association Basics: Single Table Inheritance. + + *Andrey Nering* + +* New section in Active Record Querying: Understanding The Method Chaining. + + *Andrey Nering* + +* New section in Configuring: Search Engines Indexing. + + *Andrey Nering* + +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/guides/CHANGELOG.md) for previous changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 604211f..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,20 +0,0 @@ -贡献 Ruby on Rails 指南 -====================== - -* [翻译](https://github.com/ruby-china/guides#%E7%BF%BB%E8%AF%91%E6%B5%81%E7%A8%8B) - -* [修正错误](https://github.com/ruby-china/guides#%E5%8B%98%E8%AF%AF) - -* [回报错误](https://github.com/ruby-china/guides/issues/new) - -文章格式要求 ----------- - -见:[[Ruby on Rails 指南准则](http://guides.ruby-china.org/ruby_on_rails_guides_guidelines.html)](http://guides.ruby-china.org/ruby_on_rails_guides_guidelines.html)。 - -译文要求 -------- - -中英文之间空一格,使用正确的标点符号。尽量使用与现有译文一致的词汇。 - -贡献的内容将会采用协议 CC BY-SA 4.0。 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3b79bda --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM ruby:2.4.1 + +RUN mkdir /app +WORKDIR /app + +RUN apt-get update && apt-get -y install imagemagick + +RUN mkdir /tmp/kindlegen && cd /tmp/kindlegen && \ + wget http://kindlegen.s3.amazonaws.com/kindlegen_linux_2.6_i386_v2_9.tar.gz && \ + tar xfz kindlegen_linux_2.6_i386_v2_9.tar.gz && \ + mv kindlegen /usr/local/bin/ + +RUN gem install bundler + +COPY Gemfile /app +COPY Gemfile.lock /app +RUN bundle install + +ENV GUIDES_LANGUAGE=zh-CN +ENV RAILS_VERSION=v5.1.1 diff --git a/Gemfile b/Gemfile index 6627af6..c963e14 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,6 @@ +# frozen_string_literal: true source 'https://rubygems.org/' -gem 'activesupport' - -gem 'actionpack' - -group :doc do - gem 'redcarpet' -end - -gem 'nokogiri' - -gem 'rake' - +gem 'rails', '5.1.1' +gem 'redcarpet' +gem 'kindlerb', '1.2.0' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..729dd93 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,117 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.1.1) + actionpack (= 5.1.1) + nio4r (~> 2.0) + websocket-driver (~> 0.6.1) + actionmailer (5.1.1) + actionpack (= 5.1.1) + actionview (= 5.1.1) + activejob (= 5.1.1) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.1.1) + actionview (= 5.1.1) + activesupport (= 5.1.1) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.1.1) + activesupport (= 5.1.1) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.1.1) + activesupport (= 5.1.1) + globalid (>= 0.3.6) + activemodel (5.1.1) + activesupport (= 5.1.1) + activerecord (5.1.1) + activemodel (= 5.1.1) + activesupport (= 5.1.1) + arel (~> 8.0) + activesupport (5.1.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + arel (8.0.0) + builder (3.2.3) + concurrent-ruby (1.0.5) + erubi (1.6.0) + globalid (0.4.0) + activesupport (>= 4.2.0) + i18n (0.8.1) + kindlerb (1.2.0) + mustache + nokogiri + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.5) + mime-types (>= 1.16, < 4) + method_source (0.8.2) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mini_portile2 (2.1.0) + minitest (5.10.2) + mustache (1.0.5) + nio4r (2.0.0) + nokogiri (1.7.2) + mini_portile2 (~> 2.1.0) + rack (2.0.3) + rack-test (0.6.3) + rack (>= 1.0) + rails (5.1.1) + actioncable (= 5.1.1) + actionmailer (= 5.1.1) + actionpack (= 5.1.1) + actionview (= 5.1.1) + activejob (= 5.1.1) + activemodel (= 5.1.1) + activerecord (= 5.1.1) + activesupport (= 5.1.1) + bundler (>= 1.3.0, < 2.0) + railties (= 5.1.1) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (5.1.1) + actionpack (= 5.1.1) + activesupport (= 5.1.1) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (12.0.0) + redcarpet (3.4.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + thor (0.19.4) + thread_safe (0.3.6) + tzinfo (1.2.3) + thread_safe (~> 0.1) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + +PLATFORMS + ruby + +DEPENDENCIES + kindlerb (= 1.2.0) + rails (= 5.1.1) + redcarpet + +BUNDLED WITH + 1.14.6 diff --git a/README.md b/README.md index 2511f94..fbceff3 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,28 @@ -Rails 指南翻译 -============= +# Rails Guide 中文翻译 -翻译准备工作 ----------- +## 构建(基于 Docker) -使用 `curl`: +为了管理依赖,建议使用 Docker。 -```bash -ruby -e "$(curl -sSL https://rawgithub.com/ruby-china/guides/master/install.rb)" -``` - -或使用 `wget`: - -```bash -ruby <(wget --no-check-certificate https://rawgithub.com/ruby-china/guides/master/install.rb -O -) -``` - -会抓取 `ruby-china/rails` 与 `ruby-china/guides` 这两个代码库。 - -`ruby-china/rails`:更新原文用。 - -`ruby-china/guides`:存放译文用。 - -这俩个代码库默认会存放在: - -``` -~/docs/rails-guides-translation-cn -``` - -若是手动抓取,需修改这两个代码库存放的位置,并存成 `BASE_PATH` 文件。 - -翻译流程 -------- - -* 1. 先更新原文。 - -比如 `getting_started.md`: - -```bash -$ rake guides:update_guide getting_started.md -``` - -这样 `source/getting_started.md` 便是最新的,拷贝原文内容到 `source/zh-CN/getting_started.md` 下便可开始翻译。 +### 安装 Docker -* 2. 进行翻译。 +https://www.docker.com/ -### 预览 +### 构建镜像 ```bash -$ GUIDES_LANGUAGE=zh-CN rake guides:generate +$ docker build -t rails-guides . ``` -命令过长可在 `~/.bashrc` 或 `~/.zshrc` 设别名: - -```bash -alias gen="GUIDES_LANGUAGE=zh-CN rake guides:generate"`。 -``` - -* 3. 翻译完成发送 Pull Request。 - -**注意,翻译完成的译文必须与原文是相同版本。** - -### 无力翻译 - -把目前的工作成果发送 Pull Request,让其他人接手。 - -更新翻译 -------- - -使用: +### 构建 ```bash -$ rake guides:update_guides +$ docker run -it -v $(pwd):/app rails-guides rake guides:generate:html +$ docker run -it -v $(pwd):/app rails-guides rake guides:generate:kindle ``` -来看所有上游有更新的原文。 - -### 更新“已完成”的翻译 - -**以 `getting_started.md` 为例。** - -运行: - -```bash -$ rake guides:update_guide getting_started.md - -$ git status -``` - -开新分支: - -```bash -$ git checkout -b new-branch-name -``` - -将原文更动的部份,翻译、修正到 `source/zh-CN/getting_started.md`,提交、发送 Pull Request。 - -### 更新“未开始”的翻译 - -**以 `getting_started.md` 为例。** - -```bash -$ rake guides:update_guides - -$ git status -``` - -开新分支: - -```bash -$ git checkout -b new-branch-name -``` - -把原文 `source/getting_started.md` 的内容拷贝到 `source/zh-CN/getting_started.md` ,提交,发送 Pull Request。 - -勘误 ----- - -翻译的错误,可以修正后发 Pull Request;或是[回报](https://github.com/ruby-china/guides/issues/new)等别人修正也可以。 - -若不是翻译的错误,是原文的错误,请检查 http://edgeguides.rubyonrails.org 是否已经修正了,没有的话可以去 [rails/rails][rails] 帮忙修正。记得有关文档的改动,在提交信息要加上 `[ci skip]`。 - -在给 [rails/rails][rails] 提报或修正前,最好先阅读:[Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html)。 - -部署 ----- - -```bash -$ rake guides:deploy -``` - -会把 `ruby-china/guides/output/zh-CN/*` 的静态文件,拷贝到 `ruby-china/ruby-china.github.io`。 - -建议 ----- - -[欢迎提意见](https://github.com/ruby-china/guides/issues/new)。 - -其它格式 -------- - -> PDF, MOBI, EPUB 格式 - -请支持安道所翻译的 [Rails 指南](https://selfstore.io/products/13)。 - -协议 ----- - -![CC-BY-SA](CC-BY-SA.png) - -简体译文由 [@Andor_Chen](http://about.ac) 所翻译。 - -译文授权协议为 [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/)。 +## 发布 -代码来自 [rails/rails][rails],采用相同的 [MIT license](http://opensource.org/licenses/MIT) 协议。 +另外 clone 一份 repo,checkout 到 `gh-pages` 分支,将 HTML 版内容拷贝进去,commit,push。 -[rails]: https://github.com/rails/rails +Kindle 版通过 GitHub Release 发布。 diff --git a/Rakefile b/Rakefile index 2eb5f05..0c37e58 100644 --- a/Rakefile +++ b/Rakefile @@ -1,101 +1,65 @@ -require 'pathname' - -BASE_PATH = Pathname(IO.readlines('./BASE_PATH').first.chomp) - -RAILS_PATH = BASE_PATH + 'rails' -GUIDES_PATH = BASE_PATH + 'guides' -PAGES_PATH = BASE_PATH + 'ruby-china.github.io' - -RAILS_GUIDE_SOURCE_PATH = RAILS_PATH + 'guides/source/' - -def update_rails_repo! - FileUtils.cd(RAILS_PATH.expand_path) { `git pull origin master` } -end - -def get_rails_latest_sha1 - sha1 = nil - FileUtils.cd(RAILS_PATH.expand_path) { sha1 = `git rev-parse HEAD` } - sha1[0, 7] -end - -task :sanity_checks do - abort("Abort. please clone the rails/rails repo under #{BASE_PATH}") if !File.exist? RAILS_PATH.expand_path - abort("Abort. please clone the ruby-china/guides repo under #{BASE_PATH}") if !File.exist? GUIDES_PATH.expand_path -end - namespace :guides do desc 'Generate guides (for authors), use ONLY=foo to process just "foo.md"' - task :generate => 'generate:html' - - desc 'Deploy generated guides to github pages repository' - task :deploy => :sanity_checks do - ENV['RAILS_VERSION'] = get_rails_latest_sha1 - ENV['ALL'] = '1' - ENV['GUIDES_LANGUAGE'] = 'zh-CN' - Rake::Task['guides:generate:html'].invoke - - # the dot will copy contents under a folder, instead of copy the folder. - FileUtils.cp_r("#{GUIDES_PATH.expand_path}/output/zh-CN/.", PAGES_PATH.expand_path) - - Dir.chdir(PAGES_PATH.expand_path) do - `git add -A .` - `git commit -m '#{%Q[Site updated @ #{Time.now.strftime("%a %b %-d %H:%M:%S %Z %Y")}]}'` - `git push origin master` + task generate: "generate:html" + + # Guides are written in UTF-8, but the environment may be configured for some + # other locale, these tasks are responsible for ensuring the default external + # encoding is UTF-8. + # + # Real use cases: Generation was reported to fail on a machine configured with + # GBK (Chinese). The docs server once got misconfigured somehow and had "C", + # which broke generation too. + task :encoding do + %w(LANG LANGUAGE LC_ALL).each do |env_var| + ENV[env_var] = "en_US.UTF-8" end - - puts 'Deploy Complete. : )' end - desc 'Update a given English guide' - task :update_guide => :sanity_checks do - update_rails_repo! - - guide_to_be_updated = ARGV.last - guide_path = (RAILS_GUIDE_SOURCE_PATH + guide_to_be_updated).expand_path - - if File.exist? guide_path - FileUtils.cp(guide_path, "#{GUIDES_PATH.expand_path}/source/") - puts "Update: #{guide_path} Complete. : )" - else - `ls #{guide_path}` + namespace :generate do + desc "Generate HTML guides" + task html: :encoding do + ENV["WARNINGS"] = "1" # authors can't disable this + ruby "-E UTF-8 rails_guides.rb" end - # trick rake that ARGV.last is a task :P - task guide_to_be_updated.to_sym do; end + desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/gp/feature.html?docId=1000765211" + task kindle: :encoding do + require "kindlerb" + unless Kindlerb.kindlegen_available? + abort "Please run `setupkindlerb` to install kindlegen" + end + unless `convert` =~ /convert/ + abort "Please install ImageMagick" + end + ENV["KINDLE"] = "1" + Rake::Task["guides:generate:html"].invoke + end end - desc 'Update all English guides' - task :update_guides => :sanity_checks do - update_rails_repo! - - FileUtils.cp_r(Pathname.glob("#{RAILS_GUIDE_SOURCE_PATH.expand_path}/*.md"), "#{GUIDES_PATH.expand_path}/source") - - puts 'Update all English Guides. : D' - end - - namespace :generate do - desc "Generate HTML guides" - task :html do - ENV["WARN_BROKEN_LINKS"] = "1" # authors can't disable this - ruby "rails_guides.rb" - end + # Validate guides ------------------------------------------------------------------------- + desc 'Validate guides, use ONLY=foo to process just "foo.html"' + task validate: :encoding do + ruby "w3c_validator.rb" end desc "Show help" task :help do - puts <<-help + puts < folder (such as source/es) - EDGE=1 - Indicate generated guides should be marked as edge. - Examples: - $ rake guides:generate ALL=1 - $ rake guides:generate EDGE=1 - $ rake guides:generate:kindle EDGE=1 + $ rake guides:generate ALL=1 RAILS_VERSION=v5.1.0 + $ rake guides:generate ONLY=migrations + $ rake guides:generate:kindle $ rake guides:generate GUIDES_LANGUAGE=es - help +HELP end end -task :default do - Rake::Task['guides:generate'].invoke -end +task default: "guides:help" diff --git a/assets/images/belongs_to.png b/assets/images/belongs_to.png index 43c963f..077d237 100644 Binary files a/assets/images/belongs_to.png and b/assets/images/belongs_to.png differ diff --git a/assets/images/favicon.ico b/assets/images/favicon.ico index e0e80cf..87192a8 100644 Binary files a/assets/images/favicon.ico and b/assets/images/favicon.ico differ diff --git a/assets/images/getting_started/article_with_comments.png b/assets/images/getting_started/article_with_comments.png index 117a78a..c489e4c 100644 Binary files a/assets/images/getting_started/article_with_comments.png and b/assets/images/getting_started/article_with_comments.png differ diff --git a/assets/images/getting_started/rails_welcome.png b/assets/images/getting_started/rails_welcome.png index 3e07c94..baccb11 100644 Binary files a/assets/images/getting_started/rails_welcome.png and b/assets/images/getting_started/rails_welcome.png differ diff --git a/assets/images/getting_started/template_is_missing_articles_new.png b/assets/images/getting_started/template_is_missing_articles_new.png index 4e636d0..f4f054f 100644 Binary files a/assets/images/getting_started/template_is_missing_articles_new.png and b/assets/images/getting_started/template_is_missing_articles_new.png differ diff --git a/assets/images/has_many.png b/assets/images/has_many.png index e7589e3..79da261 100644 Binary files a/assets/images/has_many.png and b/assets/images/has_many.png differ diff --git a/assets/images/rails_guides_logo.gif b/assets/images/rails_guides_logo.gif index 9b0ad5a..f7149a0 100644 Binary files a/assets/images/rails_guides_logo.gif and b/assets/images/rails_guides_logo.gif differ diff --git a/assets/javascripts/guides.js b/assets/javascripts/guides.js index e7911a7..e4d25df 100644 --- a/assets/javascripts/guides.js +++ b/assets/javascripts/guides.js @@ -51,9 +51,3 @@ var guidesIndex = { window.location = url; } }; - -// Disable autolink inside example code blocks of guides. -$(document).ready(function() { - SyntaxHighlighter.defaults['auto-links'] = false; - SyntaxHighlighter.all(); -}); \ No newline at end of file diff --git a/assets/javascripts/responsive-tables.js b/assets/javascripts/responsive-tables.js old mode 100755 new mode 100644 diff --git a/assets/javascripts/syntaxhighlighter.js b/assets/javascripts/syntaxhighlighter.js new file mode 100644 index 0000000..584aaed --- /dev/null +++ b/assets/javascripts/syntaxhighlighter.js @@ -0,0 +1,20 @@ +/*! + * SyntaxHighlighter + * https://github.com/syntaxhighlighter/syntaxhighlighter + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/SyntaxHighlighter/donate.html + * + * @version + * 4.0.1 (Sun, 03 Jul 2016 06:45:54 GMT) + * + * @copyright + * Copyright (C) 2004-2016 Alex Gorbatchev. + * + * @license + * Dual licensed under the MIT and GPL licenses. + */ + + +!function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return e[r].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t["default"]=e,t}function i(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(1);Object.keys(a).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return a[e]}})});var s=n(28),o=i(s),l=i(a),u=n(29),c=r(u);n(30),(0,o["default"])(function(){return l["default"].highlight(c.object(window.syntaxhighlighterConfig||{}))})},function(e,t,n){"use strict";function r(e){window.alert("SyntaxHighlighter\n\n"+e)}function i(e,t){var n=h.vars.discoveredBrushes,i=null;if(null==n){n={};for(var a in h.brushes){var s=h.brushes[a],o=s.aliases;if(null!=o){s.className=s.className||s.aliases[0],s.brushName=s.className||a.toLowerCase();for(var l=0,u=o.length;u>l;l++)n[o[l]]=a}}h.vars.discoveredBrushes=n}return i=h.brushes[n[e]],null==i&&t&&r(h.config.strings.noBrush+e),i}function a(e){var t="",r=u.trim(e),i=!1,a=t.length,s=n.length;0==r.indexOf(t)&&(r=r.substring(a),i=!0);var o=r.length;return r.indexOf(n)==o-s&&(r=r.substring(0,o-s),i=!0),i?r:e}Object.defineProperty(t,"__esModule",{value:!0});var s=n(2),o=n(5),l=n(9)["default"],u=n(10),c=n(11),f=n(17),g=n(18),p=n(19),d=n(20),h={Match:o.Match,Highlighter:n(22),config:n(18),regexLib:n(3).commonRegExp,vars:{discoveredBrushes:null,highlighters:{}},brushes:{},findElements:function(e,t){var n=t?[t]:u.toArray(document.getElementsByTagName(h.config.tagName)),r=(h.config,[]);if(n=n.concat(f.getSyntaxHighlighterScriptTags()),0===n.length)return r;for(var i=0,a=n.length;a>i;i++){var o={target:n[i],params:s.defaults(s.parse(n[i].className),e)};null!=o.params.brush&&r.push(o)}return r},highlight:function(e,t){var n,r=h.findElements(e,t),u="innerHTML",m=null,x=h.config;if(0!==r.length)for(var v=0,y=r.length;y>v;v++){var m,w,b,t=r[v],E=t.target,S=t.params,C=S.brush;null!=C&&(m=i(C),m&&(S=s.defaults(S||{},p),S=s.defaults(S,g),1==S["html-script"]||1==p["html-script"]?(m=new d(i("xml"),m),C="htmlscript"):m=new m,b=E[u],x.useScriptTags&&(b=a(b)),""!=(E.title||"")&&(S.title=E.title),S.brush=C,b=c(b,S),w=o.applyRegexList(b,m.regexList,S),n=new l(b,w,S),t=f.create("div"),t.innerHTML=n.getHtml(),S.quickCode&&f.attachEvent(f.findElement(t,".code"),"dblclick",f.quickCodeHandler),""!=(E.id||"")&&(t.id=E.id),E.parentNode.replaceChild(t,E)))}}},m=0;t["default"]=h;var x=t.registerBrush=function(e){return h.brushes["brush"+m++]=e["default"]||e};t.clearRegisteredBrushes=function(){h.brushes={},m=0};x(n(23)),x(n(24)),x(n(25)),x(n(26)),x(n(27))},function(e,t,n){"use strict";function r(e){return e.replace(/-(\w+)/g,function(e,t){return t.charAt(0).toUpperCase()+t.substr(1)})}function i(e){var t=s[e];return null==t?e:t}var a=n(3).XRegExp,s={"true":!0,"false":!1};e.exports={defaults:function(e,t){for(var n in t||{})e.hasOwnProperty(n)||(e[n]=e[r(n)]=t[n]);return e},parse:function(e){for(var t,n={},s=a("^\\[(?(.*?))\\]$"),o=0,l=a("(?[\\w-]+)\\s*:\\s*(?[\\w%#-]+|\\[.*?\\]|\".*?\"|'.*?')\\s*;?","g");null!=(t=a.exec(e,l,o));){var u=t.value.replace(/^['"]|['"]$/g,"");if(null!=u&&s.test(u)){var c=a.exec(u,s);u=c.values.length>0?c.values.split(/\s*,\s*/):[]}u=i(u),n[t.name]=n[r(t.name)]=u,o=t.index+t[0].length}return n}}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0}),t.commonRegExp=t.XRegExp=void 0;var i=n(4),a=r(i);t.XRegExp=a["default"];t.commonRegExp={multiLineCComments:(0,a["default"])("/\\*.*?\\*/","gs"),singleLineCComments:/\/\/.*$/gm,singleLinePerlComments:/#.*$/gm,doubleQuotedString:/"([^\\"\n]|\\.)*"/g,singleQuotedString:/'([^\\'\n]|\\.)*'/g,multiLineDoubleQuotedString:(0,a["default"])('"([^\\\\"]|\\\\.)*"',"gs"),multiLineSingleQuotedString:(0,a["default"])("'([^\\\\']|\\\\.)*'","gs"),xmlComments:(0,a["default"])("(<|<)!--.*?--(>|>)","gs"),url:/\w+:\/\/[\w-.\/?%&=:@;#]*/g,phpScriptTags:{left:/(<|<)\?(?:=|php)?/g,right:/\?(>|>)/g,eof:!0},aspScriptTags:{left:/(<|<)%=?/g,right:/%(>|>)/g},scriptScriptTags:{left:/(<|<)\s*script.*?(>|>)/gi,right:/(<|<)\/\s*script\s*(>|>)/gi}}},function(e,t){"use strict";function n(e,t,n,r,i){var a;if(e[b]={captureNames:t},i)return e;if(e.__proto__)e.__proto__=w.prototype;else for(a in w.prototype)e[a]=w.prototype[a];return e[b].source=n,e[b].flags=r?r.split("").sort().join(""):r,e}function r(e){return S.replace.call(e,/([\s\S])(?=[\s\S]*\1)/g,"")}function i(e,t){if(!w.isRegExp(e))throw new TypeError("Type RegExp expected");var i=e[b]||{},a=s(e),l="",u="",c=null,f=null;return t=t||{},t.removeG&&(u+="g"),t.removeY&&(u+="y"),u&&(a=S.replace.call(a,new RegExp("["+u+"]+","g"),"")),t.addG&&(l+="g"),t.addY&&(l+="y"),l&&(a=r(a+l)),t.isInternalOnly||(void 0!==i.source&&(c=i.source),null!=i.flags&&(f=l?r(i.flags+l):i.flags)),e=n(new RegExp(e.source,a),o(e)?i.captureNames.slice(0):null,c,f,t.isInternalOnly)}function a(e){return parseInt(e,16)}function s(e){return M?e.flags:S.exec.call(/\/([a-z]*)$/i,RegExp.prototype.toString.call(e))[1]}function o(e){return!(!e[b]||!e[b].captureNames)}function l(e){return parseInt(e,10).toString(16)}function u(e,t){var n,r=e.length;for(n=0;r>n;++n)if(e[n]===t)return n;return-1}function c(e,t){return $.call(e)==="[object "+t+"]"}function f(e,t,n){return S.test.call(n.indexOf("x")>-1?/^(?:\s+|#.*|\(\?#[^)]*\))*(?:[?*+]|{\d+(?:,\d*)?})/:/^(?:\(\?#[^)]*\))*(?:[?*+]|{\d+(?:,\d*)?})/,e.slice(t))}function g(e){for(;e.length<4;)e="0"+e;return e}function p(e,t){var n;if(r(t)!==t)throw new SyntaxError("Invalid duplicate regex flag "+t);for(e=S.replace.call(e,/^\(\?([\w$]+)\)/,function(e,n){if(S.test.call(/[gy]/,n))throw new SyntaxError("Cannot use flag g or y in mode modifier "+e);return t=r(t+n),""}),n=0;n"}else if(i)return"\\"+(+i+n);return e};if(!c(e,"Array")||!e.length)throw new TypeError("Must provide a nonempty array of patterns to merge");for(a=0;a1&&u(s,"")>-1&&(n=i(this,{removeG:!0,isInternalOnly:!0}),S.replace.call(String(e).slice(s.index),n,function(){var e,t=arguments.length;for(e=1;t-2>e;++e)void 0===arguments[e]&&(s[e]=void 0)})),this[b]&&this[b].captureNames)for(r=1;rs.index&&(this.lastIndex=s.index)}return this.global||(this.lastIndex=a),s},C.test=function(e){return!!C.exec.call(this,e)},C.match=function(e){var t;if(w.isRegExp(e)){if(e.global)return t=S.match.apply(this,arguments),e.lastIndex=0,t}else e=new RegExp(e);return C.exec.call(e,y(this))},C.replace=function(e,t){var n,r,i,a=w.isRegExp(e);return a?(e[b]&&(r=e[b].captureNames),n=e.lastIndex):e+="",i=c(t,"Function")?S.replace.call(String(this),e,function(){var n,i=arguments;if(r)for(i[0]=new String(i[0]),n=0;na)throw new SyntaxError("Backreference to undefined group "+t);return e[a+1]||""}if("$"===i)return"$";if("&"===i||0===+i)return e[0];if("`"===i)return e[e.length-1].slice(0,e[e.length-2]);if("'"===i)return e[e.length-1].slice(e[e.length-2]+e[0].length);if(i=+i,!isNaN(i)){if(i>e.length-3)throw new SyntaxError("Backreference to undefined group "+t);return e[i]||""}throw new SyntaxError("Invalid token "+t)})}),a&&(e.global?e.lastIndex=0:e.lastIndex=n),i},C.split=function(e,t){if(!w.isRegExp(e))return S.split.apply(this,arguments);var n,r=String(this),i=[],a=e.lastIndex,s=0;return t=(void 0===t?-1:t)>>>0,w.forEach(r,e,function(e){e.index+e[0].length>s&&(i.push(r.slice(s,e.index)),e.length>1&&e.indext?i.slice(0,t):i},w.addToken(/\\([ABCE-RTUVXYZaeg-mopqyz]|c(?![A-Za-z])|u(?![\dA-Fa-f]{4}|{[\dA-Fa-f]+})|x(?![\dA-Fa-f]{2}))/,function(e,t){if("B"===e[1]&&t===R)return e[0];throw new SyntaxError("Invalid escape "+e[0])},{scope:"all",leadChar:"\\"}),w.addToken(/\\u{([\dA-Fa-f]+)}/,function(e,t,n){var r=a(e[1]);if(r>1114111)throw new SyntaxError("Invalid Unicode code point "+e[0]);if(65535>=r)return"\\u"+g(l(r));if(I&&n.indexOf("u")>-1)return e[0];throw new SyntaxError("Cannot use Unicode code point above \\u{FFFF} without flag u")},{scope:"all",leadChar:"\\"}),w.addToken(/\[(\^?)]/,function(e){return e[1]?"[\\s\\S]":"\\b\\B"},{leadChar:"["}),w.addToken(/\(\?#[^)]*\)/,function(e,t,n){return f(e.input,e.index+e[0].length,n)?"":"(?:)"},{leadChar:"("}),w.addToken(/\s+|#.*/,function(e,t,n){return f(e.input,e.index+e[0].length,n)?"":"(?:)"},{flag:"x"}),w.addToken(/\./,function(){return"[\\s\\S]"},{flag:"s",leadChar:"."}),w.addToken(/\\k<([\w$]+)>/,function(e){var t=isNaN(e[1])?u(this.captureNames,e[1])+1:+e[1],n=e.index+e[0].length;if(!t||t>this.captureNames.length)throw new SyntaxError("Backreference to undefined group "+e[0]);return"\\"+t+(n===e.input.length||isNaN(e.input.charAt(n))?"":"(?:)")},{leadChar:"\\"}),w.addToken(/\\(\d+)/,function(e,t){if(!(t===R&&/^[1-9]/.test(e[1])&&+e[1]<=this.captureNames.length)&&"0"!==e[1])throw new SyntaxError("Cannot use octal escape or backreference to undefined group "+e[0]);return e[0]},{scope:"all",leadChar:"\\"}),w.addToken(/\(\?P?<([\w$]+)>/,function(e){if(!isNaN(e[1]))throw new SyntaxError("Cannot use integer as capture name "+e[0]);if("length"===e[1]||"__proto__"===e[1])throw new SyntaxError("Cannot use reserved word as capture name "+e[0]);if(u(this.captureNames,e[1])>-1)throw new SyntaxError("Cannot use same name for multiple groups "+e[0]);return this.captureNames.push(e[1]),this.hasNamedCapture=!0,"("},{leadChar:"("}),w.addToken(/\((?!\?)/,function(e,t,n){return n.indexOf("n")>-1?"(?:":(this.captureNames.push(null),"(")},{optionalFlags:"n",leadChar:"("}),e.exports=w},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(6);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})});var i=n(7);Object.keys(i).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return i[e]}})})},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;nr;r++)"object"===i(t[r])&&(n=n.concat((0,a.find)(e,t[r])));return n=(0,a.sort)(n),n=(0,a.removeNested)(n),n=(0,a.compact)(n)}Object.defineProperty(t,"__esModule",{value:!0});var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};t.applyRegexList=r;var a=n(8)},function(e,t,n){"use strict";function r(e,t){function n(e,t){return e[0]}for(var r=null,i=[],a=t.func?t.func:n,s=0;r=l.XRegExp.exec(e,t.regex,s);){var u=a(r,t);"string"==typeof u&&(u=[new o.Match(u,r.index,t.css)]),i=i.concat(u),s=r.index+r[0].length}return i}function i(e){function t(e,t){return e.indext.index?1:e.lengtht.length?1:0}return e.sort(t)}function a(e){var t,n,r=[];for(t=0,n=e.length;n>t;t++)e[t]&&r.push(e[t]);return r}function s(e){for(var t=0,n=e.length;n>t;t++)if(null!==e[t])for(var r=e[t],i=r.index+r.length,a=t+1,n=e.length;n>a&&null!==e[t];a++){var s=e[a];if(null!==s){if(s.index>i)break;s.index==r.index&&s.length>r.length?e[t]=null:s.index>=r.index&&s.indexr;r++)i[t[r]]=!0;return i}function a(e,t,n){var a=this;a.opts=n,a.code=e,a.matches=t,a.lines=r(e),a.linesToHighlight=i(n)}Object.defineProperty(t,"__esModule",{value:!0}),t["default"]=a,a.prototype={wrapLinesWithCode:function(e,t){if(null==e||0==e.length||"\n"==e||null==t)return e;var n,i,a,s,o,l=this,u=[];for(e=e.replace(/s;s++)a+=l.opts.space;return a+" "}),n=r(e),s=0,o=n.length;o>s;s++)i=n[s],a="",i.length>0&&(i=i.replace(/^( | )+/,function(e){return a=e,""}),i=0===i.length?a:a+''+i+""),u.push(i);return u.join("\n")},processUrls:function(e){var t=/(.*)((>|<).*)/,n=/\w+:\/\/[\w-.\/?%&=:@;#]*/g;return e.replace(n,function(e){var n="",r=null;return(r=t.exec(e))&&(e=r[1],n=r[2]),''+e+""+n})},figureOutLineNumbers:function(e){var t,n,r=[],i=this.lines,a=parseInt(this.opts.firstLine||1);for(t=0,n=i.length;n>t;t++)r.push(t+a);return r},wrapLine:function(e,t,n){var r=["line","number"+t,"index"+e,"alt"+(t%2==0?1:2).toString()];return this.linesToHighlight[t]&&r.push("highlighted"),0==t&&r.push("break"),'
'+n+"
"},renderLineNumbers:function(e,t){var r,i,a=this,s=a.opts,o="",l=a.lines.length,u=parseInt(s.firstLine||1),c=s.padLineNumbers;for(1==c?c=(u+l-1).toString().length:1==isNaN(c)&&(c=0),i=0;l>i;i++)r=t?t[i]:u+i,e=0==r?s.space:n(r,c),o+=a.wrapLine(i,r,e);return o},getCodeLinesHtml:function(e,t){for(var n=this,i=n.opts,a=r(e),s=(i.padLineNumbers,parseInt(i.firstLine||1)),o=i.brush,e="",l=0,u=a.length;u>l;l++){var c=a[l],f=/^( |\s)+/.exec(c),g=null,p=t?t[l]:s+l;null!=f&&(g=f[0].toString(),c=c.substr(g.length),g=g.replace(" ",i.space)),0==c.length&&(c=i.space),e+=n.wrapLine(l,p,(null!=g?''+g+"":"")+c)}return e},getTitleHtml:function(e){return e?""+e+"":""},getMatchesHtml:function(e,t){function n(e){var t=e?e.brushName||c:c;return t?t+" ":""}var r,i,a,s,o=this,l=0,u="",c=o.opts.brush||"";for(a=0,s=t.length;s>a;a++)r=t[a],null!==r&&0!==r.length&&(i=n(r),u+=o.wrapLinesWithCode(e.substr(l,r.index-l),i+"plain")+o.wrapLinesWithCode(r.value,i+r.css),l=r.index+r.length+(r.offset||0));return u+=o.wrapLinesWithCode(e.substr(l),n()+"plain")},getHtml:function(){var e,t,n,r=this,i=r.opts,a=r.code,s=r.matches,o=["syntaxhighlighter"];return i.collapse===!0&&o.push("collapsed"),t=i.gutter!==!1,t||o.push("nogutter"),o.push(i.className),o.push(i.brush),t&&(e=r.figureOutLineNumbers(a)),n=r.getMatchesHtml(a,s),n=r.getCodeLinesHtml(n,e),i.autoLinks&&(n=r.processUrls(n)),n='\n
\n \n '+r.getTitleHtml(i.title)+"\n \n \n "+(t?'":"")+'\n \n \n \n
'+r.renderLineNumbers(a)+"\n
'+n+"
\n
\n
\n "}}},function(e,t){"use strict";function n(e){return e.split(/\r?\n/)}function r(e,t){for(var r=n(e),i=0,a=r.length;a>i;i++)r[i]=t(r[i],i);return r.join("\n")}function i(e){return(e||"")+Math.round(1e6*Math.random()).toString()}function a(e,t){var n,r={};for(n in e)r[n]=e[n];for(n in t)r[n]=t[n];return r}function s(e){return e.replace(/^\s+|\s+$/g,"")}function o(e){return Array.prototype.slice.apply(e)}function l(e){var t={"true":!0,"false":!1}[e];return null==t?e:t}e.exports={splitLines:n,eachLine:r,guid:i,merge:a,trim:s,toArray:o,toBoolean:l}},function(e,t,n){"use strict";var r=n(12),i=n(13),a=n(14),s=n(15),o=n(16);e.exports=function(e,t){e=r(e,t),e=i(e,t),e=a(e,t),e=s.unindent(e,t);var n=t["tab-size"];return e=t["smart-tabs"]===!0?o.smart(e,n):o.regular(e,n)}},function(e,t){"use strict";e.exports=function(e,t){return e.replace(/^[ ]*[\n]+|[\n]*[ ]*$/g,"").replace(/\r/g," ")}},function(e,t){"use strict";e.exports=function(e,t){var n=/|<br\s*\/?>/gi;return t.bloggerMode===!0&&(e=e.replace(n,"\n")),e}},function(e,t){"use strict";e.exports=function(e,t){var n=/|<br\s*\/?>/gi;return t.stripBrs===!0&&(e=e.replace(n,"")),e}},function(e,t){"use strict";function n(e){return/^\s*$/.test(e)}e.exports={unindent:function(e){var t,r,i,a,s=e.split(/\r?\n/),o=/^\s*/,l=1e3;for(i=0,a=s.length;a>i&&l>0;i++)if(t=s[i],!n(t)){if(r=o.exec(t),null==r)return e;l=Math.min(r[0].length,l)}if(l>0)for(i=0,a=s.length;a>i;i++)n(s[i])||(s[i]=s[i].substr(l));return s.join("\n")}}},function(e,t){"use strict";function n(e,t,n){return e.substr(0,t)+r.substr(0,n)+e.substr(t+1,e.length)}for(var r="",i=0;50>i;i++)r+=" ";e.exports={smart:function(e,t){var r,i,a,s,o=e.split(/\r?\n/),l=" ";for(a=0,s=o.length;s>a;a++)if(r=o[a],-1!==r.indexOf(l)){for(i=0;-1!==(i=r.indexOf(l));)r=n(r,i,t-i%t);o[a]=r}return o.join("\n")},regular:function(e,t){return e.replace(/\t/g,r.substr(0,t))}}},function(e,t){"use strict";function n(){for(var e=document.getElementsByTagName("script"),t=[],n=0;nl&&null==i;l++)i=o(a[l],t,n);return i}function l(e,t){return o(e,t,!0)}function u(e,t,n,r,i){var a=(screen.width-n)/2,s=(screen.height-r)/2;i+=", left="+a+", top="+s+", width="+n+", height="+r,i=i.replace(/^,/,"");var o=window.open(e,t,i);return o.focus(),o}function c(e){return document.getElementsByTagName(e)}function f(e){var t,n,r=c(e.tagName);if(e.useScriptTags)for(t=c("script"),n=0;ng;g++)f.push(c[g].innerText||c[g].textContent);f=f.join("\r"),f=f.replace(/\u00a0/g," "),u.readOnly=!0,u.appendChild(document.createTextNode(f)),r.appendChild(u),u.focus(),u.select(),s(u,"blur",function(e){u.parentNode.removeChild(u),a(n,"source")})}}e.exports={quickCodeHandler:p,create:g,popup:u,hasClass:r,addClass:i,removeClass:a,attachEvent:s,findElement:o,findParentElement:l,getSyntaxHighlighterScriptTags:n,findElementsToHighlight:f}},function(e,t){"use strict";e.exports={space:" ",useScriptTags:!0,bloggerMode:!1,stripBrs:!1,tagName:"pre"}},function(e,t){"use strict";e.exports={"class-name":"","first-line":1,"pad-line-numbers":!1,highlight:null,title:null,"smart-tabs":!0,"tab-size":4,gutter:!0,"quick-code":!0,collapse:!1,"auto-links":!0,unindent:!0,"html-script":!1}},function(e,t,n){(function(t){"use strict";function r(e,t){function n(e,t){for(var n=0,r=e.length;r>n;n++)e[n].index+=t}function r(e,r){function s(e){u=u.concat(e)}var o,l=e.code,u=[],c=a.regexList,f=e.index+e.left.length,g=a.htmlScript;o=i(l,c),n(o,f),s(o),null!=g.left&&null!=e.left&&(o=i(e.left,[g.left]),n(o,e.index),s(o)),null!=g.right&&null!=e.right&&(o=i(e.right,[g.right]),n(o,e.index+e[0].lastIndexOf(e.right)),s(o));for(var p=0,d=u.length;d>p;p++)u[p].brushName=t.brushName;return u}var a,s=new e;if(null!=t){if(a=new t,null==a.htmlScript)throw new Error("Brush wasn't configured for html-script option: "+t.brushName);s.regexList.push({regex:a.htmlScript.code,func:r}),this.regexList=s.regexList}}var i=n(5).applyRegexList;e.exports=r}).call(t,n(21))},function(e,t){"use strict";function n(){f&&u&&(f=!1,u.length?c=u.concat(c):g=-1,c.length&&r())}function r(){if(!f){var e=s(n);f=!0;for(var t=c.length;t;){for(u=c,c=[];++g1)for(var n=1;n"+e.left.source+")(?.*?)(?"+t.end+")","sgi")}}},{key:"getHtml",value:function(e){var t=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=(0,u.applyRegexList)(e,this.regexList),r=new o["default"](e,n,t);return r.getHtml()}}]),e}()},function(e,t,n){"use strict";function r(){var e="break case catch class continue default delete do else enum export extends false for from as function if implements import in instanceof interface let new null package private protected static return super switch this throw true try typeof var while with yield";this.regexList=[{regex:a.multiLineDoubleQuotedString,css:"string"},{regex:a.multiLineSingleQuotedString,css:"string"},{regex:a.singleLineCComments,css:"comments"},{regex:a.multiLineCComments,css:"comments"},{regex:/\s*#.*/gm,css:"preprocessor"},{regex:new RegExp(this.getKeywords(e),"gm"),css:"keyword"}],this.forHtmlScript(a.scriptScriptTags)}var i=n(22),a=n(3).commonRegExp;r.prototype=new i,r.aliases=["js","jscript","javascript","json"],e.exports=r},function(e,t,n){"use strict";function r(){var e="alias and BEGIN begin break case class def define_method defined do each else elsif END end ensure false for if in module new next nil not or raise redo rescue retry return self super then throw true undef unless until when while yield",t="Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol ThreadGroup Thread Time TrueClass";this.regexList=[{regex:a.singleLinePerlComments,css:"comments"},{regex:a.doubleQuotedString,css:"string"},{regex:a.singleQuotedString,css:"string"},{regex:/\b[A-Z0-9_]+\b/g,css:"constants"},{regex:/:[a-z][A-Za-z0-9_]*/g,css:"color2"},{regex:/(\$|@@|@)\w+/g,css:"variable bold"},{regex:new RegExp(this.getKeywords(e),"gm"),css:"keyword"},{regex:new RegExp(this.getKeywords(t),"gm"),css:"color1"}],this.forHtmlScript(a.aspScriptTags)}var i=n(22),a=n(3).commonRegExp;r.prototype=new i,r.aliases=["ruby","rails","ror","rb"],e.exports=r},function(e,t,n){"use strict";function r(){function e(e,t){var n=e[0],r=s.exec(n,s("(<|<)[\\s\\/\\?!]*(?[:\\w-\\.]+)","xg")),i=[];if(null!=e.attributes)for(var a,l=0,u=s("(? [\\w:.-]+)\\s*=\\s*(? \".*?\"|'.*?'|\\w+)","xg");null!=(a=s.exec(n,u,l));)i.push(new o(a.name,e.index+a.index,"color1")),i.push(new o(a.value,e.index+a.index+a[0].indexOf(a.value),"string")),l=a.index+a[0].length;return null!=r&&i.push(new o(r.name,e.index+r[0].indexOf(r.name),"keyword")),i}this.regexList=[{regex:s("(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)","gm"),css:"color2"},{regex:a.xmlComments,css:"comments"},{regex:s("(<|<)[\\s\\/\\?!]*(\\w+)(?.*?)[\\s\\/\\?]*(>|>)","sg"),func:e}]}var i=n(22),a=n(3).commonRegExp,s=n(3).XRegExp,o=n(5).Match;r.prototype=new i,r.aliases=["xml","xhtml","xslt","html","plist"],e.exports=r},function(e,t,n){"use strict";function r(){var e="abs avg case cast coalesce convert count current_timestamp current_user day isnull left lower month nullif replace right session_user space substring sum system_user upper user year",t="absolute action add after alter as asc at authorization begin bigint binary bit by cascade char character check checkpoint close collate column commit committed connect connection constraint contains continue create cube current current_date current_time cursor database date deallocate dec decimal declare default delete desc distinct double drop dynamic else end end-exec escape except exec execute false fetch first float for force foreign forward free from full function global goto grant group grouping having hour ignore index inner insensitive insert instead int integer intersect into is isolation key last level load local max min minute modify move name national nchar next no numeric of off on only open option order out output partial password precision prepare primary prior privileges procedure public read real references relative repeatable restrict return returns revoke rollback rollup rows rule schema scroll second section select sequence serializable set size smallint static statistics table temp temporary then time timestamp to top transaction translation trigger true truncate uncommitted union unique update values varchar varying view when where with work",n="all and any between cross in join like not null or outer some"; + this.regexList=[{regex:/--(.*)$/gm,css:"comments"},{regex:/\/\*([^\*][\s\S]*?)?\*\//gm,css:"comments"},{regex:a.multiLineDoubleQuotedString,css:"string"},{regex:a.multiLineSingleQuotedString,css:"string"},{regex:new RegExp(this.getKeywords(e),"gmi"),css:"color2"},{regex:new RegExp(this.getKeywords(n),"gmi"),css:"color1"},{regex:new RegExp(this.getKeywords(t),"gmi"),css:"keyword"}]}var i=n(22),a=n(3).commonRegExp;r.prototype=new i,r.aliases=["sql"],e.exports=r},function(e,t,n){"use strict";function r(){this.regexList=[]}var i=n(22);n(3).commonRegExp;r.prototype=new i,r.aliases=["text","plain"],e.exports=r},function(e,t,n){"use strict";"function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};!function(t,n){e.exports=n()}("domready",function(){var e,t=[],n=document,r=n.documentElement.doScroll,i="DOMContentLoaded",a=(r?/^loaded|^c/:/^loaded|^i|^c/).test(n.readyState);return a||n.addEventListener(i,e=function(){for(n.removeEventListener(i,e),a=1;e=t.shift();)e()}),function(e){a?setTimeout(e,0):t.push(e)}})},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=t.string=function(e){return e.replace(/^([A-Z])/g,function(e,t){return t.toLowerCase()}).replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()})};t.object=function(e){var t={};return Object.keys(e).forEach(function(r){return t[n(r)]=e[r]}),t}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}var i=n(1),a=r(i);window.SyntaxHighlighter=a["default"],"undefined"==typeof window.XRegExp&&(window.XRegExp=n(3).XRegExp)}]); diff --git a/assets/javascripts/syntaxhighlighter/shBrushAS3.js b/assets/javascripts/syntaxhighlighter/shBrushAS3.js deleted file mode 100644 index 8aa3ed2..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushAS3.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Created by Peter Atoria @ http://iAtoria.com - - var inits = 'class interface function package'; - - var keywords = '-Infinity ...rest Array as AS3 Boolean break case catch const continue Date decodeURI ' + - 'decodeURIComponent default delete do dynamic each else encodeURI encodeURIComponent escape ' + - 'extends false final finally flash_proxy for get if implements import in include Infinity ' + - 'instanceof int internal is isFinite isNaN isXMLName label namespace NaN native new null ' + - 'Null Number Object object_proxy override parseFloat parseInt private protected public ' + - 'return set static String super switch this throw true try typeof uint undefined unescape ' + - 'use void while with' - ; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments - { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings - { regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi, css: 'value' }, // numbers - { regex: new RegExp(this.getKeywords(inits), 'gm'), css: 'color3' }, // initializations - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords - { regex: new RegExp('var', 'gm'), css: 'variable' }, // variable - { regex: new RegExp('trace', 'gm'), css: 'color1' } // trace - ]; - - this.forHtmlScript(SyntaxHighlighter.regexLib.scriptScriptTags); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['actionscript3', 'as3']; - - SyntaxHighlighter.brushes.AS3 = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushAppleScript.js b/assets/javascripts/syntaxhighlighter/shBrushAppleScript.js deleted file mode 100644 index d40bbd7..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushAppleScript.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // AppleScript brush by David Chambers - // http://davidchambersdesign.com/ - var keywords = 'after before beginning continue copy each end every from return get global in local named of set some that the then times to where whose with without'; - var ordinals = 'first second third fourth fifth sixth seventh eighth ninth tenth last front back middle'; - var specials = 'activate add alias AppleScript ask attachment boolean class constant delete duplicate empty exists false id integer list make message modal modified new no paragraph pi properties quit real record remove rest result reveal reverse run running save string true word yes'; - - this.regexList = [ - - { regex: /(--|#).*$/gm, - css: 'comments' }, - - { regex: /\(\*(?:[\s\S]*?\(\*[\s\S]*?\*\))*[\s\S]*?\*\)/gm, // support nested comments - css: 'comments' }, - - { regex: /"[\s\S]*?"/gm, - css: 'string' }, - - { regex: /(?:,|:|¬|'s\b|\(|\)|\{|\}|«|\b\w*»)/g, - css: 'color1' }, - - { regex: /(-)?(\d)+(\.(\d)?)?(E\+(\d)+)?/g, // numbers - css: 'color1' }, - - { regex: /(?:&(amp;|gt;|lt;)?|=|� |>|<|≥|>=|≤|<=|\*|\+|-|\/|÷|\^)/g, - css: 'color2' }, - - { regex: /\b(?:and|as|div|mod|not|or|return(?!\s&)(ing)?|equals|(is(n't| not)? )?equal( to)?|does(n't| not) equal|(is(n't| not)? )?(greater|less) than( or equal( to)?)?|(comes|does(n't| not) come) (after|before)|is(n't| not)?( in)? (back|front) of|is(n't| not)? behind|is(n't| not)?( (in|contained by))?|does(n't| not) contain|contain(s)?|(start|begin|end)(s)? with|((but|end) )?(consider|ignor)ing|prop(erty)?|(a )?ref(erence)?( to)?|repeat (until|while|with)|((end|exit) )?repeat|((else|end) )?if|else|(end )?(script|tell|try)|(on )?error|(put )?into|(of )?(it|me)|its|my|with (timeout( of)?|transaction)|end (timeout|transaction))\b/g, - css: 'keyword' }, - - { regex: /\b\d+(st|nd|rd|th)\b/g, // ordinals - css: 'keyword' }, - - { regex: /\b(?:about|above|against|around|at|below|beneath|beside|between|by|(apart|aside) from|(instead|out) of|into|on(to)?|over|since|thr(ough|u)|under)\b/g, - css: 'color3' }, - - { regex: /\b(?:adding folder items to|after receiving|choose( ((remote )?application|color|folder|from list|URL))?|clipboard info|set the clipboard to|(the )?clipboard|entire contents|display(ing| (alert|dialog|mode))?|document( (edited|file|nib name))?|file( (name|type))?|(info )?for|giving up after|(name )?extension|quoted form|return(ed)?|second(?! item)(s)?|list (disks|folder)|text item(s| delimiters)?|(Unicode )?text|(disk )?item(s)?|((current|list) )?view|((container|key) )?window|with (data|icon( (caution|note|stop))?|parameter(s)?|prompt|properties|seed|title)|case|diacriticals|hyphens|numeric strings|punctuation|white space|folder creation|application(s( folder)?| (processes|scripts position|support))?|((desktop )?(pictures )?|(documents|downloads|favorites|home|keychain|library|movies|music|public|scripts|sites|system|users|utilities|workflows) )folder|desktop|Folder Action scripts|font(s| panel)?|help|internet plugins|modem scripts|(system )?preferences|printer descriptions|scripting (additions|components)|shared (documents|libraries)|startup (disk|items)|temporary items|trash|on server|in AppleTalk zone|((as|long|short) )?user name|user (ID|locale)|(with )?password|in (bundle( with identifier)?|directory)|(close|open for) access|read|write( permission)?|(g|s)et eof|using( delimiters)?|starting at|default (answer|button|color|country code|entr(y|ies)|identifiers|items|name|location|script editor)|hidden( answer)?|open(ed| (location|untitled))?|error (handling|reporting)|(do( shell)?|load|run|store) script|administrator privileges|altering line endings|get volume settings|(alert|boot|input|mount|output|set) volume|output muted|(fax|random )?number|round(ing)?|up|down|toward zero|to nearest|as taught in school|system (attribute|info)|((AppleScript( Studio)?|system) )?version|(home )?directory|(IPv4|primary Ethernet) address|CPU (type|speed)|physical memory|time (stamp|to GMT)|replacing|ASCII (character|number)|localized string|from table|offset|summarize|beep|delay|say|(empty|multiple) selections allowed|(of|preferred) type|invisibles|showing( package contents)?|editable URL|(File|FTP|News|Media|Web) [Ss]ervers|Telnet hosts|Directory services|Remote applications|waiting until completion|saving( (in|to))?|path (for|to( (((current|frontmost) )?application|resource))?)|POSIX (file|path)|(background|RGB) color|(OK|cancel) button name|cancel button|button(s)?|cubic ((centi)?met(re|er)s|yards|feet|inches)|square ((kilo)?met(re|er)s|miles|yards|feet)|(centi|kilo)?met(re|er)s|miles|yards|feet|inches|lit(re|er)s|gallons|quarts|(kilo)?grams|ounces|pounds|degrees (Celsius|Fahrenheit|Kelvin)|print( (dialog|settings))?|clos(e(able)?|ing)|(de)?miniaturized|miniaturizable|zoom(ed|able)|attribute run|action (method|property|title)|phone|email|((start|end)ing|home) page|((birth|creation|current|custom|modification) )?date|((((phonetic )?(first|last|middle))|computer|host|maiden|related) |nick)?name|aim|icq|jabber|msn|yahoo|address(es)?|save addressbook|should enable action|city|country( code)?|formatte(r|d address)|(palette )?label|state|street|zip|AIM [Hh]andle(s)?|my card|select(ion| all)?|unsaved|(alpha )?value|entr(y|ies)|group|(ICQ|Jabber|MSN) handle|person|people|company|department|icon image|job title|note|organization|suffix|vcard|url|copies|collating|pages (across|down)|request print time|target( printer)?|((GUI Scripting|Script menu) )?enabled|show Computer scripts|(de)?activated|awake from nib|became (key|main)|call method|of (class|object)|center|clicked toolbar item|closed|for document|exposed|(can )?hide|idle|keyboard (down|up)|event( (number|type))?|launch(ed)?|load (image|movie|nib|sound)|owner|log|mouse (down|dragged|entered|exited|moved|up)|move|column|localization|resource|script|register|drag (info|types)|resigned (active|key|main)|resiz(e(d)?|able)|right mouse (down|dragged|up)|scroll wheel|(at )?index|should (close|open( untitled)?|quit( after last window closed)?|zoom)|((proposed|screen) )?bounds|show(n)?|behind|in front of|size (mode|to fit)|update(d| toolbar item)?|was (hidden|miniaturized)|will (become active|close|finish launching|hide|miniaturize|move|open|quit|(resign )?active|((maximum|minimum|proposed) )?size|show|zoom)|bundle|data source|movie|pasteboard|sound|tool(bar| tip)|(color|open|save) panel|coordinate system|frontmost|main( (bundle|menu|window))?|((services|(excluded from )?windows) )?menu|((executable|frameworks|resource|scripts|shared (frameworks|support)) )?path|(selected item )?identifier|data|content(s| view)?|character(s)?|click count|(command|control|option|shift) key down|context|delta (x|y|z)|key( code)?|location|pressure|unmodified characters|types|(first )?responder|playing|(allowed|selectable) identifiers|allows customization|(auto saves )?configuration|visible|image( name)?|menu form representation|tag|user(-| )defaults|associated file name|(auto|needs) display|current field editor|floating|has (resize indicator|shadow)|hides when deactivated|level|minimized (image|title)|opaque|position|release when closed|sheet|title(d)?)\b/g, - css: 'color3' }, - - { regex: new RegExp(this.getKeywords(specials), 'gm'), css: 'color3' }, - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, - { regex: new RegExp(this.getKeywords(ordinals), 'gm'), css: 'keyword' } - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['applescript']; - - SyntaxHighlighter.brushes.AppleScript = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushBash.js b/assets/javascripts/syntaxhighlighter/shBrushBash.js deleted file mode 100644 index 8c29696..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushBash.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - var keywords = 'if fi then elif else for do done until while break continue case function return in eq ne ge le'; - var commands = 'alias apropos awk basename bash bc bg builtin bzip2 cal cat cd cfdisk chgrp chmod chown chroot' + - 'cksum clear cmp comm command cp cron crontab csplit cut date dc dd ddrescue declare df ' + - 'diff diff3 dig dir dircolors dirname dirs du echo egrep eject enable env ethtool eval ' + - 'exec exit expand export expr false fdformat fdisk fg fgrep file find fmt fold format ' + - 'free fsck ftp gawk getopts grep groups gzip hash head history hostname id ifconfig ' + - 'import install join kill less let ln local locate logname logout look lpc lpr lprint ' + - 'lprintd lprintq lprm ls lsof make man mkdir mkfifo mkisofs mknod more mount mtools ' + - 'mv netstat nice nl nohup nslookup open op passwd paste pathchk ping popd pr printcap ' + - 'printenv printf ps pushd pwd quota quotacheck quotactl ram rcp read readonly renice ' + - 'remsync rm rmdir rsync screen scp sdiff sed select seq set sftp shift shopt shutdown ' + - 'sleep sort source split ssh strace su sudo sum symlink sync tail tar tee test time ' + - 'times touch top traceroute trap tr true tsort tty type ulimit umask umount unalias ' + - 'uname unexpand uniq units unset unshar useradd usermod users uuencode uudecode v vdir ' + - 'vi watch wc whereis which who whoami Wget xargs yes' - ; - - this.regexList = [ - { regex: /^#!.*$/gm, css: 'preprocessor bold' }, - { regex: /\/[\w-\/]+/gm, css: 'plain' }, - { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, // one line comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords - { regex: new RegExp(this.getKeywords(commands), 'gm'), css: 'functions' } // commands - ]; - } - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['bash', 'shell']; - - SyntaxHighlighter.brushes.Bash = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushCSharp.js b/assets/javascripts/syntaxhighlighter/shBrushCSharp.js deleted file mode 100644 index 079214e..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushCSharp.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - var keywords = 'abstract as base bool break byte case catch char checked class const ' + - 'continue decimal default delegate do double else enum event explicit ' + - 'extern false finally fixed float for foreach get goto if implicit in int ' + - 'interface internal is lock long namespace new null object operator out ' + - 'override params private protected public readonly ref return sbyte sealed set ' + - 'short sizeof stackalloc static string struct switch this throw true try ' + - 'typeof uint ulong unchecked unsafe ushort using virtual void while'; - - function fixComments(match, regexInfo) - { - var css = (match[0].indexOf("///") == 0) - ? 'color1' - : 'comments' - ; - - return [new SyntaxHighlighter.Match(match[0], match.index, css)]; - } - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLineCComments, func : fixComments }, // one line comments - { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments - { regex: /@"(?:[^"]|"")*"/g, css: 'string' }, // @-quoted strings - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings - { regex: /^\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // c# keyword - { regex: /\bpartial(?=\s+(?:class|interface|struct)\b)/g, css: 'keyword' }, // contextual keyword: 'partial' - { regex: /\byield(?=\s+(?:return|break)\b)/g, css: 'keyword' } // contextual keyword: 'yield' - ]; - - this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['c#', 'c-sharp', 'csharp']; - - SyntaxHighlighter.brushes.CSharp = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); - diff --git a/assets/javascripts/syntaxhighlighter/shBrushColdFusion.js b/assets/javascripts/syntaxhighlighter/shBrushColdFusion.js deleted file mode 100644 index 627dbb9..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushColdFusion.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributed by Jen - // http://www.jensbits.com/2009/05/14/coldfusion-brush-for-syntaxhighlighter-plus - - var funcs = 'Abs ACos AddSOAPRequestHeader AddSOAPResponseHeader AjaxLink AjaxOnLoad ArrayAppend ArrayAvg ArrayClear ArrayDeleteAt ' + - 'ArrayInsertAt ArrayIsDefined ArrayIsEmpty ArrayLen ArrayMax ArrayMin ArraySet ArraySort ArraySum ArraySwap ArrayToList ' + - 'Asc ASin Atn BinaryDecode BinaryEncode BitAnd BitMaskClear BitMaskRead BitMaskSet BitNot BitOr BitSHLN BitSHRN BitXor ' + - 'Ceiling CharsetDecode CharsetEncode Chr CJustify Compare CompareNoCase Cos CreateDate CreateDateTime CreateObject ' + - 'CreateODBCDate CreateODBCDateTime CreateODBCTime CreateTime CreateTimeSpan CreateUUID DateAdd DateCompare DateConvert ' + - 'DateDiff DateFormat DatePart Day DayOfWeek DayOfWeekAsString DayOfYear DaysInMonth DaysInYear DE DecimalFormat DecrementValue ' + - 'Decrypt DecryptBinary DeleteClientVariable DeserializeJSON DirectoryExists DollarFormat DotNetToCFType Duplicate Encrypt ' + - 'EncryptBinary Evaluate Exp ExpandPath FileClose FileCopy FileDelete FileExists FileIsEOF FileMove FileOpen FileRead ' + - 'FileReadBinary FileReadLine FileSetAccessMode FileSetAttribute FileSetLastModified FileWrite Find FindNoCase FindOneOf ' + - 'FirstDayOfMonth Fix FormatBaseN GenerateSecretKey GetAuthUser GetBaseTagData GetBaseTagList GetBaseTemplatePath ' + - 'GetClientVariablesList GetComponentMetaData GetContextRoot GetCurrentTemplatePath GetDirectoryFromPath GetEncoding ' + - 'GetException GetFileFromPath GetFileInfo GetFunctionList GetGatewayHelper GetHttpRequestData GetHttpTimeString ' + - 'GetK2ServerDocCount GetK2ServerDocCountLimit GetLocale GetLocaleDisplayName GetLocalHostIP GetMetaData GetMetricData ' + - 'GetPageContext GetPrinterInfo GetProfileSections GetProfileString GetReadableImageFormats GetSOAPRequest GetSOAPRequestHeader ' + - 'GetSOAPResponse GetSOAPResponseHeader GetTempDirectory GetTempFile GetTemplatePath GetTickCount GetTimeZoneInfo GetToken ' + - 'GetUserRoles GetWriteableImageFormats Hash Hour HTMLCodeFormat HTMLEditFormat IIf ImageAddBorder ImageBlur ImageClearRect ' + - 'ImageCopy ImageCrop ImageDrawArc ImageDrawBeveledRect ImageDrawCubicCurve ImageDrawLine ImageDrawLines ImageDrawOval ' + - 'ImageDrawPoint ImageDrawQuadraticCurve ImageDrawRect ImageDrawRoundRect ImageDrawText ImageFlip ImageGetBlob ImageGetBufferedImage ' + - 'ImageGetEXIFTag ImageGetHeight ImageGetIPTCTag ImageGetWidth ImageGrayscale ImageInfo ImageNegative ImageNew ImageOverlay ImagePaste ' + - 'ImageRead ImageReadBase64 ImageResize ImageRotate ImageRotateDrawingAxis ImageScaleToFit ImageSetAntialiasing ImageSetBackgroundColor ' + - 'ImageSetDrawingColor ImageSetDrawingStroke ImageSetDrawingTransparency ImageSharpen ImageShear ImageShearDrawingAxis ImageTranslate ' + - 'ImageTranslateDrawingAxis ImageWrite ImageWriteBase64 ImageXORDrawingMode IncrementValue InputBaseN Insert Int IsArray IsBinary ' + - 'IsBoolean IsCustomFunction IsDate IsDDX IsDebugMode IsDefined IsImage IsImageFile IsInstanceOf IsJSON IsLeapYear IsLocalHost ' + - 'IsNumeric IsNumericDate IsObject IsPDFFile IsPDFObject IsQuery IsSimpleValue IsSOAPRequest IsStruct IsUserInAnyRole IsUserInRole ' + - 'IsUserLoggedIn IsValid IsWDDX IsXML IsXmlAttribute IsXmlDoc IsXmlElem IsXmlNode IsXmlRoot JavaCast JSStringFormat LCase Left Len ' + - 'ListAppend ListChangeDelims ListContains ListContainsNoCase ListDeleteAt ListFind ListFindNoCase ListFirst ListGetAt ListInsertAt ' + - 'ListLast ListLen ListPrepend ListQualify ListRest ListSetAt ListSort ListToArray ListValueCount ListValueCountNoCase LJustify Log ' + - 'Log10 LSCurrencyFormat LSDateFormat LSEuroCurrencyFormat LSIsCurrency LSIsDate LSIsNumeric LSNumberFormat LSParseCurrency LSParseDateTime ' + - 'LSParseEuroCurrency LSParseNumber LSTimeFormat LTrim Max Mid Min Minute Month MonthAsString Now NumberFormat ParagraphFormat ParseDateTime ' + - 'Pi PrecisionEvaluate PreserveSingleQuotes Quarter QueryAddColumn QueryAddRow QueryConvertForGrid QueryNew QuerySetCell QuotedValueList Rand ' + - 'Randomize RandRange REFind REFindNoCase ReleaseComObject REMatch REMatchNoCase RemoveChars RepeatString Replace ReplaceList ReplaceNoCase ' + - 'REReplace REReplaceNoCase Reverse Right RJustify Round RTrim Second SendGatewayMessage SerializeJSON SetEncoding SetLocale SetProfileString ' + - 'SetVariable Sgn Sin Sleep SpanExcluding SpanIncluding Sqr StripCR StructAppend StructClear StructCopy StructCount StructDelete StructFind ' + - 'StructFindKey StructFindValue StructGet StructInsert StructIsEmpty StructKeyArray StructKeyExists StructKeyList StructKeyList StructNew ' + - 'StructSort StructUpdate Tan TimeFormat ToBase64 ToBinary ToScript ToString Trim UCase URLDecode URLEncodedFormat URLSessionFormat Val ' + - 'ValueList VerifyClient Week Wrap Wrap WriteOutput XmlChildPos XmlElemNew XmlFormat XmlGetNodeType XmlNew XmlParse XmlSearch XmlTransform ' + - 'XmlValidate Year YesNoFormat'; - - var keywords = 'cfabort cfajaximport cfajaxproxy cfapplet cfapplication cfargument cfassociate cfbreak cfcache cfcalendar ' + - 'cfcase cfcatch cfchart cfchartdata cfchartseries cfcol cfcollection cfcomponent cfcontent cfcookie cfdbinfo ' + - 'cfdefaultcase cfdirectory cfdiv cfdocument cfdocumentitem cfdocumentsection cfdump cfelse cfelseif cferror ' + - 'cfexchangecalendar cfexchangeconnection cfexchangecontact cfexchangefilter cfexchangemail cfexchangetask ' + - 'cfexecute cfexit cffeed cffile cfflush cfform cfformgroup cfformitem cfftp cffunction cfgrid cfgridcolumn ' + - 'cfgridrow cfgridupdate cfheader cfhtmlhead cfhttp cfhttpparam cfif cfimage cfimport cfinclude cfindex ' + - 'cfinput cfinsert cfinterface cfinvoke cfinvokeargument cflayout cflayoutarea cfldap cflocation cflock cflog ' + - 'cflogin cfloginuser cflogout cfloop cfmail cfmailparam cfmailpart cfmenu cfmenuitem cfmodule cfNTauthenticate ' + - 'cfobject cfobjectcache cfoutput cfparam cfpdf cfpdfform cfpdfformparam cfpdfparam cfpdfsubform cfpod cfpop ' + - 'cfpresentation cfpresentationslide cfpresenter cfprint cfprocessingdirective cfprocparam cfprocresult ' + - 'cfproperty cfquery cfqueryparam cfregistry cfreport cfreportparam cfrethrow cfreturn cfsavecontent cfschedule ' + - 'cfscript cfsearch cfselect cfset cfsetting cfsilent cfslider cfsprydataset cfstoredproc cfswitch cftable ' + - 'cftextarea cfthread cfthrow cftimer cftooltip cftrace cftransaction cftree cftreeitem cftry cfupdate cfwddx ' + - 'cfwindow cfxml cfzip cfzipparam'; - - var operators = 'all and any between cross in join like not null or outer some'; - - this.regexList = [ - { regex: new RegExp('--(.*)$', 'gm'), css: 'comments' }, // one line and multiline comments - { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // single quoted strings - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings - { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // functions - { regex: new RegExp(this.getKeywords(operators), 'gmi'), css: 'color1' }, // operators and such - { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword - ]; - } - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['coldfusion','cf']; - - SyntaxHighlighter.brushes.ColdFusion = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushCpp.js b/assets/javascripts/syntaxhighlighter/shBrushCpp.js deleted file mode 100644 index 9f70d3a..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushCpp.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Copyright 2006 Shin, YoungJin - - var datatypes = 'ATOM BOOL BOOLEAN BYTE CHAR COLORREF DWORD DWORDLONG DWORD_PTR ' + - 'DWORD32 DWORD64 FLOAT HACCEL HALF_PTR HANDLE HBITMAP HBRUSH ' + - 'HCOLORSPACE HCONV HCONVLIST HCURSOR HDC HDDEDATA HDESK HDROP HDWP ' + - 'HENHMETAFILE HFILE HFONT HGDIOBJ HGLOBAL HHOOK HICON HINSTANCE HKEY ' + - 'HKL HLOCAL HMENU HMETAFILE HMODULE HMONITOR HPALETTE HPEN HRESULT ' + - 'HRGN HRSRC HSZ HWINSTA HWND INT INT_PTR INT32 INT64 LANGID LCID LCTYPE ' + - 'LGRPID LONG LONGLONG LONG_PTR LONG32 LONG64 LPARAM LPBOOL LPBYTE LPCOLORREF ' + - 'LPCSTR LPCTSTR LPCVOID LPCWSTR LPDWORD LPHANDLE LPINT LPLONG LPSTR LPTSTR ' + - 'LPVOID LPWORD LPWSTR LRESULT PBOOL PBOOLEAN PBYTE PCHAR PCSTR PCTSTR PCWSTR ' + - 'PDWORDLONG PDWORD_PTR PDWORD32 PDWORD64 PFLOAT PHALF_PTR PHANDLE PHKEY PINT ' + - 'PINT_PTR PINT32 PINT64 PLCID PLONG PLONGLONG PLONG_PTR PLONG32 PLONG64 POINTER_32 ' + - 'POINTER_64 PSHORT PSIZE_T PSSIZE_T PSTR PTBYTE PTCHAR PTSTR PUCHAR PUHALF_PTR ' + - 'PUINT PUINT_PTR PUINT32 PUINT64 PULONG PULONGLONG PULONG_PTR PULONG32 PULONG64 ' + - 'PUSHORT PVOID PWCHAR PWORD PWSTR SC_HANDLE SC_LOCK SERVICE_STATUS_HANDLE SHORT ' + - 'SIZE_T SSIZE_T TBYTE TCHAR UCHAR UHALF_PTR UINT UINT_PTR UINT32 UINT64 ULONG ' + - 'ULONGLONG ULONG_PTR ULONG32 ULONG64 USHORT USN VOID WCHAR WORD WPARAM WPARAM WPARAM ' + - 'char bool short int __int32 __int64 __int8 __int16 long float double __wchar_t ' + - 'clock_t _complex _dev_t _diskfree_t div_t ldiv_t _exception _EXCEPTION_POINTERS ' + - 'FILE _finddata_t _finddatai64_t _wfinddata_t _wfinddatai64_t __finddata64_t ' + - '__wfinddata64_t _FPIEEE_RECORD fpos_t _HEAPINFO _HFILE lconv intptr_t ' + - 'jmp_buf mbstate_t _off_t _onexit_t _PNH ptrdiff_t _purecall_handler ' + - 'sig_atomic_t size_t _stat __stat64 _stati64 terminate_function ' + - 'time_t __time64_t _timeb __timeb64 tm uintptr_t _utimbuf ' + - 'va_list wchar_t wctrans_t wctype_t wint_t signed'; - - var keywords = 'break case catch class const __finally __exception __try ' + - 'const_cast continue private public protected __declspec ' + - 'default delete deprecated dllexport dllimport do dynamic_cast ' + - 'else enum explicit extern if for friend goto inline ' + - 'mutable naked namespace new noinline noreturn nothrow ' + - 'register reinterpret_cast return selectany ' + - 'sizeof static static_cast struct switch template this ' + - 'thread throw true false try typedef typeid typename union ' + - 'using uuid virtual void volatile whcar_t while'; - - var functions = 'assert isalnum isalpha iscntrl isdigit isgraph islower isprint' + - 'ispunct isspace isupper isxdigit tolower toupper errno localeconv ' + - 'setlocale acos asin atan atan2 ceil cos cosh exp fabs floor fmod ' + - 'frexp ldexp log log10 modf pow sin sinh sqrt tan tanh jmp_buf ' + - 'longjmp setjmp raise signal sig_atomic_t va_arg va_end va_start ' + - 'clearerr fclose feof ferror fflush fgetc fgetpos fgets fopen ' + - 'fprintf fputc fputs fread freopen fscanf fseek fsetpos ftell ' + - 'fwrite getc getchar gets perror printf putc putchar puts remove ' + - 'rename rewind scanf setbuf setvbuf sprintf sscanf tmpfile tmpnam ' + - 'ungetc vfprintf vprintf vsprintf abort abs atexit atof atoi atol ' + - 'bsearch calloc div exit free getenv labs ldiv malloc mblen mbstowcs ' + - 'mbtowc qsort rand realloc srand strtod strtol strtoul system ' + - 'wcstombs wctomb memchr memcmp memcpy memmove memset strcat strchr ' + - 'strcmp strcoll strcpy strcspn strerror strlen strncat strncmp ' + - 'strncpy strpbrk strrchr strspn strstr strtok strxfrm asctime ' + - 'clock ctime difftime gmtime localtime mktime strftime time'; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments - { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings - { regex: /^ *#.*/gm, css: 'preprocessor' }, - { regex: new RegExp(this.getKeywords(datatypes), 'gm'), css: 'color1 bold' }, - { regex: new RegExp(this.getKeywords(functions), 'gm'), css: 'functions bold' }, - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword bold' } - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['cpp', 'c']; - - SyntaxHighlighter.brushes.Cpp = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushCss.js b/assets/javascripts/syntaxhighlighter/shBrushCss.js deleted file mode 100644 index 4297a9a..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushCss.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - function getKeywordsCSS(str) - { - return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b'; - }; - - function getValuesCSS(str) - { - return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b'; - }; - - var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' + - 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' + - 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' + - 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' + - 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' + - 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' + - 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' + - 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' + - 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' + - 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' + - 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' + - 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' + - 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' + - 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index'; - - var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+ - 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+ - 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero default digits disc dotted double '+ - 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+ - 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+ - 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+ - 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+ - 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+ - 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+ - 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+ - 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+ - 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+ - 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+ - 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow'; - - var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif'; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings - { regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors - { regex: /(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)/g, css: 'value' }, // sizes - { regex: /!important/g, css: 'color3' }, // !important - { regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords - { regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values - { regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' } // fonts - ]; - - this.forHtmlScript({ - left: /(<|<)\s*style.*?(>|>)/gi, - right: /(<|<)\/\s*style\s*(>|>)/gi - }); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['css']; - - SyntaxHighlighter.brushes.CSS = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushDelphi.js b/assets/javascripts/syntaxhighlighter/shBrushDelphi.js deleted file mode 100644 index e1060d4..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushDelphi.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - var keywords = 'abs addr and ansichar ansistring array as asm begin boolean byte cardinal ' + - 'case char class comp const constructor currency destructor div do double ' + - 'downto else end except exports extended false file finalization finally ' + - 'for function goto if implementation in inherited int64 initialization ' + - 'integer interface is label library longint longword mod nil not object ' + - 'of on or packed pansichar pansistring pchar pcurrency pdatetime pextended ' + - 'pint64 pointer private procedure program property pshortstring pstring ' + - 'pvariant pwidechar pwidestring protected public published raise real real48 ' + - 'record repeat set shl shortint shortstring shr single smallint string then ' + - 'threadvar to true try type unit until uses val var varirnt while widechar ' + - 'widestring with word write writeln xor'; - - this.regexList = [ - { regex: /\(\*[\s\S]*?\*\)/gm, css: 'comments' }, // multiline comments (* *) - { regex: /{(?!\$)[\s\S]*?}/gm, css: 'comments' }, // multiline comments { } - { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings - { regex: /\{\$[a-zA-Z]+ .+\}/g, css: 'color1' }, // compiler Directives and Region tags - { regex: /\b[\d\.]+\b/g, css: 'value' }, // numbers 12345 - { regex: /\$[a-zA-Z0-9]+\b/g, css: 'value' }, // numbers $F5D3 - { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['delphi', 'pascal', 'pas']; - - SyntaxHighlighter.brushes.Delphi = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushDiff.js b/assets/javascripts/syntaxhighlighter/shBrushDiff.js deleted file mode 100644 index e9b14fc..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushDiff.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - this.regexList = [ - { regex: /^\+\+\+.*$/gm, css: 'color2' }, - { regex: /^\-\-\-.*$/gm, css: 'color2' }, - { regex: /^\s.*$/gm, css: 'color1' }, - { regex: /^@@.*@@$/gm, css: 'variable' }, - { regex: /^\+[^\+]{1}.*$/gm, css: 'string' }, - { regex: /^\-[^\-]{1}.*$/gm, css: 'comments' } - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['diff', 'patch']; - - SyntaxHighlighter.brushes.Diff = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushErlang.js b/assets/javascripts/syntaxhighlighter/shBrushErlang.js deleted file mode 100644 index 6ba7d9d..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushErlang.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributed by Jean-Lou Dupont - // http://jldupont.blogspot.com/2009/06/erlang-syntax-highlighter.html - - // According to: http://erlang.org/doc/reference_manual/introduction.html#1.5 - var keywords = 'after and andalso band begin bnot bor bsl bsr bxor '+ - 'case catch cond div end fun if let not of or orelse '+ - 'query receive rem try when xor'+ - // additional - ' module export import define'; - - this.regexList = [ - { regex: new RegExp("[A-Z][A-Za-z0-9_]+", 'g'), css: 'constants' }, - { regex: new RegExp("\\%.+", 'gm'), css: 'comments' }, - { regex: new RegExp("\\?[A-Za-z0-9_]+", 'g'), css: 'preprocessor' }, - { regex: new RegExp("[a-z0-9_]+:[a-z0-9_]+", 'g'), css: 'functions' }, - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['erl', 'erlang']; - - SyntaxHighlighter.brushes.Erland = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushGroovy.js b/assets/javascripts/syntaxhighlighter/shBrushGroovy.js deleted file mode 100644 index 6ec5c18..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushGroovy.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributed by Andres Almiray - // http://jroller.com/aalmiray/entry/nice_source_code_syntax_highlighter - - var keywords = 'as assert break case catch class continue def default do else extends finally ' + - 'if in implements import instanceof interface new package property return switch ' + - 'throw throws try while public protected private static'; - var types = 'void boolean byte char short int long float double'; - var constants = 'null'; - var methods = 'allProperties count get size '+ - 'collect each eachProperty eachPropertyName eachWithIndex find findAll ' + - 'findIndexOf grep inject max min reverseEach sort ' + - 'asImmutable asSynchronized flatten intersect join pop reverse subMap toList ' + - 'padRight padLeft contains eachMatch toCharacter toLong toUrl tokenize ' + - 'eachFile eachFileRecurse eachB yte eachLine readBytes readLine getText ' + - 'splitEachLine withReader append encodeBase64 decodeBase64 filterLine ' + - 'transformChar transformLine withOutputStream withPrintWriter withStream ' + - 'withStreams withWriter withWriterAppend write writeLine '+ - 'dump inspect invokeMethod print println step times upto use waitForOrKill '+ - 'getText'; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments - { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings - { regex: /""".*"""/g, css: 'string' }, // GStrings - { regex: new RegExp('\\b([\\d]+(\\.[\\d]+)?|0x[a-f0-9]+)\\b', 'gi'), css: 'value' }, // numbers - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // goovy keyword - { regex: new RegExp(this.getKeywords(types), 'gm'), css: 'color1' }, // goovy/java type - { regex: new RegExp(this.getKeywords(constants), 'gm'), css: 'constants' }, // constants - { regex: new RegExp(this.getKeywords(methods), 'gm'), css: 'functions' } // methods - ]; - - this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); - } - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['groovy']; - - SyntaxHighlighter.brushes.Groovy = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushJScript.js b/assets/javascripts/syntaxhighlighter/shBrushJScript.js deleted file mode 100644 index ff98dab..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushJScript.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - var keywords = 'break case catch continue ' + - 'default delete do else false ' + - 'for function if in instanceof ' + - 'new null return super switch ' + - 'this throw true try typeof var while with' - ; - - var r = SyntaxHighlighter.regexLib; - - this.regexList = [ - { regex: r.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings - { regex: r.multiLineSingleQuotedString, css: 'string' }, // single quoted strings - { regex: r.singleLineCComments, css: 'comments' }, // one line comments - { regex: r.multiLineCComments, css: 'comments' }, // multiline comments - { regex: /\s*#.*/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keywords - ]; - - this.forHtmlScript(r.scriptScriptTags); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['js', 'jscript', 'javascript']; - - SyntaxHighlighter.brushes.JScript = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushJava.js b/assets/javascripts/syntaxhighlighter/shBrushJava.js deleted file mode 100644 index d692fd6..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushJava.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - var keywords = 'abstract assert boolean break byte case catch char class const ' + - 'continue default do double else enum extends ' + - 'false final finally float for goto if implements import ' + - 'instanceof int interface long native new null ' + - 'package private protected public return ' + - 'short static strictfp super switch synchronized this throw throws true ' + - 'transient try void volatile while'; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments - { regex: /\/\*([^\*][\s\S]*)?\*\//gm, css: 'comments' }, // multiline comments - { regex: /\/\*(?!\*\/)\*[\s\S]*?\*\//gm, css: 'preprocessor' }, // documentation comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings - { regex: /\b([\d]+(\.[\d]+)?|0x[a-f0-9]+)\b/gi, css: 'value' }, // numbers - { regex: /(?!\@interface\b)\@[\$\w]+\b/g, css: 'color1' }, // annotation @anno - { regex: /\@interface\b/g, css: 'color2' }, // @interface keyword - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // java keyword - ]; - - this.forHtmlScript({ - left : /(<|<)%[@!=]?/g, - right : /%(>|>)/g - }); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['java']; - - SyntaxHighlighter.brushes.Java = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushJavaFX.js b/assets/javascripts/syntaxhighlighter/shBrushJavaFX.js deleted file mode 100644 index 1a150a6..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushJavaFX.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributed by Patrick Webster - // http://patrickwebster.blogspot.com/2009/04/javafx-brush-for-syntaxhighlighter.html - var datatypes = 'Boolean Byte Character Double Duration ' - + 'Float Integer Long Number Short String Void' - ; - - var keywords = 'abstract after and as assert at before bind bound break catch class ' - + 'continue def delete else exclusive extends false finally first for from ' - + 'function if import in indexof init insert instanceof into inverse last ' - + 'lazy mixin mod nativearray new not null on or override package postinit ' - + 'protected public public-init public-read replace return reverse sizeof ' - + 'step super then this throw true try tween typeof var where while with ' - + 'attribute let private readonly static trigger' - ; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, - { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, - { regex: /(-?\.?)(\b(\d*\.?\d+|\d+\.?\d*)(e[+-]?\d+)?|0x[a-f\d]+)\b\.?/gi, css: 'color2' }, // numbers - { regex: new RegExp(this.getKeywords(datatypes), 'gm'), css: 'variable' }, // datatypes - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } - ]; - this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['jfx', 'javafx']; - - SyntaxHighlighter.brushes.JavaFX = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushPerl.js b/assets/javascripts/syntaxhighlighter/shBrushPerl.js deleted file mode 100644 index d94a2e0..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushPerl.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributed by David Simmons-Duffin and Marty Kube - - var funcs = - 'abs accept alarm atan2 bind binmode chdir chmod chomp chop chown chr ' + - 'chroot close closedir connect cos crypt defined delete each endgrent ' + - 'endhostent endnetent endprotoent endpwent endservent eof exec exists ' + - 'exp fcntl fileno flock fork format formline getc getgrent getgrgid ' + - 'getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr ' + - 'getnetbyname getnetent getpeername getpgrp getppid getpriority ' + - 'getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid ' + - 'getservbyname getservbyport getservent getsockname getsockopt glob ' + - 'gmtime grep hex index int ioctl join keys kill lc lcfirst length link ' + - 'listen localtime lock log lstat map mkdir msgctl msgget msgrcv msgsnd ' + - 'oct open opendir ord pack pipe pop pos print printf prototype push ' + - 'quotemeta rand read readdir readline readlink readpipe recv rename ' + - 'reset reverse rewinddir rindex rmdir scalar seek seekdir select semctl ' + - 'semget semop send setgrent sethostent setnetent setpgrp setpriority ' + - 'setprotoent setpwent setservent setsockopt shift shmctl shmget shmread ' + - 'shmwrite shutdown sin sleep socket socketpair sort splice split sprintf ' + - 'sqrt srand stat study substr symlink syscall sysopen sysread sysseek ' + - 'system syswrite tell telldir time times tr truncate uc ucfirst umask ' + - 'undef unlink unpack unshift utime values vec wait waitpid warn write'; - - var keywords = - 'bless caller continue dbmclose dbmopen die do dump else elsif eval exit ' + - 'for foreach goto if import last local my next no our package redo ref ' + - 'require return sub tie tied unless untie until use wantarray while'; - - this.regexList = [ - { regex: new RegExp('#[^!].*$', 'gm'), css: 'comments' }, - { regex: new RegExp('^\\s*#!.*$', 'gm'), css: 'preprocessor' }, // shebang - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, - { regex: new RegExp('(\\$|@|%)\\w+', 'g'), css: 'variable' }, - { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } - ]; - - this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags); - } - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['perl', 'Perl', 'pl']; - - SyntaxHighlighter.brushes.Perl = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushPhp.js b/assets/javascripts/syntaxhighlighter/shBrushPhp.js deleted file mode 100644 index 95e6e43..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushPhp.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - var funcs = 'abs acos acosh addcslashes addslashes ' + - 'array_change_key_case array_chunk array_combine array_count_values array_diff '+ - 'array_diff_assoc array_diff_key array_diff_uassoc array_diff_ukey array_fill '+ - 'array_filter array_flip array_intersect array_intersect_assoc array_intersect_key '+ - 'array_intersect_uassoc array_intersect_ukey array_key_exists array_keys array_map '+ - 'array_merge array_merge_recursive array_multisort array_pad array_pop array_product '+ - 'array_push array_rand array_reduce array_reverse array_search array_shift '+ - 'array_slice array_splice array_sum array_udiff array_udiff_assoc '+ - 'array_udiff_uassoc array_uintersect array_uintersect_assoc '+ - 'array_uintersect_uassoc array_unique array_unshift array_values array_walk '+ - 'array_walk_recursive atan atan2 atanh base64_decode base64_encode base_convert '+ - 'basename bcadd bccomp bcdiv bcmod bcmul bindec bindtextdomain bzclose bzcompress '+ - 'bzdecompress bzerrno bzerror bzerrstr bzflush bzopen bzread bzwrite ceil chdir '+ - 'checkdate checkdnsrr chgrp chmod chop chown chr chroot chunk_split class_exists '+ - 'closedir closelog copy cos cosh count count_chars date decbin dechex decoct '+ - 'deg2rad delete ebcdic2ascii echo empty end ereg ereg_replace eregi eregi_replace error_log '+ - 'error_reporting escapeshellarg escapeshellcmd eval exec exit exp explode extension_loaded '+ - 'feof fflush fgetc fgetcsv fgets fgetss file_exists file_get_contents file_put_contents '+ - 'fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype '+ - 'floatval flock floor flush fmod fnmatch fopen fpassthru fprintf fputcsv fputs fread fscanf '+ - 'fseek fsockopen fstat ftell ftok getallheaders getcwd getdate getenv gethostbyaddr gethostbyname '+ - 'gethostbynamel getimagesize getlastmod getmxrr getmygid getmyinode getmypid getmyuid getopt '+ - 'getprotobyname getprotobynumber getrandmax getrusage getservbyname getservbyport gettext '+ - 'gettimeofday gettype glob gmdate gmmktime ini_alter ini_get ini_get_all ini_restore ini_set '+ - 'interface_exists intval ip2long is_a is_array is_bool is_callable is_dir is_double '+ - 'is_executable is_file is_finite is_float is_infinite is_int is_integer is_link is_long '+ - 'is_nan is_null is_numeric is_object is_readable is_real is_resource is_scalar is_soap_fault '+ - 'is_string is_subclass_of is_uploaded_file is_writable is_writeable mkdir mktime nl2br '+ - 'parse_ini_file parse_str parse_url passthru pathinfo print readlink realpath rewind rewinddir rmdir '+ - 'round str_ireplace str_pad str_repeat str_replace str_rot13 str_shuffle str_split '+ - 'str_word_count strcasecmp strchr strcmp strcoll strcspn strftime strip_tags stripcslashes '+ - 'stripos stripslashes stristr strlen strnatcasecmp strnatcmp strncasecmp strncmp strpbrk '+ - 'strpos strptime strrchr strrev strripos strrpos strspn strstr strtok strtolower strtotime '+ - 'strtoupper strtr strval substr substr_compare'; - - var keywords = 'abstract and array as break case catch cfunction class clone const continue declare default die do ' + - 'else elseif enddeclare endfor endforeach endif endswitch endwhile extends final for foreach ' + - 'function include include_once global goto if implements interface instanceof namespace new ' + - 'old_function or private protected public return require require_once static switch ' + - 'throw try use var while xor '; - - var constants = '__FILE__ __LINE__ __METHOD__ __FUNCTION__ __CLASS__'; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments - { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings - { regex: /\$\w+/g, css: 'variable' }, // variables - { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // common functions - { regex: new RegExp(this.getKeywords(constants), 'gmi'), css: 'constants' }, // constants - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keyword - ]; - - this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['php']; - - SyntaxHighlighter.brushes.Php = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushPlain.js b/assets/javascripts/syntaxhighlighter/shBrushPlain.js deleted file mode 100644 index 9f7d9e9..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushPlain.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['text', 'plain']; - - SyntaxHighlighter.brushes.Plain = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushPowerShell.js b/assets/javascripts/syntaxhighlighter/shBrushPowerShell.js deleted file mode 100644 index 0be1752..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushPowerShell.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributes by B.v.Zanten, Getronics - // http://confluence.atlassian.com/display/CONFEXT/New+Code+Macro - - var keywords = 'Add-Content Add-History Add-Member Add-PSSnapin Clear(-Content)? Clear-Item ' + - 'Clear-ItemProperty Clear-Variable Compare-Object ConvertFrom-SecureString Convert-Path ' + - 'ConvertTo-Html ConvertTo-SecureString Copy(-Item)? Copy-ItemProperty Export-Alias ' + - 'Export-Clixml Export-Console Export-Csv ForEach(-Object)? Format-Custom Format-List ' + - 'Format-Table Format-Wide Get-Acl Get-Alias Get-AuthenticodeSignature Get-ChildItem Get-Command ' + - 'Get-Content Get-Credential Get-Culture Get-Date Get-EventLog Get-ExecutionPolicy ' + - 'Get-Help Get-History Get-Host Get-Item Get-ItemProperty Get-Location Get-Member ' + - 'Get-PfxCertificate Get-Process Get-PSDrive Get-PSProvider Get-PSSnapin Get-Service ' + - 'Get-TraceSource Get-UICulture Get-Unique Get-Variable Get-WmiObject Group-Object ' + - 'Import-Alias Import-Clixml Import-Csv Invoke-Expression Invoke-History Invoke-Item ' + - 'Join-Path Measure-Command Measure-Object Move(-Item)? Move-ItemProperty New-Alias ' + - 'New-Item New-ItemProperty New-Object New-PSDrive New-Service New-TimeSpan ' + - 'New-Variable Out-Default Out-File Out-Host Out-Null Out-Printer Out-String Pop-Location ' + - 'Push-Location Read-Host Remove-Item Remove-ItemProperty Remove-PSDrive Remove-PSSnapin ' + - 'Remove-Variable Rename-Item Rename-ItemProperty Resolve-Path Restart-Service Resume-Service ' + - 'Select-Object Select-String Set-Acl Set-Alias Set-AuthenticodeSignature Set-Content ' + - 'Set-Date Set-ExecutionPolicy Set-Item Set-ItemProperty Set-Location Set-PSDebug ' + - 'Set-Service Set-TraceSource Set(-Variable)? Sort-Object Split-Path Start-Service ' + - 'Start-Sleep Start-Transcript Stop-Process Stop-Service Stop-Transcript Suspend-Service ' + - 'Tee-Object Test-Path Trace-Command Update-FormatData Update-TypeData Where(-Object)? ' + - 'Write-Debug Write-Error Write(-Host)? Write-Output Write-Progress Write-Verbose Write-Warning'; - var alias = 'ac asnp clc cli clp clv cpi cpp cvpa diff epal epcsv fc fl ' + - 'ft fw gal gc gci gcm gdr ghy gi gl gm gp gps group gsv ' + - 'gsnp gu gv gwmi iex ihy ii ipal ipcsv mi mp nal ndr ni nv oh rdr ' + - 'ri rni rnp rp rsnp rv rvpa sal sasv sc select si sl sleep sort sp ' + - 'spps spsv sv tee cat cd cp h history kill lp ls ' + - 'mount mv popd ps pushd pwd r rm rmdir echo cls chdir del dir ' + - 'erase rd ren type % \\?'; - - this.regexList = [ - { regex: /#.*$/gm, css: 'comments' }, // one line comments - { regex: /\$[a-zA-Z0-9]+\b/g, css: 'value' }, // variables $Computer1 - { regex: /\-[a-zA-Z]+\b/g, css: 'keyword' }, // Operators -not -and -eq - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings - { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' }, - { regex: new RegExp(this.getKeywords(alias), 'gmi'), css: 'keyword' } - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['powershell', 'ps']; - - SyntaxHighlighter.brushes.PowerShell = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushPython.js b/assets/javascripts/syntaxhighlighter/shBrushPython.js deleted file mode 100644 index ce77462..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushPython.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributed by Gheorghe Milas and Ahmad Sherif - - var keywords = 'and assert break class continue def del elif else ' + - 'except exec finally for from global if import in is ' + - 'lambda not or pass print raise return try yield while'; - - var funcs = '__import__ abs all any apply basestring bin bool buffer callable ' + - 'chr classmethod cmp coerce compile complex delattr dict dir ' + - 'divmod enumerate eval execfile file filter float format frozenset ' + - 'getattr globals hasattr hash help hex id input int intern ' + - 'isinstance issubclass iter len list locals long map max min next ' + - 'object oct open ord pow print property range raw_input reduce ' + - 'reload repr reversed round set setattr slice sorted staticmethod ' + - 'str sum super tuple type type unichr unicode vars xrange zip'; - - var special = 'None True False self cls class_'; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, - { regex: /^\s*@\w+/gm, css: 'decorator' }, - { regex: /(['\"]{3})([^\1])*?\1/gm, css: 'comments' }, - { regex: /"(?!")(?:\.|\\\"|[^\""\n])*"/gm, css: 'string' }, - { regex: /'(?!')(?:\.|(\\\')|[^\''\n])*'/gm, css: 'string' }, - { regex: /\+|\-|\*|\/|\%|=|==/gm, css: 'keyword' }, - { regex: /\b\d+\.?\w*/g, css: 'value' }, - { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, - { regex: new RegExp(this.getKeywords(special), 'gm'), css: 'color1' } - ]; - - this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['py', 'python']; - - SyntaxHighlighter.brushes.Python = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushRuby.js b/assets/javascripts/syntaxhighlighter/shBrushRuby.js deleted file mode 100644 index ff82130..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushRuby.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributed by Erik Peterson. - - var keywords = 'alias and BEGIN begin break case class def define_method defined do each else elsif ' + - 'END end ensure false for if in module new next nil not or raise redo rescue retry return ' + - 'self super then throw true undef unless until when while yield'; - - var builtins = 'Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload ' + - 'Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol ' + - 'ThreadGroup Thread Time TrueClass'; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, // one line comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings - { regex: /\b[A-Z0-9_]+\b/g, css: 'constants' }, // constants - { regex: /:[a-z][A-Za-z0-9_]*/g, css: 'color2' }, // symbols - { regex: /(\$|@@|@)\w+/g, css: 'variable bold' }, // $global, @instance, and @@class variables - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords - { regex: new RegExp(this.getKeywords(builtins), 'gm'), css: 'color1' } // builtins - ]; - - this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['ruby', 'rails', 'ror', 'rb']; - - SyntaxHighlighter.brushes.Ruby = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushSass.js b/assets/javascripts/syntaxhighlighter/shBrushSass.js deleted file mode 100644 index aa04da0..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushSass.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - function getKeywordsCSS(str) - { - return '\\b([a-z_]|)' + str.replace(/ /g, '(?=:)\\b|\\b([a-z_\\*]|\\*|)') + '(?=:)\\b'; - }; - - function getValuesCSS(str) - { - return '\\b' + str.replace(/ /g, '(?!-)(?!:)\\b|\\b()') + '\:\\b'; - }; - - var keywords = 'ascent azimuth background-attachment background-color background-image background-position ' + - 'background-repeat background baseline bbox border-collapse border-color border-spacing border-style border-top ' + - 'border-right border-bottom border-left border-top-color border-right-color border-bottom-color border-left-color ' + - 'border-top-style border-right-style border-bottom-style border-left-style border-top-width border-right-width ' + - 'border-bottom-width border-left-width border-width border bottom cap-height caption-side centerline clear clip color ' + - 'content counter-increment counter-reset cue-after cue-before cue cursor definition-src descent direction display ' + - 'elevation empty-cells float font-size-adjust font-family font-size font-stretch font-style font-variant font-weight font ' + - 'height left letter-spacing line-height list-style-image list-style-position list-style-type list-style margin-top ' + - 'margin-right margin-bottom margin-left margin marker-offset marks mathline max-height max-width min-height min-width orphans ' + - 'outline-color outline-style outline-width outline overflow padding-top padding-right padding-bottom padding-left padding page ' + - 'page-break-after page-break-before page-break-inside pause pause-after pause-before pitch pitch-range play-during position ' + - 'quotes right richness size slope src speak-header speak-numeral speak-punctuation speak speech-rate stemh stemv stress ' + - 'table-layout text-align top text-decoration text-indent text-shadow text-transform unicode-bidi unicode-range units-per-em ' + - 'vertical-align visibility voice-family volume white-space widows width widths word-spacing x-height z-index'; - - var values = 'above absolute all always aqua armenian attr aural auto avoid baseline behind below bidi-override black blink block blue bold bolder '+ - 'both bottom braille capitalize caption center center-left center-right circle close-quote code collapse compact condensed '+ - 'continuous counter counters crop cross crosshair cursive dashed decimal decimal-leading-zero digits disc dotted double '+ - 'embed embossed e-resize expanded extra-condensed extra-expanded fantasy far-left far-right fast faster fixed format fuchsia '+ - 'gray green groove handheld hebrew help hidden hide high higher icon inline-table inline inset inside invert italic '+ - 'justify landscape large larger left-side left leftwards level lighter lime line-through list-item local loud lower-alpha '+ - 'lowercase lower-greek lower-latin lower-roman lower low ltr marker maroon medium message-box middle mix move narrower '+ - 'navy ne-resize no-close-quote none no-open-quote no-repeat normal nowrap n-resize nw-resize oblique olive once open-quote outset '+ - 'outside overline pointer portrait pre print projection purple red relative repeat repeat-x repeat-y rgb ridge right right-side '+ - 'rightwards rtl run-in screen scroll semi-condensed semi-expanded separate se-resize show silent silver slower slow '+ - 'small small-caps small-caption smaller soft solid speech spell-out square s-resize static status-bar sub super sw-resize '+ - 'table-caption table-cell table-column table-column-group table-footer-group table-header-group table-row table-row-group teal '+ - 'text-bottom text-top thick thin top transparent tty tv ultra-condensed ultra-expanded underline upper-alpha uppercase upper-latin '+ - 'upper-roman url visible wait white wider w-resize x-fast x-high x-large x-loud x-low x-slow x-small x-soft xx-large xx-small yellow'; - - var fonts = '[mM]onospace [tT]ahoma [vV]erdana [aA]rial [hH]elvetica [sS]ans-serif [sS]erif [cC]ourier mono sans serif'; - - var statements = '!important !default'; - var preprocessor = '@import @extend @debug @warn @if @for @while @mixin @include'; - - var r = SyntaxHighlighter.regexLib; - - this.regexList = [ - { regex: r.multiLineCComments, css: 'comments' }, // multiline comments - { regex: r.singleLineCComments, css: 'comments' }, // singleline comments - { regex: r.doubleQuotedString, css: 'string' }, // double quoted strings - { regex: r.singleQuotedString, css: 'string' }, // single quoted strings - { regex: /\#[a-fA-F0-9]{3,6}/g, css: 'value' }, // html colors - { regex: /\b(-?\d+)(\.\d+)?(px|em|pt|\:|\%|)\b/g, css: 'value' }, // sizes - { regex: /\$\w+/g, css: 'variable' }, // variables - { regex: new RegExp(this.getKeywords(statements), 'g'), css: 'color3' }, // statements - { regex: new RegExp(this.getKeywords(preprocessor), 'g'), css: 'preprocessor' }, // preprocessor - { regex: new RegExp(getKeywordsCSS(keywords), 'gm'), css: 'keyword' }, // keywords - { regex: new RegExp(getValuesCSS(values), 'g'), css: 'value' }, // values - { regex: new RegExp(this.getKeywords(fonts), 'g'), css: 'color1' } // fonts - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['sass', 'scss']; - - SyntaxHighlighter.brushes.Sass = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushScala.js b/assets/javascripts/syntaxhighlighter/shBrushScala.js deleted file mode 100644 index 4b0b6f0..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushScala.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - // Contributed by Yegor Jbanov and David Bernard. - - var keywords = 'val sealed case def true trait implicit forSome import match object null finally super ' + - 'override try lazy for var catch throw type extends class while with new final yield abstract ' + - 'else do if return protected private this package false'; - - var keyops = '[_:=><%#@]+'; - - this.regexList = [ - { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments - { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments - { regex: SyntaxHighlighter.regexLib.multiLineSingleQuotedString, css: 'string' }, // multi-line strings - { regex: SyntaxHighlighter.regexLib.multiLineDoubleQuotedString, css: 'string' }, // double-quoted string - { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // strings - { regex: /0x[a-f0-9]+|\d+(\.\d+)?/gi, css: 'value' }, // numbers - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, // keywords - { regex: new RegExp(keyops, 'gm'), css: 'keyword' } // scala keyword - ]; - } - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['scala']; - - SyntaxHighlighter.brushes.Scala = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushSql.js b/assets/javascripts/syntaxhighlighter/shBrushSql.js deleted file mode 100644 index 5c2cd88..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushSql.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - var funcs = 'abs avg case cast coalesce convert count current_timestamp ' + - 'current_user day isnull left lower month nullif replace right ' + - 'session_user space substring sum system_user upper user year'; - - var keywords = 'absolute action add after alter as asc at authorization begin bigint ' + - 'binary bit by cascade char character check checkpoint close collate ' + - 'column commit committed connect connection constraint contains continue ' + - 'create cube current current_date current_time cursor database date ' + - 'deallocate dec decimal declare default delete desc distinct double drop ' + - 'dynamic else end end-exec escape except exec execute false fetch first ' + - 'float for force foreign forward free from full function global goto grant ' + - 'group grouping having hour ignore index inner insensitive insert instead ' + - 'int integer intersect into is isolation key last level load local max min ' + - 'minute modify move name national nchar next no numeric of off on only ' + - 'open option order out output partial password precision prepare primary ' + - 'prior privileges procedure public read real references relative repeatable ' + - 'restrict return returns revoke rollback rollup rows rule schema scroll ' + - 'second section select sequence serializable set size smallint static ' + - 'statistics table temp temporary then time timestamp to top transaction ' + - 'translation trigger true truncate uncommitted union unique update values ' + - 'varchar varying view when where with work'; - - var operators = 'all and any between cross in join like not null or outer some'; - - this.regexList = [ - { regex: /--(.*)$/gm, css: 'comments' }, // one line and multiline comments - { regex: SyntaxHighlighter.regexLib.multiLineDoubleQuotedString, css: 'string' }, // double quoted strings - { regex: SyntaxHighlighter.regexLib.multiLineSingleQuotedString, css: 'string' }, // single quoted strings - { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'color2' }, // functions - { regex: new RegExp(this.getKeywords(operators), 'gmi'), css: 'color1' }, // operators and such - { regex: new RegExp(this.getKeywords(keywords), 'gmi'), css: 'keyword' } // keyword - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['sql']; - - SyntaxHighlighter.brushes.Sql = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); - diff --git a/assets/javascripts/syntaxhighlighter/shBrushVb.js b/assets/javascripts/syntaxhighlighter/shBrushVb.js deleted file mode 100644 index be845dc..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushVb.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - var keywords = 'AddHandler AddressOf AndAlso Alias And Ansi As Assembly Auto ' + - 'Boolean ByRef Byte ByVal Call Case Catch CBool CByte CChar CDate ' + - 'CDec CDbl Char CInt Class CLng CObj Const CShort CSng CStr CType ' + - 'Date Decimal Declare Default Delegate Dim DirectCast Do Double Each ' + - 'Else ElseIf End Enum Erase Error Event Exit False Finally For Friend ' + - 'Function Get GetType GoSub GoTo Handles If Implements Imports In ' + - 'Inherits Integer Interface Is Let Lib Like Long Loop Me Mod Module ' + - 'MustInherit MustOverride MyBase MyClass Namespace New Next Not Nothing ' + - 'NotInheritable NotOverridable Object On Option Optional Or OrElse ' + - 'Overloads Overridable Overrides ParamArray Preserve Private Property ' + - 'Protected Public RaiseEvent ReadOnly ReDim REM RemoveHandler Resume ' + - 'Return Select Set Shadows Shared Short Single Static Step Stop String ' + - 'Structure Sub SyncLock Then Throw To True Try TypeOf Unicode Until ' + - 'Variant When While With WithEvents WriteOnly Xor'; - - this.regexList = [ - { regex: /'.*$/gm, css: 'comments' }, // one line comments - { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // strings - { regex: /^\s*#.*$/gm, css: 'preprocessor' }, // preprocessor tags like #region and #endregion - { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // vb keyword - ]; - - this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['vb', 'vbnet']; - - SyntaxHighlighter.brushes.Vb = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shBrushXml.js b/assets/javascripts/syntaxhighlighter/shBrushXml.js deleted file mode 100644 index 69d9fd0..0000000 --- a/assets/javascripts/syntaxhighlighter/shBrushXml.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -;(function() -{ - // CommonJS - typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; - - function Brush() - { - function process(match, regexInfo) - { - var constructor = SyntaxHighlighter.Match, - code = match[0], - tag = new XRegExp('(<|<)[\\s\\/\\?]*(?[:\\w-\\.]+)', 'xg').exec(code), - result = [] - ; - - if (match.attributes != null) - { - var attributes, - regex = new XRegExp('(? [\\w:\\-\\.]+)' + - '\\s*=\\s*' + - '(? ".*?"|\'.*?\'|\\w+)', - 'xg'); - - while ((attributes = regex.exec(code)) != null) - { - result.push(new constructor(attributes.name, match.index + attributes.index, 'color1')); - result.push(new constructor(attributes.value, match.index + attributes.index + attributes[0].indexOf(attributes.value), 'string')); - } - } - - if (tag != null) - result.push( - new constructor(tag.name, match.index + tag[0].indexOf(tag.name), 'keyword') - ); - - return result; - } - - this.regexList = [ - { regex: new XRegExp('(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)', 'gm'), css: 'color2' }, // - { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // - { regex: new XRegExp('(<|<)[\\s\\/\\?]*(\\w+)(?.*?)[\\s\\/\\?]*(>|>)', 'sg'), func: process } - ]; - }; - - Brush.prototype = new SyntaxHighlighter.Highlighter(); - Brush.aliases = ['xml', 'xhtml', 'xslt', 'html']; - - SyntaxHighlighter.brushes.Xml = Brush; - - // CommonJS - typeof(exports) != 'undefined' ? exports.Brush = Brush : null; -})(); diff --git a/assets/javascripts/syntaxhighlighter/shCore.js b/assets/javascripts/syntaxhighlighter/shCore.js deleted file mode 100644 index b47b645..0000000 --- a/assets/javascripts/syntaxhighlighter/shCore.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('K M;I(M)1S 2U("2a\'t 4k M 4K 2g 3l 4G 4H");(6(){6 r(f,e){I(!M.1R(f))1S 3m("3s 15 4R");K a=f.1w;f=M(f.1m,t(f)+(e||""));I(a)f.1w={1m:a.1m,19:a.19?a.19.1a(0):N};H f}6 t(f){H(f.1J?"g":"")+(f.4s?"i":"")+(f.4p?"m":"")+(f.4v?"x":"")+(f.3n?"y":"")}6 B(f,e,a,b){K c=u.L,d,h,g;v=R;5K{O(;c--;){g=u[c];I(a&g.3r&&(!g.2p||g.2p.W(b))){g.2q.12=e;I((h=g.2q.X(f))&&h.P===e){d={3k:g.2b.W(b,h,a),1C:h};1N}}}}5v(i){1S i}5q{v=11}H d}6 p(f,e,a){I(3b.Z.1i)H f.1i(e,a);O(a=a||0;a-1},3d:6(g){e+=g}};c1&&p(e,"")>-1){a=15(J.1m,n.Q.W(t(J),"g",""));n.Q.W(f.1a(e.P),a,6(){O(K c=1;c<14.L-2;c++)I(14[c]===1d)e[c]=1d})}I(J.1w&&J.1w.19)O(K b=1;be.P&&J.12--}H e};I(!D)15.Z.1A=6(f){(f=n.X.W(J,f))&&J.1J&&!f[0].L&&J.12>f.P&&J.12--;H!!f};1r.Z.1C=6(f){M.1R(f)||(f=15(f));I(f.1J){K e=n.1C.1p(J,14);f.12=0;H e}H f.X(J)};1r.Z.Q=6(f,e){K a=M.1R(f),b,c;I(a&&1j e.58()==="3f"&&e.1i("${")===-1&&y)H n.Q.1p(J,14);I(a){I(f.1w)b=f.1w.19}Y f+="";I(1j e==="6")c=n.Q.W(J,f,6(){I(b){14[0]=1f 1r(14[0]);O(K d=0;dd.L-3;){i=1r.Z.1a.W(g,-1)+i;g=1Q.3i(g/10)}H(g?d[g]||"":"$")+i}Y{g=+i;I(g<=d.L-3)H d[g];g=b?p(b,i):-1;H g>-1?d[g+1]:h}})})}I(a&&f.1J)f.12=0;H c};1r.Z.1e=6(f,e){I(!M.1R(f))H n.1e.1p(J,14);K a=J+"",b=[],c=0,d,h;I(e===1d||+e<0)e=5D;Y{e=1Q.3i(+e);I(!e)H[]}O(f=M.3c(f);d=f.X(a);){I(f.12>c){b.U(a.1a(c,d.P));d.L>1&&d.P=e)1N}f.12===d.P&&f.12++}I(c===a.L){I(!n.1A.W(f,"")||h)b.U("")}Y b.U(a.1a(c));H b.L>e?b.1a(0,e):b};M.1h(/\\(\\?#[^)]*\\)/,6(f){H n.1A.W(A,f.2S.1a(f.P+f[0].L))?"":"(?:)"});M.1h(/\\((?!\\?)/,6(){J.19.U(N);H"("});M.1h(/\\(\\?<([$\\w]+)>/,6(f){J.19.U(f[1]);J.2N=R;H"("});M.1h(/\\\\k<([\\w$]+)>/,6(f){K e=p(J.19,f[1]);H e>-1?"\\\\"+(e+1)+(3R(f.2S.3a(f.P+f[0].L))?"":"(?:)"):f[0]});M.1h(/\\[\\^?]/,6(f){H f[0]==="[]"?"\\\\b\\\\B":"[\\\\s\\\\S]"});M.1h(/^\\(\\?([5A]+)\\)/,6(f){J.3d(f[1]);H""});M.1h(/(?:\\s+|#.*)+/,6(f){H n.1A.W(A,f.2S.1a(f.P+f[0].L))?"":"(?:)"},M.1B,6(){H J.2K("x")});M.1h(/\\./,6(){H"[\\\\s\\\\S]"},M.1B,6(){H J.2K("s")})})();1j 2e!="1d"&&(2e.M=M);K 1v=6(){6 r(a,b){a.1l.1i(b)!=-1||(a.1l+=" "+b)}6 t(a){H a.1i("3e")==0?a:"3e"+a}6 B(a){H e.1Y.2A[t(a)]}6 p(a,b,c){I(a==N)H N;K d=c!=R?a.3G:[a.2G],h={"#":"1c",".":"1l"}[b.1o(0,1)]||"3h",g,i;g=h!="3h"?b.1o(1):b.5u();I((a[h]||"").1i(g)!=-1)H a;O(a=0;d&&a\'+c+""});H a}6 n(a,b){a.1e("\\n");O(K c="",d=0;d<50;d++)c+=" ";H a=v(a,6(h){I(h.1i("\\t")==-1)H h;O(K g=0;(g=h.1i("\\t"))!=-1;)h=h.1o(0,g)+c.1o(0,b-g%b)+h.1o(g+1,h.L);H h})}6 x(a){H a.Q(/^\\s+|\\s+$/g,"")}6 D(a,b){I(a.Pb.P)H 1;Y I(a.Lb.L)H 1;H 0}6 y(a,b){6 c(k){H k[0]}O(K d=N,h=[],g=b.2D?b.2D:c;(d=b.1I.X(a))!=N;){K i=g(d,b);I(1j i=="3f")i=[1f e.2L(i,d.P,b.23)];h=h.1O(i)}H h}6 E(a){K b=/(.*)((&1G;|&1y;).*)/;H a.Q(e.3A.3M,6(c){K d="",h=N;I(h=b.X(c)){c=h[1];d=h[2]}H\'\'+c+""+d})}6 z(){O(K a=1E.36("1k"),b=[],c=0;c<1z 4I="1Z://2y.3L.3K/4L/5L"><3J><4N 1Z-4M="5G-5M" 6K="2O/1z; 6J=6I-8" /><1t>6L 1v<3B 1L="25-6M:6Q,6P,6O,6N-6F;6y-2f:#6x;2f:#6w;25-22:6v;2O-3D:3C;">1v3v 3.0.76 (72 73 3x)1Z://3u.2w/1v70 17 6U 71.6T 6X-3x 6Y 6D.6t 61 60 J 1k, 5Z 5R 5V <2R/>5U 5T 5S!\'}},1Y:{2j:N,2A:{}},1U:{},3A:{6n:/\\/\\*[\\s\\S]*?\\*\\//2c,6m:/\\/\\/.*$/2c,6l:/#.*$/2c,6k:/"([^\\\\"\\n]|\\\\.)*"/g,6o:/\'([^\\\\\'\\n]|\\\\.)*\'/g,6p:1f M(\'"([^\\\\\\\\"]|\\\\\\\\.)*"\',"3z"),6s:1f M("\'([^\\\\\\\\\']|\\\\\\\\.)*\'","3z"),6q:/(&1y;|<)!--[\\s\\S]*?--(&1G;|>)/2c,3M:/\\w+:\\/\\/[\\w-.\\/?%&=:@;]*/g,6a:{18:/(&1y;|<)\\?=?/g,1b:/\\?(&1G;|>)/g},69:{18:/(&1y;|<)%=?/g,1b:/%(&1G;|>)/g},6d:{18:/(&1y;|<)\\s*1k.*?(&1G;|>)/2T,1b:/(&1y;|<)\\/\\s*1k\\s*(&1G;|>)/2T}},16:{1H:6(a){6 b(i,k){H e.16.2o(i,k,e.13.1x[k])}O(K c=\'\',d=e.16.2x,h=d.2X,g=0;g";H c},2o:6(a,b,c){H\'<2W>\'+c+""},2b:6(a){K b=a.1F,c=b.1l||"";b=B(p(b,".20",R).1c);K d=6(h){H(h=15(h+"6f(\\\\w+)").X(c))?h[1]:N}("6g");b&&d&&e.16.2x[d].2B(b);a.3N()},2x:{2X:["21","2P"],21:{1H:6(a){I(a.V("2l")!=R)H"";K b=a.V("1t");H e.16.2o(a,"21",b?b:e.13.1x.21)},2B:6(a){a=1E.6j(t(a.1c));a.1l=a.1l.Q("47","")}},2P:{2B:6(){K a="68=0";a+=", 18="+(31.30-33)/2+", 32="+(31.2Z-2Y)/2+", 30=33, 2Z=2Y";a=a.Q(/^,/,"");a=1P.6Z("","38",a);a.2C();K b=a.1E;b.6W(e.13.1x.37);b.6V();a.2C()}}}},35:6(a,b){K c;I(b)c=[b];Y{c=1E.36(e.13.34);O(K d=[],h=0;h(.*?))\\\\]$"),s=1f M("(?<27>[\\\\w-]+)\\\\s*:\\\\s*(?<1T>[\\\\w-%#]+|\\\\[.*?\\\\]|\\".*?\\"|\'.*?\')\\\\s*;?","g");(j=s.X(k))!=N;){K o=j.1T.Q(/^[\'"]|[\'"]$/g,"");I(o!=N&&m.1A(o)){o=m.X(o);o=o.2V.L>0?o.2V.1e(/\\s*,\\s*/):[]}l[j.27]=o}g={1F:g,1n:C(i,l)};g.1n.1D!=N&&d.U(g)}H d},1M:6(a,b){K c=J.35(a,b),d=N,h=e.13;I(c.L!==0)O(K g=0;g")==o-3){m=m.4h(0,o-3);s=R}l=s?m:l}I((i.1t||"")!="")k.1t=i.1t;k.1D=j;d.2Q(k);b=d.2F(l);I((i.1c||"")!="")b.1c=i.1c;i.2G.74(b,i)}}},2E:6(a){w(1P,"4k",6(){e.1M(a)})}};e.2E=e.2E;e.1M=e.1M;e.2L=6(a,b,c){J.1T=a;J.P=b;J.L=a.L;J.23=c;J.1V=N};e.2L.Z.1q=6(){H J.1T};e.4l=6(a){6 b(j,l){O(K m=0;md)1N;Y I(g.P==c.P&&g.L>c.L)a[b]=N;Y I(g.P>=c.P&&g.P\'+c+""},3Q:6(a,b){K c="",d=a.1e("\\n").L,h=2u(J.V("2i-1s")),g=J.V("2z-1s-2t");I(g==R)g=(h+d-1).1q().L;Y I(3R(g)==R)g=0;O(K i=0;i\'+j+"":"")+i)}H a},4f:6(a){H a?"<4a>"+a+"":""},4b:6(a,b){6 c(l){H(l=l?l.1V||g:g)?l+" ":""}O(K d=0,h="",g=J.V("1D",""),i=0;i|&1y;2R\\s*\\/?&1G;/2T;I(e.13.46==R)b=b.Q(h,"\\n");I(e.13.44==R)b=b.Q(h,"");b=b.1e("\\n");h=/^\\s*/;g=4Q;O(K i=0;i0;i++){K k=b[i];I(x(k).L!=0){k=h.X(k);I(k==N){a=a;1N a}g=1Q.4q(k[0].L,g)}}I(g>0)O(i=0;i\'+(J.V("16")?e.16.1H(J):"")+\'<3Z 5z="0" 5H="0" 5J="0">\'+J.4f(J.V("1t"))+"<3T><3P>"+(1u?\'<2d 1g="1u">\'+J.3Q(a)+"":"")+\'<2d 1g="17">\'+b+""},2F:6(a){I(a===N)a="";J.17=a;K b=J.3Y("T");b.3X=J.1H(a);J.V("16")&&w(p(b,".16"),"5c",e.16.2b);J.V("3V-17")&&w(p(b,".17"),"56",f);H b},2Q:6(a){J.1c=""+1Q.5d(1Q.5n()*5k).1q();e.1Y.2A[t(J.1c)]=J;J.1n=C(e.2v,a||{});I(J.V("2k")==R)J.1n.16=J.1n.1u=11},5j:6(a){a=a.Q(/^\\s+|\\s+$/g,"").Q(/\\s+/g,"|");H"\\\\b(?:"+a+")\\\\b"},5f:6(a){J.28={18:{1I:a.18,23:"1k"},1b:{1I:a.1b,23:"1k"},17:1f M("(?<18>"+a.18.1m+")(?<17>.*?)(?<1b>"+a.1b.1m+")","5o")}}};H e}();1j 2e!="1d"&&(2e.1v=1v);',62,441,'||||||function|||||||||||||||||||||||||||||||||||||return|if|this|var|length|XRegExp|null|for|index|replace|true||div|push|getParam|call|exec|else|prototype||false|lastIndex|config|arguments|RegExp|toolbar|code|left|captureNames|slice|right|id|undefined|split|new|class|addToken|indexOf|typeof|script|className|source|params|substr|apply|toString|String|line|title|gutter|SyntaxHighlighter|_xregexp|strings|lt|html|test|OUTSIDE_CLASS|match|brush|document|target|gt|getHtml|regex|global|join|style|highlight|break|concat|window|Math|isRegExp|throw|value|brushes|brushName|space|alert|vars|http|syntaxhighlighter|expandSource|size|css|case|font|Fa|name|htmlScript|dA|can|handler|gm|td|exports|color|in|href|first|discoveredBrushes|light|collapse|object|cache|getButtonHtml|trigger|pattern|getLineHtml|nbsp|numbers|parseInt|defaults|com|items|www|pad|highlighters|execute|focus|func|all|getDiv|parentNode|navigator|INSIDE_CLASS|regexList|hasFlag|Match|useScriptTags|hasNamedCapture|text|help|init|br|input|gi|Error|values|span|list|250|height|width|screen|top|500|tagName|findElements|getElementsByTagName|aboutDialog|_blank|appendChild|charAt|Array|copyAsGlobal|setFlag|highlighter_|string|attachEvent|nodeName|floor|backref|output|the|TypeError|sticky|Za|iterate|freezeTokens|scope|type|textarea|alexgorbatchev|version|margin|2010|005896|gs|regexLib|body|center|align|noBrush|require|childNodes|DTD|xhtml1|head|org|w3|url|preventDefault|container|tr|getLineNumbersHtml|isNaN|userAgent|tbody|isLineHighlighted|quick|void|innerHTML|create|table|links|auto|smart|tab|stripBrs|tabs|bloggerMode|collapsed|plain|getCodeLinesHtml|caption|getMatchesHtml|findMatches|figureOutLineNumbers|removeNestedMatches|getTitleHtml|brushNotHtmlScript|substring|createElement|Highlighter|load|HtmlScript|Brush|pre|expand|multiline|min|Can|ignoreCase|find|blur|extended|toLowerCase|aliases|addEventListener|innerText|textContent|wasn|select|createTextNode|removeChild|option|same|frame|xmlns|dtd|twice|1999|equiv|meta|htmlscript|transitional|1E3|expected|PUBLIC|DOCTYPE|on|W3C|XHTML|TR|EN|Transitional||configured|srcElement|Object|after|run|dblclick|matchChain|valueOf|constructor|default|switch|click|round|execAt|forHtmlScript|token|gimy|functions|getKeywords|1E6|escape|within|random|sgi|another|finally|supply|MSIE|ie|toUpperCase|catch|returnValue|definition|event|border|imsx|constructing|one|Infinity|from|when|Content|cellpadding|flags|cellspacing|try|xhtml|Type|spaces|2930402|hosted_button_id|lastIndexOf|donate|active|development|keep|to|xclick|_s|Xml|please|like|you|paypal|cgi|cmd|webscr|bin|highlighted|scrollbars|aspScriptTags|phpScriptTags|sort|max|scriptScriptTags|toolbar_item|_|command|command_|number|getElementById|doubleQuotedString|singleLinePerlComments|singleLineCComments|multiLineCComments|singleQuotedString|multiLineDoubleQuotedString|xmlComments|alt|multiLineSingleQuotedString|If|https|1em|000|fff|background|5em|xx|bottom|75em|Gorbatchev|large|serif|CDATA|continue|utf|charset|content|About|family|sans|Helvetica|Arial|Geneva|3em|nogutter|Copyright|syntax|close|write|2004|Alex|open|JavaScript|highlighter|July|02|replaceChild|offset|83'.split('|'),0,{})) diff --git a/assets/stylesheets/main.css b/assets/stylesheets/main.css index 46bfbfd..ed558e4 100644 --- a/assets/stylesheets/main.css +++ b/assets/stylesheets/main.css @@ -34,7 +34,7 @@ pre, code { overflow: auto; color: #222; } -pre,tt,code,.note>p { +pre, tt, code { white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ @@ -384,7 +384,7 @@ a, a:link, a:visited { background-position: 96% 16px; -webkit-appearance: none; } - .guides-index-small .guides-index-item:hover{ + .guides-index-small .guides-index-item:hover{ background-position: 96% -65px; } } diff --git a/assets/stylesheets/responsive-tables.css b/assets/stylesheets/responsive-tables.css index 9ecb15f..f5fbcbf 100755 --- a/assets/stylesheets/responsive-tables.css +++ b/assets/stylesheets/responsive-tables.css @@ -1,7 +1,7 @@ /* Foundation v2.1.4 http://foundation.zurb.com */ /* Artfully masterminded by ZURB */ -/* -------------------------------------------------- +/* -------------------------------------------------- Table of Contents ----------------------------------------------------- :: Shared Styles @@ -19,21 +19,21 @@ table td, table th { padding: 9px 10px; text-align: left; } /* Mobile */ @media only screen and (max-width: 767px) { - + table { margin-bottom: 0; } - + .pinned { position: absolute; left: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-right: 1px solid #ccc; border-left: 1px solid #ccc; } .pinned table { border-right: none; border-left: none; width: 100%; } .pinned table th, .pinned table td { white-space: nowrap; } .pinned td:last-child { border-bottom: 0; } - + div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-right: 1px solid #ccc; } div.table-wrapper div.scrollable table { margin-left: 35%; } - div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; } - + div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; } + table td, table th { position: relative; white-space: nowrap; overflow: hidden; } table th:first-child, table td:first-child, table td:first-child, table.pinned td { display: none; } - + } /* ----------------------------------------- diff --git a/assets/stylesheets/syntaxhighlighter/shCoreDefault.css b/assets/stylesheets/syntaxhighlighter/shCoreDefault.css deleted file mode 100644 index 08f9e10..0000000 --- a/assets/stylesheets/syntaxhighlighter/shCoreDefault.css +++ /dev/null @@ -1,328 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter a, -.syntaxhighlighter div, -.syntaxhighlighter code, -.syntaxhighlighter table, -.syntaxhighlighter table td, -.syntaxhighlighter table tr, -.syntaxhighlighter table tbody, -.syntaxhighlighter table thead, -.syntaxhighlighter table caption, -.syntaxhighlighter textarea { - -moz-border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - background: none !important; - border: 0 !important; - bottom: auto !important; - float: none !important; - height: auto !important; - left: auto !important; - line-height: 1.1em !important; - margin: 0 !important; - outline: 0 !important; - overflow: visible !important; - padding: 0 !important; - position: static !important; - right: auto !important; - text-align: left !important; - top: auto !important; - vertical-align: baseline !important; - width: auto !important; - box-sizing: content-box !important; - font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; - font-weight: normal !important; - font-style: normal !important; - font-size: 1em !important; - min-height: inherit !important; - min-height: auto !important; -} - -.syntaxhighlighter { - width: 100% !important; - margin: 1em 0 1em 0 !important; - position: relative !important; - overflow: auto !important; - font-size: 1em !important; -} -.syntaxhighlighter.source { - overflow: hidden !important; -} -.syntaxhighlighter .bold { - font-weight: bold !important; -} -.syntaxhighlighter .italic { - font-style: italic !important; -} -.syntaxhighlighter .line { - white-space: pre !important; -} -.syntaxhighlighter table { - width: 100% !important; -} -.syntaxhighlighter table caption { - text-align: left !important; - padding: .5em 0 0.5em 1em !important; -} -.syntaxhighlighter table td.code { - width: 100% !important; -} -.syntaxhighlighter table td.code .container { - position: relative !important; -} -.syntaxhighlighter table td.code .container textarea { - box-sizing: border-box !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; - height: 100% !important; - border: none !important; - background: white !important; - padding-left: 1em !important; - overflow: hidden !important; - white-space: pre !important; -} -.syntaxhighlighter table td.gutter .line { - text-align: right !important; - padding: 0 0.5em 0 1em !important; -} -.syntaxhighlighter table td.code .line { - padding: 0 1em !important; -} -.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { - padding-left: 0em !important; -} -.syntaxhighlighter.show { - display: block !important; -} -.syntaxhighlighter.collapsed table { - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar { - padding: 0.1em 0.8em 0em 0.8em !important; - font-size: 1em !important; - position: static !important; - width: auto !important; - height: auto !important; -} -.syntaxhighlighter.collapsed .toolbar span { - display: inline !important; - margin-right: 1em !important; -} -.syntaxhighlighter.collapsed .toolbar span a { - padding: 0 !important; - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar span a.expandSource { - display: inline !important; -} -.syntaxhighlighter .toolbar { - position: absolute !important; - right: 1px !important; - top: 1px !important; - width: 11px !important; - height: 11px !important; - font-size: 10px !important; - z-index: 10 !important; -} -.syntaxhighlighter .toolbar span.title { - display: inline !important; -} -.syntaxhighlighter .toolbar a { - display: block !important; - text-align: center !important; - text-decoration: none !important; - padding-top: 1px !important; -} -.syntaxhighlighter .toolbar a.expandSource { - display: none !important; -} -.syntaxhighlighter.ie { - font-size: .9em !important; - padding: 1px 0 1px 0 !important; -} -.syntaxhighlighter.ie .toolbar { - line-height: 8px !important; -} -.syntaxhighlighter.ie .toolbar a { - padding-top: 0px !important; -} -.syntaxhighlighter.printing .line.alt1 .content, -.syntaxhighlighter.printing .line.alt2 .content, -.syntaxhighlighter.printing .line.highlighted .number, -.syntaxhighlighter.printing .line.highlighted.alt1 .content, -.syntaxhighlighter.printing .line.highlighted.alt2 .content { - background: none !important; -} -.syntaxhighlighter.printing .line .number { - color: #bbbbbb !important; -} -.syntaxhighlighter.printing .line .content { - color: black !important; -} -.syntaxhighlighter.printing .toolbar { - display: none !important; -} -.syntaxhighlighter.printing a { - text-decoration: none !important; -} -.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { - color: black !important; -} -.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { - color: #008200 !important; -} -.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { - color: blue !important; -} -.syntaxhighlighter.printing .keyword { - color: #006699 !important; - font-weight: bold !important; -} -.syntaxhighlighter.printing .preprocessor { - color: gray !important; -} -.syntaxhighlighter.printing .variable { - color: #aa7700 !important; -} -.syntaxhighlighter.printing .value { - color: #009900 !important; -} -.syntaxhighlighter.printing .functions { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .constants { - color: #0066cc !important; -} -.syntaxhighlighter.printing .script { - font-weight: bold !important; -} -.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { - color: gray !important; -} -.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { - color: red !important; -} -.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { - color: black !important; -} - -.syntaxhighlighter { - background-color: white !important; -} -.syntaxhighlighter .line.alt1 { - background-color: white !important; -} -.syntaxhighlighter .line.alt2 { - background-color: white !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #e0e0e0 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: black !important; -} -.syntaxhighlighter table caption { - color: black !important; -} -.syntaxhighlighter .gutter { - color: #afafaf !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #6ce26c !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #6ce26c !important; - color: white !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: blue !important; - background: white !important; - border: 1px solid #6ce26c !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: blue !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: red !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #6ce26c !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: black !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: black !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #008200 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: blue !important; -} -.syntaxhighlighter .keyword { - color: #006699 !important; -} -.syntaxhighlighter .preprocessor { - color: gray !important; -} -.syntaxhighlighter .variable { - color: #aa7700 !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #ff1493 !important; -} -.syntaxhighlighter .constants { - color: #0066cc !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #006699 !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: gray !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: red !important; -} - -.syntaxhighlighter .keyword { - font-weight: bold !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shCoreDjango.css b/assets/stylesheets/syntaxhighlighter/shCoreDjango.css deleted file mode 100644 index 1db1f70..0000000 --- a/assets/stylesheets/syntaxhighlighter/shCoreDjango.css +++ /dev/null @@ -1,331 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter a, -.syntaxhighlighter div, -.syntaxhighlighter code, -.syntaxhighlighter table, -.syntaxhighlighter table td, -.syntaxhighlighter table tr, -.syntaxhighlighter table tbody, -.syntaxhighlighter table thead, -.syntaxhighlighter table caption, -.syntaxhighlighter textarea { - -moz-border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - background: none !important; - border: 0 !important; - bottom: auto !important; - float: none !important; - height: auto !important; - left: auto !important; - line-height: 1.1em !important; - margin: 0 !important; - outline: 0 !important; - overflow: visible !important; - padding: 0 !important; - position: static !important; - right: auto !important; - text-align: left !important; - top: auto !important; - vertical-align: baseline !important; - width: auto !important; - box-sizing: content-box !important; - font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; - font-weight: normal !important; - font-style: normal !important; - font-size: 1em !important; - min-height: inherit !important; - min-height: auto !important; -} - -.syntaxhighlighter { - width: 100% !important; - margin: 1em 0 1em 0 !important; - position: relative !important; - overflow: auto !important; - font-size: 1em !important; -} -.syntaxhighlighter.source { - overflow: hidden !important; -} -.syntaxhighlighter .bold { - font-weight: bold !important; -} -.syntaxhighlighter .italic { - font-style: italic !important; -} -.syntaxhighlighter .line { - white-space: pre !important; -} -.syntaxhighlighter table { - width: 100% !important; -} -.syntaxhighlighter table caption { - text-align: left !important; - padding: .5em 0 0.5em 1em !important; -} -.syntaxhighlighter table td.code { - width: 100% !important; -} -.syntaxhighlighter table td.code .container { - position: relative !important; -} -.syntaxhighlighter table td.code .container textarea { - box-sizing: border-box !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; - height: 100% !important; - border: none !important; - background: white !important; - padding-left: 1em !important; - overflow: hidden !important; - white-space: pre !important; -} -.syntaxhighlighter table td.gutter .line { - text-align: right !important; - padding: 0 0.5em 0 1em !important; -} -.syntaxhighlighter table td.code .line { - padding: 0 1em !important; -} -.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { - padding-left: 0em !important; -} -.syntaxhighlighter.show { - display: block !important; -} -.syntaxhighlighter.collapsed table { - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar { - padding: 0.1em 0.8em 0em 0.8em !important; - font-size: 1em !important; - position: static !important; - width: auto !important; - height: auto !important; -} -.syntaxhighlighter.collapsed .toolbar span { - display: inline !important; - margin-right: 1em !important; -} -.syntaxhighlighter.collapsed .toolbar span a { - padding: 0 !important; - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar span a.expandSource { - display: inline !important; -} -.syntaxhighlighter .toolbar { - position: absolute !important; - right: 1px !important; - top: 1px !important; - width: 11px !important; - height: 11px !important; - font-size: 10px !important; - z-index: 10 !important; -} -.syntaxhighlighter .toolbar span.title { - display: inline !important; -} -.syntaxhighlighter .toolbar a { - display: block !important; - text-align: center !important; - text-decoration: none !important; - padding-top: 1px !important; -} -.syntaxhighlighter .toolbar a.expandSource { - display: none !important; -} -.syntaxhighlighter.ie { - font-size: .9em !important; - padding: 1px 0 1px 0 !important; -} -.syntaxhighlighter.ie .toolbar { - line-height: 8px !important; -} -.syntaxhighlighter.ie .toolbar a { - padding-top: 0px !important; -} -.syntaxhighlighter.printing .line.alt1 .content, -.syntaxhighlighter.printing .line.alt2 .content, -.syntaxhighlighter.printing .line.highlighted .number, -.syntaxhighlighter.printing .line.highlighted.alt1 .content, -.syntaxhighlighter.printing .line.highlighted.alt2 .content { - background: none !important; -} -.syntaxhighlighter.printing .line .number { - color: #bbbbbb !important; -} -.syntaxhighlighter.printing .line .content { - color: black !important; -} -.syntaxhighlighter.printing .toolbar { - display: none !important; -} -.syntaxhighlighter.printing a { - text-decoration: none !important; -} -.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { - color: black !important; -} -.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { - color: #008200 !important; -} -.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { - color: blue !important; -} -.syntaxhighlighter.printing .keyword { - color: #006699 !important; - font-weight: bold !important; -} -.syntaxhighlighter.printing .preprocessor { - color: gray !important; -} -.syntaxhighlighter.printing .variable { - color: #aa7700 !important; -} -.syntaxhighlighter.printing .value { - color: #009900 !important; -} -.syntaxhighlighter.printing .functions { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .constants { - color: #0066cc !important; -} -.syntaxhighlighter.printing .script { - font-weight: bold !important; -} -.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { - color: gray !important; -} -.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { - color: red !important; -} -.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { - color: black !important; -} - -.syntaxhighlighter { - background-color: #0a2b1d !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #0a2b1d !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #0a2b1d !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #233729 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: #f8f8f8 !important; -} -.syntaxhighlighter .gutter { - color: #497958 !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #41a83e !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #41a83e !important; - color: #0a2b1d !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #96dd3b !important; - background: black !important; - border: 1px solid #41a83e !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #96dd3b !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: white !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #41a83e !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #ffe862 !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #f8f8f8 !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #336442 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #9df39f !important; -} -.syntaxhighlighter .keyword { - color: #96dd3b !important; -} -.syntaxhighlighter .preprocessor { - color: #91bb9e !important; -} -.syntaxhighlighter .variable { - color: #ffaa3e !important; -} -.syntaxhighlighter .value { - color: #f7e741 !important; -} -.syntaxhighlighter .functions { - color: #ffaa3e !important; -} -.syntaxhighlighter .constants { - color: #e0e8ff !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #96dd3b !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #eb939a !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #91bb9e !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #edef7d !important; -} - -.syntaxhighlighter .comments { - font-style: italic !important; -} -.syntaxhighlighter .keyword { - font-weight: bold !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shCoreEclipse.css b/assets/stylesheets/syntaxhighlighter/shCoreEclipse.css deleted file mode 100644 index a45de9f..0000000 --- a/assets/stylesheets/syntaxhighlighter/shCoreEclipse.css +++ /dev/null @@ -1,339 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter a, -.syntaxhighlighter div, -.syntaxhighlighter code, -.syntaxhighlighter table, -.syntaxhighlighter table td, -.syntaxhighlighter table tr, -.syntaxhighlighter table tbody, -.syntaxhighlighter table thead, -.syntaxhighlighter table caption, -.syntaxhighlighter textarea { - -moz-border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - background: none !important; - border: 0 !important; - bottom: auto !important; - float: none !important; - height: auto !important; - left: auto !important; - line-height: 1.1em !important; - margin: 0 !important; - outline: 0 !important; - overflow: visible !important; - padding: 0 !important; - position: static !important; - right: auto !important; - text-align: left !important; - top: auto !important; - vertical-align: baseline !important; - width: auto !important; - box-sizing: content-box !important; - font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; - font-weight: normal !important; - font-style: normal !important; - font-size: 1em !important; - min-height: inherit !important; - min-height: auto !important; -} - -.syntaxhighlighter { - width: 100% !important; - margin: 1em 0 1em 0 !important; - position: relative !important; - overflow: auto !important; - font-size: 1em !important; -} -.syntaxhighlighter.source { - overflow: hidden !important; -} -.syntaxhighlighter .bold { - font-weight: bold !important; -} -.syntaxhighlighter .italic { - font-style: italic !important; -} -.syntaxhighlighter .line { - white-space: pre !important; -} -.syntaxhighlighter table { - width: 100% !important; -} -.syntaxhighlighter table caption { - text-align: left !important; - padding: .5em 0 0.5em 1em !important; -} -.syntaxhighlighter table td.code { - width: 100% !important; -} -.syntaxhighlighter table td.code .container { - position: relative !important; -} -.syntaxhighlighter table td.code .container textarea { - box-sizing: border-box !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; - height: 100% !important; - border: none !important; - background: white !important; - padding-left: 1em !important; - overflow: hidden !important; - white-space: pre !important; -} -.syntaxhighlighter table td.gutter .line { - text-align: right !important; - padding: 0 0.5em 0 1em !important; -} -.syntaxhighlighter table td.code .line { - padding: 0 1em !important; -} -.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { - padding-left: 0em !important; -} -.syntaxhighlighter.show { - display: block !important; -} -.syntaxhighlighter.collapsed table { - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar { - padding: 0.1em 0.8em 0em 0.8em !important; - font-size: 1em !important; - position: static !important; - width: auto !important; - height: auto !important; -} -.syntaxhighlighter.collapsed .toolbar span { - display: inline !important; - margin-right: 1em !important; -} -.syntaxhighlighter.collapsed .toolbar span a { - padding: 0 !important; - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar span a.expandSource { - display: inline !important; -} -.syntaxhighlighter .toolbar { - position: absolute !important; - right: 1px !important; - top: 1px !important; - width: 11px !important; - height: 11px !important; - font-size: 10px !important; - z-index: 10 !important; -} -.syntaxhighlighter .toolbar span.title { - display: inline !important; -} -.syntaxhighlighter .toolbar a { - display: block !important; - text-align: center !important; - text-decoration: none !important; - padding-top: 1px !important; -} -.syntaxhighlighter .toolbar a.expandSource { - display: none !important; -} -.syntaxhighlighter.ie { - font-size: .9em !important; - padding: 1px 0 1px 0 !important; -} -.syntaxhighlighter.ie .toolbar { - line-height: 8px !important; -} -.syntaxhighlighter.ie .toolbar a { - padding-top: 0px !important; -} -.syntaxhighlighter.printing .line.alt1 .content, -.syntaxhighlighter.printing .line.alt2 .content, -.syntaxhighlighter.printing .line.highlighted .number, -.syntaxhighlighter.printing .line.highlighted.alt1 .content, -.syntaxhighlighter.printing .line.highlighted.alt2 .content { - background: none !important; -} -.syntaxhighlighter.printing .line .number { - color: #bbbbbb !important; -} -.syntaxhighlighter.printing .line .content { - color: black !important; -} -.syntaxhighlighter.printing .toolbar { - display: none !important; -} -.syntaxhighlighter.printing a { - text-decoration: none !important; -} -.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { - color: black !important; -} -.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { - color: #008200 !important; -} -.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { - color: blue !important; -} -.syntaxhighlighter.printing .keyword { - color: #006699 !important; - font-weight: bold !important; -} -.syntaxhighlighter.printing .preprocessor { - color: gray !important; -} -.syntaxhighlighter.printing .variable { - color: #aa7700 !important; -} -.syntaxhighlighter.printing .value { - color: #009900 !important; -} -.syntaxhighlighter.printing .functions { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .constants { - color: #0066cc !important; -} -.syntaxhighlighter.printing .script { - font-weight: bold !important; -} -.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { - color: gray !important; -} -.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { - color: red !important; -} -.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { - color: black !important; -} - -.syntaxhighlighter { - background-color: white !important; -} -.syntaxhighlighter .line.alt1 { - background-color: white !important; -} -.syntaxhighlighter .line.alt2 { - background-color: white !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #c3defe !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: black !important; -} -.syntaxhighlighter .gutter { - color: #787878 !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #d4d0c8 !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #d4d0c8 !important; - color: white !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #3f5fbf !important; - background: white !important; - border: 1px solid #d4d0c8 !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #3f5fbf !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #aa7700 !important; -} -.syntaxhighlighter .toolbar { - color: #a0a0a0 !important; - background: #d4d0c8 !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: #a0a0a0 !important; -} -.syntaxhighlighter .toolbar a:hover { - color: red !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: black !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #3f5fbf !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #2a00ff !important; -} -.syntaxhighlighter .keyword { - color: #7f0055 !important; -} -.syntaxhighlighter .preprocessor { - color: #646464 !important; -} -.syntaxhighlighter .variable { - color: #aa7700 !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #ff1493 !important; -} -.syntaxhighlighter .constants { - color: #0066cc !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #7f0055 !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: gray !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: red !important; -} - -.syntaxhighlighter .keyword { - font-weight: bold !important; -} -.syntaxhighlighter .xml .keyword { - color: #3f7f7f !important; - font-weight: normal !important; -} -.syntaxhighlighter .xml .color1, .syntaxhighlighter .xml .color1 a { - color: #7f007f !important; -} -.syntaxhighlighter .xml .string { - font-style: italic !important; - color: #2a00ff !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shCoreEmacs.css b/assets/stylesheets/syntaxhighlighter/shCoreEmacs.css deleted file mode 100644 index 706c77a..0000000 --- a/assets/stylesheets/syntaxhighlighter/shCoreEmacs.css +++ /dev/null @@ -1,324 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter a, -.syntaxhighlighter div, -.syntaxhighlighter code, -.syntaxhighlighter table, -.syntaxhighlighter table td, -.syntaxhighlighter table tr, -.syntaxhighlighter table tbody, -.syntaxhighlighter table thead, -.syntaxhighlighter table caption, -.syntaxhighlighter textarea { - -moz-border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - background: none !important; - border: 0 !important; - bottom: auto !important; - float: none !important; - height: auto !important; - left: auto !important; - line-height: 1.1em !important; - margin: 0 !important; - outline: 0 !important; - overflow: visible !important; - padding: 0 !important; - position: static !important; - right: auto !important; - text-align: left !important; - top: auto !important; - vertical-align: baseline !important; - width: auto !important; - box-sizing: content-box !important; - font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; - font-weight: normal !important; - font-style: normal !important; - font-size: 1em !important; - min-height: inherit !important; - min-height: auto !important; -} - -.syntaxhighlighter { - width: 100% !important; - margin: 1em 0 1em 0 !important; - position: relative !important; - overflow: auto !important; - font-size: 1em !important; -} -.syntaxhighlighter.source { - overflow: hidden !important; -} -.syntaxhighlighter .bold { - font-weight: bold !important; -} -.syntaxhighlighter .italic { - font-style: italic !important; -} -.syntaxhighlighter .line { - white-space: pre !important; -} -.syntaxhighlighter table { - width: 100% !important; -} -.syntaxhighlighter table caption { - text-align: left !important; - padding: .5em 0 0.5em 1em !important; -} -.syntaxhighlighter table td.code { - width: 100% !important; -} -.syntaxhighlighter table td.code .container { - position: relative !important; -} -.syntaxhighlighter table td.code .container textarea { - box-sizing: border-box !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; - height: 100% !important; - border: none !important; - background: white !important; - padding-left: 1em !important; - overflow: hidden !important; - white-space: pre !important; -} -.syntaxhighlighter table td.gutter .line { - text-align: right !important; - padding: 0 0.5em 0 1em !important; -} -.syntaxhighlighter table td.code .line { - padding: 0 1em !important; -} -.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { - padding-left: 0em !important; -} -.syntaxhighlighter.show { - display: block !important; -} -.syntaxhighlighter.collapsed table { - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar { - padding: 0.1em 0.8em 0em 0.8em !important; - font-size: 1em !important; - position: static !important; - width: auto !important; - height: auto !important; -} -.syntaxhighlighter.collapsed .toolbar span { - display: inline !important; - margin-right: 1em !important; -} -.syntaxhighlighter.collapsed .toolbar span a { - padding: 0 !important; - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar span a.expandSource { - display: inline !important; -} -.syntaxhighlighter .toolbar { - position: absolute !important; - right: 1px !important; - top: 1px !important; - width: 11px !important; - height: 11px !important; - font-size: 10px !important; - z-index: 10 !important; -} -.syntaxhighlighter .toolbar span.title { - display: inline !important; -} -.syntaxhighlighter .toolbar a { - display: block !important; - text-align: center !important; - text-decoration: none !important; - padding-top: 1px !important; -} -.syntaxhighlighter .toolbar a.expandSource { - display: none !important; -} -.syntaxhighlighter.ie { - font-size: .9em !important; - padding: 1px 0 1px 0 !important; -} -.syntaxhighlighter.ie .toolbar { - line-height: 8px !important; -} -.syntaxhighlighter.ie .toolbar a { - padding-top: 0px !important; -} -.syntaxhighlighter.printing .line.alt1 .content, -.syntaxhighlighter.printing .line.alt2 .content, -.syntaxhighlighter.printing .line.highlighted .number, -.syntaxhighlighter.printing .line.highlighted.alt1 .content, -.syntaxhighlighter.printing .line.highlighted.alt2 .content { - background: none !important; -} -.syntaxhighlighter.printing .line .number { - color: #bbbbbb !important; -} -.syntaxhighlighter.printing .line .content { - color: black !important; -} -.syntaxhighlighter.printing .toolbar { - display: none !important; -} -.syntaxhighlighter.printing a { - text-decoration: none !important; -} -.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { - color: black !important; -} -.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { - color: #008200 !important; -} -.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { - color: blue !important; -} -.syntaxhighlighter.printing .keyword { - color: #006699 !important; - font-weight: bold !important; -} -.syntaxhighlighter.printing .preprocessor { - color: gray !important; -} -.syntaxhighlighter.printing .variable { - color: #aa7700 !important; -} -.syntaxhighlighter.printing .value { - color: #009900 !important; -} -.syntaxhighlighter.printing .functions { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .constants { - color: #0066cc !important; -} -.syntaxhighlighter.printing .script { - font-weight: bold !important; -} -.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { - color: gray !important; -} -.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { - color: red !important; -} -.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { - color: black !important; -} - -.syntaxhighlighter { - background-color: black !important; -} -.syntaxhighlighter .line.alt1 { - background-color: black !important; -} -.syntaxhighlighter .line.alt2 { - background-color: black !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #2a3133 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: #d3d3d3 !important; -} -.syntaxhighlighter .gutter { - color: #d3d3d3 !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #990000 !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #990000 !important; - color: black !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #ebdb8d !important; - background: black !important; - border: 1px solid #990000 !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #ebdb8d !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #ff7d27 !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #990000 !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #9ccff4 !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #d3d3d3 !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #ff7d27 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #ff9e7b !important; -} -.syntaxhighlighter .keyword { - color: aqua !important; -} -.syntaxhighlighter .preprocessor { - color: #aec4de !important; -} -.syntaxhighlighter .variable { - color: #ffaa3e !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #81cef9 !important; -} -.syntaxhighlighter .constants { - color: #ff9e7b !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: aqua !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #ebdb8d !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #ff7d27 !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #aec4de !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css b/assets/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css deleted file mode 100644 index 6101eba..0000000 --- a/assets/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css +++ /dev/null @@ -1,328 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter a, -.syntaxhighlighter div, -.syntaxhighlighter code, -.syntaxhighlighter table, -.syntaxhighlighter table td, -.syntaxhighlighter table tr, -.syntaxhighlighter table tbody, -.syntaxhighlighter table thead, -.syntaxhighlighter table caption, -.syntaxhighlighter textarea { - -moz-border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - background: none !important; - border: 0 !important; - bottom: auto !important; - float: none !important; - height: auto !important; - left: auto !important; - line-height: 1.1em !important; - margin: 0 !important; - outline: 0 !important; - overflow: visible !important; - padding: 0 !important; - position: static !important; - right: auto !important; - text-align: left !important; - top: auto !important; - vertical-align: baseline !important; - width: auto !important; - box-sizing: content-box !important; - font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; - font-weight: normal !important; - font-style: normal !important; - font-size: 1em !important; - min-height: inherit !important; - min-height: auto !important; -} - -.syntaxhighlighter { - width: 100% !important; - margin: 1em 0 1em 0 !important; - position: relative !important; - overflow: auto !important; - font-size: 1em !important; -} -.syntaxhighlighter.source { - overflow: hidden !important; -} -.syntaxhighlighter .bold { - font-weight: bold !important; -} -.syntaxhighlighter .italic { - font-style: italic !important; -} -.syntaxhighlighter .line { - white-space: pre !important; -} -.syntaxhighlighter table { - width: 100% !important; -} -.syntaxhighlighter table caption { - text-align: left !important; - padding: .5em 0 0.5em 1em !important; -} -.syntaxhighlighter table td.code { - width: 100% !important; -} -.syntaxhighlighter table td.code .container { - position: relative !important; -} -.syntaxhighlighter table td.code .container textarea { - box-sizing: border-box !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; - height: 100% !important; - border: none !important; - background: white !important; - padding-left: 1em !important; - overflow: hidden !important; - white-space: pre !important; -} -.syntaxhighlighter table td.gutter .line { - text-align: right !important; - padding: 0 0.5em 0 1em !important; -} -.syntaxhighlighter table td.code .line { - padding: 0 1em !important; -} -.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { - padding-left: 0em !important; -} -.syntaxhighlighter.show { - display: block !important; -} -.syntaxhighlighter.collapsed table { - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar { - padding: 0.1em 0.8em 0em 0.8em !important; - font-size: 1em !important; - position: static !important; - width: auto !important; - height: auto !important; -} -.syntaxhighlighter.collapsed .toolbar span { - display: inline !important; - margin-right: 1em !important; -} -.syntaxhighlighter.collapsed .toolbar span a { - padding: 0 !important; - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar span a.expandSource { - display: inline !important; -} -.syntaxhighlighter .toolbar { - position: absolute !important; - right: 1px !important; - top: 1px !important; - width: 11px !important; - height: 11px !important; - font-size: 10px !important; - z-index: 10 !important; -} -.syntaxhighlighter .toolbar span.title { - display: inline !important; -} -.syntaxhighlighter .toolbar a { - display: block !important; - text-align: center !important; - text-decoration: none !important; - padding-top: 1px !important; -} -.syntaxhighlighter .toolbar a.expandSource { - display: none !important; -} -.syntaxhighlighter.ie { - font-size: .9em !important; - padding: 1px 0 1px 0 !important; -} -.syntaxhighlighter.ie .toolbar { - line-height: 8px !important; -} -.syntaxhighlighter.ie .toolbar a { - padding-top: 0px !important; -} -.syntaxhighlighter.printing .line.alt1 .content, -.syntaxhighlighter.printing .line.alt2 .content, -.syntaxhighlighter.printing .line.highlighted .number, -.syntaxhighlighter.printing .line.highlighted.alt1 .content, -.syntaxhighlighter.printing .line.highlighted.alt2 .content { - background: none !important; -} -.syntaxhighlighter.printing .line .number { - color: #bbbbbb !important; -} -.syntaxhighlighter.printing .line .content { - color: black !important; -} -.syntaxhighlighter.printing .toolbar { - display: none !important; -} -.syntaxhighlighter.printing a { - text-decoration: none !important; -} -.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { - color: black !important; -} -.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { - color: #008200 !important; -} -.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { - color: blue !important; -} -.syntaxhighlighter.printing .keyword { - color: #006699 !important; - font-weight: bold !important; -} -.syntaxhighlighter.printing .preprocessor { - color: gray !important; -} -.syntaxhighlighter.printing .variable { - color: #aa7700 !important; -} -.syntaxhighlighter.printing .value { - color: #009900 !important; -} -.syntaxhighlighter.printing .functions { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .constants { - color: #0066cc !important; -} -.syntaxhighlighter.printing .script { - font-weight: bold !important; -} -.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { - color: gray !important; -} -.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { - color: red !important; -} -.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { - color: black !important; -} - -.syntaxhighlighter { - background-color: #121212 !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #121212 !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #121212 !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #2c2c29 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: white !important; -} -.syntaxhighlighter .gutter { - color: #afafaf !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #3185b9 !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #3185b9 !important; - color: #121212 !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #3185b9 !important; - background: black !important; - border: 1px solid #3185b9 !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #3185b9 !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #d01d33 !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #3185b9 !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #96daff !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: white !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #696854 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #e3e658 !important; -} -.syntaxhighlighter .keyword { - color: #d01d33 !important; -} -.syntaxhighlighter .preprocessor { - color: #435a5f !important; -} -.syntaxhighlighter .variable { - color: #898989 !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #aaaaaa !important; -} -.syntaxhighlighter .constants { - color: #96daff !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #d01d33 !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #ffc074 !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #4a8cdb !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #96daff !important; -} - -.syntaxhighlighter .functions { - font-weight: bold !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shCoreMDUltra.css b/assets/stylesheets/syntaxhighlighter/shCoreMDUltra.css deleted file mode 100644 index 2923ce7..0000000 --- a/assets/stylesheets/syntaxhighlighter/shCoreMDUltra.css +++ /dev/null @@ -1,324 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter a, -.syntaxhighlighter div, -.syntaxhighlighter code, -.syntaxhighlighter table, -.syntaxhighlighter table td, -.syntaxhighlighter table tr, -.syntaxhighlighter table tbody, -.syntaxhighlighter table thead, -.syntaxhighlighter table caption, -.syntaxhighlighter textarea { - -moz-border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - background: none !important; - border: 0 !important; - bottom: auto !important; - float: none !important; - height: auto !important; - left: auto !important; - line-height: 1.1em !important; - margin: 0 !important; - outline: 0 !important; - overflow: visible !important; - padding: 0 !important; - position: static !important; - right: auto !important; - text-align: left !important; - top: auto !important; - vertical-align: baseline !important; - width: auto !important; - box-sizing: content-box !important; - font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; - font-weight: normal !important; - font-style: normal !important; - font-size: 1em !important; - min-height: inherit !important; - min-height: auto !important; -} - -.syntaxhighlighter { - width: 100% !important; - margin: 1em 0 1em 0 !important; - position: relative !important; - overflow: auto !important; - font-size: 1em !important; -} -.syntaxhighlighter.source { - overflow: hidden !important; -} -.syntaxhighlighter .bold { - font-weight: bold !important; -} -.syntaxhighlighter .italic { - font-style: italic !important; -} -.syntaxhighlighter .line { - white-space: pre !important; -} -.syntaxhighlighter table { - width: 100% !important; -} -.syntaxhighlighter table caption { - text-align: left !important; - padding: .5em 0 0.5em 1em !important; -} -.syntaxhighlighter table td.code { - width: 100% !important; -} -.syntaxhighlighter table td.code .container { - position: relative !important; -} -.syntaxhighlighter table td.code .container textarea { - box-sizing: border-box !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; - height: 100% !important; - border: none !important; - background: white !important; - padding-left: 1em !important; - overflow: hidden !important; - white-space: pre !important; -} -.syntaxhighlighter table td.gutter .line { - text-align: right !important; - padding: 0 0.5em 0 1em !important; -} -.syntaxhighlighter table td.code .line { - padding: 0 1em !important; -} -.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { - padding-left: 0em !important; -} -.syntaxhighlighter.show { - display: block !important; -} -.syntaxhighlighter.collapsed table { - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar { - padding: 0.1em 0.8em 0em 0.8em !important; - font-size: 1em !important; - position: static !important; - width: auto !important; - height: auto !important; -} -.syntaxhighlighter.collapsed .toolbar span { - display: inline !important; - margin-right: 1em !important; -} -.syntaxhighlighter.collapsed .toolbar span a { - padding: 0 !important; - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar span a.expandSource { - display: inline !important; -} -.syntaxhighlighter .toolbar { - position: absolute !important; - right: 1px !important; - top: 1px !important; - width: 11px !important; - height: 11px !important; - font-size: 10px !important; - z-index: 10 !important; -} -.syntaxhighlighter .toolbar span.title { - display: inline !important; -} -.syntaxhighlighter .toolbar a { - display: block !important; - text-align: center !important; - text-decoration: none !important; - padding-top: 1px !important; -} -.syntaxhighlighter .toolbar a.expandSource { - display: none !important; -} -.syntaxhighlighter.ie { - font-size: .9em !important; - padding: 1px 0 1px 0 !important; -} -.syntaxhighlighter.ie .toolbar { - line-height: 8px !important; -} -.syntaxhighlighter.ie .toolbar a { - padding-top: 0px !important; -} -.syntaxhighlighter.printing .line.alt1 .content, -.syntaxhighlighter.printing .line.alt2 .content, -.syntaxhighlighter.printing .line.highlighted .number, -.syntaxhighlighter.printing .line.highlighted.alt1 .content, -.syntaxhighlighter.printing .line.highlighted.alt2 .content { - background: none !important; -} -.syntaxhighlighter.printing .line .number { - color: #bbbbbb !important; -} -.syntaxhighlighter.printing .line .content { - color: black !important; -} -.syntaxhighlighter.printing .toolbar { - display: none !important; -} -.syntaxhighlighter.printing a { - text-decoration: none !important; -} -.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { - color: black !important; -} -.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { - color: #008200 !important; -} -.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { - color: blue !important; -} -.syntaxhighlighter.printing .keyword { - color: #006699 !important; - font-weight: bold !important; -} -.syntaxhighlighter.printing .preprocessor { - color: gray !important; -} -.syntaxhighlighter.printing .variable { - color: #aa7700 !important; -} -.syntaxhighlighter.printing .value { - color: #009900 !important; -} -.syntaxhighlighter.printing .functions { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .constants { - color: #0066cc !important; -} -.syntaxhighlighter.printing .script { - font-weight: bold !important; -} -.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { - color: gray !important; -} -.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { - color: red !important; -} -.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { - color: black !important; -} - -.syntaxhighlighter { - background-color: #222222 !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #222222 !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #222222 !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #253e5a !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: lime !important; -} -.syntaxhighlighter .gutter { - color: #38566f !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #435a5f !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #435a5f !important; - color: #222222 !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #428bdd !important; - background: black !important; - border: 1px solid #435a5f !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #428bdd !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: lime !important; -} -.syntaxhighlighter .toolbar { - color: #aaaaff !important; - background: #435a5f !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: #aaaaff !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #9ccff4 !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: lime !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #428bdd !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: lime !important; -} -.syntaxhighlighter .keyword { - color: #aaaaff !important; -} -.syntaxhighlighter .preprocessor { - color: #8aa6c1 !important; -} -.syntaxhighlighter .variable { - color: aqua !important; -} -.syntaxhighlighter .value { - color: #f7e741 !important; -} -.syntaxhighlighter .functions { - color: #ff8000 !important; -} -.syntaxhighlighter .constants { - color: yellow !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #aaaaff !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: red !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: yellow !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #ffaa3e !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shCoreMidnight.css b/assets/stylesheets/syntaxhighlighter/shCoreMidnight.css deleted file mode 100644 index e3733ee..0000000 --- a/assets/stylesheets/syntaxhighlighter/shCoreMidnight.css +++ /dev/null @@ -1,324 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter a, -.syntaxhighlighter div, -.syntaxhighlighter code, -.syntaxhighlighter table, -.syntaxhighlighter table td, -.syntaxhighlighter table tr, -.syntaxhighlighter table tbody, -.syntaxhighlighter table thead, -.syntaxhighlighter table caption, -.syntaxhighlighter textarea { - -moz-border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - background: none !important; - border: 0 !important; - bottom: auto !important; - float: none !important; - height: auto !important; - left: auto !important; - line-height: 1.1em !important; - margin: 0 !important; - outline: 0 !important; - overflow: visible !important; - padding: 0 !important; - position: static !important; - right: auto !important; - text-align: left !important; - top: auto !important; - vertical-align: baseline !important; - width: auto !important; - box-sizing: content-box !important; - font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; - font-weight: normal !important; - font-style: normal !important; - font-size: 1em !important; - min-height: inherit !important; - min-height: auto !important; -} - -.syntaxhighlighter { - width: 100% !important; - margin: 1em 0 1em 0 !important; - position: relative !important; - overflow: auto !important; - font-size: 1em !important; -} -.syntaxhighlighter.source { - overflow: hidden !important; -} -.syntaxhighlighter .bold { - font-weight: bold !important; -} -.syntaxhighlighter .italic { - font-style: italic !important; -} -.syntaxhighlighter .line { - white-space: pre !important; -} -.syntaxhighlighter table { - width: 100% !important; -} -.syntaxhighlighter table caption { - text-align: left !important; - padding: .5em 0 0.5em 1em !important; -} -.syntaxhighlighter table td.code { - width: 100% !important; -} -.syntaxhighlighter table td.code .container { - position: relative !important; -} -.syntaxhighlighter table td.code .container textarea { - box-sizing: border-box !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; - height: 100% !important; - border: none !important; - background: white !important; - padding-left: 1em !important; - overflow: hidden !important; - white-space: pre !important; -} -.syntaxhighlighter table td.gutter .line { - text-align: right !important; - padding: 0 0.5em 0 1em !important; -} -.syntaxhighlighter table td.code .line { - padding: 0 1em !important; -} -.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { - padding-left: 0em !important; -} -.syntaxhighlighter.show { - display: block !important; -} -.syntaxhighlighter.collapsed table { - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar { - padding: 0.1em 0.8em 0em 0.8em !important; - font-size: 1em !important; - position: static !important; - width: auto !important; - height: auto !important; -} -.syntaxhighlighter.collapsed .toolbar span { - display: inline !important; - margin-right: 1em !important; -} -.syntaxhighlighter.collapsed .toolbar span a { - padding: 0 !important; - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar span a.expandSource { - display: inline !important; -} -.syntaxhighlighter .toolbar { - position: absolute !important; - right: 1px !important; - top: 1px !important; - width: 11px !important; - height: 11px !important; - font-size: 10px !important; - z-index: 10 !important; -} -.syntaxhighlighter .toolbar span.title { - display: inline !important; -} -.syntaxhighlighter .toolbar a { - display: block !important; - text-align: center !important; - text-decoration: none !important; - padding-top: 1px !important; -} -.syntaxhighlighter .toolbar a.expandSource { - display: none !important; -} -.syntaxhighlighter.ie { - font-size: .9em !important; - padding: 1px 0 1px 0 !important; -} -.syntaxhighlighter.ie .toolbar { - line-height: 8px !important; -} -.syntaxhighlighter.ie .toolbar a { - padding-top: 0px !important; -} -.syntaxhighlighter.printing .line.alt1 .content, -.syntaxhighlighter.printing .line.alt2 .content, -.syntaxhighlighter.printing .line.highlighted .number, -.syntaxhighlighter.printing .line.highlighted.alt1 .content, -.syntaxhighlighter.printing .line.highlighted.alt2 .content { - background: none !important; -} -.syntaxhighlighter.printing .line .number { - color: #bbbbbb !important; -} -.syntaxhighlighter.printing .line .content { - color: black !important; -} -.syntaxhighlighter.printing .toolbar { - display: none !important; -} -.syntaxhighlighter.printing a { - text-decoration: none !important; -} -.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { - color: black !important; -} -.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { - color: #008200 !important; -} -.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { - color: blue !important; -} -.syntaxhighlighter.printing .keyword { - color: #006699 !important; - font-weight: bold !important; -} -.syntaxhighlighter.printing .preprocessor { - color: gray !important; -} -.syntaxhighlighter.printing .variable { - color: #aa7700 !important; -} -.syntaxhighlighter.printing .value { - color: #009900 !important; -} -.syntaxhighlighter.printing .functions { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .constants { - color: #0066cc !important; -} -.syntaxhighlighter.printing .script { - font-weight: bold !important; -} -.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { - color: gray !important; -} -.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { - color: red !important; -} -.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { - color: black !important; -} - -.syntaxhighlighter { - background-color: #0f192a !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #0f192a !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #0f192a !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #253e5a !important; -} -.syntaxhighlighter .line.highlighted.number { - color: #38566f !important; -} -.syntaxhighlighter table caption { - color: #d1edff !important; -} -.syntaxhighlighter .gutter { - color: #afafaf !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #435a5f !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #435a5f !important; - color: #0f192a !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #428bdd !important; - background: black !important; - border: 1px solid #435a5f !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #428bdd !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #1dc116 !important; -} -.syntaxhighlighter .toolbar { - color: #d1edff !important; - background: #435a5f !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: #d1edff !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #8aa6c1 !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #d1edff !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #428bdd !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #1dc116 !important; -} -.syntaxhighlighter .keyword { - color: #b43d3d !important; -} -.syntaxhighlighter .preprocessor { - color: #8aa6c1 !important; -} -.syntaxhighlighter .variable { - color: #ffaa3e !important; -} -.syntaxhighlighter .value { - color: #f7e741 !important; -} -.syntaxhighlighter .functions { - color: #ffaa3e !important; -} -.syntaxhighlighter .constants { - color: #e0e8ff !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #b43d3d !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #f8bb00 !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: white !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #ffaa3e !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shCoreRDark.css b/assets/stylesheets/syntaxhighlighter/shCoreRDark.css deleted file mode 100644 index d093683..0000000 --- a/assets/stylesheets/syntaxhighlighter/shCoreRDark.css +++ /dev/null @@ -1,324 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter a, -.syntaxhighlighter div, -.syntaxhighlighter code, -.syntaxhighlighter table, -.syntaxhighlighter table td, -.syntaxhighlighter table tr, -.syntaxhighlighter table tbody, -.syntaxhighlighter table thead, -.syntaxhighlighter table caption, -.syntaxhighlighter textarea { - -moz-border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - background: none !important; - border: 0 !important; - bottom: auto !important; - float: none !important; - height: auto !important; - left: auto !important; - line-height: 1.1em !important; - margin: 0 !important; - outline: 0 !important; - overflow: visible !important; - padding: 0 !important; - position: static !important; - right: auto !important; - text-align: left !important; - top: auto !important; - vertical-align: baseline !important; - width: auto !important; - box-sizing: content-box !important; - font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; - font-weight: normal !important; - font-style: normal !important; - font-size: 1em !important; - min-height: inherit !important; - min-height: auto !important; -} - -.syntaxhighlighter { - width: 100% !important; - margin: 1em 0 1em 0 !important; - position: relative !important; - overflow: auto !important; - font-size: 1em !important; -} -.syntaxhighlighter.source { - overflow: hidden !important; -} -.syntaxhighlighter .bold { - font-weight: bold !important; -} -.syntaxhighlighter .italic { - font-style: italic !important; -} -.syntaxhighlighter .line { - white-space: pre !important; -} -.syntaxhighlighter table { - width: 100% !important; -} -.syntaxhighlighter table caption { - text-align: left !important; - padding: .5em 0 0.5em 1em !important; -} -.syntaxhighlighter table td.code { - width: 100% !important; -} -.syntaxhighlighter table td.code .container { - position: relative !important; -} -.syntaxhighlighter table td.code .container textarea { - box-sizing: border-box !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; - height: 100% !important; - border: none !important; - background: white !important; - padding-left: 1em !important; - overflow: hidden !important; - white-space: pre !important; -} -.syntaxhighlighter table td.gutter .line { - text-align: right !important; - padding: 0 0.5em 0 1em !important; -} -.syntaxhighlighter table td.code .line { - padding: 0 1em !important; -} -.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line { - padding-left: 0em !important; -} -.syntaxhighlighter.show { - display: block !important; -} -.syntaxhighlighter.collapsed table { - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar { - padding: 0.1em 0.8em 0em 0.8em !important; - font-size: 1em !important; - position: static !important; - width: auto !important; - height: auto !important; -} -.syntaxhighlighter.collapsed .toolbar span { - display: inline !important; - margin-right: 1em !important; -} -.syntaxhighlighter.collapsed .toolbar span a { - padding: 0 !important; - display: none !important; -} -.syntaxhighlighter.collapsed .toolbar span a.expandSource { - display: inline !important; -} -.syntaxhighlighter .toolbar { - position: absolute !important; - right: 1px !important; - top: 1px !important; - width: 11px !important; - height: 11px !important; - font-size: 10px !important; - z-index: 10 !important; -} -.syntaxhighlighter .toolbar span.title { - display: inline !important; -} -.syntaxhighlighter .toolbar a { - display: block !important; - text-align: center !important; - text-decoration: none !important; - padding-top: 1px !important; -} -.syntaxhighlighter .toolbar a.expandSource { - display: none !important; -} -.syntaxhighlighter.ie { - font-size: .9em !important; - padding: 1px 0 1px 0 !important; -} -.syntaxhighlighter.ie .toolbar { - line-height: 8px !important; -} -.syntaxhighlighter.ie .toolbar a { - padding-top: 0px !important; -} -.syntaxhighlighter.printing .line.alt1 .content, -.syntaxhighlighter.printing .line.alt2 .content, -.syntaxhighlighter.printing .line.highlighted .number, -.syntaxhighlighter.printing .line.highlighted.alt1 .content, -.syntaxhighlighter.printing .line.highlighted.alt2 .content { - background: none !important; -} -.syntaxhighlighter.printing .line .number { - color: #bbbbbb !important; -} -.syntaxhighlighter.printing .line .content { - color: black !important; -} -.syntaxhighlighter.printing .toolbar { - display: none !important; -} -.syntaxhighlighter.printing a { - text-decoration: none !important; -} -.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a { - color: black !important; -} -.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a { - color: #008200 !important; -} -.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a { - color: blue !important; -} -.syntaxhighlighter.printing .keyword { - color: #006699 !important; - font-weight: bold !important; -} -.syntaxhighlighter.printing .preprocessor { - color: gray !important; -} -.syntaxhighlighter.printing .variable { - color: #aa7700 !important; -} -.syntaxhighlighter.printing .value { - color: #009900 !important; -} -.syntaxhighlighter.printing .functions { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .constants { - color: #0066cc !important; -} -.syntaxhighlighter.printing .script { - font-weight: bold !important; -} -.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a { - color: gray !important; -} -.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a { - color: red !important; -} -.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a { - color: black !important; -} - -.syntaxhighlighter { - background-color: #1b2426 !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #1b2426 !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #1b2426 !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #323e41 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: #b9bdb6 !important; -} -.syntaxhighlighter table caption { - color: #b9bdb6 !important; -} -.syntaxhighlighter .gutter { - color: #afafaf !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #435a5f !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #435a5f !important; - color: #1b2426 !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #5ba1cf !important; - background: black !important; - border: 1px solid #435a5f !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #5ba1cf !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #5ce638 !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #435a5f !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #e0e8ff !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #b9bdb6 !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #878a85 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #5ce638 !important; -} -.syntaxhighlighter .keyword { - color: #5ba1cf !important; -} -.syntaxhighlighter .preprocessor { - color: #435a5f !important; -} -.syntaxhighlighter .variable { - color: #ffaa3e !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #ffaa3e !important; -} -.syntaxhighlighter .constants { - color: #e0e8ff !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #5ba1cf !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #e0e8ff !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: white !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #ffaa3e !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeDefault.css b/assets/stylesheets/syntaxhighlighter/shThemeDefault.css deleted file mode 100644 index 1365411..0000000 --- a/assets/stylesheets/syntaxhighlighter/shThemeDefault.css +++ /dev/null @@ -1,117 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter { - background-color: white !important; -} -.syntaxhighlighter .line.alt1 { - background-color: white !important; -} -.syntaxhighlighter .line.alt2 { - background-color: white !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #e0e0e0 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: black !important; -} -.syntaxhighlighter table caption { - color: black !important; -} -.syntaxhighlighter .gutter { - color: #afafaf !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #6ce26c !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #6ce26c !important; - color: white !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: blue !important; - background: white !important; - border: 1px solid #6ce26c !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: blue !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: red !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #6ce26c !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: black !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: black !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #008200 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: blue !important; -} -.syntaxhighlighter .keyword { - color: #006699 !important; -} -.syntaxhighlighter .preprocessor { - color: gray !important; -} -.syntaxhighlighter .variable { - color: #aa7700 !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #ff1493 !important; -} -.syntaxhighlighter .constants { - color: #0066cc !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #006699 !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: gray !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: red !important; -} - -.syntaxhighlighter .keyword { - font-weight: bold !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeDjango.css b/assets/stylesheets/syntaxhighlighter/shThemeDjango.css deleted file mode 100644 index d8b4313..0000000 --- a/assets/stylesheets/syntaxhighlighter/shThemeDjango.css +++ /dev/null @@ -1,120 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter { - background-color: #0a2b1d !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #0a2b1d !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #0a2b1d !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #233729 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: #f8f8f8 !important; -} -.syntaxhighlighter .gutter { - color: #497958 !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #41a83e !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #41a83e !important; - color: #0a2b1d !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #96dd3b !important; - background: black !important; - border: 1px solid #41a83e !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #96dd3b !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: white !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #41a83e !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #ffe862 !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #f8f8f8 !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #336442 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #9df39f !important; -} -.syntaxhighlighter .keyword { - color: #96dd3b !important; -} -.syntaxhighlighter .preprocessor { - color: #91bb9e !important; -} -.syntaxhighlighter .variable { - color: #ffaa3e !important; -} -.syntaxhighlighter .value { - color: #f7e741 !important; -} -.syntaxhighlighter .functions { - color: #ffaa3e !important; -} -.syntaxhighlighter .constants { - color: #e0e8ff !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #96dd3b !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #eb939a !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #91bb9e !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #edef7d !important; -} - -.syntaxhighlighter .comments { - font-style: italic !important; -} -.syntaxhighlighter .keyword { - font-weight: bold !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeEclipse.css b/assets/stylesheets/syntaxhighlighter/shThemeEclipse.css deleted file mode 100644 index 77377d9..0000000 --- a/assets/stylesheets/syntaxhighlighter/shThemeEclipse.css +++ /dev/null @@ -1,128 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter { - background-color: white !important; -} -.syntaxhighlighter .line.alt1 { - background-color: white !important; -} -.syntaxhighlighter .line.alt2 { - background-color: white !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #c3defe !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: black !important; -} -.syntaxhighlighter .gutter { - color: #787878 !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #d4d0c8 !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #d4d0c8 !important; - color: white !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #3f5fbf !important; - background: white !important; - border: 1px solid #d4d0c8 !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #3f5fbf !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #aa7700 !important; -} -.syntaxhighlighter .toolbar { - color: #a0a0a0 !important; - background: #d4d0c8 !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: #a0a0a0 !important; -} -.syntaxhighlighter .toolbar a:hover { - color: red !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: black !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #3f5fbf !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #2a00ff !important; -} -.syntaxhighlighter .keyword { - color: #7f0055 !important; -} -.syntaxhighlighter .preprocessor { - color: #646464 !important; -} -.syntaxhighlighter .variable { - color: #aa7700 !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #ff1493 !important; -} -.syntaxhighlighter .constants { - color: #0066cc !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #7f0055 !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: gray !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #ff1493 !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: red !important; -} - -.syntaxhighlighter .keyword { - font-weight: bold !important; -} -.syntaxhighlighter .xml .keyword { - color: #3f7f7f !important; - font-weight: normal !important; -} -.syntaxhighlighter .xml .color1, .syntaxhighlighter .xml .color1 a { - color: #7f007f !important; -} -.syntaxhighlighter .xml .string { - font-style: italic !important; - color: #2a00ff !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeEmacs.css b/assets/stylesheets/syntaxhighlighter/shThemeEmacs.css deleted file mode 100644 index dae5053..0000000 --- a/assets/stylesheets/syntaxhighlighter/shThemeEmacs.css +++ /dev/null @@ -1,113 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter { - background-color: black !important; -} -.syntaxhighlighter .line.alt1 { - background-color: black !important; -} -.syntaxhighlighter .line.alt2 { - background-color: black !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #2a3133 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: #d3d3d3 !important; -} -.syntaxhighlighter .gutter { - color: #d3d3d3 !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #990000 !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #990000 !important; - color: black !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #ebdb8d !important; - background: black !important; - border: 1px solid #990000 !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #ebdb8d !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #ff7d27 !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #990000 !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #9ccff4 !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #d3d3d3 !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #ff7d27 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #ff9e7b !important; -} -.syntaxhighlighter .keyword { - color: aqua !important; -} -.syntaxhighlighter .preprocessor { - color: #aec4de !important; -} -.syntaxhighlighter .variable { - color: #ffaa3e !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #81cef9 !important; -} -.syntaxhighlighter .constants { - color: #ff9e7b !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: aqua !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #ebdb8d !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #ff7d27 !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #aec4de !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css b/assets/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css deleted file mode 100644 index 8fbd871..0000000 --- a/assets/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css +++ /dev/null @@ -1,117 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter { - background-color: #121212 !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #121212 !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #121212 !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #2c2c29 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: white !important; -} -.syntaxhighlighter .gutter { - color: #afafaf !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #3185b9 !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #3185b9 !important; - color: #121212 !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #3185b9 !important; - background: black !important; - border: 1px solid #3185b9 !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #3185b9 !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #d01d33 !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #3185b9 !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #96daff !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: white !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #696854 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #e3e658 !important; -} -.syntaxhighlighter .keyword { - color: #d01d33 !important; -} -.syntaxhighlighter .preprocessor { - color: #435a5f !important; -} -.syntaxhighlighter .variable { - color: #898989 !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #aaaaaa !important; -} -.syntaxhighlighter .constants { - color: #96daff !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #d01d33 !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #ffc074 !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #4a8cdb !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #96daff !important; -} - -.syntaxhighlighter .functions { - font-weight: bold !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeMDUltra.css b/assets/stylesheets/syntaxhighlighter/shThemeMDUltra.css deleted file mode 100755 index f4db39c..0000000 --- a/assets/stylesheets/syntaxhighlighter/shThemeMDUltra.css +++ /dev/null @@ -1,113 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter { - background-color: #222222 !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #222222 !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #222222 !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #253e5a !important; -} -.syntaxhighlighter .line.highlighted.number { - color: white !important; -} -.syntaxhighlighter table caption { - color: lime !important; -} -.syntaxhighlighter .gutter { - color: #38566f !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #435a5f !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #435a5f !important; - color: #222222 !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #428bdd !important; - background: black !important; - border: 1px solid #435a5f !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #428bdd !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: lime !important; -} -.syntaxhighlighter .toolbar { - color: #aaaaff !important; - background: #435a5f !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: #aaaaff !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #9ccff4 !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: lime !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #428bdd !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: lime !important; -} -.syntaxhighlighter .keyword { - color: #aaaaff !important; -} -.syntaxhighlighter .preprocessor { - color: #8aa6c1 !important; -} -.syntaxhighlighter .variable { - color: aqua !important; -} -.syntaxhighlighter .value { - color: #f7e741 !important; -} -.syntaxhighlighter .functions { - color: #ff8000 !important; -} -.syntaxhighlighter .constants { - color: yellow !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #aaaaff !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: red !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: yellow !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #ffaa3e !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeMidnight.css b/assets/stylesheets/syntaxhighlighter/shThemeMidnight.css deleted file mode 100644 index c49563c..0000000 --- a/assets/stylesheets/syntaxhighlighter/shThemeMidnight.css +++ /dev/null @@ -1,113 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter { - background-color: #0f192a !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #0f192a !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #0f192a !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #253e5a !important; -} -.syntaxhighlighter .line.highlighted.number { - color: #38566f !important; -} -.syntaxhighlighter table caption { - color: #d1edff !important; -} -.syntaxhighlighter .gutter { - color: #afafaf !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #435a5f !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #435a5f !important; - color: #0f192a !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #428bdd !important; - background: black !important; - border: 1px solid #435a5f !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #428bdd !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #1dc116 !important; -} -.syntaxhighlighter .toolbar { - color: #d1edff !important; - background: #435a5f !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: #d1edff !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #8aa6c1 !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #d1edff !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #428bdd !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #1dc116 !important; -} -.syntaxhighlighter .keyword { - color: #b43d3d !important; -} -.syntaxhighlighter .preprocessor { - color: #8aa6c1 !important; -} -.syntaxhighlighter .variable { - color: #ffaa3e !important; -} -.syntaxhighlighter .value { - color: #f7e741 !important; -} -.syntaxhighlighter .functions { - color: #ffaa3e !important; -} -.syntaxhighlighter .constants { - color: #e0e8ff !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #b43d3d !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #f8bb00 !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: white !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #ffaa3e !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeRDark.css b/assets/stylesheets/syntaxhighlighter/shThemeRDark.css deleted file mode 100644 index 6305a10..0000000 --- a/assets/stylesheets/syntaxhighlighter/shThemeRDark.css +++ /dev/null @@ -1,113 +0,0 @@ -/** - * SyntaxHighlighter - * http://alexgorbatchev.com/SyntaxHighlighter - * - * SyntaxHighlighter is donationware. If you are using it, please donate. - * http://alexgorbatchev.com/SyntaxHighlighter/donate.html - * - * @version - * 3.0.83 (July 02 2010) - * - * @copyright - * Copyright (C) 2004-2010 Alex Gorbatchev. - * - * @license - * Dual licensed under the MIT and GPL licenses. - */ -.syntaxhighlighter { - background-color: #1b2426 !important; -} -.syntaxhighlighter .line.alt1 { - background-color: #1b2426 !important; -} -.syntaxhighlighter .line.alt2 { - background-color: #1b2426 !important; -} -.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { - background-color: #323e41 !important; -} -.syntaxhighlighter .line.highlighted.number { - color: #b9bdb6 !important; -} -.syntaxhighlighter table caption { - color: #b9bdb6 !important; -} -.syntaxhighlighter .gutter { - color: #afafaf !important; -} -.syntaxhighlighter .gutter .line { - border-right: 3px solid #435a5f !important; -} -.syntaxhighlighter .gutter .line.highlighted { - background-color: #435a5f !important; - color: #1b2426 !important; -} -.syntaxhighlighter.printing .line .content { - border: none !important; -} -.syntaxhighlighter.collapsed { - overflow: visible !important; -} -.syntaxhighlighter.collapsed .toolbar { - color: #5ba1cf !important; - background: black !important; - border: 1px solid #435a5f !important; -} -.syntaxhighlighter.collapsed .toolbar a { - color: #5ba1cf !important; -} -.syntaxhighlighter.collapsed .toolbar a:hover { - color: #5ce638 !important; -} -.syntaxhighlighter .toolbar { - color: white !important; - background: #435a5f !important; - border: none !important; -} -.syntaxhighlighter .toolbar a { - color: white !important; -} -.syntaxhighlighter .toolbar a:hover { - color: #e0e8ff !important; -} -.syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #b9bdb6 !important; -} -.syntaxhighlighter .comments, .syntaxhighlighter .comments a { - color: #878a85 !important; -} -.syntaxhighlighter .string, .syntaxhighlighter .string a { - color: #5ce638 !important; -} -.syntaxhighlighter .keyword { - color: #5ba1cf !important; -} -.syntaxhighlighter .preprocessor { - color: #435a5f !important; -} -.syntaxhighlighter .variable { - color: #ffaa3e !important; -} -.syntaxhighlighter .value { - color: #009900 !important; -} -.syntaxhighlighter .functions { - color: #ffaa3e !important; -} -.syntaxhighlighter .constants { - color: #e0e8ff !important; -} -.syntaxhighlighter .script { - font-weight: bold !important; - color: #5ba1cf !important; - background-color: none !important; -} -.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { - color: #e0e8ff !important; -} -.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: white !important; -} -.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { - color: #ffaa3e !important; -} diff --git a/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css b/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css index 6d2edb2..bc7afd3 100644 --- a/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css +++ b/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css @@ -90,7 +90,7 @@ } .syntaxhighlighter .script { color: #222 !important; - background-color: none !important; + background-color: transparent !important; } .syntaxhighlighter .color1, .syntaxhighlighter .color1 a { color: gray !important; diff --git a/bug_report_templates/action_controller_gem.rb b/bug_report_templates/action_controller_gem.rb new file mode 100644 index 0000000..e22c932 --- /dev/null +++ b/bug_report_templates/action_controller_gem.rb @@ -0,0 +1,56 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org/' + # Activate the gem you are reporting the issue against. + gem 'rails', '5.0.0' +end + +require 'rack/test' +require 'action_controller/railtie' + +class TestApp < Rails::Application + config.root = File.dirname(__FILE__) + config.session_store :cookie_store, key: 'cookie_store_key' + secrets.secret_token = 'secret_token' + secrets.secret_key_base = 'secret_key_base' + + config.logger = Logger.new($stdout) + Rails.logger = config.logger + + routes.draw do + get '/' => 'test#index' + end +end + +class TestController < ActionController::Base + include Rails.application.routes.url_helpers + + def index + render plain: 'Home' + end +end + +require 'minitest/autorun' + +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +class BugTest < Minitest::Test + include Rack::Test::Methods + + def test_returns_success + get '/' + assert last_response.ok? + end + + private + def app + Rails.application + end +end diff --git a/bug_report_templates/action_controller_master.rb b/bug_report_templates/action_controller_master.rb new file mode 100644 index 0000000..8322707 --- /dev/null +++ b/bug_report_templates/action_controller_master.rb @@ -0,0 +1,52 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org/' + gem 'rails', github: 'rails/rails' +end + +require 'action_controller/railtie' + +class TestApp < Rails::Application + config.root = File.dirname(__FILE__) + config.session_store :cookie_store, key: 'cookie_store_key' + secrets.secret_token = 'secret_token' + secrets.secret_key_base = 'secret_key_base' + + config.logger = Logger.new($stdout) + Rails.logger = config.logger + + routes.draw do + get '/' => 'test#index' + end +end + +class TestController < ActionController::Base + include Rails.application.routes.url_helpers + + def index + render plain: 'Home' + end +end + +require 'minitest/autorun' +require 'rack/test' + +class BugTest < Minitest::Test + include Rack::Test::Methods + + def test_returns_success + get '/' + assert last_response.ok? + end + + private + def app + Rails.application + end +end diff --git a/bug_report_templates/active_record_gem.rb b/bug_report_templates/active_record_gem.rb new file mode 100644 index 0000000..46fd2f1 --- /dev/null +++ b/bug_report_templates/active_record_gem.rb @@ -0,0 +1,52 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org/' + # Activate the gem you are reporting the issue against. + gem 'activerecord', '5.0.0' + gem 'sqlite3' +end + +require 'active_record' +require 'minitest/autorun' +require 'logger' + +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +# This connection will do for database-independent bug reports. +ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') +ActiveRecord::Base.logger = Logger.new(STDOUT) + +ActiveRecord::Schema.define do + create_table :posts, force: true do |t| + end + + create_table :comments, force: true do |t| + t.integer :post_id + end +end + +class Post < ActiveRecord::Base + has_many :comments +end + +class Comment < ActiveRecord::Base + belongs_to :post +end + +class BugTest < Minitest::Test + def test_association_stuff + post = Post.create! + post.comments << Comment.create! + + assert_equal 1, post.comments.count + assert_equal 1, Comment.count + assert_equal post.id, Comment.first.post.id + end +end diff --git a/bug_report_templates/active_record_master.rb b/bug_report_templates/active_record_master.rb new file mode 100644 index 0000000..6fd401b --- /dev/null +++ b/bug_report_templates/active_record_master.rb @@ -0,0 +1,48 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org/' + gem 'rails', github: 'rails/rails' + gem 'sqlite3' +end + +require 'active_record' +require 'minitest/autorun' +require 'logger' + +# This connection will do for database-independent bug reports. +ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') +ActiveRecord::Base.logger = Logger.new(STDOUT) + +ActiveRecord::Schema.define do + create_table :posts, force: true do |t| + end + + create_table :comments, force: true do |t| + t.integer :post_id + end +end + +class Post < ActiveRecord::Base + has_many :comments +end + +class Comment < ActiveRecord::Base + belongs_to :post +end + +class BugTest < Minitest::Test + def test_association_stuff + post = Post.create! + post.comments << Comment.create! + + assert_equal 1, post.comments.count + assert_equal 1, Comment.count + assert_equal post.id, Comment.first.post.id + end +end diff --git a/bug_report_templates/generic_gem.rb b/bug_report_templates/generic_gem.rb new file mode 100644 index 0000000..2aaaf10 --- /dev/null +++ b/bug_report_templates/generic_gem.rb @@ -0,0 +1,25 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org/' + # Activate the gem you are reporting the issue against. + gem 'activesupport', '5.0.0' +end + +require 'active_support/core_ext/object/blank' +require 'minitest/autorun' + +# Ensure backward compatibility with Minitest 4 +Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) + +class BugTest < Minitest::Test + def test_stuff + assert "zomg".present? + refute "".present? + end +end diff --git a/bug_report_templates/generic_master.rb b/bug_report_templates/generic_master.rb new file mode 100644 index 0000000..70cf931 --- /dev/null +++ b/bug_report_templates/generic_master.rb @@ -0,0 +1,22 @@ +begin + require 'bundler/inline' +rescue LoadError => e + $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler' + raise e +end + +gemfile(true) do + source 'https://rubygems.org/' + gem 'rails', github: 'rails/rails' +end + +require 'active_support' +require 'active_support/core_ext/object/blank' +require 'minitest/autorun' + +class BugTest < Minitest::Test + def test_stuff + assert "zomg".present? + refute "".present? + end +end diff --git a/install.rb b/install.rb deleted file mode 100644 index 18141fd..0000000 --- a/install.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'fileutils' -require 'pathname' - -# ========== Helpers ========== - -def base_path_change? - BASE_PATH == '~/docs/rails-guides-translation-cn' ? false : true -end - -def has_guides_repo?(base_path) - BASE_PATH.join('guides').exist? -end - -def clone_all_for_translator! - clone_rails! - clone_guides! -end - -def clone_all_for_maintainer! - clone_all_for_translator! - clone_rails_guides_github_pages! -end - -def clone_rails! - return if BASE_PATH.join('rails').exist? - `git clone https://github.com/rails/rails.git` -end - -def clone_guides! - return if BASE_PATH.join('guides').exist? - `git clone https://github.com/ruby-china/guides.git` -end - -def clone_rails_guides_github_pages! - return if BASE_PATH.join('ruby-china.github.io').exist? - `git clone https://github.com/ruby-china/ruby-china.github.io` -end - -def clone_by_option!(option) - option_map = { - '1' => 'rails/rails', - '2' => 'ruby-china/guides', - '3' => 'ruby-china/ruby-china.github.io', - '4' => 'all for translator', - '5' => 'all for maintainer' - } - - case option_map[option] - when 'rails/rails' then clone_rails! - when 'ruby-china/guides' then clone_guides! - when 'ruby-china/ruby-china.github.io' then clone_rails_guides_github_pages! - when 'all for translator' then clone_all_for_translator! - when 'all for maintainer' then clone_all_for_maintainer! - else clone_all_for_translator! - end -end - -def yes? msg - puts msg - response = gets.chomp - /yes|y/i.match(response).nil? ? false : true -end - -def ask msg - puts msg - gets.chomp -end - -# ========== End of Helpers ========== - -if yes? 'Do you want to change default base path? (~/docs/rails-guides-translation-cn) (y/N)' - puts 'this is the location to clone rails/rails, ruby-china/guides' - new_base_path = ask("Where would you like to store those repositories?") - if new_base_path.empty? - BASE_PATH = Pathname('./') - else - BASE_PATH = Pathname(new_base_path) - end -else - BASE_PATH = Pathname('~/docs/rails-guides-translation-cn') -end - -unless File.exist? BASE_PATH - puts "Create directories #{BASE_PATH}" - FileUtils.mkdir_p(BASE_PATH.expand_path) -end - -clone_option = ask(<= 3.0' -end - -begin - require 'redcarpet' -rescue LoadError - # This can happen if doc:guides is executed in an application. - $stderr.puts('Generating guides requires Redcarpet 3.1.2+.') - $stderr.puts(< 3.1.2' - -to the Gemfile, run - - bundle install - -and try again. -ERROR - exit 1 -end - -begin - require 'nokogiri' -rescue LoadError - # This can happen if doc:guides is executed in an application. - $stderr.puts('Generating guides requires Nokogiri.') - $stderr.puts(<(name) { ENV[name].presence } +env_flag = ->(name) { "1" == env_value[name] } + +version = env_value["RAILS_VERSION"] +edge = `git rev-parse HEAD`.strip unless version + +RailsGuides::Generator.new( + edge: edge, + version: version, + all: env_flag["ALL"], + only: env_value["ONLY"], + kindle: env_flag["KINDLE"], + language: env_value["GUIDES_LANGUAGE"] +).generate diff --git a/rails_guides/cn.rb b/rails_guides/cn.rb new file mode 100644 index 0000000..e4d1c24 --- /dev/null +++ b/rails_guides/cn.rb @@ -0,0 +1,65 @@ +# Patch for zh-CN generation + +module RailsGuides + class Markdown + private + + def dom_id(nodes) + dom_id = dom_id_text(nodes.last) + + # Fix duplicate node by prefix with its parent node + if @node_ids[dom_id] + if @node_ids[dom_id].size > 1 + duplicate_nodes = @node_ids.delete(dom_id) + new_node_id = "#{duplicate_nodes[-2][:id]}-#{duplicate_nodes.last[:id]}" + duplicate_nodes.last[:id] = new_node_id + @node_ids[new_node_id] = duplicate_nodes + end + + dom_id = "#{nodes[-2][:id]}-#{dom_id}" + end + + @node_ids[dom_id] = nodes + dom_id + end + + def dom_id_text(node) + if node.previous_element && node.previous_element.inner_html.include?('class="anchor"') + node.previous_element.children.first['id'] + else + escaped_chars = Regexp.escape('\\/`*_{}[]()#+-.!:,;|&<>^~=\'"') + + node.text.downcase.gsub(/\?/, "-questionmark") + .gsub(/!/, "-bang") + .gsub(/[#{escaped_chars}]+/, " ").strip + .gsub(/\s+/, "-") + end + end + + def generate_index + if @headings_for_index.present? + raw_index = "" + @headings_for_index.each do |level, node, label| + if level == 1 + raw_index += "1. [#{label}](##{node[:id]})\n" + elsif level == 2 + raw_index += " * [#{label}](##{node[:id]})\n" + end + end + + @index = Nokogiri::HTML.fragment(engine.render(raw_index)).tap do |doc| + doc.at("ol")[:class] = "chapters" + end.to_html + + # Only change `Chapters' to `目录' + @index = <<-INDEX.html_safe +
+

目录

+ #{@index} +
+ INDEX + end + end + + end +end diff --git a/rails_guides/generator.rb b/rails_guides/generator.rb index aa90045..35f0147 100644 --- a/rails_guides/generator.rb +++ b/rails_guides/generator.rb @@ -1,248 +1,206 @@ -# --------------------------------------------------------------------------- -# -# This script generates the guides. It can be invoked via the -# guides:generate rake task within the guides directory. -# -# Guides are taken from the source directory, and the resulting HTML goes into the -# output directory. Assets are stored under files, and copied to output/files as -# part of the generation process. -# -# Some arguments may be passed via environment variables: -# -# WARNINGS -# If you are writing a guide, please work always with WARNINGS=1. Users can -# generate the guides, and thus this flag is off by default. -# -# Internal links (anchors) are checked. If a reference is broken levenshtein -# distance is used to suggest an existing one. This is useful since IDs are -# generated by Markdown from headers and thus edits alter them. -# -# Also detects duplicated IDs. They happen if there are headers with the same -# text. Please do resolve them, if any, so guides are valid XHTML. -# -# ALL -# Set to "1" to force the generation of all guides. -# -# ONLY -# Use ONLY if you want to generate only one or a set of guides. Prefixes are -# enough: -# -# # generates only association_basics.html -# ONLY=assoc ruby rails_guides.rb -# -# Separate many using commas: -# -# # generates only association_basics.html and migrations.html -# ONLY=assoc,migrations ruby rails_guides.rb -# -# Note that if you are working on a guide generation will by default process -# only that one, so ONLY is rarely used nowadays. -# -# GUIDES_LANGUAGE -# Use GUIDES_LANGUAGE when you want to generate translated guides in -# source/ folder (such as source/es). -# Ignore it when generating English guides. -# -# EDGE -# Set to "1" to indicate generated guides should be marked as edge. This -# inserts a badge and changes the preamble of the home page. -# -# --------------------------------------------------------------------------- - -require 'set' -require 'fileutils' - -require 'active_support/core_ext/string/output_safety' -require 'active_support/core_ext/object/blank' -require 'action_controller' -require 'action_view' - -require 'rails_guides/indexer' -require 'rails_guides/helpers' -require 'rails_guides/levenshtein' +require "set" +require "fileutils" + +require "active_support/core_ext/string/output_safety" +require "active_support/core_ext/object/blank" +require "action_controller" +require "action_view" + +require "rails_guides/markdown" +require "rails_guides/indexer" +require "rails_guides/helpers" +require "rails_guides/levenshtein" module RailsGuides class Generator - attr_reader :guides_dir, :source_dir, :output_dir, :edge, :warnings, :all - GUIDES_RE = /\.(?:erb|md)\z/ - def initialize(output=nil) - set_flags_from_environment + def initialize(edge:, version:, all:, only:, kindle:, language:) + @edge = edge + @version = version + @all = all + @only = only + @kindle = kindle + @language = language - if kindle? + if @kindle check_for_kindlegen register_kindle_mime_types end - initialize_dirs(output) + initialize_dirs create_output_dir_if_needed - end - - def set_flags_from_environment - @edge = ENV['EDGE'] == '1' - @warnings = ENV['WARNINGS'] == '1' - @all = ENV['ALL'] == '1' - @kindle = ENV['KINDLE'] == '1' - @version = ENV['RAILS_VERSION'] || 'local' - @lang = ENV['GUIDES_LANGUAGE'] - end - - def register_kindle_mime_types - Mime::Type.register_alias("application/xml", :opf, %w(opf)) - Mime::Type.register_alias("application/xml", :ncx, %w(ncx)) + initialize_markdown_renderer end def generate generate_guides copy_assets - generate_mobi if kindle? + generate_mobi if @kindle end private - def kindle? - @kindle - end + def register_kindle_mime_types + Mime::Type.register_alias("application/xml", :opf, %w(opf)) + Mime::Type.register_alias("application/xml", :ncx, %w(ncx)) + end - def check_for_kindlegen - if `which kindlegen`.blank? - raise "Can't create a kindle version without `kindlegen`." + def check_for_kindlegen + if `which kindlegen`.blank? + raise "Can't create a kindle version without `kindlegen`." + end end - end - def generate_mobi - require 'rails_guides/kindle' - out = "#{output_dir}/kindlegen.out" - Kindle.generate(output_dir, mobi, out) - puts "(kindlegen log at #{out})." - end + def generate_mobi + require "rails_guides/kindle" + out = "#{@output_dir}/kindlegen.out" + Kindle.generate(@output_dir, mobi, out) + puts "(kindlegen log at #{out})." + end - def mobi - "ruby_on_rails_guides_#@version%s.mobi" % (@lang.present? ? ".#@lang" : '') - end + def mobi + mobi = "ruby_on_rails_guides_#{@version || @edge[0, 7]}" + mobi += ".#{@language}" if @language + mobi += ".mobi" + end - def initialize_dirs(output) - @guides_dir = File.join(File.dirname(__FILE__), '..') - @source_dir = "#@guides_dir/source/#@lang" - @output_dir = if output - output - elsif kindle? - "#@guides_dir/output/kindle/#@lang" - else - "#@guides_dir/output/#@lang" - end.sub(%r, '') - end + def initialize_dirs + @guides_dir = File.expand_path("..", __dir__) - def create_output_dir_if_needed - FileUtils.mkdir_p(output_dir) - end + @source_dir = "#{@guides_dir}/source" + @source_dir += "/#{@language}" if @language - def generate_guides - guides_to_generate.each do |guide| - output_file = output_file_for(guide) - generate_guide(guide, output_file) if generate?(guide, output_file) + @output_dir = "#{@guides_dir}/output" + @output_dir += "/kindle" if @kindle + @output_dir += "/#{@language}" if @language end - end - def guides_to_generate - guides = Dir.entries(source_dir).grep(GUIDES_RE) - - if kindle? - Dir.entries("#{source_dir}/kindle").grep(GUIDES_RE).map do |entry| - next if entry == 'KINDLE.md' - guides << "kindle/#{entry}" - end + def create_output_dir_if_needed + FileUtils.mkdir_p(@output_dir) end - ENV.key?('ONLY') ? select_only(guides) : guides - end - - def select_only(guides) - prefixes = ENV['ONLY'].split(",").map(&:strip) - guides.select do |guide| - prefixes.any? { |p| guide.start_with?(p) || guide.start_with?("kindle") } + def initialize_markdown_renderer + Markdown::Renderer.edge = @edge + Markdown::Renderer.version = @version end - end - - def copy_assets - FileUtils.cp_r(Dir.glob("#{guides_dir}/assets/*"), output_dir) - end - def output_file_for(guide) - if guide.end_with?('.md') - guide.sub(/md\z/, 'html') - else - guide.sub(/\.erb\z/, '') + def generate_guides + guides_to_generate.each do |guide| + output_file = output_file_for(guide) + generate_guide(guide, output_file) if generate?(guide, output_file) + end end - end - def output_path_for(output_file) - File.join(output_dir, File.basename(output_file)) - end + def guides_to_generate + guides = Dir.entries(@source_dir).grep(GUIDES_RE) - def generate?(source_file, output_file) - fin = File.join(source_dir, source_file) - fout = output_path_for(output_file) - all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin) - end + if @kindle + Dir.entries("#{@source_dir}/kindle").grep(GUIDES_RE).map do |entry| + next if entry == "KINDLE.md" + guides << "kindle/#{entry}" + end + end - def generate_guide(guide, output_file) - output_path = output_path_for(output_file) - puts "Generating #{guide} as #{output_file}" - layout = kindle? ? 'kindle/layout' : 'layout' + @only ? select_only(guides) : guides + end + + def select_only(guides) + prefixes = @only.split(",").map(&:strip) + guides.select do |guide| + guide.start_with?("kindle", *prefixes) + end + end - File.open(output_path, 'w') do |f| - view = ActionView::Base.new(source_dir, :edge => @edge, :version => @version, :mobi => "kindle/#{mobi}") - view.extend(Helpers) + def copy_assets + FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir) + end - if guide =~ /\.(\w+)\.erb$/ - # Generate the special pages like the home. - # Passing a template handler in the template name is deprecated. So pass the file name without the extension. - result = view.render(:layout => layout, :formats => [$1], :file => $`) + def output_file_for(guide) + if guide.end_with?(".md") + guide.sub(/md\z/, "html") else - body = File.read(File.join(source_dir, guide)) - result = RailsGuides::Markdown.new(view, layout).render(body) - - warn_about_broken_links(result) if @warnings + guide.sub(/\.erb\z/, "") end + end - f.write(result) + def output_path_for(output_file) + File.join(@output_dir, File.basename(output_file)) end - end - def warn_about_broken_links(html) - anchors = extract_anchors(html) - check_fragment_identifiers(html, anchors) - end + def generate?(source_file, output_file) + fin = File.join(@source_dir, source_file) + fout = output_path_for(output_file) + @all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin) + end - def extract_anchors(html) - # Markdown generates headers with IDs computed from titles. - anchors = Set.new - html.scan(/ Levenshtein.distance(fragment_identifier, b) - } - puts "*** BROKEN LINK: ##{fragment_identifier}, perhaps you meant ##{guess}." + def extract_anchors(html) + # Markdown generates headers with IDs computed from titles. + anchors = Set.new + html.scan(/ Levenshtein.distance(fragment_identifier, b) + } + puts "*** BROKEN LINK: ##{fragment_identifier}, perhaps you meant ##{guess}." + end end end - end end end diff --git a/rails_guides/helpers.rb b/rails_guides/helpers.rb index 9d778f7..2a193ca 100644 --- a/rails_guides/helpers.rb +++ b/rails_guides/helpers.rb @@ -1,17 +1,13 @@ -require 'yaml' +require "yaml" module RailsGuides module Helpers def guide(name, url, options = {}, &block) - link = content_tag(:a, :href => url) { name } + link = content_tag(:a, href: url) { name } result = content_tag(:dt, link) if options[:work_in_progress] - result << content_tag(:dd, '原文撰写中', :class => 'work-in-progress') - end - - if options[:needs_translation] - result << content_tag(:dd, '待翻译', :class => 'work-in-progress') + result << content_tag(:dd, "Work in progress", class: "work-in-progress") end result << content_tag(:dd, capture(&block)) @@ -19,38 +15,34 @@ def guide(name, url, options = {}, &block) end def documents_by_section - if ENV['GUIDES_LANGUAGE'] == 'zh-CN' - @documents_by_section ||= YAML.load_file(File.expand_path('../../source/documents_zh-CN.yaml', __FILE__)) - else - @documents_by_section ||= YAML.load_file(File.expand_path('../../source/documents.yaml', __FILE__)) - end + @documents_by_section ||= YAML.load_file(File.expand_path("../../source/#{@language ? @language + '/' : ''}documents.yaml", __FILE__)) end def documents_flat - documents_by_section.flat_map {|section| section['documents']} + documents_by_section.flat_map { |section| section["documents"] } end def finished_documents(documents) - documents.reject { |document| document['work_in_progress'] } + documents.reject { |document| document["work_in_progress"] } end - def docs_for_menu(position=nil) + def docs_for_menu(position = nil) if position.nil? documents_by_section - elsif position == 'L' + elsif position == "L" documents_by_section.to(3) else documents_by_section.from(4) end end - def author(name, nick, image = 'credits_pic_blank.gif', &block) + def author(name, nick, image = "credits_pic_blank.gif", &block) image = "images/#{image}" - result = tag(:img, :src => image, :class => 'left pic', :alt => name, :width => 91, :height => 91) + result = tag(:img, src: image, class: "left pic", alt: name, width: 91, height: 91) result << content_tag(:h3, name) result << content_tag(:p, capture(&block)) - content_tag(:div, result, :class => 'clearfix', :id => nick) + content_tag(:div, result, class: "clearfix", id: nick) end def code(&block) diff --git a/rails_guides/indexer.rb b/rails_guides/indexer.rb index 89fbccb..c58b6b8 100644 --- a/rails_guides/indexer.rb +++ b/rails_guides/indexer.rb @@ -1,5 +1,5 @@ -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/string/inflections' +require "active_support/core_ext/object/blank" +require "active_support/core_ext/string/inflections" module RailsGuides class Indexer @@ -17,52 +17,52 @@ def index private - def process(string, current_level=3, counters=[1]) - s = StringScanner.new(string) + def process(string, current_level = 3, counters = [1]) + s = StringScanner.new(string) - level_hash = {} + level_hash = {} - while !s.eos? - re = %r{^h(\d)(?:\((#.*?)\))?\s*\.\s*(.*)$} - s.match?(re) - if matched = s.matched - matched =~ re - level, idx, title = $1.to_i, $2, $3.strip + while !s.eos? + re = %r{^h(\d)(?:\((#.*?)\))?\s*\.\s*(.*)$} + s.match?(re) + if matched = s.matched + matched =~ re + level, idx, title = $1.to_i, $2, $3.strip - if level < current_level - # This is needed. Go figure. - return level_hash - elsif level == current_level - index = counters.join(".") - idx ||= '#' + title_to_idx(title) + if level < current_level + # This is needed. Go figure. + return level_hash + elsif level == current_level + index = counters.join(".") + idx ||= "#" + title_to_idx(title) - raise "Parsing Fail" unless @result.sub!(matched, "h#{level}(#{idx}). #{index} #{title}") + raise "Parsing Fail" unless @result.sub!(matched, "h#{level}(#{idx}). #{index} #{title}") - key = { - :title => title, - :id => idx - } - # Recurse - counters << 1 - level_hash[key] = process(s.post_match, current_level + 1, counters) - counters.pop + key = { + title: title, + id: idx + } + # Recurse + counters << 1 + level_hash[key] = process(s.post_match, current_level + 1, counters) + counters.pop - # Increment the current level - last = counters.pop - counters << last + 1 + # Increment the current level + last = counters.pop + counters << last + 1 + end end + s.getch end - s.getch + level_hash end - level_hash - end - def title_to_idx(title) - idx = title.strip.parameterize.sub(/^\d+/, '') - if warnings && idx.blank? - puts "BLANK ID: please put an explicit ID for section #{title}, as in h5(#my-id)" + def title_to_idx(title) + idx = title.strip.parameterize.sub(/^\d+/, "") + if warnings && idx.blank? + puts "BLANK ID: please put an explicit ID for section #{title}, as in h5(#my-id)" + end + idx end - idx - end end end diff --git a/rails_guides/kindle.rb b/rails_guides/kindle.rb index 512c13a..9536d0b 100644 --- a/rails_guides/kindle.rb +++ b/rails_guides/kindle.rb @@ -1,13 +1,10 @@ #!/usr/bin/env ruby -unless `which kindlerb` - abort "Please gem install kindlerb" -end - -require 'nokogiri' -require 'fileutils' -require 'yaml' -require 'date' +require "kindlerb" +require "nokogiri" +require "fileutils" +require "yaml" +require "date" module Kindle extend self @@ -18,8 +15,8 @@ def generate(output_dir, mobi_outfile, logfile) puts "=> Using output dir: #{output_dir}" puts "=> Arranging html pages in document order" toc = File.read("toc.ncx") - doc = Nokogiri::XML(toc).xpath("//ncx:content", 'ncx' => "http://www.daisy.org/z3986/2005/ncx/") - html_pages = doc.select {|c| c[:src]}.map {|c| c[:src]}.uniq + doc = Nokogiri::XML(toc).xpath("//ncx:content", "ncx" => "http://www.daisy.org/z3986/2005/ncx/") + html_pages = doc.select { |c| c[:src] }.map { |c| c[:src] }.uniq generate_front_matter(html_pages) @@ -27,36 +24,35 @@ def generate(output_dir, mobi_outfile, logfile) generate_document_metadata(mobi_outfile) - puts "Creating MOBI document with kindlegen. This make take a while." - cmd = "kindlerb . > #{File.absolute_path logfile} 2>&1" - puts cmd - system(cmd) - puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}" + puts "Creating MOBI document with kindlegen. This may take a while." + if Kindlerb.run(output_dir) + puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}" + end end end def generate_front_matter(html_pages) frontmatter = [] - html_pages.delete_if {|x| + html_pages.delete_if { |x| if x =~ /(toc|welcome|credits|copyright).html/ frontmatter << x unless x =~ /toc/ true end } - html = frontmatter.map {|x| + html = frontmatter.map { |x| Nokogiri::HTML(File.open(x)).at("body").inner_html }.join("\n") fdoc = Nokogiri::HTML(html) fdoc.search("h3").each do |h3| - h3.name = 'h4' + h3.name = "h4" end fdoc.search("h2").each do |h2| - h2.name = 'h3' - h2['id'] = h2.inner_text.gsub(/\s/, '-') + h2.name = "h3" + h2["id"] = h2.inner_text.gsub(/\s/, "-") end add_head_section fdoc, "Front Matter" - File.open("frontmatter.html",'w') {|f| f.puts fdoc.to_html} + File.open("frontmatter.html", "w") { |f| f.puts fdoc.to_html } html_pages.unshift "frontmatter.html" end @@ -65,20 +61,20 @@ def generate_sections(html_pages) html_pages.each_with_index do |page, section_idx| FileUtils::mkdir_p("sections/%03d" % section_idx) doc = Nokogiri::HTML(File.open(page)) - title = doc.at("title").inner_text.gsub("Ruby on Rails Guides: ", '') - title = page.capitalize.gsub('.html', '') if title.strip == '' - File.open("sections/%03d/_section.txt" % section_idx, 'w') {|f| f.puts title} - doc.xpath("//h3[@id]").each_with_index do |h3,item_idx| + title = doc.at("title").inner_text.gsub("Ruby on Rails Guides: ", "") + title = page.capitalize.gsub(".html", "") if title.strip == "" + File.open("sections/%03d/_section.txt" % section_idx, "w") { |f| f.puts title } + doc.xpath("//h3[@id]").each_with_index do |h3, item_idx| subsection = h3.inner_text - content = h3.xpath("./following-sibling::*").take_while {|x| x.name != "h3"}.map {|x| x.to_html} + content = h3.xpath("./following-sibling::*").take_while { |x| x.name != "h3" }.map(&:to_html) item = Nokogiri::HTML(h3.to_html + content.join("\n")) item_path = "sections/%03d/%03d.html" % [section_idx, item_idx] add_head_section(item, subsection) item.search("img").each do |img| - img['src'] = "#{Dir.pwd}/#{img['src']}" + img["src"] = "#{Dir.pwd}/#{img['src']}" end - item.xpath("//li/p").each {|p| p.swap(p.children); p.remove} - File.open(item_path, 'w') {|f| f.puts item.to_html} + item.xpath("//li/p").each { |p| p.swap(p.children); p.remove } + File.open(item_path, "w") { |f| f.puts item.to_html } end end end @@ -87,21 +83,21 @@ def generate_document_metadata(mobi_outfile) puts "=> Generating _document.yml" x = Nokogiri::XML(File.open("rails_guides.opf")).remove_namespaces! cover_jpg = "#{Dir.pwd}/images/rails_guides_kindle_cover.jpg" - cover_gif = cover_jpg.sub(/jpg$/, 'gif') + cover_gif = cover_jpg.sub(/jpg$/, "gif") puts `convert #{cover_jpg} #{cover_gif}` document = { - 'doc_uuid' => x.at("package")['unique-identifier'], - 'title' => x.at("title").inner_text.gsub(/\(.*$/, " v2"), - 'publisher' => x.at("publisher").inner_text, - 'author' => x.at("creator").inner_text, - 'subject' => x.at("subject").inner_text, - 'date' => x.at("date").inner_text, - 'cover' => cover_gif, - 'masthead' => nil, - 'mobi_outfile' => mobi_outfile + "doc_uuid" => x.at("package")["unique-identifier"], + "title" => x.at("title").inner_text.gsub(/\(.*$/, " v2"), + "publisher" => x.at("publisher").inner_text, + "author" => x.at("creator").inner_text, + "subject" => x.at("subject").inner_text, + "date" => x.at("date").inner_text, + "cover" => cover_gif, + "masthead" => nil, + "mobi_outfile" => mobi_outfile } puts document.to_yaml - File.open("_document.yml", 'w'){|f| f.puts document.to_yaml} + File.open("_document.yml", "w") { |f| f.puts document.to_yaml } end def add_head_section(doc, title) @@ -110,9 +106,9 @@ def add_head_section(doc, title) title_node.content = title title_node.parent = head css = Nokogiri::XML::Node.new "link", doc - css['rel'] = 'stylesheet' - css['type'] = 'text/css' - css['href'] = "#{Dir.pwd}/stylesheets/kindle.css" + css["rel"] = "stylesheet" + css["type"] = "text/css" + css["href"] = "#{Dir.pwd}/stylesheets/kindle.css" css.parent = head doc.at("body").before head end diff --git a/rails_guides/levenshtein.rb b/rails_guides/levenshtein.rb index abd4efe..40c6a5c 100644 --- a/rails_guides/levenshtein.rb +++ b/rails_guides/levenshtein.rb @@ -1,27 +1,31 @@ module RailsGuides module Levenshtein - # Based on the pseudocode in http://en.wikipedia.org/wiki/Levenshtein_distance - def self.distance(s1, s2) + # This code is based directly on the Text gem implementation. + # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. + # + # Returns a value representing the "cost" of transforming str1 into str2 + def self.distance(str1, str2) s = str1 t = str2 n = s.length m = t.length - max = n/2 return m if (0 == n) return n if (0 == m) - return n if (n - m).abs > max d = (0..m).to_a x = nil - str1.each_char.each_with_index do |char1,i| - e = i+1 + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint - str2.each_char.each_with_index do |char2,j| + str1.each_codepoint.with_index do |char1, i| + e = i + 1 + + str2_codepoint_enumerable.with_index do |char2, j| cost = (char1 == char2) ? 0 : 1 x = [ - d[j+1] + 1, # insertion + d[j + 1] + 1, # insertion e + 1, # deletion d[j] + cost # substitution ].min diff --git a/rails_guides/markdown.rb b/rails_guides/markdown.rb index 6c7b98d..bf2cc82 100644 --- a/rails_guides/markdown.rb +++ b/rails_guides/markdown.rb @@ -1,17 +1,17 @@ -# encoding: utf-8 - -require 'redcarpet' -require 'nokogiri' -require 'rails_guides/markdown/renderer' +require "redcarpet" +require "nokogiri" +require "rails_guides/markdown/renderer" module RailsGuides class Markdown - def initialize(view, layout) - @view = view - @layout = layout + def initialize(view:, layout:, edge:, version:) + @view = view + @layout = layout + @edge = edge + @version = version @index_counter = Hash.new(0) - @raw_header = '' - @node_ids = {} + @raw_header = "" + @node_ids = {} end def render(body) @@ -39,11 +39,7 @@ def dom_id(nodes) @node_ids[new_node_id] = duplicate_nodes end - # begin - dom_id = "#{nodes[-2][:id]}-#{dom_id}" - # rescue NoMethodError - # # ... - # end + dom_id = "#{nodes[-2][:id]}-#{dom_id}" end @node_ids[dom_id] = nodes @@ -51,18 +47,23 @@ def dom_id(nodes) end def dom_id_text(text) - text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/\s+/, '-') + escaped_chars = Regexp.escape('\\/`*_{}[]()#+-.!:,;|&<>^~=\'"') + + text.downcase.gsub(/\?/, "-questionmark") + .gsub(/!/, "-bang") + .gsub(/[#{escaped_chars}]+/, " ").strip + .gsub(/\s+/, "-") end def engine - @engine ||= Redcarpet::Markdown.new(Renderer, { + @engine ||= Redcarpet::Markdown.new(Renderer, no_intra_emphasis: true, fenced_code_blocks: true, autolink: true, strikethrough: true, superscript: true, tables: true - }) + ) end def extract_raw_header_and_body @@ -88,15 +89,15 @@ def generate_structure doc.children.each do |node| if node.name =~ /^h[3-6]$/ case node.name - when 'h3' + when "h3" hierarchy = [node] @headings_for_index << [1, node, node.inner_html] - when 'h4' + when "h4" hierarchy = hierarchy[0, 1] + [node] @headings_for_index << [2, node, node.inner_html] - when 'h5' + when "h5" hierarchy = hierarchy[0, 2] + [node] - when 'h6' + when "h6" hierarchy = hierarchy[0, 3] + [node] end @@ -110,7 +111,7 @@ def generate_structure def generate_index if @headings_for_index.present? - raw_index = '' + raw_index = "" @headings_for_index.each do |level, node, label| if level == 1 raw_index += "1. [#{label}](##{node[:id]})\n" @@ -120,7 +121,7 @@ def generate_index end @index = Nokogiri::HTML.fragment(engine.render(raw_index)).tap do |doc| - doc.at('ol')[:class] = 'chapters' + doc.at("ol")[:class] = "chapters" end.to_html @index = <<-INDEX.html_safe @@ -134,9 +135,9 @@ def generate_index def generate_title if heading = Nokogiri::HTML.fragment(@header).at(:h2) - @title = "#{heading.text} — Ruby on Rails 指南" + @title = "#{heading.text} — Ruby on Rails Guides" else - @title = "Ruby on Rails 指南" + @title = "Ruby on Rails Guides" end end @@ -160,7 +161,7 @@ def render_page @view.content_for(:header_section) { @header } @view.content_for(:page_title) { @title } @view.content_for(:index_section) { @index } - @view.render(:layout => @layout, :text => @body) + @view.render(layout: @layout, html: @body.html_safe) end end end diff --git a/rails_guides/markdown/renderer.rb b/rails_guides/markdown/renderer.rb index 2eb7ca1..9d43c10 100644 --- a/rails_guides/markdown/renderer.rb +++ b/rails_guides/markdown/renderer.rb @@ -1,9 +1,7 @@ module RailsGuides class Markdown class Renderer < Redcarpet::Render::HTML - def initialize(options={}) - super - end + cattr_accessor :edge, :version def block_code(code, language) <<-HTML @@ -15,16 +13,29 @@ def block_code(code, language) HTML end + def link(url, title, content) + if url.start_with?("http://api.rubyonrails.org/") + %(#{content}) + elsif title + %(#{content}) + else + %(#{content}) + end + end + def header(text, header_level) - # Always increase the heading level by, so we can use h1, h2 heading in the document + # Always increase the heading level by 1, so we can use h1, h2 heading in the document header_level += 1 %(#{text}) end def paragraph(text) - if text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)/ + if text =~ %r{^NOTE:\s+Defined\s+in\s+(.*?)\.?$} + %(

Defined in #{$1}.

) + elsif text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/ convert_notes(text) + elsif text.include?("DO NOT READ THIS FILE ON GITHUB") elsif text =~ /^\[(\d+)\]:<\/sup> (.+)$/ linkback = %(#{$1}) %(

#{linkback} #{$2}

) @@ -45,14 +56,14 @@ def convert_footnotes(text) def brush_for(code_type) case code_type - when 'ruby', 'sql', 'plain' - code_type - when 'erb' - 'ruby; html-script: true' - when 'html' - 'xml' # html is understood, but there are .xml rules in the CSS - else - 'plain' + when "ruby", "sql", "plain" + code_type + when "erb", "html+erb" + "ruby; html-script: true" + when "html" + "xml" # HTML is understood, but there are .xml rules in the CSS + else + "plain" end end @@ -66,17 +77,45 @@ def convert_notes(body) # as a list item, but as a paragraph starting with a plain # asterisk. body.gsub(/^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)(\n(?=\n)|\Z)/m) do - css_class = case $1 - when 'CAUTION', 'IMPORTANT' - 'warning' - when 'TIP' - 'info' - else - $1.downcase - end + css_class = \ + case $1 + when "CAUTION", "IMPORTANT" + "warning" + when "TIP" + "info" + else + $1.downcase + end %(

#{$2.strip}

) end end + + def github_file_url(https://github.com/file_path) + tree = version || edge + + root = file_path[%r{(.+)/}, 1] + path = \ + case root + when "abstract_controller", "action_controller", "action_dispatch" + "actionpack/lib/#{file_path}" + when /\A(action|active)_/ + "#{root.sub("_", "")}/lib/#{file_path}" + else + file_path + end + + "https://github.com/rails/rails/tree/#{tree}/#{path}" + end + + def api_link(url) + if url =~ %r{http://api\.rubyonrails\.org/v\d+\.} + url + elsif edge + url.sub("api", "edgeapi") + else + url.sub(/(?<=\.org)/, "/#{version}") + end + end end end end diff --git a/source/2_2_release_notes.md b/source/2_2_release_notes.md index 522f628..ac5833e 100644 --- a/source/2_2_release_notes.md +++ b/source/2_2_release_notes.md @@ -1,7 +1,9 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 2.2 Release Notes =============================== -Rails 2.2 delivers a number of new and improved features. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/master) in the main Rails repository on GitHub. +Rails 2.2 delivers a number of new and improved features. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/2-2-stable) in the main Rails repository on GitHub. Along with Rails, 2.2 marks the launch of the [Ruby on Rails Guides](http://guides.rubyonrails.org/), the first results of the ongoing [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide). This site will deliver high-quality documentation of the major features of Rails. @@ -19,8 +21,8 @@ Rails 2.2 supplies an easy system for internationalization (or i18n, for those o * Lead Contributors: Rails i18 Team * More information : * [Official Rails i18 website](http://rails-i18n.org) - * [Finally. Ruby on Rails gets internationalized](http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized) - * [Localizing Rails : Demo application](http://github.com/clemens/i18n_demo_app) + * [Finally. Ruby on Rails gets internationalized](https://web.archive.org/web/20140407075019/http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized) + * [Localizing Rails : Demo application](https://github.com/clemens/i18n_demo_app) ### Compatibility with Ruby 1.9 and JRuby @@ -32,7 +34,7 @@ Documentation The internal documentation of Rails, in the form of code comments, has been improved in numerous places. In addition, the [Ruby on Rails Guides](http://guides.rubyonrails.org/) project is the definitive source for information on major Rails components. In its first official release, the Guides page includes: * [Getting Started with Rails](getting_started.html) -* [Rails Database Migrations](migrations.html) +* [Rails Database Migrations](active_record_migrations.html) * [Active Record Associations](association_basics.html) * [Active Record Query Interface](active_record_querying.html) * [Layouts and Rendering in Rails](layouts_and_rendering.html) @@ -43,7 +45,6 @@ The internal documentation of Rails, in the form of code comments, has been impr * [A Guide to Testing Rails Applications](testing.html) * [Securing Rails Applications](security.html) * [Debugging Rails Applications](debugging_rails_applications.html) -* [Performance Testing Rails Applications](performance_testing.html) * [The Basics of Creating Rails Plugins](plugins.html) All told, the Guides provide tens of thousands of words of guidance for beginning and intermediate Rails developers. @@ -57,7 +58,7 @@ rake doc:guides This will put the guides inside `Rails.root/doc/guides` and you may start surfing straight away by opening `Rails.root/doc/guides/index.html` in your favourite browser. * Lead Contributors: [Rails Documentation Team](credits.html) -* Major contributions from [Xavier Noria":http://advogato.org/person/fxn/diary.html and "Hongli Lai](http://izumi.plan99.net/blog/.) +* Major contributions from [Xavier Noria](http://advogato.org/person/fxn/diary.html) and [Hongli Lai](http://izumi.plan99.net/blog/). * More information: * [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide) * [Help improve Rails documentation on Git branch](http://weblog.rubyonrails.org/2008/5/2/help-improve-rails-documentation-on-git-branch) @@ -144,7 +145,7 @@ development: * Lead Contributor: [Nick Sieger](http://blog.nicksieger.com/) * More information: - * [What's New in Edge Rails: Connection Pools](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-connection-pools) + * [What's New in Edge Rails: Connection Pools](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-connection-pools) ### Hashes for Join Table Conditions @@ -164,7 +165,7 @@ Product.all(:joins => :photos, :conditions => { :photos => { :copyright => false ``` * More information: - * [What's New in Edge Rails: Easy Join Table Conditions](http://ryandaigle.com/articles/2008/7/7/what-s-new-in-edge-rails-easy-join-table-conditions) + * [What's New in Edge Rails: Easy Join Table Conditions](http://archives.ryandaigle.com/articles/2008/7/7/what-s-new-in-edge-rails-easy-join-table-conditions) ### New Dynamic Finders @@ -237,7 +238,7 @@ This will enable recognition of (among others) these routes: * Lead Contributor: [S. Brent Faulkner](http://www.unwwwired.net/) * More information: * [Rails Routing from the Outside In](routing.html#nested-resources) - * [What's New in Edge Rails: Shallow Routes](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-shallow-routes) + * [What's New in Edge Rails: Shallow Routes](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-shallow-routes) ### Method Arrays for Member or Collection Routes @@ -285,7 +286,7 @@ Action Mailer Action Mailer now supports mailer layouts. You can make your HTML emails as pretty as your in-browser views by supplying an appropriately-named layout - for example, the `CustomerMailer` class expects to use `layouts/customer_mailer.html.erb`. * More information: - * [What's New in Edge Rails: Mailer Layouts](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-mailer-layouts) + * [What's New in Edge Rails: Mailer Layouts](http://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-mailer-layouts) Action Mailer now offers built-in support for GMail's SMTP servers, by turning on STARTTLS automatically. This requires Ruby 1.8.7 to be installed. @@ -319,7 +320,7 @@ Other features of memoization include `unmemoize`, `unmemoize_all`, and `memoize * Lead Contributor: [Josh Peek](http://joshpeek.com/) * More information: - * [What's New in Edge Rails: Easy Memoization](http://ryandaigle.com/articles/2008/7/16/what-s-new-in-edge-rails-memoization) + * [What's New in Edge Rails: Easy Memoization](http://archives.ryandaigle.com/articles/2008/7/16/what-s-new-in-edge-rails-memoization) * [Memo-what? A Guide to Memoization](http://www.railway.at/articles/2008/09/20/a-guide-to-memoization) ### each_with_object @@ -387,9 +388,9 @@ To avoid deployment issues and make Rails applications more self-contained, it's You can unpack or install a single gem by specifying `GEM=_gem_name_` on the command line. -* Lead Contributor: [Matt Jones](http://github.com/al2o3cr) +* Lead Contributor: [Matt Jones](https://github.com/al2o3cr) * More information: - * [What's New in Edge Rails: Gem Dependencies](http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies) + * [What's New in Edge Rails: Gem Dependencies](http://archives.ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies) * [Rails 2.1.2 and 2.2RC1: Update Your RubyGems](http://afreshcup.com/2008/10/25/rails-212-and-22rc1-update-your-rubygems/) * [Detailed discussion on Lighthouse](http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1128) @@ -409,7 +410,7 @@ Deprecated A few pieces of older code are deprecated in this release: * `Rails::SecretKeyGenerator` has been replaced by `ActiveSupport::SecureRandom` -* `render_component` is deprecated. There's a [render_components plugin](http://github.com/rails/render_component/tree/master) available if you need this functionality. +* `render_component` is deprecated. There's a [render_components plugin](https://github.com/rails/render_component/tree/master) available if you need this functionality. * Implicit local assignments when rendering partials has been deprecated. ```ruby diff --git a/source/2_3_release_notes.md b/source/2_3_release_notes.md index 52eeb4c..6976848 100644 --- a/source/2_3_release_notes.md +++ b/source/2_3_release_notes.md @@ -1,7 +1,9 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 2.3 Release Notes =============================== -Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/master) in the main Rails repository on GitHub or review the `CHANGELOG` files for the individual Rails components. +Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/2-3-stable) in the main Rails repository on GitHub or review the `CHANGELOG` files for the individual Rails components. -------------------------------------------------------------------------------- @@ -52,7 +54,7 @@ Documentation The [Ruby on Rails guides](http://guides.rubyonrails.org/) project has published several additional guides for Rails 2.3. In addition, a [separate site](http://edgeguides.rubyonrails.org/) maintains updated copies of the Guides for Edge Rails. Other documentation efforts include a relaunch of the [Rails wiki](http://newwiki.rubyonrails.org/) and early planning for a Rails Book. -* More Information: [Rails Documentation Projects](http://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects.) +* More Information: [Rails Documentation Projects](http://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects) Ruby 1.9.1 Support ------------------ @@ -123,14 +125,14 @@ Order.scoped_by_customer_id(12).scoped_by_status("open") There's nothing to define to use dynamic scopes: they just work. * Lead Contributor: [Yaroslav Markin](http://evilmartians.com/) -* More Information: [What's New in Edge Rails: Dynamic Scope Methods](http://ryandaigle.com/articles/2008/12/29/what-s-new-in-edge-rails-dynamic-scope-methods.) +* More Information: [What's New in Edge Rails: Dynamic Scope Methods](http://archives.ryandaigle.com/articles/2008/12/29/what-s-new-in-edge-rails-dynamic-scope-methods) ### Default Scopes Rails 2.3 will introduce the notion of _default scopes_ similar to named scopes, but applying to all named scopes or find methods within the model. For example, you can write `default_scope :order => 'name ASC'` and any time you retrieve records from that model they'll come out sorted by name (unless you override the option, of course). * Lead Contributor: Paweł Kondzior -* More Information: [What's New in Edge Rails: Default Scoping](http://ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping) +* More Information: [What's New in Edge Rails: Default Scoping](http://archives.ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping) ### Batch Processing @@ -156,7 +158,7 @@ Note that you should only use this method for batch processing: for small number * More Information (at that point the convenience method was called just `each`): * [Rails 2.3: Batch Finding](http://afreshcup.com/2009/02/23/rails-23-batch-finding/) - * [What's New in Edge Rails: Batched Find](http://ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find) + * [What's New in Edge Rails: Batched Find](http://archives.ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find) ### Multiple Conditions for Callbacks @@ -177,7 +179,7 @@ developers = Developer.find(:all, :group => "salary", :having => "sum(salary) > 10000", :select => "salary") ``` -* Lead Contributor: [Emilio Tagua](http://github.com/miloops) +* Lead Contributor: [Emilio Tagua](https://github.com/miloops) ### Reconnecting MySQL Connections @@ -185,7 +187,7 @@ MySQL supports a reconnect flag in its connections - if set to true, then the cl * Lead Contributor: [Dov Murik](http://twitter.com/dubek) * More information: - * [Controlling Automatic Reconnection Behavior](http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html) + * [Controlling Automatic Reconnection Behavior](http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html) * [MySQL auto-reconnect revisited](http://groups.google.com/group/rubyonrails-core/browse_thread/thread/49d2a7e9c96cb9f4) ### Other Active Record Changes @@ -233,7 +235,7 @@ If you're one of the people who has always been bothered by the special-case nam * More Information: * [The Death of Application.rb](http://afreshcup.com/2008/11/17/rails-2x-the-death-of-applicationrb/) - * [What's New in Edge Rails: Application.rb Duality is no More](http://ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more) + * [What's New in Edge Rails: Application.rb Duality is no More](http://archives.ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more) ### HTTP Digest Authentication Support @@ -259,7 +261,7 @@ end ``` * Lead Contributor: [Gregg Kellogg](http://www.kellogg-assoc.com/) -* More Information: [What's New in Edge Rails: HTTP Digest Authentication](http://ryandaigle.com/articles/2009/1/30/what-s-new-in-edge-rails-http-digest-authentication) +* More Information: [What's New in Edge Rails: HTTP Digest Authentication](http://archives.ryandaigle.com/articles/2009/1/30/what-s-new-in-edge-rails-http-digest-authentication) ### More Efficient Routing @@ -375,8 +377,8 @@ You can write this view in Rails 2.3: * Lead Contributor: [Eloy Duran](http://superalloy.nl/) * More Information: * [Nested Model Forms](http://weblog.rubyonrails.org/2009/1/26/nested-model-forms) - * [complex-form-examples](http://github.com/alloy/complex-form-examples) - * [What's New in Edge Rails: Nested Object Forms](http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes) + * [complex-form-examples](https://github.com/alloy/complex-form-examples) + * [What's New in Edge Rails: Nested Object Forms](http://archives.ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes) ### Smart Rendering of Partials @@ -392,7 +394,7 @@ render @article render @articles ``` -* More Information: [What's New in Edge Rails: render Stops Being High-Maintenance](http://ryandaigle.com/articles/2008/11/20/what-s-new-in-edge-rails-render-stops-being-high-maintenance) +* More Information: [What's New in Edge Rails: render Stops Being High-Maintenance](http://archives.ryandaigle.com/articles/2008/11/20/what-s-new-in-edge-rails-render-stops-being-high-maintenance) ### Prompts for Date Select Helpers @@ -419,7 +421,7 @@ You're likely familiar with Rails' practice of adding timestamps to static asset Asset hosts get more flexible in edge Rails with the ability to declare an asset host as a specific object that responds to a call. This allows you to implement any complex logic you need in your asset hosting. -* More Information: [asset-hosting-with-minimum-ssl](http://github.com/dhh/asset-hosting-with-minimum-ssl/tree/master) +* More Information: [asset-hosting-with-minimum-ssl](https://github.com/dhh/asset-hosting-with-minimum-ssl/tree/master) ### grouped_options_for_select Helper Method @@ -496,7 +498,7 @@ Active Support has a few interesting changes, including the introduction of `Obj A lot of folks have adopted the notion of using try() to attempt operations on objects. It's especially helpful in views where you can avoid nil-checking by writing code like `<%= @person.try(:name) %>`. Well, now it's baked right into Rails. As implemented in Rails, it raises `NoMethodError` on private methods and always returns `nil` if the object is nil. -* More Information: [try()](http://ozmm.org/posts/try.html.) +* More Information: [try()](http://ozmm.org/posts/try.html) ### Object#tap Backport @@ -531,7 +533,7 @@ If you look up the spec on the "json.org" site, you'll discover that all keys in ### Other Active Support Changes * You can use `Enumerable#none?` to check that none of the elements match the supplied block. -* If you're using Active Support [delegates](http://afreshcup.com/2008/10/19/coming-in-rails-22-delegate-prefixes/,) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil. +* If you're using Active Support [delegates](http://afreshcup.com/2008/10/19/coming-in-rails-22-delegate-prefixes/) the new `:allow_nil` option lets you return `nil` instead of raising an exception when the target object is nil. * `ActiveSupport::OrderedHash`: now implements `each_key` and `each_value`. * `ActiveSupport::MessageEncryptor` provides a simple way to encrypt information for storage in an untrusted location (like cookies). * Active Support's `from_xml` no longer depends on XmlSimple. Instead, Rails now includes its own XmlMini implementation, with just the functionality that it requires. This lets Rails dispense with the bundled copy of XmlSimple that it's been carting around. @@ -553,11 +555,11 @@ Rails Metal is a new mechanism that provides superfast endpoints inside of your * [Introducing Rails Metal](http://weblog.rubyonrails.org/2008/12/17/introducing-rails-metal) * [Rails Metal: a micro-framework with the power of Rails](http://soylentfoo.jnewland.com/articles/2008/12/16/rails-metal-a-micro-framework-with-the-power-of-rails-m) * [Metal: Super-fast Endpoints within your Rails Apps](http://www.railsinside.com/deployment/180-metal-super-fast-endpoints-within-your-rails-apps.html) - * [What's New in Edge Rails: Rails Metal](http://ryandaigle.com/articles/2008/12/18/what-s-new-in-edge-rails-rails-metal) + * [What's New in Edge Rails: Rails Metal](http://archives.ryandaigle.com/articles/2008/12/18/what-s-new-in-edge-rails-rails-metal) ### Application Templates -Rails 2.3 incorporates Jeremy McAnally's [rg](http://github.com/jeremymcanally/rg/tree/master) application generator. What this means is that we now have template-based application generation built right into Rails; if you have a set of plugins you include in every application (among many other use cases), you can just set up a template once and use it over and over again when you run the `rails` command. There's also a rake task to apply a template to an existing application: +Rails 2.3 incorporates Jeremy McAnally's [rg](https://github.com/jm/rg) application generator. What this means is that we now have template-based application generation built right into Rails; if you have a set of plugins you include in every application (among many other use cases), you can just set up a template once and use it over and over again when you run the `rails` command. There's also a rake task to apply a template to an existing application: ``` rake rails:template LOCATION=~/template.rb @@ -570,7 +572,7 @@ This will layer the changes from the template on top of whatever code the projec ### Quieter Backtraces -Building on Thoughtbot's [Quiet Backtrace](https://github.com/thoughtbot/quietbacktrace) plugin, which allows you to selectively remove lines from `Test::Unit` backtraces, Rails 2.3 implements `ActiveSupport::BacktraceCleaner` and `Rails::BacktraceCleaner` in core. This supports both filters (to perform regex-based substitutions on backtrace lines) and silencers (to remove backtrace lines entirely). Rails automatically adds silencers to get rid of the most common noise in a new application, and builds a `config/backtrace_silencers.rb` file to hold your own additions. This feature also enables prettier printing from any gem in the backtrace. +Building on thoughtbot's [Quiet Backtrace](https://github.com/thoughtbot/quietbacktrace) plugin, which allows you to selectively remove lines from `Test::Unit` backtraces, Rails 2.3 implements `ActiveSupport::BacktraceCleaner` and `Rails::BacktraceCleaner` in core. This supports both filters (to perform regex-based substitutions on backtrace lines) and silencers (to remove backtrace lines entirely). Rails automatically adds silencers to get rid of the most common noise in a new application, and builds a `config/backtrace_silencers.rb` file to hold your own additions. This feature also enables prettier printing from any gem in the backtrace. ### Faster Boot Time in Development Mode with Lazy Loading/Autoload @@ -603,8 +605,8 @@ Deprecated A few pieces of older code are deprecated in this release: -* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](http://github.com/rails/irs_process_scripts/tree) plugin. -* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master). +* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](https://github.com/rails/irs_process_scripts/tree) plugin. +* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](https://github.com/rails/render_component/tree/master). * Support for Rails components has been removed. * If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back. * `ActionController::Base#session_enabled?` is deprecated because sessions are lazy-loaded now. @@ -612,7 +614,7 @@ A few pieces of older code are deprecated in this release: * Some integration test helpers have been removed. `response.headers["Status"]` and `headers["Status"]` will no longer return anything. Rack does not allow "Status" in its return headers. However you can still use the `status` and `status_message` helpers. `response.headers["cookie"]` and `headers["cookie"]` will no longer return any CGI cookies. You can inspect `headers["Set-Cookie"]` to see the raw cookie header or use the `cookies` helper to get a hash of the cookies sent to the client. * `formatted_polymorphic_url` is deprecated. Use `polymorphic_url` with `:format` instead. * The `:http_only` option in `ActionController::Response#set_cookie` has been renamed to `:httponly`. -* The `:connector` and `:skip_last_comma` options of `to_sentence` have been replaced by `:words_connnector`, `:two_words_connector`, and `:last_word_connector` options. +* The `:connector` and `:skip_last_comma` options of `to_sentence` have been replaced by `:words_connector`, `:two_words_connector`, and `:last_word_connector` options. * Posting a multipart form with an empty `file_field` control used to submit an empty string to the controller. Now it submits a nil, due to differences between Rack's multipart parser and the old Rails one. Credits diff --git a/source/3_0_release_notes.md b/source/3_0_release_notes.md index aec3a38..517b38b 100644 --- a/source/3_0_release_notes.md +++ b/source/3_0_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 3.0 Release Notes =============================== @@ -15,7 +17,7 @@ Even if you don't give a hoot about any of our internal cleanups, Rails 3.0 is g On top of all that, we've tried our best to deprecate the old APIs with nice warnings. That means that you can move your existing application to Rails 3 without immediately rewriting all your old code to the latest best practices. -These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/master) in the main Rails repository on GitHub. +These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/3-0-stable) in the main Rails repository on GitHub. -------------------------------------------------------------------------------- @@ -61,7 +63,7 @@ The `config.gem` method is gone and has been replaced by using `bundler` and a ` ### Upgrade Process -To help with the upgrade process, a plugin named [Rails Upgrade](http://github.com/rails/rails_upgrade) has been created to automate part of it. +To help with the upgrade process, a plugin named [Rails Upgrade](https://github.com/rails/rails_upgrade) has been created to automate part of it. Simply install the plugin, then run `rake rails:upgrade:check` to check your app for pieces that need to be updated (with links to information on how to update them). It also offers a task to generate a `Gemfile` based on your current `config.gem` calls and a task to generate a new routes file from your current one. To get the plugin, simply run the following: @@ -84,9 +86,9 @@ $ cd myapp ### Vendoring Gems -Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](http://github.com/carlhuda/bundler,) which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. +Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/bundler/bundler) which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: - [bundler homepage](http://gembundler.com) +More information: - [bundler homepage](http://bundler.io/) ### Living on the Edge @@ -136,14 +138,14 @@ More Information: - [Rails Edge Architecture](http://yehudakatz.com/2009/06/11/r ### Arel Integration -[Arel](http://github.com/brynary/arel) (or Active Relation) has been taken on as the underpinnings of Active Record and is now required for Rails. Arel provides an SQL abstraction that simplifies out Active Record and provides the underpinnings for the relation functionality in Active Record. +[Arel](https://github.com/brynary/arel) (or Active Relation) has been taken on as the underpinnings of Active Record and is now required for Rails. Arel provides an SQL abstraction that simplifies out Active Record and provides the underpinnings for the relation functionality in Active Record. -More information: - [Why I wrote Arel](http://magicscalingsprinkles.wordpress.com/2010/01/28/why-i-wrote-arel/.) +More information: - [Why I wrote Arel](https://web.archive.org/web/20120718093140/http://magicscalingsprinkles.wordpress.com/2010/01/28/why-i-wrote-arel/) ### Mail Extraction -Action Mailer ever since its beginnings has had monkey patches, pre parsers and even delivery and receiver agents, all in addition to having TMail vendored in the source tree. Version 3 changes that with all email message related functionality abstracted out to the [Mail](http://github.com/mikel/mail) gem. This again reduces code duplication and helps create definable boundaries between Action Mailer and the email parser. +Action Mailer ever since its beginnings has had monkey patches, pre parsers and even delivery and receiver agents, all in addition to having TMail vendored in the source tree. Version 3 changes that with all email message related functionality abstracted out to the [Mail](https://github.com/mikel/mail) gem. This again reduces code duplication and helps create definable boundaries between Action Mailer and the email parser. More information: - [New Action Mailer API in Rails 3](http://lindsaar.net/2010/1/26/new-actionmailer-api-in-rails-3) @@ -153,13 +155,13 @@ Documentation The documentation in the Rails tree is being updated with all the API changes, additionally, the [Rails Edge Guides](http://edgeguides.rubyonrails.org/) are being updated one by one to reflect the changes in Rails 3.0. The guides at [guides.rubyonrails.org](http://guides.rubyonrails.org/) however will continue to contain only the stable version of Rails (at this point, version 2.3.5, until 3.0 is released). -More Information: - [Rails Documentation Projects](http://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects.) +More Information: - [Rails Documentation Projects](http://weblog.rubyonrails.org/2009/1/15/rails-documentation-projects) Internationalization -------------------- -A large amount of work has been done with I18n support in Rails 3, including the latest [I18n](http://github.com/svenfuchs/i18n) gem supplying many speed improvements. +A large amount of work has been done with I18n support in Rails 3, including the latest [I18n](https://github.com/svenfuchs/i18n) gem supplying many speed improvements. * I18n for any object - I18n behavior can be added to any object by including `ActiveModel::Translation` and `ActiveModel::Validations`. There is also an `errors.messages` fallback for translations. * Attributes can have default translations. @@ -211,7 +213,6 @@ Railties now deprecates: More information: * [Discovering Rails 3 generators](http://blog.plataformatec.com.br/2010/01/discovering-rails-3-generators) -* [Making Generators for Rails 3 with Thor](http://caffeinedd.com/guides/331-making-generators-for-rails-3-with-thor) * [The Rails Module (in Rails 3)](http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/) Action Pack @@ -248,7 +249,7 @@ Deprecations: More Information: -* [Render Options in Rails 3](http://www.engineyard.com/blog/2010/render-options-in-rails-3/) +* [Render Options in Rails 3](https://blog.engineyard.com/2010/render-options-in-rails-3) * [Three reasons to love ActionController::Responder](http://weblog.rubyonrails.org/2009/8/31/three-reasons-love-responder) @@ -298,7 +299,7 @@ Deprecations More Information: * [The Rails 3 Router: Rack it Up](http://yehudakatz.com/2009/12/26/the-rails-3-router-rack-it-up/) -* [Revamped Routes in Rails 3](http://rizwanreza.com/2009/12/20/revamped-routes-in-rails-3) +* [Revamped Routes in Rails 3](https://medium.com/fusion-of-thoughts/revamped-routes-in-rails-3-b6d00654e5b0) * [Generic Actions in Rails 3](http://yehudakatz.com/2009/12/20/generic-actions-in-rails-3/) @@ -308,7 +309,7 @@ More Information: Major re-write was done in the Action View helpers, implementing Unobtrusive JavaScript (UJS) hooks and removing the old inline AJAX commands. This enables Rails to use any compliant UJS driver to implement the UJS hooks in the helpers. -What this means is that all previous `remote_` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example: +What this means is that all previous `remote_` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](https://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example: ```ruby form_for @post, :remote => true @@ -545,7 +546,7 @@ These are the main changes in Active Support: * `String#to_time` and `String#to_datetime` handle fractional seconds. * Added support to new callbacks for around filter object that respond to `:before` and `:after` used in before and after callbacks. * The `ActiveSupport::OrderedHash#to_a` method returns an ordered set of arrays. Matches Ruby 1.9's `Hash#to_a`. -* `MissingSourceFile` exists as a constant but it is now just equals to `LoadError`. +* `MissingSourceFile` exists as a constant but it is now just equal to `LoadError`. * Added `Class#class_attribute`, to be able to declare a class-level attribute whose value is inheritable and overwritable by subclasses. * Finally removed `DeprecatedCallbacks` in `ActiveRecord::Associations`. * `Object#metaclass` is now `Kernel#singleton_class` to match Ruby. @@ -574,7 +575,7 @@ The following methods have been removed because they are no longer used in the f Action Mailer ------------- -Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably. +Action Mailer has been given a new API with TMail being replaced out with the new [Mail](https://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably. * All mailers are now in `app/mailers` by default. * Can now send email using new API with three methods: `attachments`, `headers` and `mail`. diff --git a/source/3_1_release_notes.md b/source/3_1_release_notes.md index 7626296..fd90cf9 100644 --- a/source/3_1_release_notes.md +++ b/source/3_1_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 3.1 Release Notes =============================== @@ -8,7 +10,10 @@ Highlights in Rails 3.1: * Assets Pipeline * jQuery as the default JavaScript library -This release notes cover the major changes, but don't include every little bug fix and change. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/master) in the main Rails repository on GitHub. +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/3-1-stable) in the main Rails +repository on GitHub. -------------------------------------------------------------------------------- @@ -94,7 +99,7 @@ gem 'jquery-rails' # config.assets.manifest = YOUR_PATH # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - # config.assets.precompile `= %w( search.js ) + # config.assets.precompile `= %w( admin.js admin.css ) # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. @@ -146,7 +151,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: - [bundler homepage](http://gembundler.com) +More information: - [bundler homepage](http://bundler.io/) ### Living on the Edge @@ -169,7 +174,7 @@ Rails Architectural Changes The major change in Rails 3.1 is the Assets Pipeline. It makes CSS and JavaScript first-class code citizens and enables proper organization, including use in plugins and engines. -The assets pipeline is powered by [Sprockets](https://github.com/sstephenson/sprockets) and is covered in the [Asset Pipeline](asset_pipeline.html) guide. +The assets pipeline is powered by [Sprockets](https://github.com/rails/sprockets) and is covered in the [Asset Pipeline](asset_pipeline.html) guide. ### HTTP Streaming @@ -194,7 +199,7 @@ Railties * jQuery is the new default JavaScript library. -* jQuery and Prototype are no longer vendored and is provided from now on by the jquery-rails and prototype-rails gems. +* jQuery and Prototype are no longer vendored and is provided from now on by the `jquery-rails` and `prototype-rails` gems. * The application generator accepts an option `-j` which can be an arbitrary string. If passed "foo", the gem "foo-rails" is added to the `Gemfile`, and the application JavaScript manifest requires "foo" and "foo_ujs". Currently only "prototype-rails" and "jquery-rails" exist and provide those files via the asset pipeline. @@ -553,4 +558,4 @@ Credits See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them. -Rails 3.1 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev.) +Rails 3.1 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev) diff --git a/source/3_2_release_notes.md b/source/3_2_release_notes.md index 2416e1a..f16d509 100644 --- a/source/3_2_release_notes.md +++ b/source/3_2_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 3.2 Release Notes =============================== @@ -8,7 +10,10 @@ Highlights in Rails 3.2: * Automatic Query Explains * Tagged Logging -These release notes cover the major changes, but do not include each bug-fix and changes. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/3-2-stable) in the main Rails repository on GitHub. +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/3-2-stable) in the main Rails +repository on GitHub. -------------------------------------------------------------------------------- @@ -76,7 +81,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: [Bundler homepage](http://gembundler.com) +More information: [Bundler homepage](http://bundler.io/) ### Living on the Edge @@ -149,7 +154,7 @@ Railties rails g scaffold Post title:string:index author:uniq price:decimal{7,2} ``` - will create indexes for `title` and `author` with the latter being an unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively. + will create indexes for `title` and `author` with the latter being a unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively. * Turn gem has been removed from default Gemfile. @@ -322,7 +327,7 @@ Active Record * Implemented `ActiveRecord::Relation#explain`. -* Implements `AR::Base.silence_auto_explain` which allows the user to selectively disable automatic EXPLAINs within a block. +* Implements `ActiveRecord::Base.silence_auto_explain` which allows the user to selectively disable automatic EXPLAINs within a block. * Implements automatic EXPLAIN logging for slow queries. A new configuration parameter `config.active_record.auto_explain_threshold_in_seconds` determines what's to be considered a slow query. Setting that to nil disables this feature. Defaults are 0.5 in development mode, and nil in test and production modes. Rails 3.2 supports this feature in SQLite, MySQL (mysql2 adapter), and PostgreSQL. diff --git a/source/4_0_release_notes.md b/source/4_0_release_notes.md index 19c6902..4615cf1 100644 --- a/source/4_0_release_notes.md +++ b/source/4_0_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 4.0 Release Notes =============================== @@ -8,7 +10,10 @@ Highlights in Rails 4.0: * Turbolinks * Russian Doll Caching -These release notes cover only the major changes. To know about various bug fixes and changes, please refer to the change logs or check out the [list of commits](https://github.com/rails/rails/commits/master) in the main Rails repository on GitHub. +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/4-0-stable) in the main Rails +repository on GitHub. -------------------------------------------------------------------------------- @@ -31,7 +36,7 @@ $ cd myapp Rails now uses a `Gemfile` in the application root to determine the gems you require for your application to start. This `Gemfile` is processed by the [Bundler](https://github.com/carlhuda/bundler) gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. -More information: [Bundler homepage](http://gembundler.com) +More information: [Bundler homepage](http://bundler.io) ### Living on the Edge @@ -54,25 +59,25 @@ Major Features ### Upgrade - * **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required - * **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. - * **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. - * **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. - * **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. - * **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. - * **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. - * **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. +* **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required +* **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1. +* **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching. +* **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code. +* **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store. +* **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters. +* **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used. +* **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems. ### ActionPack - * **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). - * **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). - * **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. - * **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation - * **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. - * **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. - * **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1. - * **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. +* **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`). +* **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`). +* **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`. +* **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation. +* **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object. +* **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body. +* **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1. +* **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel. ### General @@ -82,14 +87,17 @@ Major Features * **Support for specifying transaction isolation level** ([commit](https://github.com/rails/rails/commit/392eeecc11a291e406db927a18b75f41b2658253)) - Choose whether repeatable reads or improved performance (less locking) is more important. * **Dalli** ([commit](https://github.com/rails/rails/commit/82663306f428a5bbc90c511458432afb26d2f238)) - Use Dalli memcache client for the memcache store. * **Notifications start & finish** ([commit](https://github.com/rails/rails/commit/f08f8750a512f741acb004d0cebe210c5f949f28)) - Active Support instrumentation reports start and finish notifications to subscribers. - * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. Note: Check that the gems you are using are threadsafe. + * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. + +NOTE: Check that the gems you are using are threadsafe. + * **PATCH verb** ([commit](https://github.com/rails/rails/commit/eed9f2539e3ab5a68e798802f464b8e4e95e619e)) - In Rails, PATCH replaces PUT. PATCH is used for partial updates of resources. ### Security - * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. - * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. - * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). +* **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified. +* **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called. +* **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe). Extraction of features to gems --------------------------- @@ -100,7 +108,7 @@ In Rails 4.0, several features have been extracted into gems. You can simply add * Mass assignment protection in Active Record models ([GitHub](https://github.com/rails/protected_attributes), [Pull Request](https://github.com/rails/rails/pull/7251)) * ActiveRecord::SessionStore ([GitHub](https://github.com/rails/activerecord-session_store), [Pull Request](https://github.com/rails/rails/pull/7436)) * Active Record Observers ([GitHub](https://github.com/rails/rails-observers), [Commit](https://github.com/rails/rails/commit/39e85b3b90c58449164673909a6f1893cba290b2)) -* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource)) +* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia-blog-blog.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource)) * Action Caching ([GitHub](https://github.com/rails/actionpack-action_caching), [Pull Request](https://github.com/rails/rails/pull/7833)) * Page Caching ([GitHub](https://github.com/rails/actionpack-page_caching), [Pull Request](https://github.com/rails/rails/pull/7833)) * Sprockets ([GitHub](https://github.com/rails/sprockets-rails)) @@ -176,7 +184,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a * `String#to_date` now raises `ArgumentError: invalid date` instead of `NoMethodError: undefined method 'div' for nil:NilClass` when given an invalid date. It is now the same as `Date.parse`, and it accepts more invalid dates than 3.x, such as: - ``` + ```ruby # ActiveSupport 3.x "asdf".to_date # => NoMethodError: undefined method `div' for nil:NilClass "333".to_date # => NoMethodError: undefined method `div' for nil:NilClass @@ -226,11 +234,11 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a The method `change_table` is also reversible, as long as its block doesn't call `remove`, `change` or `change_default` * New method `reversible` makes it possible to specify code to be run when migrating up or down. - See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#using-the-reversible-method) + See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/active_record_migrations.md#using-reversible) * New method `revert` will revert a whole migration or the given block. If migrating down, the given migration / block is run normally. - See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#reverting-previous-migrations) + See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/active_record_migrations.md#reverting-previous-migrations) * Adds PostgreSQL array type support. Any datatype can be used to create an array column, with full migration and schema dumper support. diff --git a/source/4_1_release_notes.md b/source/4_1_release_notes.md index 5f4bdaa..6bf6575 100644 --- a/source/4_1_release_notes.md +++ b/source/4_1_release_notes.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 4.1 Release Notes =============================== @@ -8,10 +10,10 @@ Highlights in Rails 4.1: * Action Pack variants * Action Mailer previews -These release notes cover only the major changes. To know about various bug -fixes and changes, please refer to the change logs or check out the -[list of commits](https://github.com/rails/rails/commits/master) in the main -Rails repository on GitHub. +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/4-1-stable) in the main Rails +repository on GitHub. -------------------------------------------------------------------------------- @@ -136,7 +138,7 @@ end ### Action Mailer Previews -Action Mailer previews provide a way to visually see how emails look by visiting +Action Mailer previews provide a way to see how emails look by visiting a special URL that renders them. You implement a preview class whose methods return the mail object you'd like @@ -315,15 +317,15 @@ for detailed changes. * Removed deprecated constants from Action Controller: - | Removed | Successor | - |:-----------------------------------|:--------------------------------| - | ActionController::AbstractRequest | ActionDispatch::Request | - | ActionController::Request | ActionDispatch::Request | - | ActionController::AbstractResponse | ActionDispatch::Response | - | ActionController::Response | ActionDispatch::Response | - | ActionController::Routing | ActionDispatch::Routing | - | ActionController::Integration | ActionDispatch::Integration | - | ActionController::IntegrationTest | ActionDispatch::IntegrationTest | +| Removed | Successor | +|:-----------------------------------|:--------------------------------| +| ActionController::AbstractRequest | ActionDispatch::Request | +| ActionController::Request | ActionDispatch::Request | +| ActionController::AbstractResponse | ActionDispatch::Response | +| ActionController::Response | ActionDispatch::Response | +| ActionController::Routing | ActionDispatch::Routing | +| ActionController::Integration | ActionDispatch::Integration | +| ActionController::IntegrationTest | ActionDispatch::IntegrationTest | ### Notable changes diff --git a/source/4_2_release_notes.md b/source/4_2_release_notes.md index a39dd9a..a30bfc4 100644 --- a/source/4_2_release_notes.md +++ b/source/4_2_release_notes.md @@ -1,12 +1,20 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails 4.2 Release Notes =============================== Highlights in Rails 4.2: -These release notes cover only the major changes. To know about various bug -fixes and changes, please refer to the change logs or check out the -[list of commits](https://github.com/rails/rails/commits/master) in the main -Rails repository on GitHub. +* Active Job +* Asynchronous mails +* Adequate Record +* Web Console +* Foreign key support + +These release notes cover only the major changes. To learn about other +features, bug fixes, and changes, please refer to the changelogs or check out +the [list of commits](https://github.com/rails/rails/commits/4-2-stable) in +the main Rails repository on GitHub. -------------------------------------------------------------------------------- @@ -16,16 +24,116 @@ Upgrading to Rails 4.2 If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 4.1 in case you haven't and make sure your application still runs as expected before attempting -an update to Rails 4.2. A list of things to watch out for when upgrading is -available in the -[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-1-to-rails-4-2) -guide. +to upgrade to Rails 4.2. A list of things to watch out for when upgrading is +available in the guide [Upgrading Ruby on +Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-1-to-rails-4-2). Major Features -------------- -### Foreign key support +### Active Job + +Active Job is a new framework in Rails 4.2. It is a common interface on top of +queuing systems like [Resque](https://github.com/resque/resque), [Delayed +Job](https://github.com/collectiveidea/delayed_job), +[Sidekiq](https://github.com/mperham/sidekiq), and more. + +Jobs written with the Active Job API run on any of the supported queues thanks +to their respective adapters. Active Job comes pre-configured with an inline +runner that executes jobs right away. + +Jobs often need to take Active Record objects as arguments. Active Job passes +object references as URIs (uniform resource identifiers) instead of marshaling +the object itself. The new [Global ID](https://github.com/rails/globalid) +library builds URIs and looks up the objects they reference. Passing Active +Record objects as job arguments just works by using Global ID internally. + +For example, if `trashable` is an Active Record object, then this job runs +just fine with no serialization involved: + +```ruby +class TrashableCleanupJob < ActiveJob::Base + def perform(trashable, depth) + trashable.cleanup(depth) + end +end +``` + +See the [Active Job Basics](active_job_basics.html) guide for more +information. + +### Asynchronous Mails + +Building on top of Active Job, Action Mailer now comes with a `deliver_later` +method that sends emails via the queue, so it doesn't block the controller or +model if the queue is asynchronous (the default inline queue blocks). + +Sending emails right away is still possible with `deliver_now`. + +### Adequate Record + +Adequate Record is a set of performance improvements in Active Record that makes +common `find` and `find_by` calls and some association queries up to 2x faster. + +It works by caching common SQL queries as prepared statements and reusing them +on similar calls, skipping most of the query-generation work on subsequent +calls. For more details, please refer to [Aaron Patterson's blog +post](http://tenderlovemaking.com/2014/02/19/adequaterecord-pro-like-activerecord.html). + +Active Record will automatically take advantage of this feature on +supported operations without any user involvement or code changes. Here are +some examples of supported operations: + +```ruby +Post.find(1) # First call generates and cache the prepared statement +Post.find(2) # Subsequent calls reuse the cached prepared statement + +Post.find_by_title('first post') +Post.find_by_title('second post') + +Post.find_by(title: 'first post') +Post.find_by(title: 'second post') + +post.comments +post.comments(true) +``` + +It's important to highlight that, as the examples above suggest, the prepared +statements do not cache the values passed in the method calls; rather, they +have placeholders for them. + +Caching is not used in the following scenarios: + +- The model has a default scope +- The model uses single table inheritance +- `find` with a list of ids, e.g.: + + ```ruby + # not cached + Post.find(1, 2, 3) + Post.find([1,2]) + ``` + +- `find_by` with SQL fragments: + + ```ruby + Post.find_by('published_at < ?', 2.weeks.ago) + ``` + +### Web Console + +New applications generated with Rails 4.2 now come with the [Web +Console](https://github.com/rails/web-console) gem by default. Web Console adds +an interactive Ruby console on every error page and provides a `console` view +and controller helpers. + +The interactive console on error pages lets you execute code in the context of +the place where the exception originated. The `console` helper, if called +anywhere in a view or controller, launches an interactive console with the final +context, once rendering has completed. + +### Foreign Key Support The migration DSL now supports adding and removing foreign keys. They are dumped to `schema.rb` as well. At this time, only the `mysql`, `mysql2` and `postgresql` @@ -52,6 +160,184 @@ and for a full description. +Incompatibilities +----------------- + +Previously deprecated functionality has been removed. Please refer to the +individual components for new deprecations in this release. + +The following changes may require immediate action upon upgrade. + +### `render` with a String Argument + +Previously, calling `render "foo/bar"` in a controller action was equivalent to +`render file: "foo/bar"`. In Rails 4.2, this has been changed to mean +`render template: "foo/bar"` instead. If you need to render a file, please +change your code to use the explicit form (`render file: "foo/bar"`) instead. + +### `respond_with` / Class-Level `respond_to` + +`respond_with` and the corresponding class-level `respond_to` have been moved +to the [responders](https://github.com/plataformatec/responders) gem. Add +`gem 'responders', '~> 2.0'` to your Gemfile to use it: + +```ruby +# app/controllers/users_controller.rb + +class UsersController < ApplicationController + respond_to :html, :json + + def show + @user = User.find(params[:id]) + respond_with @user + end +end +``` + +Instance-level `respond_to` is unaffected: + +```ruby +# app/controllers/users_controller.rb + +class UsersController < ApplicationController + def show + @user = User.find(params[:id]) + respond_to do |format| + format.html + format.json { render json: @user } + end + end +end +``` + +### Default Host for `rails server` + +Due to a [change in Rack](https://github.com/rack/rack/commit/28b014484a8ac0bbb388e7eaeeef159598ec64fc), +`rails server` now listens on `localhost` instead of `0.0.0.0` by default. This +should have minimal impact on the standard development workflow as both +http://127.0.0.1:3000 and http://localhost:3000 will continue to work as before +on your own machine. + +However, with this change you will no longer be able to access the Rails +server from a different machine, for example if your development environment +is in a virtual machine and you would like to access it from the host machine. +In such cases, please start the server with `rails server -b 0.0.0.0` to +restore the old behavior. + +If you do this, be sure to configure your firewall properly such that only +trusted machines on your network can access your development server. + +### Changed status option symbols for `render` + +Due to a [change in Rack](https://github.com/rack/rack/commit/be28c6a2ac152fe4adfbef71f3db9f4200df89e8), the symbols that the `render` method accepts for the `:status` option have changed: + +- 306: `:reserved` has been removed. +- 413: `:request_entity_too_large` has been renamed to `:payload_too_large`. +- 414: `:request_uri_too_long` has been renamed to `:uri_too_long`. +- 416: `:requested_range_not_satisfiable` has been renamed to `:range_not_satisfiable`. + +Keep in mind that if calling `render` with an unknown symbol, the response status will default to 500. + +### HTML Sanitizer + +The HTML sanitizer has been replaced with a new, more robust, implementation +built upon [Loofah](https://github.com/flavorjones/loofah) and +[Nokogiri](https://github.com/sparklemotion/nokogiri). The new sanitizer is +more secure and its sanitization is more powerful and flexible. + +Due to the new algorithm, the sanitized output may be different for certain +pathological inputs. + +If you have a particular need for the exact output of the old sanitizer, you +can add the [rails-deprecated_sanitizer](https://github.com/kaspth/rails-deprecated_sanitizer) +gem to the `Gemfile`, to have the old behavior. The gem does not issue +deprecation warnings because it is opt-in. + +`rails-deprecated_sanitizer` will be supported for Rails 4.2 only; it will not +be maintained for Rails 5.0. + +See [this blog post](http://blog.plataformatec.com.br/2014/07/the-new-html-sanitizer-in-rails-4-2/) +for more details on the changes in the new sanitizer. + +### `assert_select` + +`assert_select` is now based on [Nokogiri](https://github.com/sparklemotion/nokogiri). +As a result, some previously-valid selectors are now unsupported. If your +application is using any of these spellings, you will need to update them: + +* Values in attribute selectors may need to be quoted if they contain + non-alphanumeric characters. + + ```ruby + # before + a[href=/] + a[href$=/] + + # now + a[href="https://github.com/"] + a[href$="/"] + ``` + +* DOMs built from HTML source containing invalid HTML with improperly + nested elements may differ. + + For example: + + ```ruby + # content:

+ + # before: + assert_select('div > i') # => true + assert_select('div > p') # => false + assert_select('i > p') # => true + + # now: + assert_select('div > i') # => true + assert_select('div > p') # => true + assert_select('i > p') # => false + ``` + +* If the data selected contains entities, the value selected for comparison + used to be raw (e.g. `AT&T`), and now is evaluated + (e.g. `AT&T`). + + ```ruby + # content:

AT&T

+ + # before: + assert_select('p', 'AT&T') # => true + assert_select('p', 'AT&T') # => false + + # now: + assert_select('p', 'AT&T') # => true + assert_select('p', 'AT&T') # => false + ``` + +Furthermore substitutions have changed syntax. + +Now you have to use a `:match` CSS-like selector: + +```ruby +assert_select ":match('id', ?)", 'comment_1' +``` + +Additionally Regexp substitutions look different when the assertion fails. +Notice how `/hello/` here: + +```ruby +assert_select(":match('id', ?)", /hello/) +``` + +becomes `"(?-mix:hello)"`: + +``` +Expected at least 1 element matching "div:match('id', "(?-mix:hello)")", found 0.. +Expected 0 to be >= 1. +``` + +See the [Rails Dom Testing](https://github.com/rails/rails-dom-testing/tree/8798b9349fb9540ad8cb9a0ce6cb88d1384a210b) documentation for more on `assert_select`. + + Railties -------- @@ -59,45 +345,124 @@ Please refer to the [Changelog][railties] for detailed changes. ### Removals +* The `--skip-action-view` option has been removed from the + app generator. ([Pull Request](https://github.com/rails/rails/pull/17042)) + * The `rails application` command has been removed without replacement. ([Pull Request](https://github.com/rails/rails/pull/11616)) ### Deprecations +* Deprecated missing `config.log_level` for production environments. + ([Pull Request](https://github.com/rails/rails/pull/16622)) + +* Deprecated `rake test:all` in favor of `rake test` as it now run all tests + in the `test` folder. + ([Pull Request](https://github.com/rails/rails/pull/17348)) + +* Deprecated `rake test:all:db` in favor of `rake test:db`. + ([Pull Request](https://github.com/rails/rails/pull/17348)) + * Deprecated `Rails::Rack::LogTailer` without replacement. ([Commit](https://github.com/rails/rails/commit/84a13e019e93efaa8994b3f8303d635a7702dbce)) ### Notable changes -* Introduced `--skip-gems` option in the app generator to skip gems such as - `turbolinks` and `coffee-rails` that does not have their own specific flags. - ([Commit](https://github.com/rails/rails/commit/10565895805887d4faf004a6f71219da177f78b7)) +* Introduced `web-console` in the default application Gemfile. + ([Pull Request](https://github.com/rails/rails/pull/11667)) -* Introduced `bin/setup` script to bootstrap an application. +* Added a `required` option to the model generator for associations. + ([Pull Request](https://github.com/rails/rails/pull/16062)) + +* Introduced the `x` namespace for defining custom configuration options: + + ```ruby + # config/environments/production.rb + config.x.payment_processing.schedule = :daily + config.x.payment_processing.retries = 3 + config.x.super_debugger = true + ``` + + These options are then available through the configuration object: + + ```ruby + Rails.configuration.x.payment_processing.schedule # => :daily + Rails.configuration.x.payment_processing.retries # => 3 + Rails.configuration.x.super_debugger # => true + ``` + + ([Commit](https://github.com/rails/rails/commit/611849772dd66c2e4d005dcfe153f7ce79a8a7db)) + +* Introduced `Rails::Application.config_for` to load a configuration for the + current environment. + + ```ruby + # config/exception_notification.yml: + production: + url: http://127.0.0.1:8080 + namespace: my_app_production + development: + url: http://localhost:3001 + namespace: my_app_development + + # config/environments/production.rb + Rails.application.configure do + config.middleware.use ExceptionNotifier, config_for(:exception_notification) + end + ``` + + ([Pull Request](https://github.com/rails/rails/pull/16129)) + +* Introduced a `--skip-turbolinks` option in the app generator to not generate + turbolinks integration. + ([Commit](https://github.com/rails/rails/commit/bf17c8a531bc8059d50ad731398002a3e7162a7d)) + +* Introduced a `bin/setup` script as a convention for automated setup code when + bootstrapping an application. ([Pull Request](https://github.com/rails/rails/pull/15189)) -* Changed default value for `config.assets.digest` to `true` in development. +* Changed the default value for `config.assets.digest` to `true` in development. ([Pull Request](https://github.com/rails/rails/pull/15155)) * Introduced an API to register new extensions for `rake notes`. ([Pull Request](https://github.com/rails/rails/pull/14379)) -* Introduced `Rails.gem_version` as a convenience method to return `Gem::Version.new(Rails.version)`. - ([Pull Request](https://github.com/rails/rails/pull/14101)) - -* Introduced an `after_bundle` callback in the Rails templates. +* Introduced an `after_bundle` callback for use in Rails templates. ([Pull Request](https://github.com/rails/rails/pull/16359)) +* Introduced `Rails.gem_version` as a convenience method to return + `Gem::Version.new(Rails.version)`. + ([Pull Request](https://github.com/rails/rails/pull/14101)) + Action Pack ----------- Please refer to the [Changelog][action-pack] for detailed changes. +### Removals + +* `respond_with` and the class-level `respond_to` have been removed from Rails and + moved to the `responders` gem (version 2.0). Add `gem 'responders', '~> 2.0'` + to your `Gemfile` to continue using these features. + ([Pull Request](https://github.com/rails/rails/pull/16526), + [More Details](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#responders)) + +* Removed deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError` + in favor of `AbstractController::Helpers::MissingHelperError`. + ([Commit](https://github.com/rails/rails/commit/a1ddde15ae0d612ff2973de9cf768ed701b594e8)) + ### Deprecations +* Deprecated the `only_path` option on `*_path` helpers. + ([Commit](https://github.com/rails/rails/commit/aa1fadd48fb40dd9396a383696134a259aa59db9)) + +* Deprecated `assert_tag`, `assert_no_tag`, `find_tag` and `find_all_tag` in + favor of `assert_select`. + ([Commit](https://github.com/rails/rails-dom-testing/commit/b12850bc5ff23ba4b599bf2770874dd4f11bf750)) + * Deprecated support for setting the `:to` option of a router to a symbol or a - string that does not contain a `#` character: + string that does not contain a "#" character: ```ruby get '/posts', to: MyRackApp => (No change necessary) @@ -108,19 +473,22 @@ Please refer to the [Changelog][action-pack] for detailed changes. ([Commit](https://github.com/rails/rails/commit/cc26b6b7bccf0eea2e2c1a9ebdcc9d30ca7390d9)) -### Notable changes +* Deprecated support for string keys in URL helpers: -* `render nothing: true` or rendering a `nil` body no longer add a single - space padding to the response body. - ([Pull Request](https://github.com/rails/rails/pull/14883)) + ```ruby + # bad + root_path('controller' => 'posts', 'action' => 'index') -* Introduced the `always_permitted_parameters` option to configure which - parameters are permitted globally. The default value of this configuration - is `['controller', 'action']`. - ([Pull Request](https://github.com/rails/rails/pull/15933)) + # good + root_path(controller: 'posts', action: 'index') + ``` -* The `*_filter` family methods has been removed from the documentation. Their - usage are discouraged in favor of the `*_action` family methods: + ([Pull Request](https://github.com/rails/rails/pull/17743)) + +### Notable changes + +* The `*_filter` family of methods have been removed from the documentation. Their + usage is discouraged in favor of the `*_action` family of methods: ``` after_filter => after_action @@ -138,32 +506,62 @@ Please refer to the [Changelog][action-pack] for detailed changes. skip_filter => skip_action_callback ``` - If your application is depending on these methods, you should use the + If your application currently depends on these methods, you should use the replacement `*_action` methods instead. These methods will be deprecated in - the future and eventually removed from Rails. + the future and will eventually be removed from Rails. (Commit [1](https://github.com/rails/rails/commit/6c5f43bab8206747a8591435b2aa0ff7051ad3de), [2](https://github.com/rails/rails/commit/489a8f2a44dc9cea09154ee1ee2557d1f037c7d4)) -* Added HTTP method `MKCALENDAR` from RFC-4791 - ([Pull Request](https://github.com/rails/rails/pull/15121)) +* `render nothing: true` or rendering a `nil` body no longer add a single + space padding to the response body. + ([Pull Request](https://github.com/rails/rails/pull/14883)) -* `*_fragment.action_controller` notifications now include the controller and action name - in the payload. - ([Pull Request](https://github.com/rails/rails/pull/14137)) +* Rails now automatically includes the template's digest in ETags. + ([Pull Request](https://github.com/rails/rails/pull/16527)) * Segments that are passed into URL helpers are now automatically escaped. ([Commit](https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f)) -* Improved Routing Error page with fuzzy matching for route search. +* Introduced the `always_permitted_parameters` option to configure which + parameters are permitted globally. The default value of this configuration + is `['controller', 'action']`. + ([Pull Request](https://github.com/rails/rails/pull/15933)) + +* Added the HTTP method `MKCALENDAR` from [RFC 4791](https://tools.ietf.org/html/rfc4791). + ([Pull Request](https://github.com/rails/rails/pull/15121)) + +* `*_fragment.action_controller` notifications now include the controller + and action name in the payload. + ([Pull Request](https://github.com/rails/rails/pull/14137)) + +* Improved the Routing Error page with fuzzy matching for route search. ([Pull Request](https://github.com/rails/rails/pull/14619)) -* Added option to disable logging of CSRF failures. +* Added an option to disable logging of CSRF failures. ([Pull Request](https://github.com/rails/rails/pull/14280)) +* When the Rails server is set to serve static assets, gzip assets will now be + served if the client supports it and a pre-generated gzip file (`.gz`) is on disk. + By default the asset pipeline generates `.gz` files for all compressible assets. + Serving gzip files minimizes data transfer and speeds up asset requests. Always + [use a CDN](http://guides.rubyonrails.org/asset_pipeline.html#cdns) if you are + serving assets from your Rails server in production. + ([Pull Request](https://github.com/rails/rails/pull/16466)) + +* When calling the `process` helpers in an integration test the path needs to have + a leading slash. Previously you could omit it but that was a byproduct of the + implementation and not an intentional feature, e.g.: + + ```ruby + test "list all posts" do + get "/posts" + assert_response :success + end + ``` Action View -------------- +----------- Please refer to the [Changelog][action-view] for detailed changes. @@ -174,24 +572,53 @@ Please refer to the [Changelog][action-view] for detailed changes. where to find views. ([Pull Request](https://github.com/rails/rails/pull/15026)) -* Deprecated `ActionView::Digestor#digest(name, format, finder, options = {})`, - arguments should be passed as a hash instead. +* Deprecated `ActionView::Digestor#digest(name, format, finder, options = {})`. + Arguments should be passed as a hash instead. ([Pull Request](https://github.com/rails/rails/pull/14243)) ### Notable changes +* `render "foo/bar"` now expands to `render template: "foo/bar"` instead of + `render file: "foo/bar"`. + ([Pull Request](https://github.com/rails/rails/pull/16888)) + * The form helpers no longer generate a `
` element with inline CSS around the hidden fields. ([Pull Request](https://github.com/rails/rails/pull/14738)) +* Introduced a `#{partial_name}_iteration` special local variable for use with + partials that are rendered with a collection. It provides access to the + current state of the iteration via the `index`, `size`, `first?` and + `last?` methods. + ([Pull Request](https://github.com/rails/rails/pull/7698)) + +* Placeholder I18n follows the same convention as `label` I18n. + ([Pull Request](https://github.com/rails/rails/pull/16438)) + Action Mailer ------------- Please refer to the [Changelog][action-mailer] for detailed changes. +### Deprecations + +* Deprecated `*_path` helpers in mailers. Always use `*_url` helpers instead. + ([Pull Request](https://github.com/rails/rails/pull/15840)) + +* Deprecated `deliver` / `deliver!` in favor of `deliver_now` / `deliver_now!`. + ([Pull Request](https://github.com/rails/rails/pull/16582)) + ### Notable changes +* `link_to` and `url_for` generate absolute URLs by default in templates, + it is no longer needed to pass `only_path: false`. + ([Commit](https://github.com/rails/rails/commit/9685080a7677abfa5d288a81c3e078368c6bb67c)) + +* Introduced `deliver_later` which enqueues a job on the application's queue + to deliver emails asynchronously. + ([Pull Request](https://github.com/rails/rails/pull/16485)) + * Added the `show_previews` configuration option for enabling mailer previews outside of the development environment. ([Pull Request](https://github.com/rails/rails/pull/15970)) @@ -200,9 +627,7 @@ Please refer to the [Changelog][action-mailer] for detailed changes. Active Record ------------- -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) -for detailed changes. +Please refer to the [Changelog][active-record] for detailed changes. ### Removals @@ -218,102 +643,131 @@ for detailed changes. * Removed unused `:timestamp` type. Transparently alias it to `:datetime` in all cases. Fixes inconsistencies when column types are sent outside of - `ActiveRecord`, such as for XML Serialization. + Active Record, such as for XML serialization. ([Pull Request](https://github.com/rails/rails/pull/15184)) ### Deprecations +* Deprecated swallowing of errors inside `after_commit` and `after_rollback`. + ([Pull Request](https://github.com/rails/rails/pull/16537)) + * Deprecated broken support for automatic detection of counter caches on `has_many :through` associations. You should instead manually specify the counter cache on the `has_many` and `belongs_to` associations for the through records. ([Pull Request](https://github.com/rails/rails/pull/15754)) -* Deprecated `serialized_attributes` without replacement. - ([Pull Request](https://github.com/rails/rails/pull/15704)) - -* Deprecated returning `nil` from `column_for_attribute` when no column - exists. It will return a null object in Rails 5.0 - ([Pull Request](https://github.com/rails/rails/pull/15878)) - -* Deprecated using `.joins`, `.preload` and `.eager_load` with associations - that depends on the instance state (i.e. those defined with a scope that - takes an argument) without replacement. - ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1)) - * Deprecated passing Active Record objects to `.find` or `.exists?`. Call - `#id` on the objects first. + `id` on the objects first. (Commit [1](https://github.com/rails/rails/commit/d92ae6ccca3bcfd73546d612efaea011270bd270), [2](https://github.com/rails/rails/commit/d35f0033c7dec2b8d8b52058fb8db495d49596f7)) * Deprecated half-baked support for PostgreSQL range values with excluding beginnings. We currently map PostgreSQL ranges to Ruby ranges. This conversion - is not fully possible because the Ruby range does not support excluded - beginnings. + is not fully possible because Ruby ranges do not support excluded beginnings. The current solution of incrementing the beginning is not correct and is now deprecated. For subtypes where we don't know how to increment - (e.g. `#succ` is not defined) it will raise an `ArgumentError` for ranges + (e.g. `succ` is not defined) it will raise an `ArgumentError` for ranges with excluding beginnings. - ([Commit](https://github.com/rails/rails/commit/91949e48cf41af9f3e4ffba3e5eecf9b0a08bfc3)) +* Deprecated calling `DatabaseTasks.load_schema` without a connection. Use + `DatabaseTasks.load_schema_current` instead. + ([Commit](https://github.com/rails/rails/commit/f15cef67f75e4b52fd45655d7c6ab6b35623c608)) + +* Deprecated `sanitize_sql_hash_for_conditions` without replacement. Using a + `Relation` for performing queries and updates is the preferred API. + ([Commit](https://github.com/rails/rails/commit/d5902c9e)) + +* Deprecated `add_timestamps` and `t.timestamps` without passing the `:null` + option. The default of `null: true` will change in Rails 5 to `null: false`. + ([Pull Request](https://github.com/rails/rails/pull/16481)) + +* Deprecated `Reflection#source_macro` without replacement as it is no longer + needed in Active Record. + ([Pull Request](https://github.com/rails/rails/pull/16373)) + +* Deprecated `serialized_attributes` without replacement. + ([Pull Request](https://github.com/rails/rails/pull/15704)) + +* Deprecated returning `nil` from `column_for_attribute` when no column + exists. It will return a null object in Rails 5.0. + ([Pull Request](https://github.com/rails/rails/pull/15878)) + +* Deprecated using `.joins`, `.preload` and `.eager_load` with associations + that depend on the instance state (i.e. those defined with a scope that + takes an argument) without replacement. + ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1)) + ### Notable changes +* `SchemaDumper` uses `force: :cascade` on `create_table`. This makes it + possible to reload a schema when foreign keys are in place. + * Added a `:required` option to singular associations, which defines a presence validation on the association. ([Pull Request](https://github.com/rails/rails/pull/16056)) -* Introduced `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the - record is invalid. - ([Pull Request](https://github.com/rails/rails/pull/8639)) - -* `ActiveRecord::Base#reload` now behaves the same as `m = Model.find(m.id)`, - meaning that it no longer retains the extra attributes from custom - `select`s. - ([Pull Request](https://github.com/rails/rails/pull/15866)) - -* Introduced the `bin/rake db:purge` task to empty the database for the - current environment. - ([Commit](https://github.com/rails/rails/commit/e2f232aba15937a4b9d14bd91e0392c6d55be58d)) - * `ActiveRecord::Dirty` now detects in-place changes to mutable values. - Serialized attributes on ActiveRecord models will no longer save when + Serialized attributes on Active Record models are no longer saved when unchanged. This also works with other types such as string columns and json columns on PostgreSQL. (Pull Requests [1](https://github.com/rails/rails/pull/15674), [2](https://github.com/rails/rails/pull/15786), [3](https://github.com/rails/rails/pull/15788)) -* Added support for `#pretty_print` in `ActiveRecord::Base` objects. - ([Pull Request](https://github.com/rails/rails/pull/15172)) +* Introduced the `db:purge` Rake task to empty the database for the + current environment. + ([Commit](https://github.com/rails/rails/commit/e2f232aba15937a4b9d14bd91e0392c6d55be58d)) -* PostgreSQL and SQLite adapters no longer add a default limit of 255 +* Introduced `ActiveRecord::Base#validate!` that raises + `ActiveRecord::RecordInvalid` if the record is invalid. + ([Pull Request](https://github.com/rails/rails/pull/8639)) + +* Introduced `validate` as an alias for `valid?`. + ([Pull Request](https://github.com/rails/rails/pull/14456)) + +* `touch` now accepts multiple attributes to be touched at once. + ([Pull Request](https://github.com/rails/rails/pull/14423)) + +* The PostgreSQL adapter now supports the `jsonb` datatype in PostgreSQL 9.4+. + ([Pull Request](https://github.com/rails/rails/pull/16220)) + +* The PostgreSQL and SQLite adapters no longer add a default limit of 255 characters on string columns. ([Pull Request](https://github.com/rails/rails/pull/14579)) +* Added support for the `citext` column type in the PostgreSQL adapter. + ([Pull Request](https://github.com/rails/rails/pull/12523)) + +* Added support for user-created range types in the PostgreSQL adapter. + ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032)) + * `sqlite3:///some/path` now resolves to the absolute system path `/some/path`. For relative paths, use `sqlite3:some/path` instead. (Previously, `sqlite3:///some/path` resolved to the relative path - `some/path`. This behaviour was deprecated on Rails 4.1.) + `some/path`. This behavior was deprecated on Rails 4.1). ([Pull Request](https://github.com/rails/rails/pull/14569)) -* Introduced `#validate` as an alias for `#valid?`. - ([Pull Request](https://github.com/rails/rails/pull/14456)) - -* `#touch` now accepts multiple attributes to be touched at once. - ([Pull Request](https://github.com/rails/rails/pull/14423)) - * Added support for fractional seconds for MySQL 5.6 and above. (Pull Request [1](https://github.com/rails/rails/pull/8240), [2](https://github.com/rails/rails/pull/14359)) -* Added support for the `citext` column type in PostgreSQL adapter. - ([Pull Request](https://github.com/rails/rails/pull/12523)) +* Added `ActiveRecord::Base#pretty_print` to pretty print models. + ([Pull Request](https://github.com/rails/rails/pull/15172)) -* Added support for user-created range types in PostgreSQL adapter. - ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032)) +* `ActiveRecord::Base#reload` now behaves the same as `m = Model.find(m.id)`, + meaning that it no longer retains the extra attributes from custom + `SELECT`s. + ([Pull Request](https://github.com/rails/rails/pull/15866)) + +* `ActiveRecord::Base#reflections` now returns a hash with string keys instead + of symbol keys. ([Pull Request](https://github.com/rails/rails/pull/17718)) +* The `references` method in migrations now supports a `type` option for + specifying the type of the foreign key (e.g. `:uuid`). + ([Pull Request](https://github.com/rails/rails/pull/16231)) Active Model ------------ @@ -323,22 +777,35 @@ Please refer to the [Changelog][active-model] for detailed changes. ### Removals * Removed deprecated `Validator#setup` without replacement. - ([Pull Request](https://github.com/rails/rails/pull/15617)) + ([Pull Request](https://github.com/rails/rails/pull/10716)) + +### Deprecations + +* Deprecated `reset_#{attribute}` in favor of `restore_#{attribute}`. + ([Pull Request](https://github.com/rails/rails/pull/16180)) + +* Deprecated `ActiveModel::Dirty#reset_changes` in favor of + `clear_changes_information`. + ([Pull Request](https://github.com/rails/rails/pull/16180)) ### Notable changes -* Introduced `undo_changes` method in `ActiveModel::Dirty` to restore the - changed (dirty) attributes to their previous values. - ([Pull Request](https://github.com/rails/rails/pull/14861)) +* Introduced `validate` as an alias for `valid?`. + ([Pull Request](https://github.com/rails/rails/pull/14456)) + +* Introduced the `restore_attributes` method in `ActiveModel::Dirty` to restore + the changed (dirty) attributes to their previous values. + (Pull Request [1](https://github.com/rails/rails/pull/14861), + [2](https://github.com/rails/rails/pull/16180)) + +* `has_secure_password` no longer disallows blank passwords (i.e. passwords + that contains only spaces) by default. + ([Pull Request](https://github.com/rails/rails/pull/16412)) * `has_secure_password` now verifies that the given password is less than 72 characters if validations are enabled. ([Pull Request](https://github.com/rails/rails/pull/15708)) -* Introduced `#validate` as an alias for `#valid?`. - ([Pull Request](https://github.com/rails/rails/pull/14456)) - - Active Support -------------- @@ -355,6 +822,10 @@ Please refer to the [Changelog][active-support] for detailed changes. ### Deprecations +* Deprecated `Kernel#silence_stderr`, `Kernel#capture` and `Kernel#quietly` + without replacement. + ([Pull Request](https://github.com/rails/rails/pull/13392)) + * Deprecated `Class#superclass_delegating_accessor`, use `Class#class_attribute` instead. ([Pull Request](https://github.com/rails/rails/pull/14271)) @@ -365,6 +836,28 @@ Please refer to the [Changelog][active-support] for detailed changes. ### Notable changes +* Introduced a new configuration option `active_support.test_order` for + specifying the order test cases are executed. This option currently defaults + to `:sorted` but will be changed to `:random` in Rails 5.0. + ([Commit](https://github.com/rails/rails/commit/53e877f7d9291b2bf0b8c425f9e32ef35829f35b)) + +* `Object#try` and `Object#try!` can now be used without an explicit receiver in the block. + ([Commit](https://github.com/rails/rails/commit/5e51bdda59c9ba8e5faf86294e3e431bd45f1830), + [Pull Request](https://github.com/rails/rails/pull/17361)) + +* The `travel_to` test helper now truncates the `usec` component to 0. + ([Commit](https://github.com/rails/rails/commit/9f6e82ee4783e491c20f5244a613fdeb4024beb5)) + +* Introduced `Object#itself` as an identity function. + (Commit [1](https://github.com/rails/rails/commit/702ad710b57bef45b081ebf42e6fa70820fdd810), + [2](https://github.com/rails/rails/commit/64d91122222c11ad3918cc8e2e3ebc4b0a03448a)) + +* `Object#with_options` can now be used without an explicit receiver in the block. + ([Pull Request](https://github.com/rails/rails/pull/16339)) + +* Introduced `String#truncate_words` to truncate a string by a number of words. + ([Pull Request](https://github.com/rails/rails/pull/16190)) + * Added `Hash#transform_values` and `Hash#transform_values!` to simplify a common pattern where the values of a hash must change, but the keys are left the same. @@ -373,11 +866,12 @@ Please refer to the [Changelog][active-support] for detailed changes. * The `humanize` inflector helper now strips any leading underscores. ([Commit](https://github.com/rails/rails/commit/daaa21bc7d20f2e4ff451637423a25ff2d5e75c7)) -* Introduce `Concern#class_methods` as an alternative to +* Introduced `Concern#class_methods` as an alternative to `module ClassMethods`, as well as `Kernel#concern` to avoid the `module Foo; extend ActiveSupport::Concern; end` boilerplate. ([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad)) +* New [guide](autoloading_and_reloading_constants.html) about constant autoloading and reloading. Credits ------- diff --git a/source/5_0_release_notes.md b/source/5_0_release_notes.md new file mode 100644 index 0000000..5f4be07 --- /dev/null +++ b/source/5_0_release_notes.md @@ -0,0 +1,1096 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Ruby on Rails 5.0 Release Notes +=============================== + +Highlights in Rails 5.0: + +* Action Cable +* Rails API +* Active Record Attributes API +* Test Runner +* Exclusive use of `rails` CLI over Rake +* Sprockets 3 +* Turbolinks 5 +* Ruby 2.2.2+ required + +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/5-0-stable) in the main Rails +repository on GitHub. + +-------------------------------------------------------------------------------- + +Upgrading to Rails 5.0 +---------------------- + +If you're upgrading an existing application, it's a great idea to have good test +coverage before going in. You should also first upgrade to Rails 4.2 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 5.0. A list of things to watch out for when upgrading is +available in the +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-2-to-rails-5-0) +guide. + + +Major Features +-------------- + +### Action Cable + +Action Cable is a new framework in Rails 5. It seamlessly integrates +[WebSockets](https://en.wikipedia.org/wiki/WebSocket) with the rest of your +Rails application. + +Action Cable allows for real-time features to be written in Ruby in the +same style and form as the rest of your Rails application, while still being +performant and scalable. It's a full-stack offering that provides both a +client-side JavaScript framework and a server-side Ruby framework. You have +access to your full domain model written with Active Record or your ORM of +choice. + +See the [Action Cable Overview](action_cable_overview.html) guide for more +information. + +### API Applications + +Rails can now be used to create slimmed down API only applications. +This is useful for creating and serving APIs similar to [Twitter](https://dev.twitter.com) or [GitHub](http://developer.github.com) API, +that can be used to serve public facing, as well as, for custom applications. + +You can generate a new api Rails app using: + +```bash +$ rails new my_api --api +``` + +This will do three main things: + +- Configure your application to start with a more limited set of middleware + than normal. Specifically, it will not include any middleware primarily useful + for browser applications (like cookies support) by default. +- Make `ApplicationController` inherit from `ActionController::API` instead of + `ActionController::Base`. As with middleware, this will leave out any Action + Controller modules that provide functionalities primarily used by browser + applications. +- Configure the generators to skip generating views, helpers and assets when + you generate a new resource. + +The application provides a base for APIs, +that can then be [configured to pull in functionality](api_app.html) as suitable for the application's needs. + +See the [Using Rails for API-only Applications](api_app.html) guide for more +information. + +### Active Record attributes API + +Defines an attribute with a type on a model. It will override the type of existing attributes if needed. +This allows control over how values are converted to and from SQL when assigned to a model. +It also changes the behavior of values passed to `ActiveRecord::Base.where`, which lets use our domain objects across much of Active Record, +without having to rely on implementation details or monkey patching. + +Some things that you can achieve with this: + +- The type detected by Active Record can be overridden. +- A default can also be provided. +- Attributes do not need to be backed by a database column. + +```ruby + +# db/schema.rb +create_table :store_listings, force: true do |t| + t.decimal :price_in_cents + t.string :my_string, default: "original default" +end + +# app/models/store_listing.rb +class StoreListing < ActiveRecord::Base +end + +store_listing = StoreListing.new(price_in_cents: '10.1') + +# before +store_listing.price_in_cents # => BigDecimal.new(10.1) +StoreListing.new.my_string # => "original default" + +class StoreListing < ActiveRecord::Base + attribute :price_in_cents, :integer # custom type + attribute :my_string, :string, default: "new default" # default value + attribute :my_default_proc, :datetime, default: -> { Time.now } # default value + attribute :field_without_db_column, :integer, array: true +end + +# after +store_listing.price_in_cents # => 10 +StoreListing.new.my_string # => "new default" +StoreListing.new.my_default_proc # => 2015-05-30 11:04:48 -0600 +model = StoreListing.new(field_without_db_column: ["1", "2", "3"]) +model.attributes # => {field_without_db_column: [1, 2, 3]} +``` + +**Creating Custom Types:** + +You can define your own custom types, as long as they respond +to the methods defined on the value type. The method `deserialize` or +`cast` will be called on your type object, with raw input from the +database or from your controllers. This is useful, for example, when doing custom conversion, +like Money data. + +**Querying:** + +When `ActiveRecord::Base.where` is called, it will +use the type defined by the model class to convert the value to SQL, +calling `serialize` on your type object. + +This gives the objects ability to specify, how to convert values when performing SQL queries. + +**Dirty Tracking:** + +The type of an attribute is given the opportunity to change how dirty +tracking is performed. + +See its +[documentation](http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html) +for a detailed write up. + + +### Test Runner + +A new test runner has been introduced to enhance the capabilities of running tests from Rails. +To use this test runner simply type `bin/rails test`. + +Test Runner is inspired from `RSpec`, `minitest-reporters`, `maxitest` and others. +It includes some of these notable advancements: + +- Run a single test using line number of test. +- Run multiple tests pinpointing to line number of tests. +- Improved failure messages, which also add ease of re-running failed tests. +- Fail fast using `-f` option, to stop tests immediately on occurrence of failure, +instead of waiting for the suite to complete. +- Defer test output until the end of a full test run using the `-d` option. +- Complete exception backtrace output using `-b` option. +- Integration with `Minitest` to allow options like `-s` for test seed data, +`-n` for running specific test by name, `-v` for better verbose output and so forth. +- Colored test output. + +Railties +-------- + +Please refer to the [Changelog][railties] for detailed changes. + +### Removals + +* Removed debugger support, use byebug instead. `debugger` is not supported by + Ruby + 2.2. ([commit](https://github.com/rails/rails/commit/93559da4826546d07014f8cfa399b64b4a143127)) + +* Removed deprecated `test:all` and `test:all:db` tasks. + ([commit](https://github.com/rails/rails/commit/f663132eef0e5d96bf2a58cec9f7c856db20be7c)) + +* Removed deprecated `Rails::Rack::LogTailer`. + ([commit](https://github.com/rails/rails/commit/c564dcb75c191ab3d21cc6f920998b0d6fbca623)) + +* Removed deprecated `RAILS_CACHE` constant. + ([commit](https://github.com/rails/rails/commit/b7f856ce488ef8f6bf4c12bb549f462cb7671c08)) + +* Removed deprecated `serve_static_assets` configuration. + ([commit](https://github.com/rails/rails/commit/463b5d7581ee16bfaddf34ca349b7d1b5878097c)) + +* Removed the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`. + ([commit](https://github.com/rails/rails/commit/cd7cc5254b090ccbb84dcee4408a5acede25ef2a)) + +* Removed `Rack::ContentLength` middleware from the default + stack. ([Commit](https://github.com/rails/rails/commit/56903585a099ab67a7acfaaef0a02db8fe80c450)) + +### Deprecations + +* Deprecated `config.static_cache_control` in favor of + `config.public_file_server.headers`. + ([Pull Request](https://github.com/rails/rails/pull/19135)) + +* Deprecated `config.serve_static_files` in favor of `config.public_file_server.enabled`. + ([Pull Request](https://github.com/rails/rails/pull/22173)) + +* Deprecated the tasks in the `rails` task namespace in favor of the `app` namespace. + (e.g. `rails:update` and `rails:template` tasks are renamed to `app:update` and `app:template`.) + ([Pull Request](https://github.com/rails/rails/pull/23439)) + +### Notable changes + +* Added Rails test runner `bin/rails test`. + ([Pull Request](https://github.com/rails/rails/pull/19216)) + +* Newly generated applications and plugins get a `README.md` in Markdown. + ([commit](https://github.com/rails/rails/commit/89a12c931b1f00b90e74afffcdc2fc21f14ca663), + [Pull Request](https://github.com/rails/rails/pull/22068)) + +* Added `bin/rails restart` task to restart your Rails app by touching `tmp/restart.txt`. + ([Pull Request](https://github.com/rails/rails/pull/18965)) + +* Added `bin/rails initializers` task to print out all defined initializers in + the order they are invoked by Rails. + ([Pull Request](https://github.com/rails/rails/pull/19323)) + +* Added `bin/rails dev:cache` to enable or disable caching in development mode. + ([Pull Request](https://github.com/rails/rails/pull/20961)) + +* Added `bin/update` script to update the development environment automatically. + ([Pull Request](https://github.com/rails/rails/pull/20972)) + +* Proxy Rake tasks through `bin/rails`. + ([Pull Request](https://github.com/rails/rails/pull/22457), + [Pull Request](https://github.com/rails/rails/pull/22288)) + +* New applications are generated with the evented file system monitor enabled + on Linux and macOS. The feature can be opted out by passing + `--skip-listen` to the generator. + ([commit](https://github.com/rails/rails/commit/de6ad5665d2679944a9ee9407826ba88395a1003), + [commit](https://github.com/rails/rails/commit/94dbc48887bf39c241ee2ce1741ee680d773f202)) + +* Generate applications with an option to log to STDOUT in production + using the environment variable `RAILS_LOG_TO_STDOUT`. + ([Pull Request](https://github.com/rails/rails/pull/23734)) + +* Enable HSTS with IncludeSudomains header for new applications. + ([Pull Request](https://github.com/rails/rails/pull/23852)) + +* The application generator writes a new file `config/spring.rb`, which tells + Spring to watch additional common files. + ([commit](https://github.com/rails/rails/commit/b04d07337fd7bc17e88500e9d6bcd361885a45f8)) + +* Added `--skip-action-mailer` to skip Action Mailer while generating new app. + ([Pull Request](https://github.com/rails/rails/pull/18288)) + +* Removed `tmp/sessions` directory and the clear rake task associated with it. + ([Pull Request](https://github.com/rails/rails/pull/18314)) + +* Changed `_form.html.erb` generated by scaffold generator to use local variables. + ([Pull Request](https://github.com/rails/rails/pull/13434)) + +* Disabled autoloading of classes in production environment. + ([commit](https://github.com/rails/rails/commit/a71350cae0082193ad8c66d65ab62e8bb0b7853b)) + +Action Pack +----------- + +Please refer to the [Changelog][action-pack] for detailed changes. + +### Removals + +* Removed `ActionDispatch::Request::Utils.deep_munge`. + ([commit](https://github.com/rails/rails/commit/52cf1a71b393486435fab4386a8663b146608996)) + +* Removed `ActionController::HideActions`. + ([Pull Request](https://github.com/rails/rails/pull/18371)) + +* Removed `respond_to` and `respond_with` placeholder methods, this functionality + has been extracted to the + [responders](https://github.com/plataformatec/responders) gem. + ([commit](https://github.com/rails/rails/commit/afd5e9a7ff0072e482b0b0e8e238d21b070b6280)) + +* Removed deprecated assertion files. + ([commit](https://github.com/rails/rails/commit/92e27d30d8112962ee068f7b14aa7b10daf0c976)) + +* Removed deprecated usage of string keys in URL helpers. + ([commit](https://github.com/rails/rails/commit/34e380764edede47f7ebe0c7671d6f9c9dc7e809)) + +* Removed deprecated `only_path` option on `*_path` helpers. + ([commit](https://github.com/rails/rails/commit/e4e1fd7ade47771067177254cb133564a3422b8a)) + +* Removed deprecated `NamedRouteCollection#helpers`. + ([commit](https://github.com/rails/rails/commit/2cc91c37bc2e32b7a04b2d782fb8f4a69a14503f)) + +* Removed deprecated support to define routes with `:to` option that doesn't contain `#`. + ([commit](https://github.com/rails/rails/commit/1f3b0a8609c00278b9a10076040ac9c90a9cc4a6)) + +* Removed deprecated `ActionDispatch::Response#to_ary`. + ([commit](https://github.com/rails/rails/commit/4b19d5b7bcdf4f11bd1e2e9ed2149a958e338c01)) + +* Removed deprecated `ActionDispatch::Request#deep_munge`. + ([commit](https://github.com/rails/rails/commit/7676659633057dacd97b8da66e0d9119809b343e)) + +* Removed deprecated + `ActionDispatch::Http::Parameters#symbolized_path_parameters`. + ([commit](https://github.com/rails/rails/commit/7fe7973cd8bd119b724d72c5f617cf94c18edf9e)) + +* Removed deprecated option `use_route` in controller tests. + ([commit](https://github.com/rails/rails/commit/e4cfd353a47369dd32198b0e67b8cbb2f9a1c548)) + +* Removed `assigns` and `assert_template`. Both methods have been extracted + into the + [rails-controller-testing](https://github.com/rails/rails-controller-testing) + gem. + ([Pull Request](https://github.com/rails/rails/pull/20138)) + +### Deprecations + +* Deprecated all `*_filter` callbacks in favor of `*_action` callbacks. + ([Pull Request](https://github.com/rails/rails/pull/18410)) + +* Deprecated `*_via_redirect` integration test methods. Use `follow_redirect!` + manually after the request call for the same behavior. + ([Pull Request](https://github.com/rails/rails/pull/18693)) + +* Deprecated `AbstractController#skip_action_callback` in favor of individual + skip_callback methods. + ([Pull Request](https://github.com/rails/rails/pull/19060)) + +* Deprecated `:nothing` option for `render` method. + ([Pull Request](https://github.com/rails/rails/pull/20336)) + +* Deprecated passing first parameter as `Hash` and default status code for + `head` method. + ([Pull Request](https://github.com/rails/rails/pull/20407)) + +* Deprecated using strings or symbols for middleware class names. Use class + names instead. + ([commit](https://github.com/rails/rails/commit/83b767ce)) + +* Deprecated accessing mime types via constants (eg. `Mime::HTML`). Use the + subscript operator with a symbol instead (eg. `Mime[:html]`). + ([Pull Request](https://github.com/rails/rails/pull/21869)) + +* Deprecated `redirect_to :back` in favor of `redirect_back`, which accepts a + required `fallback_location` argument, thus eliminating the possibility of a + `RedirectBackError`. + ([Pull Request](https://github.com/rails/rails/pull/22506)) + +* `ActionDispatch::IntegrationTest` and `ActionController::TestCase` deprecate positional arguments in favor of + keyword arguments. ([Pull Request](https://github.com/rails/rails/pull/18323)) + +* Deprecated `:controller` and `:action` path parameters. + ([Pull Request](https://github.com/rails/rails/pull/23980)) + +* Deprecated env method on controller instances. + ([commit](https://github.com/rails/rails/commit/05934d24aff62d66fc62621aa38dae6456e276be)) + +* `ActionDispatch::ParamsParser` is deprecated and was removed from the + middleware stack. To configure the parameter parsers use + `ActionDispatch::Request.parameter_parsers=`. + ([commit](https://github.com/rails/rails/commit/38d2bf5fd1f3e014f2397898d371c339baa627b1), + [commit](https://github.com/rails/rails/commit/5ed38014811d4ce6d6f957510b9153938370173b)) + +### Notable changes + +* Added `ActionController::Renderer` to render arbitrary templates + outside controller actions. + ([Pull Request](https://github.com/rails/rails/pull/18546)) + +* Migrating to keyword arguments syntax in `ActionController::TestCase` and + `ActionDispatch::Integration` HTTP request methods. + ([Pull Request](https://github.com/rails/rails/pull/18323)) + +* Added `http_cache_forever` to Action Controller, so we can cache a response + that never gets expired. + ([Pull Request](https://github.com/rails/rails/pull/18394)) + +* Provide friendlier access to request variants. + ([Pull Request](https://github.com/rails/rails/pull/18939)) + +* For actions with no corresponding templates, render `head :no_content` + instead of raising an error. + ([Pull Request](https://github.com/rails/rails/pull/19377)) + +* Added the ability to override default form builder for a controller. + ([Pull Request](https://github.com/rails/rails/pull/19736)) + +* Added support for API only apps. + `ActionController::API` is added as a replacement of + `ActionController::Base` for this kind of applications. + ([Pull Request](https://github.com/rails/rails/pull/19832)) + +* Make `ActionController::Parameters` no longer inherits from + `HashWithIndifferentAccess`. + ([Pull Request](https://github.com/rails/rails/pull/20868)) + +* Make it easier to opt in to `config.force_ssl` and `config.ssl_options` by + making them less dangerous to try and easier to disable. + ([Pull Request](https://github.com/rails/rails/pull/21520)) + +* Added the ability of returning arbitrary headers to `ActionDispatch::Static`. + ([Pull Request](https://github.com/rails/rails/pull/19135)) + +* Changed the `protect_from_forgery` prepend default to `false`. + ([commit](https://github.com/rails/rails/commit/39794037817703575c35a75f1961b01b83791191)) + +* `ActionController::TestCase` will be moved to its own gem in Rails 5.1. Use + `ActionDispatch::IntegrationTest` instead. + ([commit](https://github.com/rails/rails/commit/4414c5d1795e815b102571425974a8b1d46d932d)) + +* Rails generates weak ETags by default. + ([Pull Request](https://github.com/rails/rails/pull/17573)) + +* Controller actions without an explicit `render` call and with no + corresponding templates will render `head :no_content` implicitly + instead of raising an error. + (Pull Request [1](https://github.com/rails/rails/pull/19377), + [2](https://github.com/rails/rails/pull/23827)) + +* Added an option for per-form CSRF tokens. + ([Pull Request](https://github.com/rails/rails/pull/22275)) + +* Added request encoding and response parsing to integration tests. + ([Pull Request](https://github.com/rails/rails/pull/21671)) + +* Add `ActionController#helpers` to get access to the view context + at the controller level. + ([Pull Request](https://github.com/rails/rails/pull/24866)) + +* Discarded flash messages get removed before storing into session. + ([Pull Request](https://github.com/rails/rails/pull/18721)) + +* Added support for passing collection of records to `fresh_when` and + `stale?`. + ([Pull Request](https://github.com/rails/rails/pull/18374)) + +* `ActionController::Live` became an `ActiveSupport::Concern`. That + means it can't be just included in other modules without extending + them with `ActiveSupport::Concern` or `ActionController::Live` + won't take effect in production. Some people may be using another + module to include some special `Warden`/`Devise` authentication + failure handling code as well since the middleware can't catch a + `:warden` thrown by a spawned thread which is the case when using + `ActionController::Live`. + ([More details in this issue](https://github.com/rails/rails/issues/25581)) + +* Introduce `Response#strong_etag=` and `#weak_etag=` and analogous + options for `fresh_when` and `stale?`. + ([Pull Request](https://github.com/rails/rails/pull/24387)) + +Action View +------------- + +Please refer to the [Changelog][action-view] for detailed changes. + +### Removals + +* Removed deprecated `AbstractController::Base::parent_prefixes`. + ([commit](https://github.com/rails/rails/commit/34bcbcf35701ca44be559ff391535c0dd865c333)) + +* Removed `ActionView::Helpers::RecordTagHelper`, this functionality + has been extracted to the + [record_tag_helper](https://github.com/rails/record_tag_helper) gem. + ([Pull Request](https://github.com/rails/rails/pull/18411)) + +* Removed `:rescue_format` option for `translate` helper since it's no longer + supported by I18n. + ([Pull Request](https://github.com/rails/rails/pull/20019)) + +### Notable Changes + +* Changed the default template handler from `ERB` to `Raw`. + ([commit](https://github.com/rails/rails/commit/4be859f0fdf7b3059a28d03c279f03f5938efc80)) + +* Collection rendering can cache and fetches multiple partials at once. + ([Pull Request](https://github.com/rails/rails/pull/18948), + [commit](https://github.com/rails/rails/commit/e93f0f0f133717f9b06b1eaefd3442bd0ff43985)) + +* Added wildcard matching to explicit dependencies. + ([Pull Request](https://github.com/rails/rails/pull/20904)) + +* Make `disable_with` the default behavior for submit tags. Disables the + button on submit to prevent double submits. + ([Pull Request](https://github.com/rails/rails/pull/21135)) + +* Partial template name no longer has to be a valid Ruby identifier. + ([commit](https://github.com/rails/rails/commit/da9038e)) + +* The `datetime_tag` helper now generates an input tag with the type of + `datetime-local`. + ([Pull Request](https://github.com/rails/rails/pull/25469)) + +* Allow blocks while rendering with the `render partial:` helper. + ([Pull Request](https://github.com/rails/rails/pull/17974)) + +Action Mailer +------------- + +Please refer to the [Changelog][action-mailer] for detailed changes. + +### Removals + +* Removed deprecated `*_path` helpers in email views. + ([commit](https://github.com/rails/rails/commit/d282125a18c1697a9b5bb775628a2db239142ac7)) + +* Removed deprecated `deliver` and `deliver!` methods. + ([commit](https://github.com/rails/rails/commit/755dcd0691f74079c24196135f89b917062b0715)) + +### Notable changes + +* Template lookup now respects default locale and I18n fallbacks. + ([commit](https://github.com/rails/rails/commit/ecb1981b)) + +* Added `_mailer` suffix to mailers created via generator, following the same + naming convention used in controllers and jobs. + ([Pull Request](https://github.com/rails/rails/pull/18074)) + +* Added `assert_enqueued_emails` and `assert_no_enqueued_emails`. + ([Pull Request](https://github.com/rails/rails/pull/18403)) + +* Added `config.action_mailer.deliver_later_queue_name` configuration to set + the mailer queue name. + ([Pull Request](https://github.com/rails/rails/pull/18587)) + +* Added support for fragment caching in Action Mailer views. + Added new config option `config.action_mailer.perform_caching` to determine + whether your templates should perform caching or not. + ([Pull Request](https://github.com/rails/rails/pull/22825)) + + +Active Record +------------- + +Please refer to the [Changelog][active-record] for detailed changes. + +### Removals + +* Removed deprecated behavior allowing nested arrays to be passed as query + values. ([Pull Request](https://github.com/rails/rails/pull/17919)) + +* Removed deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema`. This + method was replaced by `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`. + ([commit](https://github.com/rails/rails/commit/ad783136d747f73329350b9bb5a5e17c8f8800da)) + +* Removed deprecated `serialized_attributes`. + ([commit](https://github.com/rails/rails/commit/82043ab53cb186d59b1b3be06122861758f814b2)) + +* Removed deprecated automatic counter caches on `has_many :through`. + ([commit](https://github.com/rails/rails/commit/87c8ce340c6c83342df988df247e9035393ed7a0)) + +* Removed deprecated `sanitize_sql_hash_for_conditions`. + ([commit](https://github.com/rails/rails/commit/3a59dd212315ebb9bae8338b98af259ac00bbef3)) + +* Removed deprecated `Reflection#source_macro`. + ([commit](https://github.com/rails/rails/commit/ede8c199a85cfbb6457d5630ec1e285e5ec49313)) + +* Removed deprecated `symbolized_base_class` and `symbolized_sti_name`. + ([commit](https://github.com/rails/rails/commit/9013e28e52eba3a6ffcede26f85df48d264b8951)) + +* Removed deprecated `ActiveRecord::Base.disable_implicit_join_references=`. + ([commit](https://github.com/rails/rails/commit/0fbd1fc888ffb8cbe1191193bf86933110693dfc)) + +* Removed deprecated access to connection specification using a string accessor. + ([commit](https://github.com/rails/rails/commit/efdc20f36ccc37afbb2705eb9acca76dd8aabd4f)) + +* Removed deprecated support to preload instance-dependent associations. + ([commit](https://github.com/rails/rails/commit/4ed97979d14c5e92eb212b1a629da0a214084078)) + +* Removed deprecated support for PostgreSQL ranges with exclusive lower bounds. + ([commit](https://github.com/rails/rails/commit/a076256d63f64d194b8f634890527a5ed2651115)) + +* Removed deprecation when modifying a relation with cached Arel. + This raises an `ImmutableRelation` error instead. + ([commit](https://github.com/rails/rails/commit/3ae98181433dda1b5e19910e107494762512a86c)) + +* Removed `ActiveRecord::Serialization::XmlSerializer` from core. This feature + has been extracted into the + [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml) + gem. ([Pull Request](https://github.com/rails/rails/pull/21161)) + +* Removed support for the legacy `mysql` database adapter from core. Most users should + be able to use `mysql2`. It will be converted to a separate gem when we find someone + to maintain it. ([Pull Request 1](https://github.com/rails/rails/pull/22642), + [Pull Request 2](https://github.com/rails/rails/pull/22715)) + +* Removed support for the `protected_attributes` gem. + ([commit](https://github.com/rails/rails/commit/f4fbc0301021f13ae05c8e941c8efc4ae351fdf9)) + +* Removed support for PostgreSQL versions below 9.1. + ([Pull Request](https://github.com/rails/rails/pull/23434)) + +* Removed support for `activerecord-deprecated_finders` gem. + ([commit](https://github.com/rails/rails/commit/78dab2a8569408658542e462a957ea5a35aa4679)) + +* Removed `ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES` constant. + ([commit](https://github.com/rails/rails/commit/a502703c3d2151d4d3b421b29fefdac5ad05df61)) + +### Deprecations + +* Deprecated passing a class as a value in a query. Users should pass strings + instead. ([Pull Request](https://github.com/rails/rails/pull/17916)) + +* Deprecated returning `false` as a way to halt Active Record callback + chains. The recommended way is to + `throw(:abort)`. ([Pull Request](https://github.com/rails/rails/pull/17227)) + +* Deprecated `ActiveRecord::Base.errors_in_transactional_callbacks=`. + ([commit](https://github.com/rails/rails/commit/07d3d402341e81ada0214f2cb2be1da69eadfe72)) + +* Deprecated `Relation#uniq` use `Relation#distinct` instead. + ([commit](https://github.com/rails/rails/commit/adfab2dcf4003ca564d78d4425566dd2d9cd8b4f)) + +* Deprecated the PostgreSQL `:point` type in favor of a new one which will return + `Point` objects instead of an `Array` + ([Pull Request](https://github.com/rails/rails/pull/20448)) + +* Deprecated force association reload by passing a truthy argument to + association method. + ([Pull Request](https://github.com/rails/rails/pull/20888)) + +* Deprecated the keys for association `restrict_dependent_destroy` errors in favor + of new key names. + ([Pull Request](https://github.com/rails/rails/pull/20668)) + +* Synchronize behavior of `#tables`. + ([Pull Request](https://github.com/rails/rails/pull/21601)) + +* Deprecated `SchemaCache#tables`, `SchemaCache#table_exists?` and + `SchemaCache#clear_table_cache!` in favor of their new data source + counterparts. + ([Pull Request](https://github.com/rails/rails/pull/21715)) + +* Deprecated `connection.tables` on the SQLite3 and MySQL adapters. + ([Pull Request](https://github.com/rails/rails/pull/21601)) + +* Deprecated passing arguments to `#tables` - the `#tables` method of some + adapters (mysql2, sqlite3) would return both tables and views while others + (postgresql) just return tables. To make their behavior consistent, + `#tables` will return only tables in the future. + ([Pull Request](https://github.com/rails/rails/pull/21601)) + +* Deprecated `table_exists?` - The `#table_exists?` method would check both + tables and views. To make their behavior consistent with `#tables`, + `#table_exists?` will check only tables in the future. + ([Pull Request](https://github.com/rails/rails/pull/21601)) + +* Deprecate sending the `offset` argument to `find_nth`. Please use the + `offset` method on relation instead. + ([Pull Request](https://github.com/rails/rails/pull/22053)) + +* Deprecated `{insert|update|delete}_sql` in `DatabaseStatements`. + Use the `{insert|update|delete}` public methods instead. + ([Pull Request](https://github.com/rails/rails/pull/23086)) + +* Deprecated `use_transactional_fixtures` in favor of + `use_transactional_tests` for more clarity. + ([Pull Request](https://github.com/rails/rails/pull/19282)) + +* Deprecated passing a column to `ActiveRecord::Connection#quote`. + ([commit](https://github.com/rails/rails/commit/7bb620869725ad6de603f6a5393ee17df13aa96c)) + +* Added an option `end` to `find_in_batches` that complements the `start` + parameter to specify where to stop batch processing. + ([Pull Request](https://github.com/rails/rails/pull/12257)) + + +### Notable changes + +* Added a `foreign_key` option to `references` while creating the table. + ([commit](https://github.com/rails/rails/commit/99a6f9e60ea55924b44f894a16f8de0162cf2702)) + +* New attributes + API. ([commit](https://github.com/rails/rails/commit/8c752c7ac739d5a86d4136ab1e9d0142c4041e58)) + +* Added `:_prefix`/`:_suffix` option to `enum` definition. + ([Pull Request](https://github.com/rails/rails/pull/19813), + [Pull Request](https://github.com/rails/rails/pull/20999)) + +* Added `#cache_key` to `ActiveRecord::Relation`. + ([Pull Request](https://github.com/rails/rails/pull/20884)) + +* Changed the default `null` value for `timestamps` to `false`. + ([commit](https://github.com/rails/rails/commit/a939506f297b667291480f26fa32a373a18ae06a)) + +* Added `ActiveRecord::SecureToken` in order to encapsulate generation of + unique tokens for attributes in a model using `SecureRandom`. + ([Pull Request](https://github.com/rails/rails/pull/18217)) + +* Added `:if_exists` option for `drop_table`. + ([Pull Request](https://github.com/rails/rails/pull/18597)) + +* Added `ActiveRecord::Base#accessed_fields`, which can be used to quickly + discover which fields were read from a model when you are looking to only + select the data you need from the database. + ([commit](https://github.com/rails/rails/commit/be9b68038e83a617eb38c26147659162e4ac3d2c)) + +* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR + operator to combine WHERE or HAVING clauses. + ([commit](https://github.com/rails/rails/commit/b0b37942d729b6bdcd2e3178eda7fa1de203b3d0)) + +* Added `ActiveRecord::Base.suppress` to prevent the receiver from being saved + during the given block. + ([Pull Request](https://github.com/rails/rails/pull/18910)) + +* `belongs_to` will now trigger a validation error by default if the + association is not present. You can turn this off on a per-association basis + with `optional: true`. Also deprecate `required` option in favor of `optional` + for `belongs_to`. + ([Pull Request](https://github.com/rails/rails/pull/18937)) + +* Added `config.active_record.dump_schemas` to configure the behavior of + `db:structure:dump`. + ([Pull Request](https://github.com/rails/rails/pull/19347)) + +* Added `config.active_record.warn_on_records_fetched_greater_than` option. + ([Pull Request](https://github.com/rails/rails/pull/18846)) + +* Added a native JSON data type support in MySQL. + ([Pull Request](https://github.com/rails/rails/pull/21110)) + +* Added support for dropping indexes concurrently in PostgreSQL. + ([Pull Request](https://github.com/rails/rails/pull/21317)) + +* Added `#views` and `#view_exists?` methods on connection adapters. + ([Pull Request](https://github.com/rails/rails/pull/21609)) + +* Added `ActiveRecord::Base.ignored_columns` to make some columns + invisible from Active Record. + ([Pull Request](https://github.com/rails/rails/pull/21720)) + +* Added `connection.data_sources` and `connection.data_source_exists?`. + These methods determine what relations can be used to back Active Record + models (usually tables and views). + ([Pull Request](https://github.com/rails/rails/pull/21715)) + +* Allow fixtures files to set the model class in the YAML file itself. + ([Pull Request](https://github.com/rails/rails/pull/20574)) + +* Added ability to default to `uuid` as primary key when generating database + migrations. ([Pull Request](https://github.com/rails/rails/pull/21762)) + +* Added `ActiveRecord::Relation#left_joins` and + `ActiveRecord::Relation#left_outer_joins`. + ([Pull Request](https://github.com/rails/rails/pull/12071)) + +* Added `after_{create,update,delete}_commit` callbacks. + ([Pull Request](https://github.com/rails/rails/pull/22516)) + +* Version the API presented to migration classes, so we can change parameter + defaults without breaking existing migrations, or forcing them to be + rewritten through a deprecation cycle. + ([Pull Request](https://github.com/rails/rails/pull/21538)) + +* `ApplicationRecord` is a new superclass for all app models, analogous to app + controllers subclassing `ApplicationController` instead of + `ActionController::Base`. This gives apps a single spot to configure app-wide + model behavior. + ([Pull Request](https://github.com/rails/rails/pull/22567)) + +* Added ActiveRecord `#second_to_last` and `#third_to_last` methods. + ([Pull Request](https://github.com/rails/rails/pull/23583)) + +* Added ability to annotate database objects (tables, columns, indexes) + with comments stored in database metadata for PostgreSQL & MySQL. + ([Pull Request](https://github.com/rails/rails/pull/22911)) + +* Added prepared statements support to `mysql2` adapter, for mysql2 0.4.4+, + Previously this was only supported on the deprecated `mysql` legacy adapter. + To enable, set `prepared_statements: true` in config/database.yml. + ([Pull Request](https://github.com/rails/rails/pull/23461)) + +* Added ability to call `ActionRecord::Relation#update` on relation objects + which will run validations on callbacks on all objects in the relation. + ([Pull Request](https://github.com/rails/rails/pull/11898)) + +* Added `:touch` option to the `save` method so that records can be saved without + updating timestamps. + ([Pull Request](https://github.com/rails/rails/pull/18225)) + +* Added expression indexes and operator classes support for PostgreSQL. + ([commit](https://github.com/rails/rails/commit/edc2b7718725016e988089b5fb6d6fb9d6e16882)) + +* Added `:index_errors` option to add indexes to errors of nested attributes. + ([Pull Request](https://github.com/rails/rails/pull/19686)) + +* Added support for bidirectional destroy dependencies. + ([Pull Request](https://github.com/rails/rails/pull/18548)) + +* Added support for `after_commit` callbacks in transactional tests. + ([Pull Request](https://github.com/rails/rails/pull/18458)) + +* Added `foreign_key_exists?` method to see if a foreign key exists on a table + or not. + ([Pull Request](https://github.com/rails/rails/pull/18662)) + +* Added `:time` option to `touch` method to touch records with different time + than the current time. + ([Pull Request](https://github.com/rails/rails/pull/18956)) + +* Change transaction callbacks to not swallow errors. + Before this change any errors raised inside a transaction callback + were getting rescued and printed in the logs, unless you used + the (newly deprecated) `raise_in_transactional_callbacks = true` option. + + Now these errors are not rescued anymore and just bubble up, matching the + behavior of other callbacks. + ([commit](https://github.com/rails/rails/commit/07d3d402341e81ada0214f2cb2be1da69eadfe72)) + +Active Model +------------ + +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +* Removed deprecated `ActiveModel::Dirty#reset_#{attribute}` and + `ActiveModel::Dirty#reset_changes`. + ([Pull Request](https://github.com/rails/rails/commit/37175a24bd508e2983247ec5d011d57df836c743)) + +* Removed XML serialization. This feature has been extracted into the + [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml) gem. + ([Pull Request](https://github.com/rails/rails/pull/21161)) + +* Removed `ActionController::ModelNaming` module. + ([Pull Request](https://github.com/rails/rails/pull/18194)) + +### Deprecations + +* Deprecated returning `false` as a way to halt Active Model and + `ActiveModel::Validations` callback chains. The recommended way is to + `throw(:abort)`. ([Pull Request](https://github.com/rails/rails/pull/17227)) + +* Deprecated `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and + `ActiveModel::Errors#[]=` methods that have inconsistent behavior. + ([Pull Request](https://github.com/rails/rails/pull/18634)) + +* Deprecated the `:tokenizer` option for `validates_length_of`, in favor of + plain Ruby. + ([Pull Request](https://github.com/rails/rails/pull/19585)) + +* Deprecated `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank` + with no replacement. + ([Pull Request](https://github.com/rails/rails/pull/18996)) + +### Notable changes + +* Added `ActiveModel::Errors#details` to determine what validator has failed. + ([Pull Request](https://github.com/rails/rails/pull/18322)) + +* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment` + allowing to use it for any object as an includable module. + ([Pull Request](https://github.com/rails/rails/pull/10776)) + +* Added `ActiveModel::Dirty#[attr_name]_previously_changed?` and + `ActiveModel::Dirty#[attr_name]_previous_change` to improve access + to recorded changes after the model has been saved. + ([Pull Request](https://github.com/rails/rails/pull/19847)) + +* Validate multiple contexts on `valid?` and `invalid?` at once. + ([Pull Request](https://github.com/rails/rails/pull/21069)) + +* Change `validates_acceptance_of` to accept `true` as default value + apart from `1`. + ([Pull Request](https://github.com/rails/rails/pull/18439)) + +Active Job +----------- + +Please refer to the [Changelog][active-job] for detailed changes. + +### Notable changes + +* `ActiveJob::Base.deserialize` delegates to the job class. This allows jobs + to attach arbitrary metadata when they get serialized and read it back when + they get performed. + ([Pull Request](https://github.com/rails/rails/pull/18260)) + +* Add ability to configure the queue adapter on a per job basis without + affecting each other. + ([Pull Request](https://github.com/rails/rails/pull/16992)) + +* A generated job now inherits from `app/jobs/application_job.rb` by default. + ([Pull Request](https://github.com/rails/rails/pull/19034)) + +* Allow `DelayedJob`, `Sidekiq`, `qu`, `que`, and `queue_classic` to report + the job id back to `ActiveJob::Base` as `provider_job_id`. + ([Pull Request](https://github.com/rails/rails/pull/20064), + [Pull Request](https://github.com/rails/rails/pull/20056), + [commit](https://github.com/rails/rails/commit/68e3279163d06e6b04e043f91c9470e9259bbbe0)) + +* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that + queue jobs to a `concurrent-ruby` thread pool. + ([Pull Request](https://github.com/rails/rails/pull/21257)) + +* Change the default adapter from inline to async. It's a better default as + tests will then not mistakenly come to rely on behavior happening + synchronously. + ([commit](https://github.com/rails/rails/commit/625baa69d14881ac49ba2e5c7d9cac4b222d7022)) + +Active Support +-------------- + +Please refer to the [Changelog][active-support] for detailed changes. + +### Removals + +* Removed deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. + ([commit](https://github.com/rails/rails/commit/d6e06ea8275cdc3f126f926ed9b5349fde374b10)) + +* Removed deprecated methods `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=` + and `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string`. + ([commit](https://github.com/rails/rails/commit/c8019c0611791b2716c6bed48ef8dcb177b7869c)) + +* Removed deprecated `ActiveSupport::SafeBuffer#prepend`. + ([commit](https://github.com/rails/rails/commit/e1c8b9f688c56aaedac9466a4343df955b4a67ec)) + +* Removed deprecated methods from `Kernel`. `silence_stderr`, `silence_stream`, + `capture` and `quietly`. + ([commit](https://github.com/rails/rails/commit/481e49c64f790e46f4aff3ed539ed227d2eb46cb)) + +* Removed deprecated `active_support/core_ext/big_decimal/yaml_conversions` + file. + ([commit](https://github.com/rails/rails/commit/98ea19925d6db642731741c3b91bd085fac92241)) + +* Removed deprecated methods `ActiveSupport::Cache::Store.instrument` and + `ActiveSupport::Cache::Store.instrument=`. + ([commit](https://github.com/rails/rails/commit/a3ce6ca30ed0e77496c63781af596b149687b6d7)) + +* Removed deprecated `Class#superclass_delegating_accessor`. + Use `Class#class_attribute` instead. + ([Pull Request](https://github.com/rails/rails/pull/16938)) + +* Removed deprecated `ThreadSafe::Cache`. Use `Concurrent::Map` instead. + ([Pull Request](https://github.com/rails/rails/pull/21679)) + +* Removed `Object#itself` as it is implemented in Ruby 2.2. + ([Pull Request](https://github.com/rails/rails/pull/18244)) + +### Deprecations + +* Deprecated `MissingSourceFile` in favor of `LoadError`. + ([commit](https://github.com/rails/rails/commit/734d97d2)) + +* Deprecated `alias_method_chain` in favour of `Module#prepend` introduced in + Ruby 2.0. + ([Pull Request](https://github.com/rails/rails/pull/19434)) + +* Deprecated `ActiveSupport::Concurrency::Latch` in favor of + `Concurrent::CountDownLatch` from concurrent-ruby. + ([Pull Request](https://github.com/rails/rails/pull/20866)) + +* Deprecated `:prefix` option of `number_to_human_size` with no replacement. + ([Pull Request](https://github.com/rails/rails/pull/21191)) + +* Deprecated `Module#qualified_const_` in favour of the builtin + `Module#const_` methods. + ([Pull Request](https://github.com/rails/rails/pull/17845)) + +* Deprecated passing string to define callback. + ([Pull Request](https://github.com/rails/rails/pull/22598)) + +* Deprecated `ActiveSupport::Cache::Store#namespaced_key`, + `ActiveSupport::Cache::MemCachedStore#escape_key`, and + `ActiveSupport::Cache::FileStore#key_file_path`. + Use `normalize_key` instead. + ([Pull Request](https://github.com/rails/rails/pull/22215), + [commit](https://github.com/rails/rails/commit/a8f773b0)) + +* Deprecated `ActiveSupport::Cache::LocaleCache#set_cache_value` in favor of `write_cache_value`. + ([Pull Request](https://github.com/rails/rails/pull/22215)) + +* Deprecated passing arguments to `assert_nothing_raised`. + ([Pull Request](https://github.com/rails/rails/pull/23789)) + +* Deprecated `Module.local_constants` in favor of `Module.constants(false)`. + ([Pull Request](https://github.com/rails/rails/pull/23936)) + + +### Notable changes + +* Added `#verified` and `#valid_message?` methods to + `ActiveSupport::MessageVerifier`. + ([Pull Request](https://github.com/rails/rails/pull/17727)) + +* Changed the way in which callback chains can be halted. The preferred method + to halt a callback chain from now on is to explicitly `throw(:abort)`. + ([Pull Request](https://github.com/rails/rails/pull/17227)) + +* New config option + `config.active_support.halt_callback_chains_on_return_false` to specify + whether ActiveRecord, ActiveModel and ActiveModel::Validations callback + chains can be halted by returning `false` in a 'before' callback. + ([Pull Request](https://github.com/rails/rails/pull/17227)) + +* Changed the default test order from `:sorted` to `:random`. + ([commit](https://github.com/rails/rails/commit/5f777e4b5ee2e3e8e6fd0e2a208ec2a4d25a960d)) + +* Added `#on_weekend?`, `#on_weekday?`, `#next_weekday`, `#prev_weekday` methods to `Date`, + `Time`, and `DateTime`. + ([Pull Request](https://github.com/rails/rails/pull/18335), + [Pull Request](https://github.com/rails/rails/pull/23687)) + +* Added `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`, + and `DateTime`. + ([Pull Request](https://github.com/rails/rails/pull/18335)) + +* Added `#prev_day` and `#next_day` counterparts to `#yesterday` and + `#tomorrow` for `Date`, `Time`, and `DateTime`. + ([Pull Request](https://github.com/rails/rails/pull/18335)) + +* Added `SecureRandom.base58` for generation of random base58 strings. + ([commit](https://github.com/rails/rails/commit/b1093977110f18ae0cafe56c3d99fc22a7d54d1b)) + +* Added `file_fixture` to `ActiveSupport::TestCase`. + It provides a simple mechanism to access sample files in your test cases. + ([Pull Request](https://github.com/rails/rails/pull/18658)) + +* Added `#without` on `Enumerable` and `Array` to return a copy of an + enumerable without the specified elements. + ([Pull Request](https://github.com/rails/rails/pull/19157)) + +* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`. + ([Pull Request](https://github.com/rails/rails/pull/18939)) + +* Added `ActiveSupport::TimeZone#strptime` to allow parsing times as if + from a given timezone. + ([commit](https://github.com/rails/rails/commit/a5e507fa0b8180c3d97458a9b86c195e9857d8f6)) + +* Added `Integer#positive?` and `Integer#negative?` query methods + in the vein of `Integer#zero?`. + ([commit](https://github.com/rails/rails/commit/e54277a45da3c86fecdfa930663d7692fd083daa)) + +* Added a bang version to `ActiveSupport::OrderedOptions` get methods which will raise + an `KeyError` if the value is `.blank?`. + ([Pull Request](https://github.com/rails/rails/pull/20208)) + +* Added `Time.days_in_year` to return the number of days in the given year, or the + current year if no argument is provided. + ([commit](https://github.com/rails/rails/commit/2f4f4d2cf1e4c5a442459fc250daf66186d110fa)) + +* Added an evented file watcher to asynchronously detect changes in the + application source code, routes, locales, etc. + ([Pull Request](https://github.com/rails/rails/pull/22254)) + +* Added thread_m/cattr_accessor/reader/writer suite of methods for declaring + class and module variables that live per-thread. + ([Pull Request](https://github.com/rails/rails/pull/22630)) + +* Added `Array#second_to_last` and `Array#third_to_last` methods. + ([Pull Request](https://github.com/rails/rails/pull/23583)) + +* Publish `ActiveSupport::Executor` and `ActiveSupport::Reloader` APIs to allow + components and libraries to manage, and participate in, the execution of + application code, and the application reloading process. + ([Pull Request](https://github.com/rails/rails/pull/23807)) + +* `ActiveSupport::Duration` now supports ISO8601 formatting and parsing. + ([Pull Request](https://github.com/rails/rails/pull/16917)) + +* `ActiveSupport::JSON.decode` now supports parsing ISO8601 local times when + `parse_json_times` is enabled. + ([Pull Request](https://github.com/rails/rails/pull/23011)) + +* `ActiveSupport::JSON.decode` now return `Date` objects for date strings. + ([Pull Request](https://github.com/rails/rails/pull/23011)) + +* Added ability to `TaggedLogging` to allow loggers to be instantiated multiple + times so that they don't share tags with each other. + ([Pull Request](https://github.com/rails/rails/pull/9065)) + +Credits +------- + +See the +[full list of contributors to Rails](http://contributors.rubyonrails.org/) for +the many people who spent many hours making Rails, the stable and robust +framework it is. Kudos to all of them. + +[railties]: https://github.com/rails/rails/blob/5-0-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/5-0-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/5-0-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/5-0-stable/actionmailer/CHANGELOG.md +[action-cable]: https://github.com/rails/rails/blob/5-0-stable/actioncable/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/5-0-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/5-0-stable/activesupport/CHANGELOG.md +[active-job]: https://github.com/rails/rails/blob/5-0-stable/activejob/CHANGELOG.md diff --git a/source/5_1_release_notes.md b/source/5_1_release_notes.md new file mode 100644 index 0000000..fa92b9e --- /dev/null +++ b/source/5_1_release_notes.md @@ -0,0 +1,652 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Ruby on Rails 5.1 Release Notes +=============================== + +Highlights in Rails 5.1: + +* Yarn Support +* Optional Webpack support +* jQuery no longer a default dependency +* System tests +* Encrypted secrets +* Parameterized mailers +* Direct & resolved routes +* Unification of form_for and form_tag into form_with + +These release notes cover only the major changes. To learn about various bug +fixes and changes, please refer to the change logs or check out the [list of +commits](https://github.com/rails/rails/commits/5-1-stable) in the main Rails +repository on GitHub. + +-------------------------------------------------------------------------------- + +Upgrading to Rails 5.1 +---------------------- + +If you're upgrading an existing application, it's a great idea to have good test +coverage before going in. You should also first upgrade to Rails 5.0 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 5.1. A list of things to watch out for when upgrading is +available in the +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-0-to-rails-5-1) +guide. + + +Major Features +-------------- + +### Yarn Support + +[Pull Request](https://github.com/rails/rails/pull/26836) + +Rails 5.1 allows managing JavaScript dependencies +from NPM via Yarn. This will make it easy to use libraries like React, VueJS +or any other library from NPM world. The Yarn support is integrated with +the asset pipeline so that all dependencies will work seamlessly with the +Rails 5.1 app. + +### Optional Webpack support + +[Pull Request](https://github.com/rails/rails/pull/27288) + +Rails apps can integrate with [Webpack](https://webpack.js.org/), a JavaScript +asset bundler, more easily using the new [Webpacker](https://github.com/rails/webpacker) +gem. Use the `--webpack` flag when generating new applications to enable Webpack +integration. + +This is fully compatible with the asset pipeline, which you can continue to use for +images, fonts, sounds, and other assets. You can even have some JavaScript code +managed by the asset pipeline, and other code processed via Webpack. All of this is managed +by Yarn, which is enabled by default. + +### jQuery no longer a default dependency + +[Pull Request](https://github.com/rails/rails/pull/27113) + +jQuery was required by default in earlier versions of Rails to provide features +like `data-remote`, `data-confirm` and other parts of Rails' Unobtrusive JavaScript +offerings. It is no longer required, as the UJS has been rewritten to use plain, +vanilla JavaScript. This code now ships inside of Action View as +`rails-ujs`. + +You can still use jQuery if needed, but it is no longer required by default. + +### System tests + +[Pull Request](https://github.com/rails/rails/pull/26703) + +Rails 5.1 has baked-in support for writing Capybara tests, in the form of +System tests. You no longer need to worry about configuring Capybara and +database cleaning strategies for such tests. Rails 5.1 provides a wrapper +for running tests in Chrome with additional features such as failure +screenshots. + +### Encrypted secrets + +[Pull Request](https://github.com/rails/rails/pull/28038) + +Rails now allows management of application secrets in a secure way, +inspired by the [sekrets](https://github.com/ahoward/sekrets) gem. + +Run `bin/rails secrets:setup` to setup a new encrypted secrets file. This will +also generate a master key, which must be stored outside of the repository. The +secrets themselves can then be safely checked into the revision control system, +in an encrypted form. + +Secrets will be decrypted in production, using a key stored either in the +`RAILS_MASTER_KEY` environment variable, or in a key file. + +### Parameterized mailers + +[Pull Request](https://github.com/rails/rails/pull/27825) + +Allows specifying common parameters used for all methods in a mailer class in +order to share instance variables, headers and other common setup. + +``` ruby +class InvitationsMailer < ApplicationMailer + before_action { @inviter, @invitee = params[:inviter], params[:invitee] } + before_action { @account = params[:inviter].account } + + def account_invitation + mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})" + end +end + +InvitationsMailer.with(inviter: person_a, invitee: person_b) + .account_invitation.deliver_later +``` + +### Direct & resolved routes + +[Pull Request](https://github.com/rails/rails/pull/23138) + +Rails 5.1 adds two new methods, `resolve` and `direct`, to the routing +DSL. The `resolve` method allows customizing polymorphic mapping of models. + +``` ruby +resource :basket + +resolve("Basket") { [:basket] } +``` + +``` erb +<%= form_for @basket do |form| %> + +<% end %> +``` + +This will generate the singular URL `/basket` instead of the usual `/baskets/:id`. + +The `direct` method allows creation of custom URL helpers. + +``` ruby +direct(:homepage) { "http://www.rubyonrails.org/" } + +>> homepage_url +=> "http://www.rubyonrails.org/" +``` + +The return value of the block must be a valid argument for the `url_for` +method. So, you can pass a valid string URL, Hash, Array, an +Active Model instance, or an Active Model class. + +``` ruby +direct :commentable do |model| + [ model, anchor: model.dom_id ] +end + +direct :main do + { controller: 'pages', action: 'index', subdomain: 'www' } +end +``` + +### Unification of form_for and form_tag into form_with + +[Pull Request](https://github.com/rails/rails/pull/26976) + +Before Rails 5.1, there were two interfaces for handling HTML forms: +`form_for` for model instances and `form_tag` for custom URLs. + +Rails 5.1 combines both of these interfaces with `form_with`, and +can generate form tags based on URLs, scopes or models. + +Using just a URL: + +``` erb +<%= form_with url: posts_path do |form| %> + <%= form.text_field :title %> +<% end %> + +<%# Will generate %> + +
+ +
+``` + +Adding a scope prefixes the input field names: + +``` erb +<%= form_with scope: :post, url: posts_path do |form| %> + <%= form.text_field :title %> +<% end %> + +<%# Will generate %> + +
+ +
+``` + +Using a model infers both the URL and scope: + +``` erb +<%= form_with model: Post.new do |form| %> + <%= form.text_field :title %> +<% end %> + +<%# Will generate %> + +
+ +
+``` + +An existing model makes an update form and fills out field values: + +``` erb +<%= form_with model: Post.first do |form| %> + <%= form.text_field :title %> +<% end %> + +<%# Will generate %> + +
+ + +
+``` + +Incompatibilities +----------------- + +The following changes may require immediate action upon upgrade. + +### Transactional tests with multiple connections + +Transactional tests now wrap all Active Record connections in database +transactions. + +When a test spawns additional threads, and those threads obtain database +connections, those connections are now handled specially: + +The threads will share a single connection, which is inside the managed +transaction. This ensures all threads see the database in the same +state, ignoring the outermost transaction. Previously, such additional +connections were unable to see the fixture rows, for example. + +When a thread enters a nested transaction, it will temporarily obtain +exclusive use of the connection, to maintain isolation. + +If your tests currently rely on obtaining a separate, +outside-of-transaction, connection in a spawned thread, you'll need to +switch to more explicit connection management. + +If your tests spawn threads and those threads interact while also using +explicit database transactions, this change may introduce a deadlock. + +The easy way to opt out of this new behavior is to disable transactional +tests on any test cases it affects. + +Railties +-------- + +Please refer to the [Changelog][railties] for detailed changes. + +### Removals + +* Remove deprecated `config.static_cache_control`. + ([commit](https://github.com/rails/rails/commit/c861decd44198f8d7d774ee6a74194d1ac1a5a13)) + +* Remove deprecated `config.serve_static_files`. + ([commit](https://github.com/rails/rails/commit/0129ca2eeb6d5b2ea8c6e6be38eeb770fe45f1fa)) + +* Remove deprecated file `rails/rack/debugger`. + ([commit](https://github.com/rails/rails/commit/7563bf7b46e6f04e160d664e284a33052f9804b8)) + +* Remove deprecated tasks: `rails:update`, `rails:template`, `rails:template:copy`, + `rails:update:configs` and `rails:update:bin`. + ([commit](https://github.com/rails/rails/commit/f7782812f7e727178e4a743aa2874c078b722eef)) + +* Remove deprecated `CONTROLLER` environment variable for `routes` task. + ([commit](https://github.com/rails/rails/commit/f9ed83321ac1d1902578a0aacdfe55d3db754219)) + +* Remove -j (--javascript) option from `rails new` command. + ([Pull Request](https://github.com/rails/rails/pull/28546)) + +### Notable changes + +* Added a shared section to `config/secrets.yml` that will be loaded for all + environments. + ([commit](https://github.com/rails/rails/commit/e530534265d2c32b5c5f772e81cb9002dcf5e9cf)) + +* The config file `config/secrets.yml` is now loaded in with all keys as symbols. + ([Pull Request](https://github.com/rails/rails/pull/26929)) + +* Removed jquery-rails from default stack. rails-ujs, which is shipped + with Action View, is included as default UJS adapter. + ([Pull Request](https://github.com/rails/rails/pull/27113)) + +* Add Yarn support in new apps with a yarn binstub and package.json. + ([Pull Request](https://github.com/rails/rails/pull/26836)) + +* Add Webpack support in new apps via the `--webpack` option, which will delegate + to the rails/webpacker gem. + ([Pull Request](https://github.com/rails/rails/pull/27288)) + +* Initialize Git repo when generating new app, if option `--skip-git` is not + provided. + ([Pull Request](https://github.com/rails/rails/pull/27632)) + +* Add encrypted secrets in `config/secrets.yml.enc`. + ([Pull Request](https://github.com/rails/rails/pull/28038)) + +* Display railtie class name in `rails initializers`. + ([Pull Request](https://github.com/rails/rails/pull/25257)) + +Action Cable +----------- + +Please refer to the [Changelog][action-cable] for detailed changes. + +### Notable changes + +* Added support for `channel_prefix` to Redis and evented Redis adapters + in `cable.yml` to avoid name collisions when using the same Redis server + with multiple applications. + ([Pull Request](https://github.com/rails/rails/pull/27425)) + +* Add `ActiveSupport::Notifications` hook for broadcasting data. + ([Pull Request](https://github.com/rails/rails/pull/24988)) + +Action Pack +----------- + +Please refer to the [Changelog][action-pack] for detailed changes. + +### Removals + +* Removed support for non-keyword arguments in `#process`, `#get`, `#post`, + `#patch`, `#put`, `#delete`, and `#head` for the `ActionDispatch::IntegrationTest` + and `ActionController::TestCase` classes. + ([Commit](https://github.com/rails/rails/commit/98b8309569a326910a723f521911e54994b112fb), + [Commit](https://github.com/rails/rails/commit/de9542acd56f60d281465a59eac11e15ca8b3323)) + +* Removed deprecated `ActionDispatch::Callbacks.to_prepare` and + `ActionDispatch::Callbacks.to_cleanup`. + ([Commit](https://github.com/rails/rails/commit/3f2b7d60a52ffb2ad2d4fcf889c06b631db1946b)) + +* Removed deprecated methods related to controller filters. + ([Commit](https://github.com/rails/rails/commit/d7be30e8babf5e37a891522869e7b0191b79b757)) + +### Deprecations + +* Deprecated `config.action_controller.raise_on_unfiltered_parameters`. + It doesn't have any effect in Rails 5.1. + ([Commit](https://github.com/rails/rails/commit/c6640fb62b10db26004a998d2ece98baede509e5)) + +### Notable changes + +* Added the `direct` and `resolve` methods to the routing DSL. + ([Pull Request](https://github.com/rails/rails/pull/23138)) + +* Added a new `ActionDispatch::SystemTestCase` class to write system tests in + your applications. + ([Pull Request](https://github.com/rails/rails/pull/26703)) + +Action View +------------- + +Please refer to the [Changelog][action-view] for detailed changes. + +### Removals + +* Removed deprecated `#original_exception` in `ActionView::Template::Error`. + ([commit](https://github.com/rails/rails/commit/b9ba263e5aaa151808df058f5babfed016a1879f)) + +* Remove the option `encode_special_chars` misnomer from `strip_tags`. + ([Pull Request](https://github.com/rails/rails/pull/28061)) + +### Deprecations + +* Deprecated Erubis ERB handler in favor of Erubi. + ([Pull Request](https://github.com/rails/rails/pull/27757)) + +### Notable changes + +* Raw template handler (the default template handler in Rails 5) now outputs + HTML-safe strings. + ([commit](https://github.com/rails/rails/commit/1de0df86695f8fa2eeae6b8b46f9b53decfa6ec8)) + +* Change `datetime_field` and `datetime_field_tag` to generate `datetime-local` + fields. + ([Pull Request](https://github.com/rails/rails/pull/28061)) + +* New Builder-style syntax for HTML tags (`tag.div`, `tag.br`, etc.) + ([Pull Request](https://github.com/rails/rails/pull/25543)) + +* Add `form_with` to unify `form_tag` and `form_for` usage. + ([Pull Request](https://github.com/rails/rails/pull/26976)) + +* Add `check_parameters` option to `current_page?`. + ([Pull Request](https://github.com/rails/rails/pull/27549)) + +Action Mailer +------------- + +Please refer to the [Changelog][action-mailer] for detailed changes. + +### Notable changes + +* Allowed setting custom content type when attachments are included + and body is set inline. + ([Pull Request](https://github.com/rails/rails/pull/27227)) + +* Allowed passing lambdas as values to the `default` method. + ([Commit](https://github.com/rails/rails/commit/1cec84ad2ddd843484ed40b1eb7492063ce71baf)) + +* Added support for parameterized invocation of mailers to share before filters and defaults + between different mailer actions. + ([Commit](https://github.com/rails/rails/commit/1cec84ad2ddd843484ed40b1eb7492063ce71baf)) + +* Passed the incoming arguments to the mailer action to `process.action_mailer` event under + an `args` key. + ([Pull Request](https://github.com/rails/rails/pull/27900)) + +Active Record +------------- + +Please refer to the [Changelog][active-record] for detailed changes. + +### Removals + +* Removed support for passing arguments and block at the same time to + `ActiveRecord::QueryMethods#select`. + ([Commit](https://github.com/rails/rails/commit/4fc3366d9d99a0eb19e45ad2bf38534efbf8c8ce)) + +* Removed deprecated `activerecord.errors.messages.restrict_dependent_destroy.one` and + `activerecord.errors.messages.restrict_dependent_destroy.many` i18n scopes. + ([Commit](https://github.com/rails/rails/commit/00e3973a311)) + +* Removed deprecated force reload argument in singular and collection association readers. + ([Commit](https://github.com/rails/rails/commit/09cac8c67af)) + +* Removed deprecated support for passing a column to `#quote`. + ([Commit](https://github.com/rails/rails/commit/e646bad5b7c)) + +* Removed deprecated `name` arguments from `#tables`. + ([Commit](https://github.com/rails/rails/commit/d5be101dd02214468a27b6839ffe338cfe8ef5f3)) + +* Removed deprecated behavior of `#tables` and `#table_exists?` to return tables and views + to return only tables and not views. + ([Commit](https://github.com/rails/rails/commit/5973a984c369a63720c2ac18b71012b8347479a8)) + +* Removed deprecated `original_exception` argument in `ActiveRecord::StatementInvalid#initialize` + and `ActiveRecord::StatementInvalid#original_exception`. + ([Commit](https://github.com/rails/rails/commit/bc6c5df4699d3f6b4a61dd12328f9e0f1bd6cf46)) + +* Removed deprecated support of passing a class as a value in a query. + ([Commit](https://github.com/rails/rails/commit/b4664864c972463c7437ad983832d2582186e886)) + +* Removed deprecated support to query using commas on LIMIT. + ([Commit](https://github.com/rails/rails/commit/fc3e67964753fb5166ccbd2030d7382e1976f393)) + +* Removed deprecated `conditions` parameter from `#destroy_all`. + ([Commit](https://github.com/rails/rails/commit/d31a6d1384cd740c8518d0bf695b550d2a3a4e9b)) + +* Removed deprecated `conditions` parameter from `#delete_all`. + ([Commit](https://github.com/rails/rails/pull/27503/commits/e7381d289e4f8751dcec9553dcb4d32153bd922b)) + +* Removed deprecated method `#load_schema_for` in favor of `#load_schema`. + ([Commit](https://github.com/rails/rails/commit/419e06b56c3b0229f0c72d3e4cdf59d34d8e5545)) + +* Removed deprecated `#raise_in_transactional_callbacks` configuration. + ([Commit](https://github.com/rails/rails/commit/8029f779b8a1dd9848fee0b7967c2e0849bf6e07)) + +* Removed deprecated `#use_transactional_fixtures` configuration. + ([Commit](https://github.com/rails/rails/commit/3955218dc163f61c932ee80af525e7cd440514b3)) + +### Deprecations + +* Deprecated `error_on_ignored_order_or_limit` flag in favor of + `error_on_ignored_order`. + ([Commit](https://github.com/rails/rails/commit/451437c6f57e66cc7586ec966e530493927098c7)) + +* Deprecated `sanitize_conditions` in favor of `sanitize_sql`. + ([Pull Request](https://github.com/rails/rails/pull/25999)) + +* Deprecated `supports_migrations?` on connection adapters. + ([Pull Request](https://github.com/rails/rails/pull/28172)) + +* Deprecated `Migrator.schema_migrations_table_name`, use `SchemaMigration.table_name` instead. + ([Pull Request](https://github.com/rails/rails/pull/28351)) + +* Deprecated using `#quoted_id` in quoting and type casting. + ([Pull Request](https://github.com/rails/rails/pull/27962)) + +* Deprecated passing `default` argument to `#index_name_exists?`. + ([Pull Request](https://github.com/rails/rails/pull/26930)) + +### Notable changes + +* Change Default Primary Keys to BIGINT. + ([Pull Request](https://github.com/rails/rails/pull/26266)) + +* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+. + ([Commit](https://github.com/rails/rails/commit/65bf1c60053e727835e06392d27a2fb49665484c)) + +* Added support for limits in batch processing. + ([Commit](https://github.com/rails/rails/commit/451437c6f57e66cc7586ec966e530493927098c7)) + +* Transactional tests now wrap all Active Record connections in database + transactions. + ([Pull Request](https://github.com/rails/rails/pull/28726)) + +* Skipped comments in the output of `mysqldump` command by default. + ([Pull Request](https://github.com/rails/rails/pull/23301)) + +* Fixed `ActiveRecord::Relation#count` to use Ruby's `Enumerable#count` for counting + records when a block is passed as argument instead of silently ignoring the + passed block. + ([Pull Request](https://github.com/rails/rails/pull/24203)) + +* Pass `"-v ON_ERROR_STOP=1"` flag with `psql` command to not suppress SQL errors. + ([Pull Request](https://github.com/rails/rails/pull/24773)) + +* Add `ActiveRecord::Base.connection_pool.stat`. + ([Pull Request](https://github.com/rails/rails/pull/26988)) + +* Inheriting directly from `ActiveRecord::Migration` raises an error. + Specify the Rails version for which the migration was written for. + ([Commit](https://github.com/rails/rails/commit/249f71a22ab21c03915da5606a063d321f04d4d3)) + +* An error is raised when `through` association has ambiguous reflection name. + ([Commit](https://github.com/rails/rails/commit/0944182ad7ed70d99b078b22426cbf844edd3f61)) + +Active Model +------------ + +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +* Removed deprecated methods in `ActiveModel::Errors`. + ([commit](https://github.com/rails/rails/commit/9de6457ab0767ebab7f2c8bc583420fda072e2bd)) + +* Removed deprecated `:tokenizer` option in the length validator. + ([commit](https://github.com/rails/rails/commit/6a78e0ecd6122a6b1be9a95e6c4e21e10e429513)) + +* Remove deprecated behavior that halts callbacks when the return value is false. + ([commit](https://github.com/rails/rails/commit/3a25cdca3e0d29ee2040931d0cb6c275d612dffe)) + +### Notable changes + +* The original string assigned to a model attribute is no longer incorrectly + frozen. + ([Pull Request](https://github.com/rails/rails/pull/28729)) + +Active Job +----------- + +Please refer to the [Changelog][active-job] for detailed changes. + +### Removals + +* Removed deprecated support to passing the adapter class to `.queue_adapter`. + ([commit](https://github.com/rails/rails/commit/d1fc0a5eb286600abf8505516897b96c2f1ef3f6)) + +* Removed deprecated `#original_exception` in `ActiveJob::DeserializationError`. + ([commit](https://github.com/rails/rails/commit/d861a1fcf8401a173876489d8cee1ede1cecde3b)) + +### Notable changes + +* Added declarative exception handling via `ActiveJob::Base.retry_on` and `ActiveJob::Base.discard_on`. + ([Pull Request](https://github.com/rails/rails/pull/25991)) + +* Yield the job instance so you have access to things like `job.arguments` on + the custom logic after retries fail. + ([commit](https://github.com/rails/rails/commit/a1e4c197cb12fef66530a2edfaeda75566088d1f)) + +Active Support +-------------- + +Please refer to the [Changelog][active-support] for detailed changes. + +### Removals + +* Removed the `ActiveSupport::Concurrency::Latch` class. + ([Commit](https://github.com/rails/rails/commit/0d7bd2031b4054fbdeab0a00dd58b1b08fb7fea6)) + +* Removed `halt_callback_chains_on_return_false`. + ([Commit](https://github.com/rails/rails/commit/4e63ce53fc25c3bc15c5ebf54bab54fa847ee02a)) + +* Removed deprecated behavior that halts callbacks when the return is false. + ([Commit](https://github.com/rails/rails/commit/3a25cdca3e0d29ee2040931d0cb6c275d612dffe)) + +### Deprecations + +* The top level `HashWithIndifferentAccess` class has been softly deprecated + in favor of the `ActiveSupport::HashWithIndifferentAccess` one. + ([Pull Request](https://github.com/rails/rails/pull/28157)) + +* Deprecated passing string to `:if` and `:unless` conditional options on `set_callback` and `skip_callback`. + ([Commit](https://github.com/rails/rails/commit/0952552) + +### Notable changes + +* Fixed duration parsing and traveling to make it consistent across DST changes. + ([Commit](https://github.com/rails/rails/commit/8931916f4a1c1d8e70c06063ba63928c5c7eab1e), + [Pull Request](https://github.com/rails/rails/pull/26597)) + +* Updated Unicode to version 9.0.0. + ([Pull Request](https://github.com/rails/rails/pull/27822)) + +* Add Duration#before and #after as aliases for #ago and #since. + ([Pull Request](https://github.com/rails/rails/pull/27721)) + +* Added `Module#delegate_missing_to` to delegate method calls not + defined for the current object to a proxy object. + ([Pull Request](https://github.com/rails/rails/pull/23930)) + +* Added `Date#all_day` which returns a range representing the whole day + of the current date & time. + ([Pull Request](https://github.com/rails/rails/pull/24930)) + +* Introduced the `assert_changes` and `assert_no_changes` methods for tests. + ([Pull Request](https://github.com/rails/rails/pull/25393)) + +* The `travel` and `travel_to` methods now raise on nested calls. + ([Pull Request](https://github.com/rails/rails/pull/24890)) + +* Update `DateTime#change` to support usec and nsec. + ([Pull Request](https://github.com/rails/rails/pull/28242)) + +Credits +------- + +See the +[full list of contributors to Rails](http://contributors.rubyonrails.org/) for +the many people who spent many hours making Rails, the stable and robust +framework it is. Kudos to all of them. + +[railties]: https://github.com/rails/rails/blob/5-1-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md +[action-cable]: https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/5-1-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md +[active-job]: https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md diff --git a/source/_welcome.html.erb b/source/_welcome.html.erb index 6ec3aa7..8afec00 100644 --- a/source/_welcome.html.erb +++ b/source/_welcome.html.erb @@ -1,8 +1,8 @@ -

Ruby on Rails Guides (<%= @edge ? @version[0, 7] : @version %>)

+

Ruby on Rails Guides (<%= @edge ? @edge[0, 7] : @version %>)

<% if @edge %>

- These are Edge Guides, based on the current master branch. + These are Edge Guides, based on master@<%= @edge[0, 7] %>.

If you are looking for the ones for the stable version, please check @@ -10,10 +10,16 @@

<% else %>

- These are the new guides for Rails 4.1 based on <%= @version %>. + These are the new guides for Rails 5.1 based on <%= @version %>. These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together.

<% end %>

- The guides for earlier releases: Rails 4.1.1, Rails 4.0.5, Rails 3.2.18 and Rails 2.3.11. +The guides for earlier releases: +Rails 5.0, +Rails 4.2, +Rails 4.1, +Rails 4.0, +Rails 3.2, and +Rails 2.3.

diff --git a/source/action_cable_overview.md b/source/action_cable_overview.md new file mode 100644 index 0000000..e929945 --- /dev/null +++ b/source/action_cable_overview.md @@ -0,0 +1,691 @@ +Action Cable Overview +===================== + +In this guide you will learn how Action Cable works and how to use WebSockets to +incorporate real-time features into your Rails application. + +After reading this guide, you will know: + +* What Action Cable is and its integration on backend and frontend +* How to setup Action Cable +* How to setup channels +* Deployment and Architecture setup for running Action Cable + +-------------------------------------------------------------------------------- + +Introduction +------------ + +Action Cable seamlessly integrates +[WebSockets](https://en.wikipedia.org/wiki/WebSocket) with the rest of your +Rails application. It allows for real-time features to be written in Ruby in the +same style and form as the rest of your Rails application, while still being +performant and scalable. It's a full-stack offering that provides both a +client-side JavaScript framework and a server-side Ruby framework. You have +access to your full domain model written with Active Record or your ORM of +choice. + +What is Pub/Sub +--------------- + +[Pub/Sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern), or +Publish-Subscribe, refers to a message queue paradigm whereby senders of +information (publishers), send data to an abstract class of recipients +(subscribers), without specifying individual recipients. Action Cable uses this +approach to communicate between the server and many clients. + +## Server-Side Components + +### Connections + +*Connections* form the foundation of the client-server relationship. For every +WebSocket accepted by the server, a connection object is instantiated. This +object becomes the parent of all the *channel subscriptions* that are created +from there on. The connection itself does not deal with any specific application +logic beyond authentication and authorization. The client of a WebSocket +connection is called the connection *consumer*. An individual user will create +one consumer-connection pair per browser tab, window, or device they have open. + +Connections are instances of `ApplicationCable::Connection`. In this class, you +authorize the incoming connection, and proceed to establish it if the user can +be identified. + +#### Connection Setup + +```ruby +# app/channels/application_cable/connection.rb +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + private + def find_verified_user + if current_user = User.find_by(id: cookies.signed[:user_id]) + current_user + else + reject_unauthorized_connection + end + end + end +end +``` + +Here `identified_by` is a connection identifier that can be used to find the +specific connection later. Note that anything marked as an identifier will automatically +create a delegate by the same name on any channel instances created off the connection. + +This example relies on the fact that you will already have handled authentication of the user +somewhere else in your application, and that a successful authentication sets a signed +cookie with the user ID. + +The cookie is then automatically sent to the connection instance when a new connection +is attempted, and you use that to set the `current_user`. By identifying the connection +by this same current user, you're also ensuring that you can later retrieve all open +connections by a given user (and potentially disconnect them all if the user is deleted +or unauthorized). + +### Channels + +A *channel* encapsulates a logical unit of work, similar to what a controller does in a +regular MVC setup. By default, Rails creates a parent `ApplicationCable::Channel` class +for encapsulating shared logic between your channels. + +#### Parent Channel Setup + +```ruby +# app/channels/application_cable/channel.rb +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end +``` + +Then you would create your own channel classes. For example, you could have a +`ChatChannel` and an `AppearanceChannel`: + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel +end + +# app/channels/appearance_channel.rb +class AppearanceChannel < ApplicationCable::Channel +end +``` + +A consumer could then be subscribed to either or both of these channels. + +#### Subscriptions + +Consumers subscribe to channels, acting as *subscribers*. Their connection is +called a *subscription*. Produced messages are then routed to these channel +subscriptions based on an identifier sent by the cable consumer. + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + # Called when the consumer has successfully + # become a subscriber of this channel. + def subscribed + end +end +``` + +## Client-Side Components + +### Connections + +Consumers require an instance of the connection on their side. This can be +established using the following JavaScript, which is generated by default by Rails: + +#### Connect Consumer + +```js +// app/assets/javascripts/cable.js +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); +}).call(this); +``` + +This will ready a consumer that'll connect against `/cable` on your server by default. +The connection won't be established until you've also specified at least one subscription +you're interested in having. + +#### Subscriber + +A consumer becomes a subscriber by creating a subscription to a given channel: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/chat.coffee +App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" } + +# app/assets/javascripts/cable/subscriptions/appearance.coffee +App.cable.subscriptions.create { channel: "AppearanceChannel" } +``` + +While this creates the subscription, the functionality needed to respond to +received data will be described later on. + +A consumer can act as a subscriber to a given channel any number of times. For +example, a consumer could subscribe to multiple chat rooms at the same time: + +```coffeescript +App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" } +App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" } +``` + +## Client-Server Interactions + +### Streams + +*Streams* provide the mechanism by which channels route published content +(broadcasts) to their subscribers. + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + def subscribed + stream_from "chat_#{params[:room]}" + end +end +``` + +If you have a stream that is related to a model, then the broadcasting used +can be generated from the model and channel. The following example would +subscribe to a broadcasting like `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE` + +```ruby +class CommentsChannel < ApplicationCable::Channel + def subscribed + post = Post.find(params[:id]) + stream_for post + end +end +``` + +You can then broadcast to this channel like this: + +```ruby +CommentsChannel.broadcast_to(@post, @comment) +``` + +### Broadcasting + +A *broadcasting* is a pub/sub link where anything transmitted by a publisher +is routed directly to the channel subscribers who are streaming that named +broadcasting. Each channel can be streaming zero or more broadcastings. + +Broadcastings are purely an online queue and time dependent. If a consumer is +not streaming (subscribed to a given channel), they'll not get the broadcast +should they connect later. + +Broadcasts are called elsewhere in your Rails application: + +```ruby +WebNotificationsChannel.broadcast_to( + current_user, + title: 'New things!', + body: 'All the news fit to print' +) +``` + +The `WebNotificationsChannel.broadcast_to` call places a message in the current +subscription adapter (by default `redis` for production and `async` for development and +test environments)'s pubsub queue under a separate broadcasting name for each user. +For a user with an ID of 1, the broadcasting name would be `web_notifications:1`. + +The channel has been instructed to stream everything that arrives at +`web_notifications:1` directly to the client by invoking the `received` +callback. + +### Subscriptions + +When a consumer is subscribed to a channel, they act as a subscriber. This +connection is called a subscription. Incoming messages are then routed to +these channel subscriptions based on an identifier sent by the cable consumer. + +```coffeescript +# app/assets/javascripts/cable/subscriptions/chat.coffee +# Assumes you've already requested the right to send web notifications +App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, + received: (data) -> + @appendLine(data) + + appendLine: (data) -> + html = @createLine(data) + $("[data-chat-room='Best Room']").append(html) + + createLine: (data) -> + """ +
+ #{data["sent_by"]} + #{data["body"]} +
+ """ +``` + +### Passing Parameters to Channels + +You can pass parameters from the client side to the server side when creating a +subscription. For example: + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + def subscribed + stream_from "chat_#{params[:room]}" + end +end +``` + +An object passed as the first argument to `subscriptions.create` becomes the +params hash in the cable channel. The keyword `channel` is required: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/chat.coffee +App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, + received: (data) -> + @appendLine(data) + + appendLine: (data) -> + html = @createLine(data) + $("[data-chat-room='Best Room']").append(html) + + createLine: (data) -> + """ +
+ #{data["sent_by"]} + #{data["body"]} +
+ """ +``` + +```ruby +# Somewhere in your app this is called, perhaps +# from a NewCommentJob. +ActionCable.server.broadcast( + "chat_#{room}", + sent_by: 'Paul', + body: 'This is a cool chat app.' +) +``` + +### Rebroadcasting a Message + +A common use case is to *rebroadcast* a message sent by one client to any +other connected clients. + +```ruby +# app/channels/chat_channel.rb +class ChatChannel < ApplicationCable::Channel + def subscribed + stream_from "chat_#{params[:room]}" + end + + def receive(data) + ActionCable.server.broadcast("chat_#{params[:room]}", data) + end +end +``` + +```coffeescript +# app/assets/javascripts/cable/subscriptions/chat.coffee +App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, + received: (data) -> + # data => { sent_by: "Paul", body: "This is a cool chat app." } + +App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) +``` + +The rebroadcast will be received by all connected clients, _including_ the +client that sent the message. Note that params are the same as they were when +you subscribed to the channel. + +## Full-Stack Examples + +The following setup steps are common to both examples: + + 1. [Setup your connection](#connection-setup). + 2. [Setup your parent channel](#parent-channel-setup). + 3. [Connect your consumer](#connect-consumer). + +### Example 1: User Appearances + +Here's a simple example of a channel that tracks whether a user is online or not +and what page they're on. (This is useful for creating presence features like showing +a green dot next to a user name if they're online). + +Create the server-side appearance channel: + +```ruby +# app/channels/appearance_channel.rb +class AppearanceChannel < ApplicationCable::Channel + def subscribed + current_user.appear + end + + def unsubscribed + current_user.disappear + end + + def appear(data) + current_user.appear(on: data['appearing_on']) + end + + def away + current_user.away + end +end +``` + +When a subscription is initiated the `subscribed` callback gets fired and we +take that opportunity to say "the current user has indeed appeared". That +appear/disappear API could be backed by Redis, a database, or whatever else. + +Create the client-side appearance channel subscription: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/appearance.coffee +App.cable.subscriptions.create "AppearanceChannel", + # Called when the subscription is ready for use on the server. + connected: -> + @install() + @appear() + + # Called when the WebSocket connection is closed. + disconnected: -> + @uninstall() + + # Called when the subscription is rejected by the server. + rejected: -> + @uninstall() + + appear: -> + # Calls `AppearanceChannel#appear(data)` on the server. + @perform("appear", appearing_on: $("main").data("appearing-on")) + + away: -> + # Calls `AppearanceChannel#away` on the server. + @perform("away") + + + buttonSelector = "[data-behavior~=appear_away]" + + install: -> + $(document).on "turbolinks:load.appearance", => + @appear() + + $(document).on "click.appearance", buttonSelector, => + @away() + false + + $(buttonSelector).show() + + uninstall: -> + $(document).off(".appearance") + $(buttonSelector).hide() +``` + +##### Client-Server Interaction + +1. **Client** connects to the **Server** via `App.cable = +ActionCable.createConsumer("ws://cable.example.com")`. (`cable.js`). The +**Server** identifies this connection by `current_user`. + +2. **Client** subscribes to the appearance channel via +`App.cable.subscriptions.create(channel: "AppearanceChannel")`. (`appearance.coffee`) + +3. **Server** recognizes a new subscription has been initiated for the +appearance channel and runs its `subscribed` callback, calling the `appear` +method on `current_user`. (`appearance_channel.rb`) + +4. **Client** recognizes that a subscription has been established and calls +`connected` (`appearance.coffee`) which in turn calls `@install` and `@appear`. +`@appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a +data hash of `{ appearing_on: $("main").data("appearing-on") }`. This is +possible because the server-side channel instance automatically exposes all +public methods declared on the class (minus the callbacks), so that these can be +reached as remote procedure calls via a subscription's `perform` method. + +5. **Server** receives the request for the `appear` action on the appearance +channel for the connection identified by `current_user` +(`appearance_channel.rb`). **Server** retrieves the data with the +`:appearing_on` key from the data hash and sets it as the value for the `:on` +key being passed to `current_user.appear`. + +### Example 2: Receiving New Web Notifications + +The appearance example was all about exposing server functionality to +client-side invocation over the WebSocket connection. But the great thing +about WebSockets is that it's a two-way street. So now let's show an example +where the server invokes an action on the client. + +This is a web notification channel that allows you to trigger client-side +web notifications when you broadcast to the right streams: + +Create the server-side web notifications channel: + +```ruby +# app/channels/web_notifications_channel.rb +class WebNotificationsChannel < ApplicationCable::Channel + def subscribed + stream_for current_user + end +end +``` + +Create the client-side web notifications channel subscription: + +```coffeescript +# app/assets/javascripts/cable/subscriptions/web_notifications.coffee +# Client-side which assumes you've already requested +# the right to send web notifications. +App.cable.subscriptions.create "WebNotificationsChannel", + received: (data) -> + new Notification data["title"], body: data["body"] +``` + +Broadcast content to a web notification channel instance from elsewhere in your +application: + +```ruby +# Somewhere in your app this is called, perhaps from a NewCommentJob +WebNotificationsChannel.broadcast_to( + current_user, + title: 'New things!', + body: 'All the news fit to print' +) +``` + +The `WebNotificationsChannel.broadcast_to` call places a message in the current +subscription adapter's pubsub queue under a separate broadcasting name for each +user. For a user with an ID of 1, the broadcasting name would be +`web_notifications:1`. + +The channel has been instructed to stream everything that arrives at +`web_notifications:1` directly to the client by invoking the `received` +callback. The data passed as argument is the hash sent as the second parameter +to the server-side broadcast call, JSON encoded for the trip across the wire, +and unpacked for the data argument arriving to `received`. + +### More Complete Examples + +See the [rails/actioncable-examples](https://github.com/rails/actioncable-examples) +repository for a full example of how to setup Action Cable in a Rails app and adding channels. + +## Configuration + +Action Cable has two required configurations: a subscription adapter and allowed request origins. + +### Subscription Adapter + +By default, Action Cable looks for a configuration file in `config/cable.yml`. +The file must specify an adapter for each Rails environment. See the +[Dependencies](#dependencies) section for additional information on adapters. + +```yaml +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://10.10.3.153:6381 + channel_prefix: appname_production +``` +#### Adapter Configuration + +Below is a list of the subscription adapters available for end users. + +##### Async Adapter + +The async adapter is intended for development/testing and should not be used in production. + +##### Redis Adapter + +Action Cable contains two Redis adapters: "normal" Redis and Evented Redis. Both +of the adapters require users to provide a URL pointing to the Redis server. +Additionally, a channel_prefix may be provided to avoid channel name collisions +when using the same Redis server for multiple applications. See the [Redis PubSub documentation](https://redis.io/topics/pubsub#database-amp-scoping) for more details. + +##### PostgreSQL Adapter + +The PostgreSQL adapter uses Active Record's connection pool, and thus the +application's `config/database.yml` database configuration, for its connection. +This may change in the future. [#27214](https://github.com/rails/rails/issues/27214) + +### Allowed Request Origins + +Action Cable will only accept requests from specified origins, which are +passed to the server config as an array. The origins can be instances of +strings or regular expressions, against which a check for match will be performed. + +```ruby +config.action_cable.allowed_request_origins = ['http://rubyonrails.com/', %r{http://ruby.*}] +``` + +To disable and allow requests from any origin: + +```ruby +config.action_cable.disable_request_forgery_protection = true +``` + +By default, Action Cable allows all requests from localhost:3000 when running +in the development environment. + +### Consumer Configuration + +To configure the URL, add a call to `action_cable_meta_tag` in your HTML layout +HEAD. This uses a URL or path typically set via `config.action_cable.url` in the +environment configuration files. + +### Other Configurations + +The other common option to configure, is the log tags applied to the +per-connection logger. Here's an example that uses +the user account id if available, else "no-account" while tagging: + +```ruby +config.action_cable.log_tags = [ + -> request { request.env['user_account_id'] || "no-account" }, + :action_cable, + -> request { request.uuid } +] +``` + +For a full list of all configuration options, see the +`ActionCable::Server::Configuration` class. + +Also note that your server must provide at least the same number of database +connections as you have workers. The default worker pool size is set to 4, so +that means you have to make at least that available. You can change that in +`config/database.yml` through the `pool` attribute. + +## Running Standalone Cable Servers + +### In App + +Action Cable can run alongside your Rails application. For example, to +listen for WebSocket requests on `/websocket`, specify that path to +`config.action_cable.mount_path`: + +```ruby +# config/application.rb +class Application < Rails::Application + config.action_cable.mount_path = '/websocket' +end +``` + +You can use `App.cable = ActionCable.createConsumer()` to connect to the cable +server if `action_cable_meta_tag` is invoked in the layout. A custom path is +specified as first argument to `createConsumer` (e.g. `App.cable = +ActionCable.createConsumer("/websocket")`). + +For every instance of your server you create and for every worker your server +spawns, you will also have a new instance of Action Cable, but the use of Redis +keeps messages synced across connections. + +### Standalone + +The cable servers can be separated from your normal application server. It's +still a Rack application, but it is its own Rack application. The recommended +basic setup is as follows: + +```ruby +# cable/config.ru +require_relative '../config/environment' +Rails.application.eager_load! + +run ActionCable.server +``` + +Then you start the server using a binstub in `bin/cable` ala: + +``` +#!/bin/bash +bundle exec puma -p 28080 cable/config.ru +``` + +The above will start a cable server on port 28080. + +### Notes + +The WebSocket server doesn't have access to the session, but it has +access to the cookies. This can be used when you need to handle +authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication). + +## Dependencies + +Action Cable provides a subscription adapter interface to process its +pubsub internals. By default, asynchronous, inline, PostgreSQL, evented +Redis, and non-evented Redis adapters are included. The default adapter +in new Rails applications is the asynchronous (`async`) adapter. + +The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), +[nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby). + +## Deployment + +Action Cable is powered by a combination of WebSockets and threads. Both the +framework plumbing and user-specified channel work are handled internally by +utilizing Ruby's native thread support. This means you can use all your regular +Rails models with no problem, as long as you haven't committed any thread-safety sins. + +The Action Cable server implements the Rack socket hijacking API, +thereby allowing the use of a multithreaded pattern for managing connections +internally, irrespective of whether the application server is multi-threaded or not. + +Accordingly, Action Cable works with popular servers like Unicorn, Puma, and +Passenger. diff --git a/source/action_controller_overview.md b/source/action_controller_overview.md index 4c04a06..69c4a00 100644 --- a/source/action_controller_overview.md +++ b/source/action_controller_overview.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Action Controller Overview ========================== @@ -7,7 +9,7 @@ After reading this guide, you will know: * How to follow the flow of a request through a controller. * How to restrict parameters passed to your controller. -* Why and how to store data in the session or cookies. +* How and why to store data in the session or cookies. * How to work with filters to execute code during request processing. * How to use Action Controller's built-in HTTP authentication. * How to stream data directly to the user's browser. @@ -19,11 +21,11 @@ After reading this guide, you will know: What Does a Controller Do? -------------------------- -Action Controller is the C in MVC. After routing has determined which controller to use for a request, your controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. +Action Controller is the C in MVC. After the router has determined which controller to use for a request, the controller is responsible for making sense of the request and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible. For most conventional [RESTful](http://en.wikipedia.org/wiki/Representational_state_transfer) applications, the controller will receive the request (this is invisible to you as the developer), fetch or save data from a model and use a view to create HTML output. If your controller needs to do things a little differently, that's not a problem, this is just the most common way for a controller to work. -A controller can thus be thought of as a middle man between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates data from the user to the model. +A controller can thus be thought of as a middleman between models and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates user data to the model. NOTE: For more details on the routing process, see [Rails Routing from the Outside In](routing.html). @@ -32,7 +34,7 @@ Controller Naming Convention The naming convention of controllers in Rails favors pluralization of the last word in the controller's name, although it is not strictly required (e.g. `ApplicationController`). For example, `ClientsController` is preferable to `ClientController`, `SiteAdminsController` is preferable to `SiteAdminController` or `SitesAdminsController`, and so on. -Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and keeps URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details. +Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and will keep URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details. NOTE: The controller naming convention differs from the naming convention of models, which are expected to be named in singular form. @@ -49,7 +51,7 @@ class ClientsController < ApplicationController end ``` -As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and run the `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`: +As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`: ```ruby def new @@ -59,9 +61,9 @@ end The [Layouts & Rendering Guide](layouts_and_rendering.html) explains this in more detail. -`ApplicationController` inherits from `ActionController::Base`, which defines a number of helpful methods. This guide will cover some of these, but if you're curious to see what's in there, you can see all of them in the API documentation or in the source itself. +`ApplicationController` inherits from `ActionController::Base`, which defines a number of helpful methods. This guide will cover some of these, but if you're curious to see what's in there, you can see all of them in the [API documentation](http://api.rubyonrails.org/classes/ActionController.html) or in the source itself. -Only public methods are callable as actions. It is a best practice to lower the visibility of methods which are not intended to be actions, like auxiliary methods or filters. +Only public methods are callable as actions. It is a best practice to lower the visibility of methods (with `private` or `protected`) which are not intended to be actions, like auxiliary methods or filters. Parameters ---------- @@ -102,21 +104,21 @@ end ### Hash and Array Parameters -The `params` hash is not limited to one-dimensional keys and values. It can contain arrays and (nested) hashes. To send an array of values, append an empty pair of square brackets "[]" to the key name: +The `params` hash is not limited to one-dimensional keys and values. It can contain nested arrays and hashes. To send an array of values, append an empty pair of square brackets "[]" to the key name: ``` GET /clients?ids[]=1&ids[]=2&ids[]=3 ``` -NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" as "[" and "]" are not allowed in URLs. Most of the time you don't have to worry about this because the browser will take care of it for you, and Rails will decode it back when it receives it, but if you ever find yourself having to send those requests to the server manually you have to keep this in mind. +NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" as the "[" and "]" characters are not allowed in URLs. Most of the time you don't have to worry about this because the browser will encode it for you, and Rails will decode it automatically, but if you ever find yourself having to send those requests to the server manually you should keep this in mind. The value of `params[:ids]` will now be `["1", "2", "3"]`. Note that parameter values are always strings; Rails makes no attempt to guess or cast the type. -NOTE: Values such as `[]`, `[nil]` or `[nil, nil, ...]` in `params` are replaced -with `nil` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation) +NOTE: Values such as `[nil]` or `[nil, nil, ...]` in `params` are replaced +with `[]` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation) for more information. -To send a hash you include the key name inside the brackets: +To send a hash, you include the key name inside the brackets: ```html
@@ -129,11 +131,11 @@ To send a hash you include the key name inside the brackets: When this form is submitted, the value of `params[:client]` will be `{ "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }`. Note the nested hash in `params[:client][:address]`. -Note that the `params` hash is actually an instance of `ActiveSupport::HashWithIndifferentAccess`, which acts like a hash but lets you use symbols and strings interchangeably as keys. +The `params` object acts like a Hash, but lets you use symbols and strings interchangeably as keys. ### JSON parameters -If you're writing a web service application, you might find yourself more comfortable accepting parameters in JSON format. If the "Content-Type" header of your request is set to "application/json", Rails will automatically convert your parameters into the `params` hash, which you can access as you would normally. +If you're writing a web service application, you might find yourself more comfortable accepting parameters in JSON format. If the "Content-Type" header of your request is set to "application/json", Rails will automatically load your parameters into the `params` hash, which you can access as you would normally. So for example, if you are sending this JSON content: @@ -141,15 +143,15 @@ So for example, if you are sending this JSON content: { "company": { "name": "acme", "address": "123 Carrot Street" } } ``` -You'll get `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`. +Your controller will receive `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`. -Also, if you've turned on `config.wrap_parameters` in your initializer or calling `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. The parameters will be cloned and wrapped in the key according to your controller's name by default. So the above parameter can be written as: +Also, if you've turned on `config.wrap_parameters` in your initializer or called `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. In this case, the parameters will be cloned and wrapped with a key chosen based on your controller's name. So the above JSON request can be written as: ```json { "name": "acme", "address": "123 Carrot Street" } ``` -And assume that you're sending the data to `CompaniesController`, it would then be wrapped in `:company` key like this: +And, assuming that you're sending the data to `CompaniesController`, it would then be wrapped within the `:company` key like this: ```ruby { name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } } @@ -157,17 +159,17 @@ And assume that you're sending the data to `CompaniesController`, it would then You can customize the name of the key or specific parameters you want to wrap by consulting the [API documentation](http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html) -NOTE: Support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser` +NOTE: Support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser`. ### Routing Parameters -The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id` will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: +The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: ```ruby get '/clients/:status' => 'clients#index', foo: 'bar' ``` -In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar" just like it was passed in the query string. In the same way `params[:action]` will contain "index". +In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients". ### `default_url_options` @@ -181,25 +183,28 @@ class ApplicationController < ActionController::Base end ``` -These options will be used as a starting point when generating URLs, so it's possible they'll be overridden by the options passed in `url_for` calls. +These options will be used as a starting point when generating URLs, so it's possible they'll be overridden by the options passed to `url_for` calls. + +If you define `default_url_options` in `ApplicationController`, as in the example above, these defaults will be used for all URL generation. The method can also be defined in a specific controller, in which case it only affects URLs generated there. -If you define `default_url_options` in `ApplicationController`, as in the example above, it would be used for all URL generation. The method can also be defined in one specific controller, in which case it only affects URLs generated there. +In a given request, the method is not actually called for every single generated URL; for performance reasons, the returned hash is cached, there is at most one invocation per request. ### Strong Parameters With strong parameters, Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been -whitelisted. This means you'll have to make a conscious choice about -which attributes to allow for mass updating and thus prevent -accidentally exposing that which shouldn't be exposed. +whitelisted. This means that you'll have to make a conscious decision about +which attributes to allow for mass update. This is a better security +practice to help prevent accidentally allowing users to update sensitive +model attributes. -In addition, parameters can be marked as required and flow through a -predefined raise/rescue flow to end up as a 400 Bad Request with no -effort. +In addition, parameters can be marked as required and will flow through a +predefined raise/rescue flow that will result in a 400 Bad Request being +returned if not all required parameters are passed in. ```ruby class PeopleController < ActionController::Base - # This will raise an ActiveModel::ForbiddenAttributes exception + # This will raise an ActiveModel::ForbiddenAttributesError exception # because it's using mass assignment without an explicit permit # step. def create @@ -209,8 +214,8 @@ class PeopleController < ActionController::Base # This will pass with flying colors as long as there's a person key # in the parameters, otherwise it'll raise a # ActionController::ParameterMissing exception, which will get - # caught by ActionController::Base and turned into that 400 Bad - # Request reply. + # caught by ActionController::Base and turned into a 400 Bad + # Request error. def update person = current_account.people.find(params[:id]) person.update!(person_params) @@ -237,22 +242,33 @@ params.permit(:id) ``` the key `:id` will pass the whitelisting if it appears in `params` and -it has a permitted scalar value associated. Otherwise the key is going +it has a permitted scalar value associated. Otherwise, the key is going to be filtered out, so arrays, hashes, or any other objects cannot be injected. The permitted scalar types are `String`, `Symbol`, `NilClass`, `Numeric`, `TrueClass`, `FalseClass`, `Date`, `Time`, `DateTime`, -`StringIO`, `IO`, `ActionDispatch::Http::UploadedFile` and +`StringIO`, `IO`, `ActionDispatch::Http::UploadedFile`, and `Rack::Test::UploadedFile`. To declare that the value in `params` must be an array of permitted -scalar values map the key to an empty array: +scalar values, map the key to an empty array: ```ruby params.permit(id: []) ``` +Sometimes it is not possible or convenient to declare the valid keys of +a hash parameter or its internal structure. Just map to an empty hash: + +```ruby +params.permit(preferences: {}) +``` + +but be careful because this opens the door to arbitrary input. In this +case, `permit` ensures values in the returned structure are permitted +scalars and filters out anything else. + To whitelist an entire hash of parameters, the `permit!` method can be used: @@ -260,14 +276,14 @@ used: params.require(:log_entry).permit! ``` -This will mark the `:log_entry` parameters hash and any sub-hash of it -permitted. Extreme care should be taken when using `permit!` as it -will allow all current and future model attributes to be -mass-assigned. +This marks the `:log_entry` parameters hash and any sub-hash of it as +permitted and does not check for permitted scalars, anything is accepted. +Extreme care should be taken when using `permit!`, as it will allow all current +and future model attributes to be mass-assigned. #### Nested Parameters -You can also use permit on nested parameters, like: +You can also use `permit` on nested parameters, like: ```ruby params.permit(:name, { emails: [] }, @@ -275,19 +291,19 @@ params.permit(:name, { emails: [] }, { family: [ :name ], hobbies: [] }]) ``` -This declaration whitelists the `name`, `emails` and `friends` +This declaration whitelists the `name`, `emails`, and `friends` attributes. It is expected that `emails` will be an array of permitted -scalar values and that `friends` will be an array of resources with -specific attributes : they should have a `name` attribute (any +scalar values, and that `friends` will be an array of resources with +specific attributes: they should have a `name` attribute (any permitted scalar values allowed), a `hobbies` attribute as an array of permitted scalar values, and a `family` attribute which is restricted -to having a `name` (any permitted scalar values allowed, too). +to having a `name` (any permitted scalar values allowed here, too). #### More Examples -You want to also use the permitted attributes in the `new` +You may want to also use the permitted attributes in your `new` action. This raises the problem that you can't use `require` on the -root key because normally it does not exist when calling `new`: +root key because, normally, it does not exist when calling `new`: ```ruby # using `fetch` you can supply a default and use @@ -295,8 +311,8 @@ root key because normally it does not exist when calling `new`: params.fetch(:blog, {}).permit(:title, :author) ``` -`accepts_nested_attributes_for` allows you to update and destroy -associated records. This is based on the `id` and `_destroy` +The model class method `accepts_nested_attributes_for` allows you to +update and destroy associated records. This is based on the `id` and `_destroy` parameters: ```ruby @@ -304,7 +320,7 @@ parameters: params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy]) ``` -Hashes with integer keys are treated differently and you can declare +Hashes with integer keys are treated differently, and you can declare the attributes as if they were direct children. You get these kinds of parameters when you use `accepts_nested_attributes_for` in combination with a `has_many` association: @@ -321,13 +337,13 @@ params.require(:book).permit(:title, chapters_attributes: [:title]) #### Outside the Scope of Strong Parameters The strong parameter API was designed with the most common use cases -in mind. It is not meant as a silver bullet to handle all your -whitelisting problems. However you can easily mix the API with your +in mind. It is not meant as a silver bullet to handle all of your +whitelisting problems. However, you can easily mix the API with your own code to adapt to your situation. Imagine a scenario where you have parameters representing a product name and a hash of arbitrary data associated with that product, and -you want to whitelist the product name attribute but also the whole +you want to whitelist the product name attribute and also the whole data hash. The strong parameters API doesn't let you directly whitelist the whole of a nested hash with any keys, but you can use the keys of your nested hash to declare what to whitelist: @@ -358,7 +374,7 @@ If your user sessions don't store critical data or don't need to be around for l Read more about session storage in the [Security Guide](security.html). -If you need a different session storage mechanism, you can change it in the `config/initializers/session_store.rb` file: +If you need a different session storage mechanism, you can change it in an initializer: ```ruby # Use the database for sessions instead of the cookie-based default, @@ -367,7 +383,7 @@ If you need a different session storage mechanism, you can change it in the `con # Rails.application.config.session_store :active_record_store ``` -Rails sets up a session key (the name of the cookie) when signing the session data. These can also be changed in `config/initializers/session_store.rb`: +Rails sets up a session key (the name of the cookie) when signing the session data. These can also be changed in an initializer: ```ruby # Be sure to restart your server when you modify this file. @@ -391,7 +407,7 @@ Rails sets up (for the CookieStore) a secret key used for signing the session da # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. +# You can use `rails secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. @@ -666,11 +682,11 @@ You may notice in the above code that we're using `render xml: @users`, not `ren Filters ------- -Filters are methods that are run before, after or "around" a controller action. +Filters are methods that are run "before", "after" or "around" a controller action. Filters are inherited, so if you set a filter on `ApplicationController`, it will be run on every controller in your application. -"Before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: +"before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way: ```ruby class ApplicationController < ActionController::Base @@ -697,15 +713,15 @@ class LoginsController < ApplicationController end ``` -Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to only skip this filter for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place. +Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to skip this filter only for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place. ### After Filters and Around Filters In addition to "before" filters, you can also run filters after an action has been executed, or both before and after. -"After" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. +"after" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running. -"Around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. +"around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work. For example, in a website where changes have an approval workflow an administrator could be able to preview them easily, just apply them within a transaction: @@ -735,7 +751,7 @@ You can choose not to yield and build the response yourself, in which case the a While the most common way to use filters is by creating private methods and using *_action to add them, there are two other ways to do the same thing. -The first is to use a block directly with the *_action methods. The block receives the controller as an argument, and the `require_login` filter from above could be rewritten to use a block: +The first is to use a block directly with the *\_action methods. The block receives the controller as an argument. The `require_login` filter from above could be rewritten to use a block: ```ruby class ApplicationController < ActionController::Base @@ -748,7 +764,7 @@ class ApplicationController < ActionController::Base end ``` -Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter is not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. +Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter does not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and cannot be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class: @@ -807,11 +823,11 @@ The [Security Guide](security.html) has more about this and a lot of other secur The Request and Response Objects -------------------------------- -In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The `request` method contains an instance of `AbstractRequest` and the `response` method returns a response object representing what is going to be sent back to the client. +In every controller there are two accessor methods pointing to the request and the response objects associated with the request cycle that is currently in execution. The `request` method contains an instance of `ActionDispatch::Request` and the `response` method returns a response object representing what is going to be sent back to the client. ### The `request` Object -The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Request.html). Among the properties that you can access on this object are: +The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the [Rails API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Request.html) and [Rack Documentation](http://www.rubydoc.info/github/rack/rack/Rack/Request). Among the properties that you can access on this object are: | Property of `request` | Purpose | | ----------------------------------------- | -------------------------------------------------------------------------------- | @@ -833,7 +849,7 @@ Rails collects all of the parameters sent along with the request in the `params` ### The `response` Object -The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values. +The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values. To get a full list of the available methods, refer to the [Rails API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Response.html) and [Rack Documentation](http://www.rubydoc.info/github/rack/rack/Rack/Response). | Property of `response` | Purpose | | ---------------------- | --------------------------------------------------------------------------------------------------- | @@ -992,6 +1008,7 @@ you would like in a response object. The `ActionController::Live` module allows you to create a persistent connection with a browser. Using this module, you will be able to send arbitrary data to the browser at specific points in time. + #### Incorporating Live Streaming Including `ActionController::Live` inside of your controller class will provide @@ -1021,7 +1038,7 @@ There are a couple of things to notice in the above example. We need to make sure to close the response stream. Forgetting to close the stream will leave the socket open forever. We also have to set the content type to `text/event-stream` before we write to the response stream. This is because headers cannot be written -after the response has been committed (when `response.committed` returns a truthy +after the response has been committed (when `response.committed?` returns a truthy value), which occurs when you `write` or `commit` the response stream. #### Example Usage @@ -1084,6 +1101,8 @@ You can filter out sensitive request parameters from your log files by appending config.filter_parameters << :password ``` +NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`. + ### Redirects Filtering Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to. @@ -1106,11 +1125,11 @@ Rescue Most likely your application is going to contain bugs or otherwise throw an exception that needs to be handled. For example, if the user follows a link to a resource that no longer exists in the database, Active Record will throw the `ActiveRecord::RecordNotFound` exception. -Rails' default exception handling displays a "500 Server Error" message for all exceptions. If the request was made locally, a nice traceback and some added information gets displayed so you can figure out what went wrong and deal with it. If the request was remote Rails will just display a simple "500 Server Error" message to the user, or a "404 Not Found" if there was a routing error or a record could not be found. Sometimes you might want to customize how these errors are caught and how they're displayed to the user. There are several levels of exception handling available in a Rails application: +Rails default exception handling displays a "500 Server Error" message for all exceptions. If the request was made locally, a nice traceback and some added information gets displayed so you can figure out what went wrong and deal with it. If the request was remote Rails will just display a simple "500 Server Error" message to the user, or a "404 Not Found" if there was a routing error or a record could not be found. Sometimes you might want to customize how these errors are caught and how they're displayed to the user. There are several levels of exception handling available in a Rails application: ### The Default 500 and 404 Templates -By default a production application will render either a 404 or a 500 error message. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and layout, but remember that they are static; i.e. you can't use RHTML or layouts in them, just plain HTML. +By default a production application will render either a 404 or a 500 error message, in the development environment all unhandled exceptions are raised. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and style, but remember that they are static HTML; i.e. you can't use ERB, SCSS, CoffeeScript, or layouts for them. ### `rescue_from` @@ -1142,7 +1161,7 @@ class ApplicationController < ActionController::Base def user_not_authorized flash[:error] = "You don't have access to this section." - redirect_to :back + redirect_back(fallback_location: root_path) end end @@ -1164,64 +1183,13 @@ class ClientsController < ApplicationController end ``` -WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development). If you would like to dynamically generate error pages, see [Custom errors page](#custom-errors-page). - -NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's [article](http://m.onkey.org/2008/7/20/rescue-from-dispatching) on the subject for more information. - - -### Custom errors page - -You can customize the layout of your error handling using controllers and views. -First define your app own routes to display the errors page. - -* `config/application.rb` - - ```ruby - config.exceptions_app = self.routes - ``` - -* `config/routes.rb` - - ```ruby - get '/404', to: 'errors#not_found' - get '/422', to: 'errors#unprocessable_entity' - get '/500', to: 'errors#server_error' - ``` - -Create the controller and views. - -* `app/controllers/errors_controller.rb` - - ```ruby - class ErrorsController < ActionController::Base - layout 'error' - - def not_found - render status: :not_found - end - - def unprocessable_entity - render status: :unprocessable_entity - end - - def server_error - render status: :server_error - end - end - ``` - -* `app/views` +WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development). - ``` - errors/ - not_found.html.erb - unprocessable_entity.html.erb - server_error.html.erb - layouts/ - error.html.erb - ``` +NOTE: When running in the production environment, all +`ActiveRecord::RecordNotFound` errors render the 404 error page. Unless you need +a custom behavior you don't need to handle this. -Do not forget to set the correct status code on the controller as shown before. You should avoid using the database or any complex operations because the user is already on the error page. Generating another error while on an error page could cause issues. +NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. Force HTTPS protocol -------------------- diff --git a/source/action_mailer_basics.md b/source/action_mailer_basics.md index 9ad9319..9673571 100644 --- a/source/action_mailer_basics.md +++ b/source/action_mailer_basics.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Action Mailer Basics ==================== @@ -35,10 +37,26 @@ views. ```bash $ bin/rails generate mailer UserMailer create app/mailers/user_mailer.rb +create app/mailers/application_mailer.rb invoke erb create app/views/user_mailer +create app/views/layouts/mailer.text.erb +create app/views/layouts/mailer.html.erb invoke test_unit create test/mailers/user_mailer_test.rb +create test/mailers/previews/user_mailer_preview.rb +``` + +```ruby +# app/mailers/application_mailer.rb +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout 'mailer' +end + +# app/mailers/user_mailer.rb +class UserMailer < ApplicationMailer +end ``` As you can see, you can generate mailers just like you use other generators with @@ -63,8 +81,7 @@ delivered via email. `app/mailers/user_mailer.rb` contains an empty mailer: ```ruby -class UserMailer < ActionMailer::Base - default from: 'from@example.com' +class UserMailer < ApplicationMailer end ``` @@ -72,7 +89,7 @@ Let's add a method called `welcome_email`, that will send an email to the user's registered email address: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer default from: 'notifications@example.com' def welcome_email(user) @@ -143,8 +160,8 @@ When you call the `mail` method now, Action Mailer will detect the two templates #### Calling the Mailer Mailers are really just another way to render a view. Instead of rendering a -view and sending out the HTTP protocol, they are just sending it out through the -email protocols instead. Due to this, it makes sense to just have your +view and sending it over the HTTP protocol, they are just sending it out through +the email protocols instead. Due to this, it makes sense to just have your controller tell the Mailer to send an email when a user is successfully created. Setting this up is painfully simple. @@ -153,13 +170,16 @@ First, let's create a simple `User` scaffold: ```bash $ bin/rails generate scaffold user name email login -$ bin/rake db:migrate +$ bin/rails db:migrate ``` Now that we have a user model to play with, we will just edit the `app/controllers/users_controller.rb` make it instruct the `UserMailer` to deliver an email to the newly created user by editing the create action and inserting a -call to `UserMailer.welcome_email` right after the user is successfully saved: +call to `UserMailer.welcome_email` right after the user is successfully saved. + +Action Mailer is nicely integrated with Active Job so you can send emails outside +of the request-response cycle, so the user doesn't have to wait on it: ```ruby class UsersController < ApplicationController @@ -171,7 +191,7 @@ class UsersController < ApplicationController respond_to do |format| if @user.save # Tell the UserMailer to send a welcome email after save - UserMailer.welcome_email(@user).deliver + UserMailer.welcome_email(@user).deliver_later format.html { redirect_to(@user, notice: 'User was successfully created.') } format.json { render json: @user, status: :created, location: @user } @@ -184,8 +204,33 @@ class UsersController < ApplicationController end ``` -The method `welcome_email` returns a `Mail::Message` object which can then just -be told `deliver` to send itself out. +NOTE: Active Job's default behavior is to execute jobs via the `:async` adapter. So, you can use +`deliver_later` now to send emails asynchronously. +Active Job's default adapter runs jobs with an in-process thread pool. +It's well-suited for the development/test environments, since it doesn't require +any external infrastructure, but it's a poor fit for production since it drops +pending jobs on restart. +If you need a persistent backend, you will need to use an Active Job adapter +that has a persistent backend (Sidekiq, Resque, etc). + +If you want to send emails right away (from a cronjob for example) just call +`deliver_now`: + +```ruby +class SendWeeklySummary + def run + User.find_each do |user| + UserMailer.weekly_summary(user).deliver_now + end + end +end +``` + +The method `welcome_email` returns an `ActionMailer::MessageDelivery` object which +can then just be told `deliver_now` or `deliver_later` to send itself out. The +`ActionMailer::MessageDelivery` object is just a wrapper around a `Mail::Message`. If +you want to inspect, alter or do anything else with the `Mail::Message` object you can +access it with the `message` method on the `ActionMailer::MessageDelivery` object. ### Auto encoding header values @@ -237,7 +282,7 @@ different, encode your content and pass in the encoded content and encoding in a ```ruby encoded_content = SpecialEncode(File.read('/path/to/filename.jpg')) attachments['filename.jpg'] = { - mime_type: 'application/x-gzip', + mime_type: 'application/gzip', encoding: 'SpecialEncoding', content: encoded_content } @@ -274,8 +319,7 @@ Action Mailer 3.0 makes inline attachments, which involved a lot of hacking in p ```html+erb

Hello there, this is our image

- <%= image_tag attachments['image.jpg'].url, alt: 'My Photo', - class: 'photos' %> + <%= image_tag attachments['image.jpg'].url, alt: 'My Photo', class: 'photos' %> ``` #### Sending Email To Multiple Recipients @@ -286,7 +330,7 @@ key. The list of emails can be an array of email addresses or a single string with the addresses separated by commas. ```ruby -class AdminMailer < ActionMailer::Base +class AdminMailer < ApplicationMailer default to: Proc.new { Admin.pluck(:email) }, from: 'notification@example.com' @@ -304,7 +348,7 @@ The same format can be used to set carbon copy (Cc:) and blind carbon copy Sometimes you wish to show the name of the person instead of just their email address when they receive the email. The trick to doing that is to format the -email address in the format `"Full Name "`. +email address in the format `"Full Name" `. ```ruby def welcome_email(user) @@ -325,7 +369,7 @@ for the HTML version and `welcome_email.text.erb` for the plain text version. To change the default mailer view for your action you do something like: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer default from: 'notifications@example.com' def welcome_email(user) @@ -347,7 +391,7 @@ If you want more flexibility you can also pass a block and render specific templates or even render inline or text without using a template file: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer default from: 'notifications@example.com' def welcome_email(user) @@ -356,7 +400,7 @@ class UserMailer < ActionMailer::Base mail(to: @user.email, subject: 'Welcome to My Awesome Site') do |format| format.html { render 'another_template' } - format.text { render text: 'Render text' } + format.text { render plain: 'Render text' } end end end @@ -367,6 +411,22 @@ use the rendered text for the text part. The render command is the same one used inside of Action Controller, so you can use all the same options, such as `:text`, `:inline` etc. +#### Caching mailer view + +You can do cache in mailer views like in application views using `cache` method. + +``` +<% cache do %> + <%= @company.name %> +<% end %> +``` + +And in order to use this feature, you need to configure your application with this: + +``` + config.action_mailer.perform_caching = true +``` + ### Action Mailer Layouts Just like controller views, you can also have mailer layouts. The layout name @@ -377,7 +437,7 @@ layout. In order to use a different file, call `layout` in your mailer: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer layout 'awesome' # use awesome.(html|text).erb as the layout end ``` @@ -389,7 +449,7 @@ You can also pass in a `layout: 'layout_name'` option to the render call inside the format block to specify different layouts for different formats: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer def welcome_email(user) mail(to: user.email) do |format| format.html { render layout: 'my_layout' } @@ -402,6 +462,39 @@ end Will render the HTML part using the `my_layout.html.erb` file and the text part with the usual `user_mailer.text.erb` file if it exists. +### Previewing Emails + +Action Mailer previews provide a way to see how emails look by visiting a +special URL that renders them. In the above example, the preview class for +`UserMailer` should be named `UserMailerPreview` and located in +`test/mailers/previews/user_mailer_preview.rb`. To see the preview of +`welcome_email`, implement a method that has the same name and call +`UserMailer.welcome_email`: + +```ruby +class UserMailerPreview < ActionMailer::Preview + def welcome_email + UserMailer.welcome_email(User.first) + end +end +``` + +Then the preview will be available in . + +If you change something in `app/views/user_mailer/welcome_email.html.erb` +or the mailer itself, it'll automatically reload and render it so you can +visually see the new style instantly. A list of previews are also available +in . + +By default, these preview classes live in `test/mailers/previews`. +This can be configured using the `preview_path` option. For example, if you +want to change it to `lib/mailer_previews`, you can configure it in +`config/application.rb`: + +```ruby +config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" +``` + ### Generating URLs in Action Mailer Views Unlike controllers, the mailer instance doesn't have any context about the @@ -430,18 +523,9 @@ You will need to use: By using the full URL, your links will now work in your emails. -#### generating URLs with `url_for` +#### Generating URLs with `url_for` -You need to pass the `only_path: false` option when using `url_for`. This will -ensure that absolute URLs are generated because the `url_for` view helper will, -by default, generate relative URLs when a `:host` option isn't explicitly -provided. - -```erb -<%= url_for(controller: 'welcome', - action: 'greeting', - only_path: false) %> -``` +`url_for` generates a full URL by default in templates. If you did not configure the `:host` option globally make sure to pass it to `url_for`. @@ -453,10 +537,7 @@ If you did not configure the `:host` option globally make sure to pass it to action: 'greeting') %> ``` -NOTE: When you explicitly pass the `:host` Rails will always generate absolute -URLs, so there is no need to pass `only_path: false`. - -#### generating URLs with named routes +#### Generating URLs with Named Routes Email clients have no web context and so paths have no base URL to form complete web addresses. Thus, you should always use the "_url" variant of named route @@ -469,10 +550,32 @@ url helper. <%= user_url(https://github.com/@user,%20host:%20'example.com') %> ``` +NOTE: non-`GET` links require [rails-ujs](https://github.com/rails/rails-ujs) or +[jQuery UJS](https://github.com/rails/jquery-ujs), and won't work in mailer templates. +They will result in normal `GET` requests. + +### Adding images in Action Mailer Views + +Unlike controllers, the mailer instance doesn't have any context about the +incoming request so you'll need to provide the `:asset_host` parameter yourself. + +As the `:asset_host` usually is consistent across the application you can +configure it globally in config/application.rb: + +```ruby +config.action_mailer.asset_host = 'http://example.com/' +``` + +Now you can display an image inside your email. + +```ruby +<%= image_tag 'image.jpg' %> +``` + ### Sending Multipart Emails Action Mailer will automatically send multipart emails if you have different -templates for the same action. So, for our UserMailer example, if you have +templates for the same action. So, for our `UserMailer` example, if you have `welcome_email.text.erb` and `welcome_email.html.erb` in `app/views/user_mailer`, Action Mailer will automatically send a multipart email with the HTML and text versions setup as different parts. @@ -487,7 +590,7 @@ while delivering emails, you can do this using `delivery_method_options` in the mailer action. ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer def welcome_email(user, company) @user = user @url = user_url(https://github.com/@user) @@ -509,7 +612,7 @@ option. In such cases don't forget to add the `:content_type` option. Rails will default to `text/plain` otherwise. ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer def welcome_email(user, email_body) mail(to: user.email, body: email_body, @@ -539,7 +642,7 @@ mailer, and pass the email object to the mailer `receive` instance method. Here's an example: ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer def receive(email) page = Page.find_by(address: email.to.first) page.emails.create( @@ -575,7 +678,7 @@ Action Mailer allows for you to specify a `before_action`, `after_action` and using instance variables set in your mailer action. ```ruby -class UserMailer < ActionMailer::Base +class UserMailer < ApplicationMailer after_action :set_delivery_options, :prevent_delivery_to_guests, :set_business_headers @@ -632,8 +735,8 @@ files (environment.rb, production.rb, etc...) | Configuration | Description | |---------------|-------------| |`logger`|Generates information on the mailing run if available. Can be set to `nil` for no logging. Compatible with both Ruby's own `Logger` and `Log4r` loggers.| -|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:
  • `:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.
  • `:port` - On the off chance that your mail server doesn't run on port 25, you can change it.
  • `:domain` - If you need to specify a HELO domain, you can do it here.
  • `:user_name` - If your mail server requires authentication, set the username in this setting.
  • `:password` - If your mail server requires authentication, set the password in this setting.
  • `:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.
  • `:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.
| -|`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.
  • `:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.
  • `:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i -t`.
| +|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:
  • `:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.
  • `:port` - On the off chance that your mail server doesn't run on port 25, you can change it.
  • `:domain` - If you need to specify a HELO domain, you can do it here.
  • `:user_name` - If your mail server requires authentication, set the username in this setting.
  • `:password` - If your mail server requires authentication, set the password in this setting.
  • `:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain` (will send the password in the clear), `:login` (will send password Base64 encoded) or `:cram_md5` (combines a Challenge/Response mechanism to exchange information and a cryptographic Message Digest 5 algorithm to hash important information)
  • `:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. Defaults to `true`.
  • `:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name of an OpenSSL verify constant ('none' or 'peer') or directly the constant (`OpenSSL::SSL::VERIFY_NONE` or `OpenSSL::SSL::VERIFY_PEER`).
| +|`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.
  • `:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.
  • `:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i`.
| |`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.| |`delivery_method`|Defines a delivery method. Possible values are:
  • `:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.
  • `:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.
  • `:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.
  • `:test`: save emails to `ActionMailer::Base.deliveries` array.
See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.| |`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.| @@ -654,7 +757,7 @@ config.action_mailer.delivery_method = :sendmail # Defaults to: # config.action_mailer.sendmail_settings = { # location: '/usr/sbin/sendmail', -# arguments: '-i -t' +# arguments: '-i' # } config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true @@ -677,6 +780,9 @@ config.action_mailer.smtp_settings = { authentication: 'plain', enable_starttls_auto: true } ``` +Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure. +You can change your gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts or +use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider. Mailer Testing -------------- @@ -705,7 +811,9 @@ Mailer framework. You can do this in an initializer file `config/initializers/sandbox_email_interceptor.rb` ```ruby -ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.staging? +if Rails.env.staging? + ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) +end ``` NOTE: The example above uses a custom environment called "staging" for a diff --git a/source/action_view_overview.md b/source/action_view_overview.md index ef7ef5a..c835ade 100644 --- a/source/action_view_overview.md +++ b/source/action_view_overview.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Action View Overview ==================== @@ -7,14 +9,13 @@ After reading this guide, you will know: * How best to use templates, partials, and layouts. * What helpers are provided by Action View and how to make your own. * How to use localized views. -* How to use Action View outside of Rails. -------------------------------------------------------------------------------- What is Action View? -------------------- -Action View and Action Controller are the two major components of Action Pack. In Rails, web requests are handled by Action Pack, which splits the work into a controller part (performing the logic) and a view part (rendering a template). Typically, Action Controller will be concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response. +In Rails, web requests are handled by [Action Controller](action_controller_overview.html) and Action View. Typically, Action Controller is concerned with communicating with the database and performing CRUD actions where necessary. Action View is then responsible for compiling the response. Action View templates are written using embedded Ruby in tags mingled with HTML. To avoid cluttering the templates with boilerplate code, a number of helper classes provide common behavior for forms, dates, and strings. It's also easy to add new helpers to your application as it evolves. @@ -44,18 +45,18 @@ $ bin/rails generate scaffold article There is a naming convention for views in Rails. Typically, the views share their name with the associated controller action, as you can see above. For example, the index controller action of the `articles_controller.rb` will use the `index.html.erb` view file in the `app/views/articles` directory. -The complete HTML returned to the client is composed of a combination of this ERB file, a layout template that wraps it, and all the partials that the view may reference. Later on this guide you can find a more detailed documentation of each one of these three components. +The complete HTML returned to the client is composed of a combination of this ERB file, a layout template that wraps it, and all the partials that the view may reference. Within this guide you will find more detailed documentation about each of these three components. Templates, Partials and Layouts ------------------------------- -As mentioned before, the final HTML output is a composition of three Rails elements: `Templates`, `Partials` and `Layouts`. -Below is a brief overview of each one of them. +As mentioned, the final HTML output is a composition of three Rails elements: `Templates`, `Partials` and `Layouts`. +Below is a brief overview of each of them. ### Templates -Action View templates can be written in several ways. If the template file has a `.erb` extension then it uses a mixture of ERB (included in Ruby) and HTML. If the template file has a `.builder` extension then a fresh instance of `Builder::XmlMarkup` library is used. +Action View templates can be written in several ways. If the template file has a `.erb` extension then it uses a mixture of ERB (Embedded Ruby) and HTML. If the template file has a `.builder` extension then the `Builder::XmlMarkup` library is used. Rails supports multiple template systems and uses a file extension to distinguish amongst them. For example, an HTML file using the ERB template system will have `.html.erb` as a file extension. @@ -72,7 +73,7 @@ Consider the following loop for names: <% end %> ``` -The loop is set up in regular embedding tags (`<% %>`) and the name is written using the output embedding tags (`<%= %>`). Note that this is not just a usage suggestion, for regular output functions like `print` or `puts` won't work with ERB templates. So this would be wrong: +The loop is set up using regular embedding tags (`<% %>`) and the name is inserted using the output embedding tags (`<%= %>`). Note that this is not just a usage suggestion: regular output functions such as `print` and `puts` won't be rendered to the view with ERB templates. So this would be wrong: ```html+erb <%# WRONG %> @@ -146,6 +147,39 @@ xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do end ``` +#### Jbuilder +[Jbuilder](https://github.com/rails/jbuilder) is a gem that's +maintained by the Rails team and included in the default Rails Gemfile. +It's similar to Builder, but is used to generate JSON, instead of XML. + +If you don't have it, you can add the following to your Gemfile: + +```ruby +gem 'jbuilder' +``` + +A Jbuilder object named `json` is automatically made available to templates with +a `.jbuilder` extension. + +Here is a basic example: + +```ruby +json.name("Alex") +json.email("alex@example.com") +``` + +would produce: + +```json +{ + "name": "Alex", + "email": "alex@example.com" +} +``` + +See the [Jbuilder documentation](https://github.com/rails/jbuilder#jbuilder) for +more examples and information. + #### Template Caching By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will check the file's modification time and recompile it in development mode. @@ -181,7 +215,7 @@ One way to use partials is to treat them as the equivalent of subroutines; a way

Here are a few of our fine products:

<% @products.each do |product| %> - <%= render partial: "product", locals: {product: product} %> + <%= render partial: "product", locals: { product: product } %> <% end %> <%= render "shared/footer" %> @@ -189,32 +223,43 @@ One way to use partials is to treat them as the equivalent of subroutines; a way Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain content that is shared among many pages in your application. You don't need to see the details of these sections when you're concentrating on a particular page. -#### The `as` and `object` options +#### `render` without `partial` and `locals` options -By default `ActionView::Partials::PartialRenderer` has its object in a local variable with the same name as the template. So, given: +In the above example, `render` takes 2 options: `partial` and `locals`. But if +these are the only options you want to pass, you can skip using these options. +For example, instead of: ```erb -<%= render partial: "product" %> +<%= render partial: "product", locals: { product: @product } %> ``` -within product we'll get `@product` in the local variable `product`, as if we had written: +You can also do: ```erb -<%= render partial: "product", locals: {product: @product} %> +<%= render "product", product: @product %> ``` -With the `as` option we can specify a different name for the local variable. For example, if we wanted it to be `item` instead of `product` we would do: +#### The `as` and `object` options + +By default `ActionView::Partials::PartialRenderer` has its object in a local variable with the same name as the template. So, given: ```erb -<%= render partial: "product", as: "item" %> +<%= render partial: "product" %> ``` -The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (eg. in a different instance variable or in a local variable). +within `_product` partial we'll get `@product` in the local variable `product`, +as if we had written: + +```erb +<%= render partial: "product", locals: { product: @product } %> +``` + +The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (e.g. in a different instance variable or in a local variable). For example, instead of: ```erb -<%= render partial: "product", locals: {product: @item} %> +<%= render partial: "product", locals: { product: @item } %> ``` we would do: @@ -223,15 +268,21 @@ we would do: <%= render partial: "product", object: @item %> ``` -The `object` and `as` options can also be used together: +With the `as` option we can specify a different name for the said local variable. For example, if we wanted it to be `item` instead of `product` we would do: ```erb <%= render partial: "product", object: @item, as: "item" %> ``` +This is equivalent to + +```erb +<%= render partial: "product", locals: { item: @item } %> +``` + #### Rendering Collections -It is very common that a template needs to iterate over a collection and render a sub-template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders a partial for each one of the elements in the array. +It is very common that a template will need to iterate over a collection and render a sub-template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders a partial for each one of the elements in the array. So this example for rendering all the products: @@ -247,7 +298,7 @@ can be rewritten in a single line: <%= render partial: "product", collection: @products %> ``` -When a partial is called like this (eg. with a collection), the individual instances of the partial have access to the member of the collection being rendered via a variable named after the partial. In this case, the partial is `_product`, and within it you can refer to `product` to get the instance that is being rendered. +When a partial is called with a collection, the individual instances of the partial have access to the member of the collection being rendered via a variable named after the partial. In this case, the partial is `_product`, and within it you can refer to `product` to get the collection member that is being rendered. You can use a shorthand syntax for rendering collections. Assuming `@products` is a collection of `Product` instances, you can simply write the following to produce the same result: @@ -255,7 +306,7 @@ You can use a shorthand syntax for rendering collections. Assuming `@products` i <%= render @products %> ``` -Rails determines the name of the partial to use by looking at the model name in the collection, `Product` in this case. In fact, you can even create a heterogeneous collection and render it this way, and Rails will choose the proper partial for each member of the collection. +Rails determines the name of the partial to use by looking at the model name in the collection, `Product` in this case. In fact, you can even render a collection made up of instances of different models using this shorthand, and Rails will choose the proper partial for each member of the collection. #### Spacer Templates @@ -269,14 +320,14 @@ Rails will render the `_product_ruler` partial (with no data passed to it) betwe ### Layouts -Layouts can be used to render a common view template around the results of Rails controller actions. Typically, every Rails application has a couple of overall layouts that most pages are rendered within. For example, a site might have a layout for a logged in user, and a layout for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us." You would expect each layout to have a different look and feel. You can read more details about Layouts in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. +Layouts can be used to render a common view template around the results of Rails controller actions. Typically, a Rails application will have a couple of layouts that pages will be rendered within. For example, a site might have one layout for a logged in user and another for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us" pages. You would expect each layout to have a different look and feel. You can read about layouts in more detail in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. Partial Layouts --------------- -Partials can have their own layouts applied to them. These layouts are different than the ones that are specified globally for the entire action, but they work in a similar fashion. +Partials can have their own layouts applied to them. These layouts are different from those applied to a controller action, but they work in a similar fashion. -Let's say we're displaying an article on a page, that should be wrapped in a `div` for display purposes. First, we'll create a new `Article`: +Let's say we're displaying an article on a page which should be wrapped in a `div` for display purposes. Firstly, we'll create a new `Article`: ```ruby Article.create(body: 'Partial Layouts are cool!') @@ -287,7 +338,7 @@ In the `show` template, we'll render the `_article` partial wrapped in the `box` **articles/show.html.erb** ```erb -<%= render partial: 'article', layout: 'box', locals: {article: @article} %> +<%= render partial: 'article', layout: 'box', locals: { article: @article } %> ``` The `box` layout simply wraps the `_article` partial in a `div`: @@ -300,26 +351,6 @@ The `box` layout simply wraps the `_article` partial in a `div`:
``` -The `_article` partial wraps the article's `body` in a `div` with the `id` of the article using the `div_for` helper: - -**articles/_article.html.erb** - -```html+erb -<%= div_for(article) do %> -

<%= article.body %>

-<% end %> -``` - -this would output the following: - -```html -
-
-

Partial Layouts are cool!

-
-
-``` - Note that the partial layout has access to the local `article` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix. You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_article` partial, we could do this instead: @@ -327,10 +358,10 @@ You can also render a block of code within a partial layout instead of calling ` **articles/show.html.erb** ```html+erb -<% render(layout: 'box', locals: {article: @article}) do %> - <%= div_for(article) do %> +<% render(layout: 'box', locals: { article: @article }) do %> +

<%= article.body %>

- <% end %> +
<% end %> ``` @@ -339,91 +370,41 @@ Supposing we use the same `_box` partial from above, this would produce the same View Paths ---------- -TODO... - -Overview of helpers provided by Action View -------------------------------------------- - -WIP: Not all the helpers are listed here. For a full list see the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html) - -The following is only a brief overview summary of the helpers available in Action View. It's recommended that you review the [API Documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html), which covers all of the helpers in more detail, but this should serve as a good starting point. - -### RecordTagHelper +When rendering a response, the controller needs to resolve where the different +views are located. By default it only looks inside the `app/views` directory. -This module provides methods for generating container tags, such as `div`, for your record. This is the recommended way of creating a container for render your Active Record object, as it adds an appropriate class and id attributes to that container. You can then refer to those containers easily by following the convention, instead of having to think about which class or id attribute you should use. +We can add other locations and give them a certain precedence when resolving +paths using the `prepend_view_path` and `append_view_path` methods. -#### content_tag_for +### Prepend view path -Renders a container tag that relates to your Active Record Object. +This can be helpful for example, when we want to put views inside a different +directory for subdomains. -For example, given `@article` is the object of `Article` class, you can do: +We can do this by using: -```html+erb -<%= content_tag_for(:tr, @article) do %> - <%= @article.title %> -<% end %> -``` - -This will generate this HTML output: - -```html - - Hello World! - -``` - -You can also supply HTML attributes as an additional option hash. For example: - -```html+erb -<%= content_tag_for(:tr, @article, class: "frontpage") do %> - <%= @article.title %> -<% end %> +```ruby +prepend_view_path "app/views/#{request.subdomain}" ``` -Will generate this HTML output: - -```html - - Hello World! - -``` +Then Action View will look first in this directory when resolving views. -You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@articles` is an array of two `Article` objects: +### Append view path -```html+erb -<%= content_tag_for(:tr, @articles) do |article| %> - <%= article.title %> -<% end %> -``` +Similarly, we can append paths: -Will generate this HTML output: - -```html - - Hello World! - - - Ruby on Rails Rocks! - +```ruby +append_view_path "app/views/direct" ``` -#### div_for - -This is actually a convenient method which calls `content_tag_for` internally with `:div` as the tag name. You can pass either an Active Record object or a collection of objects. For example: +This will add `app/views/direct` to the end of the lookup paths. -```html+erb -<%= div_for(@article, class: "frontpage") do %> - <%= @article.title %> -<% end %> -``` +Overview of helpers provided by Action View +------------------------------------------- -Will generate this HTML output: +WIP: Not all the helpers are listed here. For a full list see the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html) -```html -
- Hello World! -
-``` +The following is only a brief overview summary of the helpers available in Action View. It's recommended that you review the [API Documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html), which covers all of the helpers in more detail, but this should serve as a good starting point. ### AssetTagHelper @@ -436,39 +417,13 @@ config.action_controller.asset_host = "assets.example.com" image_tag("rails.png") # => Rails ``` -#### register_javascript_expansion - -Register one or more JavaScript files to be included when symbol is passed to javascript_include_tag. This method is typically intended to be called from plugin initialization to register JavaScript files that the plugin installed in `vendor/assets/javascripts`. - -```ruby -ActionView::Helpers::AssetTagHelper.register_javascript_expansion monkey: ["head", "body", "tail"] - -javascript_include_tag :monkey # => - - - -``` - -#### register_stylesheet_expansion - -Register one or more stylesheet files to be included when symbol is passed to `stylesheet_link_tag`. This method is typically intended to be called from plugin initialization to register stylesheet files that the plugin installed in `vendor/assets/stylesheets`. - -```ruby -ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion monkey: ["head", "body", "tail"] - -stylesheet_link_tag :monkey # => - - - -``` - #### auto_discovery_link_tag Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed. ```ruby -auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # => - +auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # => + ``` #### image_path @@ -487,7 +442,7 @@ image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png #### image_url -Computes the url to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host. +Computes the URL to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host. ```ruby image_url("https://github.com/edit.png") # => http://www.example.com/assets/edit.png @@ -495,7 +450,7 @@ image_url("https://github.com/edit.png") # => http://www.example.com/assets/edit.png #### image_tag -Returns an html image tag for the source. The source can be a full path or a file that exists in your `app/assets/images` directory. +Returns an HTML image tag for the source. The source can be a full path or a file that exists in your `app/assets/images` directory. ```ruby image_tag("icon.png") # => Icon @@ -503,7 +458,7 @@ image_tag("icon.png") # => Icon #### javascript_include_tag -Returns an html script tag for each of the sources provided. You can pass in the filename (`.js` extension is optional) of JavaScript files that exist in your `app/assets/javascripts` directory for inclusion into the current page or you can pass the full path relative to your document root. +Returns an HTML script tag for each of the sources provided. You can pass in the filename (`.js` extension is optional) of JavaScript files that exist in your `app/assets/javascripts` directory for inclusion into the current page or you can pass the full path relative to your document root. ```ruby javascript_include_tag "common" # => @@ -538,7 +493,7 @@ javascript_path "common" # => /assets/common.js #### javascript_url -Computes the url to a JavaScript asset in the `app/assets/javascripts` directory. This will call `javascript_path` internally and merge with your current host or your asset host. +Computes the URL to a JavaScript asset in the `app/assets/javascripts` directory. This will call `javascript_path` internally and merge with your current host or your asset host. ```ruby javascript_url "common" # => http://www.example.com/assets/common.js @@ -552,7 +507,7 @@ Returns a stylesheet link tag for the sources specified as arguments. If you don stylesheet_link_tag "application" # => ``` -You can also include all styles in the stylesheet directory using :all as the source: +You can also include all styles in the stylesheet directory using `:all` as the source: ```ruby stylesheet_link_tag :all @@ -567,7 +522,7 @@ stylesheet_link_tag :all, cache: true #### stylesheet_path -Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, .css will be appended. Full paths from the document root will be passed through. Used internally by stylesheet_link_tag to build the stylesheet path. +Computes the path to a stylesheet asset in the `app/assets/stylesheets` directory. If the source filename has no extension, `.css` will be appended. Full paths from the document root will be passed through. Used internally by stylesheet_link_tag to build the stylesheet path. ```ruby stylesheet_path "application" # => /assets/application.css @@ -575,7 +530,7 @@ stylesheet_path "application" # => /assets/application.css #### stylesheet_url -Computes the url to a stylesheet asset in the `app/assets/stylesheets` directory. This will call `stylesheet_path` internally and merge with your current host or your asset host. +Computes the URL to a stylesheet asset in the `app/assets/stylesheets` directory. This will call `stylesheet_path` internally and merge with your current host or your asset host. ```ruby stylesheet_url "application" # => http://www.example.com/assets/application.css @@ -611,7 +566,7 @@ end ```ruby atom_feed do |feed| feed.title("Articles Index") - feed.updated((@articles.first.created_at)) + feed.updated(@articles.first.created_at) @articles.each do |article| feed.entry(article) do |entry| @@ -644,7 +599,7 @@ This would add something like "Process data files (0.34523)" to the log, which y #### cache -A method for caching fragments of a view rather than an entire action or page. This technique is useful caching pieces like menus, lists of news topics, static HTML fragments, and so on. This method takes a block that contains the content you wish to cache. See `ActionController::Caching::Fragments` for more information. +A method for caching fragments of a view rather than an entire action or page. This technique is useful for caching pieces like menus, lists of news topics, static HTML fragments, and so on. This method takes a block that contains the content you wish to cache. See `AbstractController::Caching::Fragments` for more information. ```erb <% cache do %> @@ -736,7 +691,7 @@ distance_of_time_in_words(Time.now, Time.now + 15.seconds, include_seconds: true #### select_date -Returns a set of html select-tags (one for year, month, and day) pre-selected with the `date` provided. +Returns a set of HTML select-tags (one for year, month, and day) pre-selected with the `date` provided. ```ruby # Generates a date select that defaults to the date provided (six days after today) @@ -748,7 +703,7 @@ select_date() #### select_datetime -Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the `datetime` provided. +Returns a set of HTML select-tags (one for year, month, day, hour, and minute) pre-selected with the `datetime` provided. ```ruby # Generates a datetime select that defaults to the datetime provided (four days after today) @@ -785,7 +740,7 @@ Returns a select tag with options for each of the minutes 0 through 59 with the ```ruby # Generates a select field for minutes that defaults to the minutes for the time provided. -select_minute(Time.now + 6.hours) +select_minute(Time.now + 10.minutes) ``` #### select_month @@ -803,12 +758,12 @@ Returns a select tag with options for each of the seconds 0 through 59 with the ```ruby # Generates a select field for seconds that defaults to the seconds for the time provided -select_second(Time.now + 16.minutes) +select_second(Time.now + 16.seconds) ``` #### select_time -Returns a set of html select-tags (one for hour and minute). +Returns a set of HTML select-tags (one for hour and minute). ```ruby # Generates a time select that defaults to the time provided @@ -849,7 +804,7 @@ time_select("order", "submitted") Returns a `pre` tag that has object dumped by YAML. This creates a very readable way to inspect an object. ```ruby -my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]} +my_hash = { 'first' => 1, 'second' => 'two', 'third' => [1,2,3] } debug(my_hash) ``` @@ -868,13 +823,13 @@ third: Form helpers are designed to make working with models much easier compared to using just standard HTML elements by providing a set of methods for creating forms based on your models. This helper generates the HTML for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form is submitted (i.e., when the user hits the submit button or form.submit is called via JavaScript), the form inputs will be bundled into the params object and passed back to the controller. -There are two types of form helpers: those that specifically work with model attributes and those that don't. This helper deals with those that work with model attributes; to see an example of form helpers that don't work with model attributes, check the ActionView::Helpers::FormTagHelper documentation. +There are two types of form helpers: those that specifically work with model attributes and those that don't. This helper deals with those that work with model attributes; to see an example of form helpers that don't work with model attributes, check the `ActionView::Helpers::FormTagHelper` documentation. -The core method of this helper, form_for, gives you the ability to create a form for a model instance; for example, let's say that you have a model Person and want to create a new instance of it: +The core method of this helper, `form_for`, gives you the ability to create a form for a model instance; for example, let's say that you have a model Person and want to create a new instance of it: ```html+erb # Note: a @person variable will have been created in the controller (e.g. @person = Person.new) -<%= form_for @person, url: {action: "create"} do |f| %> +<%= form_for @person, url: { action: "create" } do |f| %> <%= f.text_field :first_name %> <%= f.text_field :last_name %> <%= submit_tag 'Create' %> @@ -894,7 +849,7 @@ The HTML generated for this would be: The params object created when this form is submitted would look like: ```ruby -{"action" => "create", "controller" => "people", "person" => {"first_name" => "William", "last_name" => "Smith"}} +{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } } ``` The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller. @@ -912,10 +867,10 @@ check_box("article", "validated") #### fields_for -Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes fields_for suitable for specifying additional model objects in the same form: +Creates a scope around a specific model object like `form_for`, but doesn't create the form tags themselves. This makes `fields_for` suitable for specifying additional model objects in the same form: ```html+erb -<%= form_for @person, url: {action: "update"} do |person_form| %> +<%= form_for @person, url: { action: "update" } do |person_form| %> First name: <%= person_form.text_field :first_name %> Last name : <%= person_form.text_field :last_name %> @@ -1035,11 +990,11 @@ Returns `select` and `option` tags for the collection of existing return values Example object structure for use with this method: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :author end -class Author < ActiveRecord::Base +class Author < ApplicationRecord has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" @@ -1050,7 +1005,7 @@ end Sample usage (selecting the associated Author for an instance of Article, `@article`): ```ruby -collection_select(:article, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) +collection_select(:article, :author_id, Author.all, :id, :name_with_initial, { prompt: true }) ``` If `@article.author_id` is 1, this would return: @@ -1071,11 +1026,11 @@ Returns `radio_button` tags for the collection of existing return values of `met Example object structure for use with this method: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :author end -class Author < ActiveRecord::Base +class Author < ApplicationRecord has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" @@ -1107,11 +1062,11 @@ Returns `check_box` tags for the collection of existing return values of `method Example object structure for use with this method: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_and_belongs_to_many :authors end -class Author < ActiveRecord::Base +class Author < ApplicationRecord has_and_belongs_to_many :articles def name_with_initial "#{first_name.first}. #{last_name}" @@ -1137,14 +1092,6 @@ If `@article.author_ids` is [1], this would return: ``` -#### country_options_for_select - -Returns a string of option tags for pretty much any country in the world. - -#### country_select - -Returns select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. - #### option_groups_from_collection_for_select Returns a string of `option` tags, like `options_from_collection_for_select`, but groups them by `optgroup` tags based on the object relationships of the arguments. @@ -1152,12 +1099,12 @@ Returns a string of `option` tags, like `options_from_collection_for_select`, bu Example object structure for use with this method: ```ruby -class Continent < ActiveRecord::Base +class Continent < ApplicationRecord has_many :countries # attribs: id, name end -class Country < ActiveRecord::Base +class Country < ApplicationRecord belongs_to :continent # attribs: id, name, continent_id end @@ -1206,7 +1153,7 @@ Returns a string of option tags that have been compiled by iterating over the `c # options_from_collection_for_select(collection, value_method, text_method, selected = nil) ``` -For example, imagine a loop iterating over each person in @project.people to generate an input tag: +For example, imagine a loop iterating over each person in `@project.people` to generate an input tag: ```ruby options_from_collection_for_select(@project.people, "id", "name") @@ -1222,7 +1169,7 @@ Create a select tag and a series of contained option tags for the provided objec Example: ```ruby -select("article", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) +select("article", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) ``` If `@article.person_id` is 1, this would become: @@ -1231,8 +1178,8 @@ If `@article.person_id` is 1, this would become: ``` @@ -1285,7 +1232,7 @@ Creates a field set for grouping HTML form elements. Creates a file upload field. ```html+erb -<%= form_tag({action:"post"}, multipart: true) do %> +<%= form_tag({ action: "post" }, multipart: true) do %> <%= file_field_tag "file" %> <%= submit_tag %> <% end %> @@ -1300,7 +1247,7 @@ file_field_tag 'attachment' #### form_tag -Starts a form tag that points the action to an url configured with `url_for_options` just like `ActionController::Base#url_for`. +Starts a form tag that points the action to a URL configured with `url_for_options` just like `ActionController::Base#url_for`. ```html+erb <%= form_tag '/articles' do %> @@ -1421,22 +1368,6 @@ date_field_tag "dob" Provides functionality for working with JavaScript in your views. -#### button_to_function - -Returns a button that'll trigger a JavaScript function using the onclick handler. Examples: - -```ruby -button_to_function "Greeting", "alert('Hello world!')" -button_to_function "Delete", "if (confirm('Really?')) do_delete()" -button_to_function "Details" do |page| - page[:details].visual_effect :toggle_slide -end -``` - -#### define_javascript_functions - -Includes the Action Pack JavaScript libraries inside a single `script` tag. - #### escape_javascript Escape carrier returns and single and double quotes for JavaScript segments. @@ -1457,15 +1388,6 @@ alert('All is good') ``` -#### link_to_function - -Returns a link that will trigger a JavaScript function using the onclick handler and return false after the fact. - -```ruby -link_to_function "Greeting", "alert('Hello world!')" -# => Greeting -``` - ### NumberHelper Provides methods for converting numbers into formatted strings. Methods are provided for phone numbers, currency, percentage, precision, positional notation, and file size. @@ -1497,7 +1419,7 @@ number_to_percentage(100, precision: 0) # => 100% #### number_to_phone -Formats a number into a US phone number. +Formats a number into a phone number (US by default). ```ruby number_to_phone(1235551234) # => 123-555-1234 @@ -1517,7 +1439,7 @@ Formats a number with the specified level of `precision`, which defaults to 3. ```ruby number_with_precision(111.2345) # => 111.235 -number_with_precision(111.2345, 2) # => 111.23 +number_with_precision(111.2345, precision: 2) # => 111.23 ``` ### SanitizeHelper @@ -1526,13 +1448,13 @@ The SanitizeHelper module provides a set of methods for scrubbing text of undesi #### sanitize -This sanitize helper will html encode all tags and strip all attributes that aren't specifically allowed. +This sanitize helper will HTML encode all tags and strip all attributes that aren't specifically allowed. ```ruby sanitize @article.body ``` -If either the :attributes or :tags options are passed, only the mentioned tags and attributes are allowed and nothing else. +If either the `:attributes` or `:tags` options are passed, only the mentioned attributes and tags are allowed and nothing else. ```ruby sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) @@ -1554,12 +1476,12 @@ Sanitizes a block of CSS code. Strips all link tags from text leaving just the link text. ```ruby -strip_links("Ruby on Rails") +strip_links('Ruby on Rails') # => Ruby on Rails ``` ```ruby -strip_links("emails to me@email.com.") +strip_links('emails to me@email.com.') # => emails to me@email.com. ``` @@ -1571,7 +1493,7 @@ strip_links('Blog: Visit.') #### strip_tags(html) Strips all HTML tags from the html, including comments. -This uses the html-scanner tokenizer and so its HTML parsing ability is limited by that of html-scanner. +This functionality is powered by the rails-html-sanitizer gem. ```ruby strip_tags("Strip these tags!") @@ -1600,9 +1522,9 @@ details can be found in the [Rails Security Guide](security.html#cross-site-requ Localized Views --------------- -Action View has the ability render different templates depending on the current locale. +Action View has the ability to render different templates depending on the current locale. -For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. +For example, suppose you have an `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available. You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages. diff --git a/source/active_job_basics.md b/source/active_job_basics.md new file mode 100644 index 0000000..b58ca61 --- /dev/null +++ b/source/active_job_basics.md @@ -0,0 +1,389 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Active Job Basics +================= + +This guide provides you with all you need to get started in creating, +enqueuing and executing background jobs. + +After reading this guide, you will know: + +* How to create jobs. +* How to enqueue jobs. +* How to run jobs in the background. +* How to send emails from your application asynchronously. + +-------------------------------------------------------------------------------- + + +Introduction +------------ + +Active Job is a framework for declaring jobs and making them run on a variety +of queuing backends. These jobs can be everything from regularly scheduled +clean-ups, to billing charges, to mailings. Anything that can be chopped up +into small units of work and run in parallel, really. + + +The Purpose of Active Job +----------------------------- +The main point is to ensure that all Rails apps will have a job infrastructure +in place. We can then have framework features and other gems build on top of that, +without having to worry about API differences between various job runners such as +Delayed Job and Resque. Picking your queuing backend becomes more of an operational +concern, then. And you'll be able to switch between them without having to rewrite +your jobs. + +NOTE: Rails by default comes with an asynchronous queuing implementation that +runs jobs with an in-process thread pool. Jobs will run asynchronously, but any +jobs in the queue will be dropped upon restart. + + +Creating a Job +-------------- + +This section will provide a step-by-step guide to creating a job and enqueuing it. + +### Create the Job + +Active Job provides a Rails generator to create jobs. The following will create a +job in `app/jobs` (with an attached test case under `test/jobs`): + +```bash +$ bin/rails generate job guests_cleanup +invoke test_unit +create test/jobs/guests_cleanup_job_test.rb +create app/jobs/guests_cleanup_job.rb +``` + +You can also create a job that will run on a specific queue: + +```bash +$ bin/rails generate job guests_cleanup --queue urgent +``` + +If you don't want to use a generator, you could create your own file inside of +`app/jobs`, just make sure that it inherits from `ApplicationJob`. + +Here's what a job looks like: + +```ruby +class GuestsCleanupJob < ApplicationJob + queue_as :default + + def perform(*guests) + # Do something later + end +end +``` + +Note that you can define `perform` with as many arguments as you want. + +### Enqueue the Job + +Enqueue a job like so: + +```ruby +# Enqueue a job to be performed as soon as the queuing system is +# free. +GuestsCleanupJob.perform_later guest +``` + +```ruby +# Enqueue a job to be performed tomorrow at noon. +GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest) +``` + +```ruby +# Enqueue a job to be performed 1 week from now. +GuestsCleanupJob.set(wait: 1.week).perform_later(guest) +``` + +```ruby +# `perform_now` and `perform_later` will call `perform` under the hood so +# you can pass as many arguments as defined in the latter. +GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter') +``` + +That's it! + +Job Execution +------------- + +For enqueuing and executing jobs in production you need to set up a queuing backend, +that is to say you need to decide for a 3rd-party queuing library that Rails should use. +Rails itself only provides an in-process queuing system, which only keeps the jobs in RAM. +If the process crashes or the machine is reset, then all outstanding jobs are lost with the +default async backend. This may be fine for smaller apps or non-critical jobs, but most +production apps will need to pick a persistent backend. + +### Backends + +Active Job has built-in adapters for multiple queuing backends (Sidekiq, +Resque, Delayed Job and others). To get an up-to-date list of the adapters +see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). + +### Setting the Backend + +You can easily set your queuing backend: + +```ruby +# config/application.rb +module YourApp + class Application < Rails::Application + # Be sure to have the adapter's gem in your Gemfile + # and follow the adapter's specific installation + # and deployment instructions. + config.active_job.queue_adapter = :sidekiq + end +end +``` + +You can also configure your backend on a per job basis. + +```ruby +class GuestsCleanupJob < ApplicationJob + self.queue_adapter = :resque + #.... +end + +# Now your job will use `resque` as it's backend queue adapter overriding what +# was configured in `config.active_job.queue_adapter`. +``` + +### Starting the Backend + +Since jobs run in parallel to your Rails application, most queuing libraries +require that you start a library-specific queuing service (in addition to +starting your Rails app) for the job processing to work. Refer to library +documentation for instructions on starting your queue backend. + +Here is a noncomprehensive list of documentation: + +- [Sidekiq](https://github.com/mperham/sidekiq/wiki/Active-Job) +- [Resque](https://github.com/resque/resque/wiki/ActiveJob) +- [Sucker Punch](https://github.com/brandonhilkert/sucker_punch#active-job) +- [Queue Classic](https://github.com/QueueClassic/queue_classic#active-job) + +Queues +------ + +Most of the adapters support multiple queues. With Active Job you can schedule +the job to run on a specific queue: + +```ruby +class GuestsCleanupJob < ApplicationJob + queue_as :low_priority + #.... +end +``` + +You can prefix the queue name for all your jobs using +`config.active_job.queue_name_prefix` in `application.rb`: + +```ruby +# config/application.rb +module YourApp + class Application < Rails::Application + config.active_job.queue_name_prefix = Rails.env + end +end + +# app/jobs/guests_cleanup_job.rb +class GuestsCleanupJob < ApplicationJob + queue_as :low_priority + #.... +end + +# Now your job will run on queue production_low_priority on your +# production environment and on staging_low_priority +# on your staging environment +``` + +The default queue name prefix delimiter is '\_'. This can be changed by setting +`config.active_job.queue_name_delimiter` in `application.rb`: + +```ruby +# config/application.rb +module YourApp + class Application < Rails::Application + config.active_job.queue_name_prefix = Rails.env + config.active_job.queue_name_delimiter = '.' + end +end + +# app/jobs/guests_cleanup_job.rb +class GuestsCleanupJob < ApplicationJob + queue_as :low_priority + #.... +end + +# Now your job will run on queue production.low_priority on your +# production environment and on staging.low_priority +# on your staging environment +``` + +If you want more control on what queue a job will be run you can pass a `:queue` +option to `#set`: + +```ruby +MyJob.set(queue: :another_queue).perform_later(record) +``` + +To control the queue from the job level you can pass a block to `#queue_as`. The +block will be executed in the job context (so you can access `self.arguments`) +and you must return the queue name: + +```ruby +class ProcessVideoJob < ApplicationJob + queue_as do + video = self.arguments.first + if video.owner.premium? + :premium_videojobs + else + :videojobs + end + end + + def perform(video) + # Do process video + end +end + +ProcessVideoJob.perform_later(Video.last) +``` + +NOTE: Make sure your queuing backend "listens" on your queue name. For some +backends you need to specify the queues to listen to. + + +Callbacks +--------- + +Active Job provides hooks during the life cycle of a job. Callbacks allow you to +trigger logic during the life cycle of a job. + +### Available callbacks + +* `before_enqueue` +* `around_enqueue` +* `after_enqueue` +* `before_perform` +* `around_perform` +* `after_perform` + +### Usage + +```ruby +class GuestsCleanupJob < ApplicationJob + queue_as :default + + before_enqueue do |job| + # Do something with the job instance + end + + around_perform do |job, block| + # Do something before perform + block.call + # Do something after perform + end + + def perform + # Do something later + end +end +``` + + +Action Mailer +------------ + +One of the most common jobs in a modern web application is sending emails outside +of the request-response cycle, so the user doesn't have to wait on it. Active Job +is integrated with Action Mailer so you can easily send emails asynchronously: + +```ruby +# If you want to send the email now use #deliver_now +UserMailer.welcome(@user).deliver_now + +# If you want to send the email through Active Job use #deliver_later +UserMailer.welcome(@user).deliver_later +``` + + +Internationalization +-------------------- + +Each job uses the `I18n.locale` set when the job was created. Useful if you send +emails asynchronously: + +```ruby +I18n.locale = :eo + +UserMailer.welcome(@user).deliver_later # Email will be localized to Esperanto. +``` + + +GlobalID +-------- + +Active Job supports GlobalID for parameters. This makes it possible to pass live +Active Record objects to your job instead of class/id pairs, which you then have +to manually deserialize. Before, jobs would look like this: + +```ruby +class TrashableCleanupJob < ApplicationJob + def perform(trashable_class, trashable_id, depth) + trashable = trashable_class.constantize.find(trashable_id) + trashable.cleanup(depth) + end +end +``` + +Now you can simply do: + +```ruby +class TrashableCleanupJob < ApplicationJob + def perform(trashable, depth) + trashable.cleanup(depth) + end +end +``` + +This works with any class that mixes in `GlobalID::Identification`, which +by default has been mixed into Active Record classes. + + +Exceptions +---------- + +Active Job provides a way to catch exceptions raised during the execution of the +job: + +```ruby +class GuestsCleanupJob < ApplicationJob + queue_as :default + + rescue_from(ActiveRecord::RecordNotFound) do |exception| + # Do something with the exception + end + + def perform + # Do something later + end +end +``` + +### Deserialization + +GlobalID allows serializing full Active Record objects passed to `#perform`. + +If a passed record is deleted after the job is enqueued but before the `#perform` +method is called Active Job will raise an `ActiveJob::DeserializationError` +exception. + +Job Testing +-------------- + +You can find detailed instructions on how to test your jobs in the +[testing guide](testing.html#testing-jobs). diff --git a/source/active_model_basics.md b/source/active_model_basics.md index 3eaeeff..e26805d 100644 --- a/source/active_model_basics.md +++ b/source/active_model_basics.md @@ -1,20 +1,34 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Model Basics =================== -This guide should provide you with all you need to get started using model classes. Active Model allows for Action Pack helpers to interact with non-Active Record models. Active Model also helps building custom ORMs for use outside of the Rails framework. +This guide should provide you with all you need to get started using model +classes. Active Model allows for Action Pack helpers to interact with +plain Ruby objects. Active Model also helps build custom ORMs for use +outside of the Rails framework. After reading this guide, you will know: +* How an Active Record model behaves. +* How Callbacks and validations work. +* How serializers work. +* How Active Model integrates with the Rails internationalization (i18n) framework. + -------------------------------------------------------------------------------- Introduction ------------ -Active Model is a library containing various modules used in developing frameworks that need to interact with the Rails Action Pack library. Active Model provides a known set of interfaces for usage in classes. Some of modules are explained below. +Active Model is a library containing various modules used in developing +classes that need some features present on Active Record. +Some of these modules are explained below. -### AttributeMethods +### Attribute Methods -The AttributeMethods module can add custom prefixes and suffixes on methods of a class. It is used by defining the prefixes and suffixes and which methods on the object will use them. +The `ActiveModel::AttributeMethods` module can add custom prefixes and suffixes +on methods of a class. It is used by defining the prefixes and suffixes and +which methods on the object will use them. ```ruby class Person @@ -38,14 +52,17 @@ end person = Person.new person.age = 110 -person.age_highest? # true -person.reset_age # 0 -person.age_highest? # false +person.age_highest? # => true +person.reset_age # => 0 +person.age_highest? # => false ``` ### Callbacks -Callbacks gives Active Record style callbacks. This provides an ability to define callbacks which run at appropriate times. After defining callbacks, you can wrap them with before, after and around custom methods. +`ActiveModel::Callbacks` gives Active Record style callbacks. This provides an +ability to define callbacks which run at appropriate times. +After defining callbacks, you can wrap them with before, after and around +custom methods. ```ruby class Person @@ -69,7 +86,9 @@ end ### Conversion -If a class defines `persisted?` and `id` methods, then you can include the `Conversion` module in that class and call the Rails conversion methods on objects of that class. +If a class defines `persisted?` and `id` methods, then you can include the +`ActiveModel::Conversion` module in that class, and call the Rails conversion +methods on objects of that class. ```ruby class Person @@ -92,11 +111,13 @@ person.to_param # => nil ### Dirty -An object becomes dirty when it has gone through one or more changes to its attributes and has not been saved. This gives the ability to check whether an object has been changed or not. It also has attribute based accessor methods. Let's consider a Person class with attributes `first_name` and `last_name`: +An object becomes dirty when it has gone through one or more changes to its +attributes and has not been saved. `ActiveModel::Dirty` gives the ability to +check whether an object has been changed or not. It also has attribute based +accessor methods. Let's consider a Person class with attributes `first_name` +and `last_name`: ```ruby -require 'active_model' - class Person include ActiveModel::Dirty define_attribute_methods :first_name, :last_name @@ -135,16 +156,17 @@ person.changed? # => false person.first_name = "First Name" person.first_name # => "First Name" -# returns if any attribute has changed. +# returns true if any of the attributes have unsaved changes. person.changed? # => true # returns a list of attributes that have changed before saving. person.changed # => ["first_name"] -# returns a hash of the attributes that have changed with their original values. +# returns a Hash of the attributes that have changed with their original values. person.changed_attributes # => {"first_name"=>nil} -# returns a hash of changes, with the attribute names as the keys, and the values will be an array of the old and new value for that field. +# returns a Hash of changes, with the attribute names as the keys, and the +# values as an array of the old and new values for that field. person.changes # => {"first_name"=>[nil, "First Name"]} ``` @@ -158,14 +180,15 @@ person.first_name # => "First Name" person.first_name_changed? # => true ``` -Track what was the previous value of the attribute. +Track the previous value of the attribute. ```ruby # attr_name_was accessor -person.first_name_was # => "First Name" +person.first_name_was # => nil ``` -Track both previous and current value of the changed attribute. Returns an array if changed, else returns nil. +Track both previous and current value of the changed attribute. Returns an array +if changed, otherwise returns nil. ```ruby # attr_name_change @@ -175,7 +198,8 @@ person.last_name_change # => nil ### Validations -Validations module adds the ability to class objects to validate them in Active Record style. +The `ActiveModel::Validations` module adds the ability to validate objects +like in Active Record. ```ruby class Person @@ -188,7 +212,8 @@ class Person validates! :token, presence: true end -person = Person.new(token: "2b1f325") +person = Person.new +person.token = "2b1f325" person.valid? # => false person.name = 'vishnu' person.email = 'me' @@ -199,9 +224,9 @@ person.token = nil person.valid? # => raises ActiveModel::StrictValidationFailed ``` -### ActiveModel::Naming +### Naming -Naming adds a number of class methods which make the naming and routing +`ActiveModel::Naming` adds a number of class methods which make naming and routing easier to manage. The module defines the `model_name` class method which will define a number of accessors using some `ActiveSupport::Inflector` methods. @@ -221,3 +246,260 @@ Person.model_name.i18n_key # => :person Person.model_name.route_key # => "people" Person.model_name.singular_route_key # => "person" ``` + +### Model + +`ActiveModel::Model` adds the ability for a class to work with Action Pack and +Action View right out of the box. + +```ruby +class EmailContact + include ActiveModel::Model + + attr_accessor :name, :email, :message + validates :name, :email, :message, presence: true + + def deliver + if valid? + # deliver email + end + end +end +``` + +When including `ActiveModel::Model` you get some features like: + +- model name introspection +- conversions +- translations +- validations + +It also gives you the ability to initialize an object with a hash of attributes, +much like any Active Record object. + +```ruby +email_contact = EmailContact.new(name: 'David', + email: 'david@example.com', + message: 'Hello World') +email_contact.name # => 'David' +email_contact.email # => 'david@example.com' +email_contact.valid? # => true +email_contact.persisted? # => false +``` + +Any class that includes `ActiveModel::Model` can be used with `form_for`, +`render` and any other Action View helper methods, just like Active Record +objects. + +### Serialization + +`ActiveModel::Serialization` provides basic serialization for your object. +You need to declare an attributes Hash which contains the attributes you want to +serialize. Attributes must be strings, not symbols. + +```ruby +class Person + include ActiveModel::Serialization + + attr_accessor :name + + def attributes + {'name' => nil} + end +end +``` + +Now you can access a serialized Hash of your object using the `serializable_hash` method. + +```ruby +person = Person.new +person.serializable_hash # => {"name"=>nil} +person.name = "Bob" +person.serializable_hash # => {"name"=>"Bob"} +``` + +#### ActiveModel::Serializers + +Active Model also provides the `ActiveModel::Serializers::JSON` module +for JSON serializing / deserializing. This module automatically includes the +previously discussed `ActiveModel::Serialization` module. + +##### ActiveModel::Serializers::JSON + +To use `ActiveModel::Serializers::JSON` you only need to change the +module you are including from `ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`. + +```ruby +class Person + include ActiveModel::Serializers::JSON + + attr_accessor :name + + def attributes + {'name' => nil} + end +end +``` + +The `as_json` method, similar to `serializable_hash`, provides a Hash representing +the model. + +```ruby +person = Person.new +person.as_json # => {"name"=>nil} +person.name = "Bob" +person.as_json # => {"name"=>"Bob"} +``` + +You can also define the attributes for a model from a JSON string. +However, you need to define the `attributes=` method on your class: + +```ruby +class Person + include ActiveModel::Serializers::JSON + + attr_accessor :name + + def attributes=(hash) + hash.each do |key, value| + send("#{key}=", value) + end + end + + def attributes + {'name' => nil} + end +end +``` + +Now it is possible to create an instance of `Person` and set attributes using `from_json`. + +```ruby +json = { name: 'Bob' }.to_json +person = Person.new +person.from_json(json) # => # +person.name # => "Bob" +``` + +### Translation + +`ActiveModel::Translation` provides integration between your object and the Rails +internationalization (i18n) framework. + +```ruby +class Person + extend ActiveModel::Translation +end +``` + +With the `human_attribute_name` method, you can transform attribute names into a +more human-readable format. The human-readable format is defined in your locale file(s). + +* config/locales/app.pt-BR.yml + + ```yml + pt-BR: + activemodel: + attributes: + person: + name: 'Nome' + ``` + +```ruby +Person.human_attribute_name('name') # => "Nome" +``` + +### Lint Tests + +`ActiveModel::Lint::Tests` allows you to test whether an object is compliant with +the Active Model API. + +* `app/models/person.rb` + + ```ruby + class Person + include ActiveModel::Model + end + ``` + +* `test/models/person_test.rb` + + ```ruby + require 'test_helper' + + class PersonTest < ActiveSupport::TestCase + include ActiveModel::Lint::Tests + + setup do + @model = Person.new + end + end + ``` + +```bash +$ rails test + +Run options: --seed 14596 + +# Running: + +...... + +Finished in 0.024899s, 240.9735 runs/s, 1204.8677 assertions/s. + +6 runs, 30 assertions, 0 failures, 0 errors, 0 skips +``` + +An object is not required to implement all APIs in order to work with +Action Pack. This module only intends to provide guidance in case you want all +features out of the box. + +### SecurePassword + +`ActiveModel::SecurePassword` provides a way to securely store any +password in an encrypted form. When you include this module, a +`has_secure_password` class method is provided which defines +a `password` accessor with certain validations on it. + +#### Requirements + +`ActiveModel::SecurePassword` depends on [`bcrypt`](https://github.com/codahale/bcrypt-ruby 'BCrypt'), +so include this gem in your Gemfile to use `ActiveModel::SecurePassword` correctly. +In order to make this work, the model must have an accessor named `password_digest`. +The `has_secure_password` will add the following validations on the `password` accessor: + +1. Password should be present. +2. Password should be equal to its confirmation (provided +password_confirmation+ is passed along). +3. The maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends) + +#### Examples + +```ruby +class Person + include ActiveModel::SecurePassword + has_secure_password + attr_accessor :password_digest +end + +person = Person.new + +# When password is blank. +person.valid? # => false + +# When the confirmation doesn't match the password. +person.password = 'aditya' +person.password_confirmation = 'nomatch' +person.valid? # => false + +# When the length of password exceeds 72. +person.password = person.password_confirmation = 'a' * 100 +person.valid? # => false + +# When only password is supplied with no password_confirmation. +person.password = 'aditya' +person.valid? # => true + +# When all validations are passed. +person.password = person.password_confirmation = 'aditya' +person.valid? # => true +``` diff --git a/source/active_record_basics.md b/source/active_record_basics.md index eff93ce..6b3aa47 100644 --- a/source/active_record_basics.md +++ b/source/active_record_basics.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Basics ==================== @@ -18,7 +20,7 @@ After reading this guide, you will know: What is Active Record? ---------------------- -Active Record is the M in [MVC](getting_started.html#the-mvc-architecture) - the +Active Record is the M in [MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an @@ -31,12 +33,12 @@ Object Relational Mapping system. in his book _Patterns of Enterprise Application Architecture_. In Active Record, objects carry both persistent data and behavior which operates on that data. Active Record takes the opinion that ensuring -data access logic is part of the object will educate users of that +data access logic as part of the object will educate users of that object on how to write to and read from the database. ### Object Relational Mapping -Object-Relational Mapping, commonly referred to as its abbreviation ORM, is +Object Relational Mapping, commonly referred to as its abbreviation ORM, is a technique that connects the rich objects of an application to tables in a relational database management system. Using ORM, the properties and relationships of the objects in an application can be easily stored and @@ -60,7 +62,7 @@ Convention over Configuration in Active Record When writing applications using other programming languages or frameworks, it may be necessary to write a lot of configuration code. This is particularly true for ORM frameworks in general. However, if you follow the conventions adopted by -Rails, you'll need to write very little configuration (in some case no +Rails, you'll need to write very little configuration (in some cases no configuration at all) when creating Active Record models. The idea is that if you configure your applications in the very same way most of the time then this should be the default way. Thus, explicit configuration would be needed @@ -72,8 +74,8 @@ By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created. Rails will pluralize your class names to find the respective database table. So, for a class `Book`, you should have a database table called **books**. The Rails -pluralization mechanisms are very powerful, being capable to pluralize (and -singularize) both regular and irregular words. When using class names composed +pluralization mechanisms are very powerful, being capable of pluralizing (and +singularizing) both regular and irregular words. When using class names composed of two or more words, the model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores. Examples: @@ -102,7 +104,7 @@ depending on the purpose of these columns. your models. * **Primary keys** - By default, Active Record will use an integer column named `id` as the table's primary key. When using [Active Record - Migrations](migrations.html) to create your tables, this column will be + Migrations](active_record_migrations.html) to create your tables, this column will be automatically created. There are also some optional column names that will add additional features @@ -116,11 +118,11 @@ to Active Record instances: locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to a model. * `type` - Specifies that the model uses [Single Table - Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance). + Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance). * `(association_name)_type` - Stores the type for [polymorphic associations](association_basics.html#polymorphic-associations). * `(table_name)_count` - Used to cache the number of belonging objects on - associations. For example, a `comments_count` column in a `Articles` class that + associations. For example, a `comments_count` column in an `Article` class that has many instances of `Comment` will cache the number of existent comments for each article. @@ -130,17 +132,17 @@ Creating Active Record Models ----------------------------- It is very easy to create Active Record models. All you have to do is to -subclass the `ActiveRecord::Base` class and you're good to go: +subclass the `ApplicationRecord` class and you're good to go: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord end ``` This will create a `Product` model, mapped to a `products` table at the database. By doing this you'll also have the ability to map the columns of each row in that table with the attributes of the instances of your model. Suppose -that the `products` table was created using an SQL sentence like: +that the `products` table was created using an SQL statement like: ```sql CREATE TABLE products ( @@ -166,23 +168,24 @@ What if you need to follow a different naming convention or need to use your Rails application with a legacy database? No problem, you can easily override the default conventions. -You can use the `ActiveRecord::Base.table_name=` method to specify the table -name that should be used: +`ApplicationRecord` inherits from `ActiveRecord::Base`, which defines a +number of helpful methods. You can use the `ActiveRecord::Base.table_name=` +method to specify the table name that should be used: ```ruby -class Product < ActiveRecord::Base - self.table_name = "PRODUCT" +class Product < ApplicationRecord + self.table_name = "my_products" end ``` If you do so, you will have to define manually the class name that is hosting -the fixtures (class_name.yml) using the `set_fixture_class` method in your test +the fixtures (my_products.yml) using the `set_fixture_class` method in your test definition: ```ruby -class FunnyJoke < ActiveSupport::TestCase - set_fixture_class funny_jokes: Joke - fixtures :funny_jokes +class ProductTest < ActiveSupport::TestCase + set_fixture_class my_products: Product + fixtures :my_products ... end ``` @@ -191,7 +194,7 @@ It's also possible to override the column that should be used as the table's primary key using the `ActiveRecord::Base.primary_key=` method: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord self.primary_key = "product_id" end ``` @@ -258,7 +261,7 @@ david = User.find_by(name: 'David') ```ruby # find all users named David who are Code Artists and sort by created_at in reverse chronological order -users = User.where(name: 'David', occupation: 'Code Artist').order('created_at DESC') +users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc) ``` You can learn more about querying an Active Record model in the [Active Record @@ -318,7 +321,7 @@ they raise the exception `ActiveRecord::RecordInvalid` if validation fails. A quick example to illustrate: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :name, presence: true end @@ -348,7 +351,7 @@ database that Active Record supports using `rake`. Here's a migration that creates a table: ```ruby -class CreatePublications < ActiveRecord::Migration +class CreatePublications < ActiveRecord::Migration[5.0] def change create_table :publications do |t| t.string :title @@ -366,9 +369,9 @@ end ``` Rails keeps track of which files have been committed to the database and -provides rollback features. To actually create the table, you'd run `rake db:migrate` -and to roll it back, `rake db:rollback`. +provides rollback features. To actually create the table, you'd run `rails db:migrate` +and to roll it back, `rails db:rollback`. Note that the above code is database-agnostic: it will run in MySQL, PostgreSQL, Oracle and others. You can learn more about migrations in the -[Active Record Migrations guide](migrations.html). +[Active Record Migrations guide](active_record_migrations.html). diff --git a/source/active_record_callbacks.md b/source/active_record_callbacks.md index 9c7e60c..77bd3c9 100644 --- a/source/active_record_callbacks.md +++ b/source/active_record_callbacks.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Callbacks ======================= @@ -29,12 +31,12 @@ Callbacks are methods that get called at certain moments of an object's life cyc In order to use the available callbacks, you need to register them. You can implement the callbacks as ordinary methods and use a macro-style class method to register them as callbacks: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :login, :email, presence: true before_validation :ensure_login_has_a_value - protected + private def ensure_login_has_a_value if login.nil? self.login = email unless email.blank? @@ -46,7 +48,7 @@ end The macro-style class methods can also receive a block. Consider using this style if the code inside your block is so short that it fits in a single line: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :login, :email, presence: true before_create do @@ -58,15 +60,15 @@ end Callbacks can also be registered to only fire on certain life cycle events: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord before_validation :normalize_name, on: :create # :on takes an array as well after_validation :set_location, on: [ :create, :update ] - protected + private def normalize_name - self.name = self.name.downcase.titleize + self.name = name.downcase.titleize end def set_location @@ -75,7 +77,7 @@ class User < ActiveRecord::Base end ``` -It is considered good practice to declare callback methods as protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation. +It is considered good practice to declare callback methods as private. If left public, they can be called from outside of the model and violate the principle of object encapsulation. Available Callbacks ------------------- @@ -124,7 +126,7 @@ The `after_find` callback will be called whenever Active Record loads a record f The `after_initialize` and `after_find` callbacks have no `before_*` counterparts, but they can be registered just like the other Active Record callbacks. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord after_initialize do |user| puts "You have initialized an object!" end @@ -149,7 +151,7 @@ You have initialized an object! The `after_touch` callback will be called whenever an Active Record object is touched. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord after_touch do |user| puts "You have touched an object" end @@ -166,14 +168,14 @@ You have touched an object It can be used along with `belongs_to`: ```ruby -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord belongs_to :company, touch: true after_touch do puts 'An Employee was touched' end end -class Company < ActiveRecord::Base +class Company < ApplicationRecord has_many :employees after_touch :log_when_employees_or_company_touched @@ -200,11 +202,9 @@ The following methods trigger callbacks: * `create` * `create!` -* `decrement!` * `destroy` * `destroy!` * `destroy_all` -* `increment!` * `save` * `save!` * `save(validate: false)` @@ -256,7 +256,7 @@ As you start registering new callbacks for your models, they will be queued for The whole callback chain is wrapped in a transaction. If any _before_ callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; _after_ callbacks can only accomplish that by raising an exception. -WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception. +WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception. Relational Callbacks -------------------- @@ -264,11 +264,11 @@ Relational Callbacks Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_many :articles, dependent: :destroy end -class Article < ActiveRecord::Base +class Article < ApplicationRecord after_destroy :log_destroy_action def log_destroy_action @@ -288,34 +288,24 @@ Article destroyed Conditional Callbacks --------------------- -As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option. +As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option. ### Using `:if` and `:unless` with a `Symbol` You can associate the `:if` and `:unless` options with a symbol corresponding to the name of a predicate method that will get called right before the callback. When using the `:if` option, the callback won't be executed if the predicate method returns false; when using the `:unless` option, the callback won't be executed if the predicate method returns true. This is the most common option. Using this form of registration it is also possible to register several different predicates that should be called to check if the callback should be executed. ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord before_save :normalize_card_number, if: :paid_with_card? end ``` -### Using `:if` and `:unless` with a String - -You can also use a string that will be evaluated using `eval` and hence needs to contain valid Ruby code. You should use this option only when the string represents a really short condition: - -```ruby -class Order < ActiveRecord::Base - before_save :normalize_card_number, if: "paid_with_card?" -end -``` - ### Using `:if` and `:unless` with a `Proc` Finally, it is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners: ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord before_save :normalize_card_number, if: Proc.new { |order| order.paid_with_card? } end @@ -326,7 +316,7 @@ end When writing conditional callbacks, it is possible to mix both `:if` and `:unless` in the same callback declaration: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord after_create :send_email_to_author, if: :author_wants_emails?, unless: Proc.new { |comment| comment.article.ignore_comments? } end @@ -352,7 +342,7 @@ end When declared inside a class, as above, the callback methods will receive the model object as a parameter. We can now use the callback class in the model: ```ruby -class PictureFile < ActiveRecord::Base +class PictureFile < ApplicationRecord after_destroy PictureFileCallbacks.new end ``` @@ -372,7 +362,7 @@ end If the callback method is declared this way, it won't be necessary to instantiate a `PictureFileCallbacks` object. ```ruby -class PictureFile < ActiveRecord::Base +class PictureFile < ApplicationRecord after_destroy PictureFileCallbacks end ``` @@ -396,8 +386,8 @@ end By using the `after_commit` callback we can account for this case. ```ruby -class PictureFile < ActiveRecord::Base - after_commit :delete_picture_file_from_disk, on: [:destroy] +class PictureFile < ApplicationRecord + after_commit :delete_picture_file_from_disk, on: :destroy def delete_picture_file_from_disk if File.exist?(filepath) @@ -407,7 +397,26 @@ class PictureFile < ActiveRecord::Base end ``` -NOTE: the `:on` option specifies when a callback will be fired. If you +NOTE: The `:on` option specifies when a callback will be fired. If you don't supply the `:on` option the callback will fire for every action. -WARNING. The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback. +Since using `after_commit` callback only on create, update or delete is +common, there are aliases for those operations: + +* `after_create_commit` +* `after_update_commit` +* `after_destroy_commit` + +```ruby +class PictureFile < ApplicationRecord + after_destroy_commit :delete_picture_file_from_disk + + def delete_picture_file_from_disk + if File.exist?(filepath) + File.delete(filepath) + end + end +end +``` + +WARNING. The `after_commit` and `after_rollback` callbacks are called for all models created, updated, or destroyed within a transaction block. However, if an exception is raised within one of these callbacks, the exception will bubble up and any remaining `after_commit` or `after_rollback` methods will _not_ be executed. As such, if your callback code could raise an exception, you'll need to rescue it and handle it within the callback in order to allow other callbacks to run. diff --git a/source/active_record_migrations.md b/source/active_record_migrations.md index 229c6ee..6e7e29e 100644 --- a/source/active_record_migrations.md +++ b/source/active_record_migrations.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Migrations ======================== @@ -10,7 +12,7 @@ After reading this guide, you will know: * The generators you can use to create them. * The methods Active Record provides to manipulate your database. -* The Rake tasks that manipulate migrations and your schema. +* The bin/rails tasks that manipulate migrations and your schema. * How migrations relate to `schema.rb`. -------------------------------------------------------------------------------- @@ -33,7 +35,7 @@ history to the latest version. Active Record will also update your Here's an example of a migration: ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name @@ -70,7 +72,7 @@ If you wish for a migration to do something that Active Record doesn't know how to reverse, you can use `reversible`: ```ruby -class ChangeProductsPrice < ActiveRecord::Migration +class ChangeProductsPrice < ActiveRecord::Migration[5.0] def change reversible do |dir| change_table :products do |t| @@ -85,7 +87,7 @@ end Alternatively, you can use `up` and `down` instead of `change`: ```ruby -class ChangeProductsPrice < ActiveRecord::Migration +class ChangeProductsPrice < ActiveRecord::Migration[5.0] def up change_table :products do |t| t.change :price, :string @@ -127,7 +129,7 @@ $ bin/rails generate migration AddPartNumberToProducts This will create an empty but appropriately named migration: ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change end end @@ -144,7 +146,7 @@ $ bin/rails generate migration AddPartNumberToProducts part_number:string will generate ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string end @@ -160,7 +162,7 @@ $ bin/rails generate migration AddPartNumberToProducts part_number:string:index will generate ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string add_index :products, :part_number @@ -178,7 +180,7 @@ $ bin/rails generate migration RemovePartNumberFromProducts part_number:string generates ```ruby -class RemovePartNumberFromProducts < ActiveRecord::Migration +class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0] def change remove_column :products, :part_number, :string end @@ -194,7 +196,7 @@ $ bin/rails generate migration AddDetailsToProducts part_number:string price:dec generates ```ruby -class AddDetailsToProducts < ActiveRecord::Migration +class AddDetailsToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string add_column :products, :price, :decimal @@ -213,7 +215,7 @@ $ bin/rails generate migration CreateProducts name:string part_number:string generates ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name @@ -227,7 +229,7 @@ As always, what has been generated for you is just a starting point. You can add or remove from it as you see fit by editing the `db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` file. -Also, the generator accepts column type as `references`(also available as +Also, the generator accepts column type as `references` (also available as `belongs_to`). For instance: ```bash @@ -237,14 +239,15 @@ $ bin/rails generate migration AddUserRefToProducts user:references generates ```ruby -class AddUserRefToProducts < ActiveRecord::Migration +class AddUserRefToProducts < ActiveRecord::Migration[5.0] def change - add_reference :products, :user, index: true + add_reference :products, :user, foreign_key: true end end ``` This migration will create a `user_id` column and appropriate index. +For more `add_reference` options, visit the [API documentation](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference). There is also a generator which will produce join tables if `JoinTable` is part of the name: @@ -255,7 +258,7 @@ $ bin/rails g migration CreateJoinTableCustomerProduct customer product will produce the following migration: ```ruby -class CreateJoinTableCustomerProduct < ActiveRecord::Migration +class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0] def change create_join_table :customers, :products do |t| # t.index [:customer_id, :product_id] @@ -279,7 +282,7 @@ $ bin/rails generate model Product name:string description:text will create a migration that looks like this ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name @@ -307,10 +310,10 @@ $ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplie will produce a migration that looks like this ```ruby -class AddDetailsToProducts < ActiveRecord::Migration +class AddDetailsToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :price, :decimal, precision: 5, scale: 2 - add_reference :products, :supplier, polymorphic: true, index: true + add_reference :products, :supplier, polymorphic: true end end ``` @@ -351,12 +354,19 @@ end ``` will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table -(when using MySQL, the default is `ENGINE=InnoDB`). +(when using MySQL or MariaDB, the default is `ENGINE=InnoDB`). + +Also you can pass the `:comment` option with any description for the table +that will be stored in database itself and can be viewed with database administration +tools, such as MySQL Workbench or PgAdmin III. It's highly recommended to specify +comments in migrations for applications with large databases as it helps people +to understand data model and generate documentation. +Currently only the MySQL and PostgreSQL adapters support comments. ### Creating a Join Table -Migration method `create_join_table` creates a HABTM join table. A typical use -would be: +The migration method `create_join_table` creates an HABTM (has and belongs to +many) join table. A typical use would be: ```ruby create_join_table :products, :categories @@ -365,23 +375,21 @@ create_join_table :products, :categories which creates a `categories_products` table with two columns called `category_id` and `product_id`. These columns have the option `:null` set to `false` by default. This can be overridden by specifying the `:column_options` -option. +option: ```ruby -create_join_table :products, :categories, column_options: {null: true} +create_join_table :products, :categories, column_options: { null: true } ``` -will create the `product_id` and `category_id` with the `:null` option as -`true`. - -You can pass the option `:table_name` when you want to customize the table -name. For example: +By default, the name of the join table comes from the union of the first two +arguments provided to create_join_table, in alphabetical order. +To customize the name of the table, provide a `:table_name` option: ```ruby create_join_table :products, :categories, table_name: :categorization ``` -will create a `categorization` table. +creates a `categorization` table. `create_join_table` also accepts a block, which you can use to add indices (which are not created by default) or additional columns: @@ -421,21 +429,23 @@ change_column :products, :part_number, :text ``` This changes the column `part_number` on products table to be a `:text` field. +Note that `change_column` command is irreversible. Besides `change_column`, the `change_column_null` and `change_column_default` -methods are used specifically to change the null and default values of a -column. +methods are used specifically to change a not null constraint and default +values of a column. ```ruby change_column_null :products, :name, false -change_column_default :products, :approved, false +change_column_default :products, :approved, from: true, to: false ``` This sets `:name` field on products to a `NOT NULL` column and the default -value of the `:approved` field to false. +value of the `:approved` field from true to false. -TIP: Unlike `change_column` (and `change_column_default`), `change_column_null` -is reversible. +Note: You could also write the above `change_column_default` migration as +`change_column_default :products, :approved, false`, but unlike the previous +example, this would make your migration irreversible. ### Column Modifiers @@ -452,10 +462,13 @@ number of digits after the decimal point. are using a dynamic value (such as a date), the default will only be calculated the first time (i.e. on the date the migration is applied). * `index` Adds an index for the column. +* `comment` Adds a comment for the column. Some adapters may support additional options; see the adapter specific API docs for further information. +NOTE: `null` and `default` cannot be specified via command line. + ### Foreign Keys While it's not required you might want to add foreign key constraints to @@ -466,16 +479,18 @@ add_foreign_key :articles, :authors ``` This adds a new foreign key to the `author_id` column of the `articles` -table. The key references the `id` column of the `articles` table. If the +table. The key references the `id` column of the `authors` table. If the column names can not be derived from the table names, you can use the `:column` and `:primary_key` options. Rails will generate a name for every foreign key starting with -`fk_rails_` followed by 10 random characters. +`fk_rails_` followed by 10 characters which are deterministically +generated from the `from_table` and `column`. There is a `:name` option to specify a different name if needed. NOTE: Active Record only supports single column foreign keys. `execute` and -`structure.sql` are required to use composite foreign keys. +`structure.sql` are required to use composite foreign keys. See +[Schema Dumping and You](#schema-dumping-and-you). Removing a foreign key is easy as well: @@ -496,7 +511,7 @@ If the helpers provided by Active Record aren't enough you can use the `execute` method to execute arbitrary SQL: ```ruby -Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1') +Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1") ``` For more details and examples of individual methods, check the API documentation. @@ -516,24 +531,39 @@ majority of cases, where Active Record knows how to reverse the migration automatically. Currently, the `change` method supports only these migration definitions: -* `add_column` -* `add_index` -* `add_reference` -* `add_timestamps` -* `add_foreign_key` -* `create_table` -* `create_join_table` -* `drop_table` (must supply a block) -* `drop_join_table` (must supply a block) -* `remove_timestamps` -* `rename_column` -* `rename_index` -* `remove_reference` -* `rename_table` +* add_column +* add_foreign_key +* add_index +* add_reference +* add_timestamps +* change_column_default (must supply a :from and :to option) +* change_column_null +* create_join_table +* create_table +* disable_extension +* drop_join_table +* drop_table (must supply a block) +* enable_extension +* remove_column (must supply a type) +* remove_foreign_key (must supply a second table) +* remove_index +* remove_reference +* remove_timestamps +* rename_column +* rename_index +* rename_table `change_table` is also reversible, as long as the block does not call `change`, `change_default` or `remove`. +`remove_column` is reversible if you supply the column type as the third +argument. Provide the original column options too, otherwise Rails can't +recreate the column exactly when rolling back: + +```ruby +remove_column :posts, :slug, :string, null: false, default: '', index: true +``` + If you're going to need to use any other methods, you should use `reversible` or write the `up` and `down` methods instead of using the `change` method. @@ -541,10 +571,10 @@ or write the `up` and `down` methods instead of using the `change` method. Complex migrations may require processing that Active Record doesn't know how to reverse. You can use `reversible` to specify what to do when running a -migration what else to do when reverting it. For example: +migration and what else to do when reverting it. For example: ```ruby -class ExampleMigration < ActiveRecord::Migration +class ExampleMigration < ActiveRecord::Migration[5.0] def change create_table :distributors do |t| t.string :zipcode @@ -593,11 +623,11 @@ schema, and the `down` method of your migration should revert the transformations done by the `up` method. In other words, the database schema should be unchanged if you do an `up` followed by a `down`. For example, if you create a table in the `up` method, you should drop it in the `down` method. It -is wise to reverse the transformations in precisely the reverse order they were +is wise to perform the transformations in precisely the reverse order they were made in the `up` method. The example in the `reversible` section is equivalent to: ```ruby -class ExampleMigration < ActiveRecord::Migration +class ExampleMigration < ActiveRecord::Migration[5.0] def up create_table :distributors do |t| t.string :zipcode @@ -638,9 +668,9 @@ can't be done. You can use Active Record's ability to rollback migrations using the `revert` method: ```ruby -require_relative '2012121212_example_migration' +require_relative '20121212123456_example_migration' -class FixupExampleMigration < ActiveRecord::Migration +class FixupExampleMigration < ActiveRecord::Migration[5.0] def change revert ExampleMigration @@ -658,7 +688,7 @@ is later decided it would be best to use Active Record validations, in place of the `CHECK` constraint, to verify the zipcode. ```ruby -class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration +class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0] def change revert do # copy-pasted code from ExampleMigration @@ -691,13 +721,17 @@ of `create_table` and `reversible`, replacing `create_table` by `drop_table`, and finally replacing `up` by `down` and vice-versa. This is all taken care of by `revert`. +NOTE: If you want to add check constraints like in the examples above, +you will have to use `structure.sql` as dump method. See +[Schema Dumping and You](#schema-dumping-and-you). + Running Migrations ------------------ -Rails provides a set of Rake tasks to run certain sets of migrations. +Rails provides a set of bin/rails tasks to run certain sets of migrations. -The very first migration related Rake task you will use will probably be -`rake db:migrate`. In its most basic form it just runs the `change` or `up` +The very first migration related bin/rails task you will use will probably be +`rails db:migrate`. In its most basic form it just runs the `change` or `up` method for all the migrations that have not yet been run. If there are no such migrations, it exits. It will run these migrations in order based on the date of the migration. @@ -711,7 +745,7 @@ is the numerical prefix on the migration's filename. For example, to migrate to version 20080906120000 run: ```bash -$ bin/rake db:migrate VERSION=20080906120000 +$ bin/rails db:migrate VERSION=20080906120000 ``` If version 20080906120000 is greater than the current version (i.e., it is @@ -728,7 +762,7 @@ mistake in it and wish to correct it. Rather than tracking down the version number associated with the previous migration you can run: ```bash -$ bin/rake db:rollback +$ bin/rails db:rollback ``` This will rollback the latest migration, either by reverting the `change` @@ -736,7 +770,7 @@ method or by running the `down` method. If you need to undo several migrations you can provide a `STEP` parameter: ```bash -$ bin/rake db:rollback STEP=3 +$ bin/rails db:rollback STEP=3 ``` will revert the last 3 migrations. @@ -746,26 +780,26 @@ back up again. As with the `db:rollback` task, you can use the `STEP` parameter if you need to go more than one version back, for example: ```bash -$ bin/rake db:migrate:redo STEP=3 +$ bin/rails db:migrate:redo STEP=3 ``` -Neither of these Rake tasks do anything you could not do with `db:migrate`. They +Neither of these bin/rails tasks do anything you could not do with `db:migrate`. They are simply more convenient, since you do not need to explicitly specify the version to migrate to. ### Setup the Database -The `rake db:setup` task will create the database, load the schema and initialize +The `rails db:setup` task will create the database, load the schema and initialize it with the seed data. ### Resetting the Database -The `rake db:reset` task will drop the database and set it up again. This is -functionally equivalent to `rake db:drop db:setup`. +The `rails db:reset` task will drop the database and set it up again. This is +functionally equivalent to `rails db:drop db:setup`. NOTE: This is not the same as running all the migrations. It will only use the -contents of the current `schema.rb` file. If a migration can't be rolled back, -`rake db:reset` may not help you. To find out more about dumping the schema see +contents of the current `db/schema.rb` or `db/structure.sql` file. If a migration can't be rolled back, +`rails db:reset` may not help you. To find out more about dumping the schema see [Schema Dumping and You](#schema-dumping-and-you) section. ### Running Specific Migrations @@ -776,7 +810,7 @@ the corresponding migration will have its `change`, `up` or `down` method invoked, for example: ```bash -$ bin/rake db:migrate:up VERSION=20080906120000 +$ bin/rails db:migrate:up VERSION=20080906120000 ``` will run the 20080906120000 migration by running the `change` method (or the @@ -786,13 +820,13 @@ Active Record believes that it has already been run. ### Running Migrations in Different Environments -By default running `rake db:migrate` will run in the `development` environment. +By default running `bin/rails db:migrate` will run in the `development` environment. To run migrations against another environment you can specify it using the `RAILS_ENV` environment variable while running the command. For example to run migrations against the `test` environment you could run: ```bash -$ bin/rake db:migrate RAILS_ENV=test +$ bin/rails db:migrate RAILS_ENV=test ``` ### Changing the Output of Running Migrations @@ -818,7 +852,7 @@ Several methods are provided in migrations that allow you to control all this: For example, this migration: ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change suppress_messages do create_table :products do |t| @@ -853,18 +887,18 @@ generates the following output == CreateProducts: migrated (10.0054s) ======================================= ``` -If you want Active Record to not output anything, then running `rake db:migrate +If you want Active Record to not output anything, then running `rails db:migrate VERBOSE=false` will suppress all output. Changing Existing Migrations ---------------------------- Occasionally you will make a mistake when writing a migration. If you have -already run the migration then you cannot just edit the migration and run the +already run the migration, then you cannot just edit the migration and run the migration again: Rails thinks it has already run the migration and so will do -nothing when you run `rake db:migrate`. You must rollback the migration (for -example with `rake db:rollback`), edit your migration and then run -`rake db:migrate` to run the corrected version. +nothing when you run `rails db:migrate`. You must rollback the migration (for +example with `bin/rails db:rollback`), edit your migration and then run +`rails db:migrate` to run the corrected version. In general, editing existing migrations is not a good idea. You will be creating extra work for yourself and your co-workers and cause major headaches @@ -910,7 +944,7 @@ There are two ways to dump the schema. This is set in `config/application.rb` by the `config.active_record.schema_format` setting, which may be either `:sql` or `:ruby`. -If `:ruby` is selected then the schema is stored in `db/schema.rb`. If you look +If `:ruby` is selected, then the schema is stored in `db/schema.rb`. If you look at this file you'll find that it looks an awful lot like one very big migration: @@ -924,10 +958,10 @@ ActiveRecord::Schema.define(version: 20080906171750) do create_table "products", force: true do |t| t.string "name" - t.text "description" + t.text "description" t.datetime "created_at" t.datetime "updated_at" - t.string "part_number" + t.string "part_number" end end ``` @@ -939,15 +973,15 @@ that Active Record supports. This could be very useful if you were to distribute an application that is able to run against multiple databases. There is however a trade-off: `db/schema.rb` cannot express database specific -items such as triggers, or stored procedures. While in a migration you can -execute custom SQL statements, the schema dumper cannot reconstitute those -statements from the database. If you are using features like this, then you -should set the schema format to `:sql`. +items such as triggers, stored procedures or check constraints. While in a +migration you can execute custom SQL statements, the schema dumper cannot +reconstitute those statements from the database. If you are using features like +this, then you should set the schema format to `:sql`. Instead of using Active Record's schema dumper, the database's structure will be dumped using a tool specific to the database (via the `db:structure:dump` -Rake task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump` -utility is used. For MySQL, this file will contain the output of +rails task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump` +utility is used. For MySQL and MariaDB, this file will contain the output of `SHOW CREATE TABLE` for the various tables. Loading these schemas is simply a question of executing the SQL statements they @@ -986,10 +1020,13 @@ such features, the `execute` method can be used to execute arbitrary SQL. Migrations and Seed Data ------------------------ -Some people use migrations to add data to the database: +The main purpose of Rails' migration feature is to issue commands that modify the +schema using a consistent process. Migrations can also be used +to add or modify data. This is useful in an existing database that can't be destroyed +and recreated, such as a production database. ```ruby -class AddInitialProducts < ActiveRecord::Migration +class AddInitialProducts < ActiveRecord::Migration[5.0] def up 5.times do |i| Product.create(name: "Product ##{i}", description: "A product.") @@ -1002,9 +1039,11 @@ class AddInitialProducts < ActiveRecord::Migration end ``` -However, Rails has a 'seeds' feature that should be used for seeding a database -with initial data. It's a really simple feature: just fill up `db/seeds.rb` -with some Ruby code, and run `rake db:seed`: +To add initial data after a database is created, Rails has a built-in +'seeds' feature that makes the process quick and easy. This is especially +useful when reloading the database frequently in development and test environments. +It's easy to get started with this feature: just fill up `db/seeds.rb` with some +Ruby code, and run `rails db:seed`: ```ruby 5.times do |i| diff --git a/source/active_record_postgresql.md b/source/active_record_postgresql.md index a5649e3..6d07291 100644 --- a/source/active_record_postgresql.md +++ b/source/active_record_postgresql.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record and PostgreSQL ============================ @@ -12,7 +14,7 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- -In order to use the PostgreSQL adapter you need to have at least version 8.2 +In order to use the PostgreSQL adapter you need to have at least version 9.1 installed. Older versions are not supported. To get started with PostgreSQL have a look at the @@ -27,8 +29,8 @@ that are supported by the PostgreSQL adapter. ### Bytea -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-binary.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-binarystring.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-binary.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-binarystring.html) ```ruby # db/migrate/20140207133952_create_documents.rb @@ -37,7 +39,7 @@ create_table :documents do |t| end # app/models/document.rb -class Document < ActiveRecord::Base +class Document < ApplicationRecord end # Usage @@ -47,8 +49,8 @@ Document.create payload: data ### Array -* [type definition](http://www.postgresql.org/docs/9.3/static/arrays.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-array.html) +* [type definition](http://www.postgresql.org/docs/current/static/arrays.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-array.html) ```ruby # db/migrate/20140207133952_create_books.rb @@ -61,7 +63,7 @@ add_index :books, :tags, using: 'gin' add_index :books, :ratings, using: 'gin' # app/models/book.rb -class Book < ActiveRecord::Base +class Book < ApplicationRecord end # Usage @@ -81,18 +83,22 @@ Book.where("array_length(ratings, 1) >= 3") ### Hstore -* [type definition](http://www.postgresql.org/docs/9.3/static/hstore.html) +* [type definition](http://www.postgresql.org/docs/current/static/hstore.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/hstore.html#AEN167712) + +NOTE: You need to enable the `hstore` extension to use hstore. ```ruby # db/migrate/20131009135255_create_profiles.rb ActiveRecord::Schema.define do + enable_extension 'hstore' unless extension_enabled?('hstore') create_table :profiles do |t| t.hstore 'settings' end end # app/models/profile.rb -class Profile < ActiveRecord::Base +class Profile < ApplicationRecord end # Usage @@ -104,16 +110,14 @@ profile.settings # => {"color"=>"blue", "resolution"=>"800x600"} profile.settings = {"color" => "yellow", "resolution" => "1280x1024"} profile.save! -## you need to call _will_change! if you are editing the store in place -profile.settings["color"] = "green" -profile.settings_will_change! -profile.save! +Profile.where("settings->'color' = ?", "yellow") +# => #"yellow", "resolution"=>"1280x1024"}>]> ``` ### JSON -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-json.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-json.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-json.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-json.html) ```ruby # db/migrate/20131220144913_create_events.rb @@ -122,7 +126,7 @@ create_table :events do |t| end # app/models/event.rb -class Event < ActiveRecord::Base +class Event < ApplicationRecord end # Usage @@ -132,15 +136,16 @@ event = Event.first event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]} ## Query based on JSON document -Event.where("payload->'kind' = ?", "user_renamed") +# The -> operator returns the original JSON type (which might be an object), whereas ->> returns text +Event.where("payload->>'kind' = ?", "user_renamed") ``` ### Range Types -* [type definition](http://www.postgresql.org/docs/9.3/static/rangetypes.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-range.html) +* [type definition](http://www.postgresql.org/docs/current/static/rangetypes.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-range.html) -This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.1.1/Range.html) objects. +This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.2.2/Range.html) objects. ```ruby # db/migrate/20130923065404_create_events.rb @@ -149,7 +154,7 @@ create_table :events do |t| end # app/models/event.rb -class Event < ActiveRecord::Base +class Event < ApplicationRecord end # Usage @@ -172,7 +177,7 @@ event.ends_at # => Thu, 13 Feb 2014 ### Composite Types -* [type definition](http://www.postgresql.org/docs/9.3/static/rowtypes.html) +* [type definition](http://www.postgresql.org/docs/current/static/rowtypes.html) Currently there is no special support for composite types. They are mapped to normal text columns: @@ -199,7 +204,7 @@ create_table :contacts do |t| end # app/models/contact.rb -class Contact < ActiveRecord::Base +class Contact < ApplicationRecord end # Usage @@ -212,22 +217,33 @@ contact.save! ### Enumerated Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-enum.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-enum.html) Currently there is no special support for enumerated types. They are mapped as normal text columns: ```ruby -# db/migrate/20131220144913_create_events.rb -execute <<-SQL - CREATE TYPE article_status AS ENUM ('draft', 'published'); -SQL -create_table :articles do |t| - t.column :status, :article_status +# db/migrate/20131220144913_create_articles.rb +def up + execute <<-SQL + CREATE TYPE article_status AS ENUM ('draft', 'published'); + SQL + create_table :articles do |t| + t.column :status, :article_status + end +end + +# NOTE: It's important to drop table before dropping enum. +def down + drop_table :articles + + execute <<-SQL + DROP TYPE article_status; + SQL end # app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord end # Usage @@ -239,20 +255,50 @@ article.status = "published" article.save! ``` +To add a new value before/after existing one you should use [ALTER TYPE](http://www.postgresql.org/docs/current/static/sql-altertype.html): + +```ruby +# db/migrate/20150720144913_add_new_state_to_articles.rb +# NOTE: ALTER TYPE ... ADD VALUE cannot be executed inside of a transaction block so here we are using disable_ddl_transaction! +disable_ddl_transaction! + +def up + execute <<-SQL + ALTER TYPE article_status ADD VALUE IF NOT EXISTS 'archived' AFTER 'published'; + SQL +end +``` + +NOTE: ENUM values can't be dropped currently. You can read why [here](http://www.postgresql.org/message-id/29F36C7C98AB09499B1A209D48EAA615B7653DBC8A@mail2a.alliedtesting.com). + +Hint: to show all the values of the all enums you have, you should call this query in `bin/rails db` or `psql` console: + +```sql +SELECT n.nspname AS enum_schema, + t.typname AS enum_name, + e.enumlabel AS enum_value + FROM pg_type t + JOIN pg_enum e ON t.oid = e.enumtypid + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace +``` + ### UUID -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html) -* [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-uuid.html) +* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN159361) +* [uuid-ossp generator functions](http://www.postgresql.org/docs/current/static/uuid-ossp.html) +NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to use uuid. ```ruby # db/migrate/20131220144913_create_revisions.rb create_table :revisions do |t| - t.column :identifier, :uuid + t.uuid :identifier end # app/models/revision.rb -class Revision < ActiveRecord::Base +class Revision < ApplicationRecord end # Usage @@ -262,10 +308,35 @@ revision = Revision.first revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" ``` +You can use `uuid` type to define references in migrations: + +```ruby +# db/migrate/20150418012400_create_blog.rb +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :posts, id: :uuid, default: 'gen_random_uuid()' + +create_table :comments, id: :uuid, default: 'gen_random_uuid()' do |t| + # t.belongs_to :post, type: :uuid + t.references :post, type: :uuid +end + +# app/models/post.rb +class Post < ApplicationRecord + has_many :comments +end + +# app/models/comment.rb +class Comment < ApplicationRecord + belongs_to :post +end +``` + +See [this section](#uuid-primary-keys) for more details on using UUIDs as primary key. + ### Bit String Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-bit.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-bitstring.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-bit.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-bitstring.html) ```ruby # db/migrate/20131220144913_create_users.rb @@ -274,13 +345,13 @@ create_table :users, force: true do |t| end # app/models/device.rb -class User < ActiveRecord::Base +class User < ApplicationRecord end # Usage User.create settings: "01010011" user = User.first -user.settings # => "(Paris,Champs-Élysées)" +user.settings # => "01010011" user.settings = "0xAF" user.settings # => 10101111 user.save! @@ -288,10 +359,10 @@ user.save! ### Network Address Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-net-types.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-net-types.html) The types `inet` and `cidr` are mapped to Ruby -[`IPAddr`](http://www.ruby-doc.org/stdlib-2.1.1/libdoc/ipaddr/rdoc/IPAddr.html) +[`IPAddr`](http://www.ruby-doc.org/stdlib-2.2.2/libdoc/ipaddr/rdoc/IPAddr.html) objects. The `macaddr` type is mapped to normal text. ```ruby @@ -303,7 +374,7 @@ create_table(:devices, force: true) do |t| end # app/models/device.rb -class Device < ActiveRecord::Base +class Device < ApplicationRecord end # Usage @@ -323,7 +394,7 @@ macbook.address ### Geometric Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-geometric.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-geometric.html) All geometric types, with the exception of `points` are mapped to normal text. A point is casted to an array containing `x` and `y` coordinates. @@ -332,17 +403,18 @@ A point is casted to an array containing `x` and `y` coordinates. UUID Primary Keys ----------------- -NOTE: you need to enable the `uuid-ossp` extension to generate UUIDs. +NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to generate random UUIDs. ```ruby # db/migrate/20131220144913_create_devices.rb -enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp') -create_table :devices, id: :uuid, default: 'uuid_generate_v4()' do |t| +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :devices, id: :uuid, default: 'gen_random_uuid()' do |t| t.string :kind end # app/models/device.rb -class Device < ActiveRecord::Base +class Device < ApplicationRecord end # Usage @@ -350,6 +422,9 @@ device = Device.create device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e" ``` +NOTE: `gen_random_uuid()` (from `pgcrypto`) is assumed if no `:default` option was +passed to `create_table`. + Full Text Search ---------------- @@ -360,10 +435,10 @@ create_table :documents do |t| t.string 'body' end -execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));" +add_index :documents, "to_tsvector('english', title || ' ' || body)", using: :gin, name: 'documents_idx' # app/models/document.rb -class Document < ActiveRecord::Base +class Document < ApplicationRecord end # Usage @@ -377,7 +452,7 @@ Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)", Database Views -------------- -* [view creation](http://www.postgresql.org/docs/9.3/static/sql-createview.html) +* [view creation](http://www.postgresql.org/docs/current/static/sql-createview.html) Imagine you need to work with a legacy database containing the following table: @@ -413,7 +488,7 @@ CREATE VIEW articles AS SQL # app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord self.primary_key = "id" def archive! update_attribute :archived, true @@ -428,9 +503,9 @@ second = Article.create! title: "Brace yourself", status: "draft", published_at: 1.month.ago -Article.count # => 1 -first.archive! Article.count # => 2 +first.archive! +Article.count # => 1 ``` NOTE: This application only cares about non-archived `Articles`. A view also diff --git a/source/active_record_querying.md b/source/active_record_querying.md index 35467fe..2902c5d 100644 --- a/source/active_record_querying.md +++ b/source/active_record_querying.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Query Interface ============================= @@ -8,7 +10,8 @@ After reading this guide, you will know: * How to find records using a variety of methods and conditions. * How to specify the order, retrieved attributes, grouping, and other properties of the found records. * How to use eager loading to reduce the number of database queries needed for data retrieval. -* How to use dynamic finders methods. +* How to use dynamic finder methods. +* How to use method chaining to use multiple Active Record methods together. * How to check for the existence of particular records. * How to perform various calculations on Active Record models. * How to run EXPLAIN on relations. @@ -22,7 +25,7 @@ Code examples throughout this guide will refer to one or more of the following m TIP: All of the following models use `id` as the primary key, unless specified otherwise. ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord has_one :address has_many :orders has_and_belongs_to_many :roles @@ -30,24 +33,24 @@ end ``` ```ruby -class Address < ActiveRecord::Base +class Address < ApplicationRecord belongs_to :client end ``` ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord belongs_to :client, counter_cache: true end ``` ```ruby -class Role < ActiveRecord::Base +class Role < ApplicationRecord has_and_belongs_to_many :clients end ``` -Active Record will perform queries on the database for you and is compatible with most database systems (MySQL, PostgreSQL and SQLite to name a few). Regardless of which database system you're using, the Active Record method format will always be the same. +Active Record will perform queries on the database for you and is compatible with most database systems, including MySQL, MariaDB, PostgreSQL, and SQLite. Regardless of which database system you're using, the Active Record method format will always be the same. Retrieving Objects from the Database ------------------------------------ @@ -56,7 +59,7 @@ To retrieve objects from the database, Active Record provides several finder met The methods are: -* `bind` +* `find` * `create_with` * `distinct` * `eager_load` @@ -66,6 +69,7 @@ The methods are: * `having` * `includes` * `joins` +* `left_outer_joins` * `limit` * `lock` * `none` @@ -77,17 +81,16 @@ The methods are: * `reorder` * `reverse_order` * `select` -* `uniq` * `where` -All of the above methods return an instance of `ActiveRecord::Relation`. +Finder methods that return a collection, such as `where` and `group`, return an instance of `ActiveRecord::Relation`. Methods that find a single entity, such as `find` and `first`, return a single instance of the model. The primary operation of `Model.find(options)` can be summarized as: * Convert the supplied options to an equivalent SQL query. * Fire the SQL query and retrieve the corresponding results from the database. * Instantiate the equivalent Ruby object of the appropriate model for every resulting row. -* Run `after_find` callbacks, if any. +* Run `after_find` and then `after_initialize` callbacks, if any. ### Retrieving a Single Object @@ -149,9 +152,9 @@ You can pass in a numerical argument to the `take` method to return up to that n ```ruby client = Client.take(2) # => [ - #, - # -] +# #, +# # +# ] ``` The SQL equivalent of the above is: @@ -166,7 +169,7 @@ TIP: The retrieved record may vary depending on the database engine. #### `first` -The `first` method finds the first record ordered by the primary key. For example: +The `first` method finds the first record ordered by primary key (default). For example: ```ruby client = Client.first @@ -181,15 +184,17 @@ SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 The `first` method returns `nil` if no matching record is found and no exception will be raised. +If your [default scope](active_record_querying.html#applying-a-default-scope) contains an order method, `first` will return the first record according to this ordering. + You can pass in a numerical argument to the `first` method to return up to that number of results. For example ```ruby client = Client.first(3) # => [ - #, - #, - # -] +# #, +# #, +# # +# ] ``` The SQL equivalent of the above is: @@ -198,11 +203,24 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3 ``` +On a collection that is ordered using `order`, `first` will return the first record ordered by the specified attribute for `order`. + +```ruby +client = Client.order(:first_name).first +# => # +``` + +The SQL equivalent of the above is: + +```sql +SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1 +``` + The `first!` method behaves exactly like `first`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. #### `last` -The `last` method finds the last record ordered by the primary key. For example: +The `last` method finds the last record ordered by primary key (default). For example: ```ruby client = Client.last @@ -217,15 +235,17 @@ SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 The `last` method returns `nil` if no matching record is found and no exception will be raised. +If your [default scope](active_record_querying.html#applying-a-default-scope) contains an order method, `last` will return the last record according to this ordering. + You can pass in a numerical argument to the `last` method to return up to that number of results. For example ```ruby client = Client.last(3) # => [ - #, - #, - # -] +# #, +# #, +# # +# ] ``` The SQL equivalent of the above is: @@ -234,6 +254,19 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3 ``` +On a collection that is ordered using `order`, `last` will return the last record ordered by the specified attribute for `order`. + +```ruby +client = Client.order(:first_name).last +# => # +``` + +The SQL equivalent of the above is: + +```sql +SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1 +``` + The `last!` method behaves exactly like `last`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. #### `find_by` @@ -254,6 +287,12 @@ It is equivalent to writing: Client.where(first_name: 'Lifo').take ``` +The SQL equivalent of the above is: + +```sql +SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1 +``` + The `find_by!` method behaves exactly like `find_by`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. For example: ```ruby @@ -274,9 +313,9 @@ We often need to iterate over a large set of records, as when we send a newslett This may appear straightforward: ```ruby -# This is very inefficient when the users table has thousands of rows. +# This may consume too much memory if the table is big. User.all.each do |user| - NewsMailer.weekly(user).deliver + NewsMailer.weekly(user).deliver_now end ``` @@ -288,27 +327,34 @@ TIP: The `find_each` and `find_in_batches` methods are intended for use in the b #### `find_each` -The `find_each` method retrieves a batch of records and then yields _each_ record to the block individually as a model. In the following example, `find_each` will retrieve 1000 records (the current default for both `find_each` and `find_in_batches`) and then yield each record individually to the block as a model. This process is repeated until all of the records have been processed: +The `find_each` method retrieves records in batches and then yields _each_ one to the block. In the following example, `find_each` retrieves users in batches of 1000 and yields them to the block one by one: ```ruby User.find_each do |user| - NewsMailer.weekly(user).deliver + NewsMailer.weekly(user).deliver_now end ``` -To add conditions to a `find_each` operation you can chain other Active Record methods such as `where`: +This process is repeated, fetching more batches as needed, until all of the records have been processed. + +`find_each` works on model classes, as seen above, and also on relations: ```ruby User.where(weekly_subscriber: true).find_each do |user| - NewsMailer.weekly(user).deliver + NewsMailer.weekly(user).deliver_now end ``` -##### Options for `find_each` +as long as they have no ordering, since the method needs to force an order +internally to iterate. -The `find_each` method accepts most of the options allowed by the regular `find` method, except for `:order` and `:limit`, which are reserved for internal use by `find_each`. +If an order is present in the receiver the behaviour depends on the flag +`config.active_record.error_on_ignored_order`. If true, `ArgumentError` is +raised, otherwise the order is ignored and a warning issued, which is the +default. This can be overridden with the option `:error_on_ignore`, explained +below. -Two additional options, `:batch_size` and `:start`, are available as well. +##### Options for `find_each` **`:batch_size`** @@ -316,7 +362,7 @@ The `:batch_size` option allows you to specify the number of records to be retri ```ruby User.find_each(batch_size: 5000) do |user| - NewsMailer.weekly(user).deliver + NewsMailer.weekly(user).deliver_now end ``` @@ -324,32 +370,61 @@ end By default, records are fetched in ascending order of the primary key, which must be an integer. The `:start` option allows you to configure the first ID of the sequence whenever the lowest ID is not the one you need. This would be useful, for example, if you wanted to resume an interrupted batch process, provided you saved the last processed ID as a checkpoint. -For example, to send newsletters only to users with the primary key starting from 2000, and to retrieve them in batches of 5000: +For example, to send newsletters only to users with the primary key starting from 2000: + +```ruby +User.find_each(start: 2000) do |user| + NewsMailer.weekly(user).deliver_now +end +``` + +**`:finish`** + +Similar to the `:start` option, `:finish` allows you to configure the last ID of the sequence whenever the highest ID is not the one you need. +This would be useful, for example, if you wanted to run a batch process using a subset of records based on `:start` and `:finish`. + +For example, to send newsletters only to users with the primary key starting from 2000 up to 10000: ```ruby -User.find_each(start: 2000, batch_size: 5000) do |user| - NewsMailer.weekly(user).deliver +User.find_each(start: 2000, finish: 10000) do |user| + NewsMailer.weekly(user).deliver_now end ``` -Another example would be if you wanted multiple workers handling the same processing queue. You could have each worker handle 10000 records by setting the appropriate `:start` option on each worker. +Another example would be if you wanted multiple workers handling the same +processing queue. You could have each worker handle 10000 records by setting the +appropriate `:start` and `:finish` options on each worker. + +**`:error_on_ignore`** + +Overrides the application config to specify if an error should be raised when an +order is present in the relation. #### `find_in_batches` The `find_in_batches` method is similar to `find_each`, since both retrieve batches of records. The difference is that `find_in_batches` yields _batches_ to the block as an array of models, instead of individually. The following example will yield to the supplied block an array of up to 1000 invoices at a time, with the final block containing any remaining invoices: ```ruby -# Give add_invoices an array of 1000 invoices at a time -Invoice.find_in_batches(include: :invoice_lines) do |invoices| +# Give add_invoices an array of 1000 invoices at a time. +Invoice.find_in_batches do |invoices| export.add_invoices(invoices) end ``` -NOTE: The `:include` option allows you to name associations that should be loaded alongside with the models. +`find_in_batches` works on model classes, as seen above, and also on relations: + +```ruby +Invoice.pending.find_in_batches do |invoice| + pending_invoices_export.add_invoices(invoices) +end +``` + +as long as they have no ordering, since the method needs to force an order +internally to iterate. ##### Options for `find_in_batches` -The `find_in_batches` method accepts the same `:batch_size` and `:start` options as `find_each`, as well as most of the options allowed by the regular `find` method, except for `:order` and `:limit`, which are reserved for internal use by `find_in_batches`. +The `find_in_batches` method accepts the same options as `find_each`. Conditions ---------- @@ -370,7 +445,7 @@ Now what if that number could vary, say as an argument from somewhere? The find Client.where("orders_count = ?", params[:orders]) ``` -Active Record will go through the first element in the conditions value and any additional elements will replace the question marks `(?)` in the first element. +Active Record will take the first argument as the conditions string and any additional arguments will replace the question marks `(?)` in it. If you want to specify multiple conditions: @@ -398,7 +473,7 @@ TIP: For more information on the dangers of SQL injection, see the [Ruby on Rail #### Placeholder Conditions -Similar to the `(?)` replacement style of params, you can also specify keys/values hash in your array conditions: +Similar to the `(?)` replacement style of params, you can also specify keys in your conditions string along with a corresponding keys/values hash: ```ruby Client.where("created_at >= :start_date AND created_at <= :end_date", @@ -409,7 +484,7 @@ This makes for clearer readability if you have a large number of variable condit ### Hash Conditions -Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want conditionalised and the values of how you want to conditionalise them: +Active Record also allows you to pass in hash conditions which can increase the readability of your conditions syntax. With hash conditions, you pass in a hash with keys of the fields you want qualified and the values of how you want to qualify them: NOTE: Only equality, range and subset checking are possible with Hash conditions. @@ -419,6 +494,12 @@ NOTE: Only equality, range and subset checking are possible with Hash conditions Client.where(locked: true) ``` +This will generate SQL like this: + +```sql +SELECT * FROM clients WHERE (clients.locked = 1) +``` + The field name can also be a string: ```ruby @@ -464,13 +545,17 @@ SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) ### NOT Conditions -`NOT` SQL queries can be built by `where.not`. +`NOT` SQL queries can be built by `where.not`: ```ruby -Article.where.not(author: author) +Client.where.not(locked: true) ``` -In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions. +In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions. This will generate SQL like this: + +```sql +SELECT * FROM clients WHERE (clients.locked != 1) +``` Ordering -------- @@ -509,12 +594,13 @@ Client.order("orders_count ASC, created_at DESC") Client.order("orders_count ASC", "created_at DESC") ``` -If you want to call `order` multiple times e.g. in different context, new order will append previous one +If you want to call `order` multiple times, subsequent orders will be appended to the first: ```ruby Client.order("orders_count ASC").order("created_at DESC") # SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC ``` +WARNING: If you are using **MySQL 5.7.5** and above, then on selecting fields from a result set using methods like `select`, `pluck` and `ids`; the `order` method will raise an `ActiveRecord::StatementInvalid` exception unless the field(s) used in `order` clause are included in the select list. See the next section for selecting fields from the result set. Selecting Specific Fields ------------------------- @@ -597,9 +683,9 @@ SELECT * FROM clients LIMIT 5 OFFSET 30 Group ----- -To apply a `GROUP BY` clause to the SQL fired by the finder, you can specify the `group` method on the find. +To apply a `GROUP BY` clause to the SQL fired by the finder, you can use the `group` method. -For example, if you want to find a collection of the dates orders were created on: +For example, if you want to find a collection of the dates on which orders were created: ```ruby Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)") @@ -617,7 +703,7 @@ GROUP BY date(created_at) ### Total of grouped items -To get the total of grouped items on a single query call `count` after the `group`. +To get the total of grouped items on a single query, call `count` after the `group`. ```ruby Order.group(:status).count @@ -635,7 +721,7 @@ GROUP BY status Having ------ -SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `:having` option to the find. +SQL uses the `HAVING` clause to specify conditions on the `GROUP BY` fields. You can add the `HAVING` clause to the SQL fired by the `Model.find` by adding the `having` method to the find. For example: @@ -653,7 +739,7 @@ GROUP BY date(created_at) HAVING sum(price) > 100 ``` -This will return single order objects for each day, but only those that are ordered more than $100 in a day. +This returns the date and total price for each order object, grouped by the day they were ordered and where the price is more than $100. Overriding Conditions --------------------- @@ -683,8 +769,7 @@ Article.where(id: 10, trashed: false).unscope(where: :id) # SELECT "articles".* FROM "articles" WHERE trashed = 0 ``` -A relation which has used `unscope` will affect any relation it is -merged in to: +A relation which has used `unscope` will affect any relation into which it is merged: ```ruby Article.order('id asc').merge(Article.unscope(:order)) @@ -714,7 +799,7 @@ SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20 The `reorder` method overrides the default scope order. For example: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments, -> { order('posted_at DESC') } end @@ -728,7 +813,7 @@ SELECT * FROM articles WHERE id = 10 SELECT * FROM comments WHERE article_id = 10 ORDER BY name ``` -In case the `reorder` clause is not used, the SQL executed would be: +In the case where the `reorder` clause is not used, the SQL executed would be: ```sql SELECT * FROM articles WHERE id = 10 @@ -817,7 +902,7 @@ end Readonly Objects ---------------- -Active Record provides `readonly` method on a relation to explicitly disallow modification of any of the returned objects. Any attempt to alter a readonly record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` exception. +Active Record provides the `readonly` method on a relation to explicitly disallow modification of any of the returned objects. Any attempt to alter a readonly record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` exception. ```ruby client = Client.readonly.first @@ -863,7 +948,7 @@ This behavior can be turned off by setting `ActiveRecord::Base.lock_optimistical To override the name of the `lock_version` column, `ActiveRecord::Base` provides a class attribute called `locking_column`: ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord self.locking_column = :lock_client_column end ``` @@ -878,7 +963,7 @@ For example: Item.transaction do i = Item.lock.first i.name = 'Jones' - i.save + i.save! end ``` @@ -914,58 +999,63 @@ end Joining Tables -------------- -Active Record provides a finder method called `joins` for specifying `JOIN` clauses on the resulting SQL. There are multiple ways to use the `joins` method. +Active Record provides two finder methods for specifying `JOIN` clauses on the +resulting SQL: `joins` and `left_outer_joins`. +While `joins` should be used for `INNER JOIN` or custom queries, +`left_outer_joins` is used for queries using `LEFT OUTER JOIN`. + +### `joins` + +There are multiple ways to use the `joins` method. -### Using a String SQL Fragment +#### Using a String SQL Fragment You can just supply the raw SQL specifying the `JOIN` clause to `joins`: ```ruby -Client.joins('LEFT OUTER JOIN addresses ON addresses.client_id = clients.id') +Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'") ``` This will result in the following SQL: ```sql -SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id +SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't' ``` -### Using Array/Hash of Named Associations - -WARNING: This method only works with `INNER JOIN`. +#### Using Array/Hash of Named Associations Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clauses for those associations when using the `joins` method. For example, consider the following `Category`, `Article`, `Comment`, `Guest` and `Tag` models: ```ruby -class Category < ActiveRecord::Base +class Category < ApplicationRecord has_many :articles end -class Article < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :category has_many :comments has_many :tags end -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :article has_one :guest end -class Guest < ActiveRecord::Base +class Guest < ApplicationRecord belongs_to :comment end -class Tag < ActiveRecord::Base +class Tag < ApplicationRecord belongs_to :article end ``` Now all of the following will produce the expected join queries using `INNER JOIN`: -#### Joining a Single Association +##### Joining a Single Association ```ruby Category.joins(:articles) @@ -978,7 +1068,7 @@ SELECT categories.* FROM categories INNER JOIN articles ON articles.category_id = categories.id ``` -Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).uniq`. +Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).distinct`. #### Joining Multiple Associations @@ -996,7 +1086,7 @@ SELECT articles.* FROM articles Or, in English: "return all articles that have a category and at least one comment". Note again that articles with multiple comments will show up multiple times. -#### Joining Nested Associations (Single Level) +##### Joining Nested Associations (Single Level) ```ruby Article.joins(comments: :guest) @@ -1012,7 +1102,7 @@ SELECT articles.* FROM articles Or, in English: "return all articles that have a comment made by a guest." -#### Joining Nested Associations (Multiple Level) +##### Joining Nested Associations (Multiple Level) ```ruby Category.joins(articles: [{ comments: :guest }, :tags]) @@ -1028,9 +1118,11 @@ SELECT categories.* FROM categories INNER JOIN tags ON tags.article_id = articles.id ``` -### Specifying Conditions on the Joined Tables +Or, in English: "return all categories that have articles, where those articles have a comment made by a guest, and where those articles also have a tag." + +#### Specifying Conditions on the Joined Tables -You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provides a special syntax for specifying conditions for the joined tables: +You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provide a special syntax for specifying conditions for the joined tables: ```ruby time_range = (Time.now.midnight - 1.day)..Time.now.midnight @@ -1046,6 +1138,26 @@ Client.joins(:orders).where(orders: { created_at: time_range }) This will find all clients who have orders that were created yesterday, again using a `BETWEEN` SQL expression. +### `left_outer_joins` + +If you want to select a set of records whether or not they have associated +records you can use the `left_outer_joins` method. + +```ruby +Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id') +``` + +Which produces: + +```sql +SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors" +LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id +``` + +Which means: "return all authors with their count of posts, whether or not they +have any posts at all" + + Eager Loading Associations -------------------------- @@ -1069,7 +1181,7 @@ This code looks fine at the first sight. But the problem lies within the total n Active Record lets you specify in advance all the associations that are going to be loaded. This is possible by specifying the `includes` method of the `Model.find` call. With `includes`, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries. -Revisiting the above case, we could rewrite `Client.limit(10)` to use eager load addresses: +Revisiting the above case, we could rewrite `Client.limit(10)` to eager load addresses: ```ruby clients = Client.includes(:address).limit(10) @@ -1127,7 +1239,7 @@ This would generate a query which contains a `LEFT OUTER JOIN` whereas the If there was no `where` condition, this would generate the normal set of two queries. NOTE: Using `where` like this will only work when you pass it a Hash. For -SQL-fragments you need use `references` to force joined tables: +SQL-fragments you need to use `references` to force joined tables: ```ruby Article.includes(:comments).where("comments.visible = true").references(:comments) @@ -1138,7 +1250,8 @@ articles, all the articles would still be loaded. By using `joins` (an INNER JOIN), the join conditions **must** match, otherwise no records will be returned. - +NOTE: If an association is eager loaded as part of a join, any fields from a custom select clause will not present be on the loaded models. +This is because it is ambiguous whether they should appear on the parent record, or the child. Scopes ------ @@ -1148,7 +1261,7 @@ Scoping allows you to specify commonly-used queries which can be referenced as m To define a simple scope, we use the `scope` method inside the class, passing the query that we'd like to run when this scope is called: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord scope :published, -> { where(published: true) } end ``` @@ -1156,7 +1269,7 @@ end This is exactly the same as defining a class method, and which you use is a matter of personal preference: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord def self.published where(published: true) end @@ -1166,7 +1279,7 @@ end Scopes are also chainable within scopes: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord scope :published, -> { where(published: true) } scope :published_and_commented, -> { published.where("comments_count > 0") } end @@ -1190,7 +1303,7 @@ category.articles.published # => [published articles belonging to this category] Your scope can take arguments: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord scope :created_before, ->(time) { where("created_at < ?", time) } end ``` @@ -1204,7 +1317,7 @@ Article.created_before(Time.zone.now) However, this is just duplicating the functionality that would be provided to you by a class method. ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord def self.created_before(time) where("created_at < ?", time) end @@ -1217,13 +1330,35 @@ Using a class method is the preferred way to accept arguments for scopes. These category.articles.created_before(time) ``` +### Using conditionals + +Your scope can utilize conditionals: + +```ruby +class Article < ApplicationRecord + scope :created_before, ->(time) { where("created_at < ?", time) if time.present? } +end +``` + +Like the other examples, this will behave similarly to a class method. + +```ruby +class Article < ApplicationRecord + def self.created_before(time) + where("created_at < ?", time) if time.present? + end +end +``` + +However, there is one important caveat: A scope will always return an `ActiveRecord::Relation` object, even if the conditional evaluates to `false`, whereas a class method, will return `nil`. This can cause `NoMethodError` when chaining class methods with conditionals, if any of the conditionals return `false`. + ### Applying a default scope If we wish for a scope to be applied across all queries to the model we can use the `default_scope` method within the model itself. ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord default_scope { where("removed_at IS NULL") } end ``` @@ -1239,19 +1374,31 @@ If you need to do more complex things with a default scope, you can alternativel define it as a class method: ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord def self.default_scope # Should return an ActiveRecord::Relation. end end ``` +NOTE: The `default_scope` is also applied while creating/building a record. +It is not applied while updating a record. E.g.: + +```ruby +class Client < ApplicationRecord + default_scope { where(active: true) } +end + +Client.new # => # +Client.unscoped.new # => # +``` + ### Merging of scopes Just like `where` clauses scopes are merged using `AND` conditions. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord scope :active, -> { where state: 'active' } scope :inactive, -> { where state: 'inactive' } end @@ -1268,7 +1415,7 @@ User.active.where(state: 'finished') # SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished' ``` -If we do want the `last where clause` to win then `Relation#merge` can +If we do want the last `where` clause to win then `Relation#merge` can be used. ```ruby @@ -1280,7 +1427,7 @@ One important caveat is that `default_scope` will be prepended in `scope` and `where` conditions. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord default_scope { where state: 'pending' } scope :active, -> { where state: 'active' } scope :inactive, -> { where state: 'inactive' } @@ -1311,8 +1458,15 @@ Client.unscoped.load This method removes all scoping and will do a normal query on the table. -Note that chaining `unscoped` with a `scope` does not work. In these cases, it is -recommended that you use the block form of `unscoped`: +```ruby +Client.unscoped.all +# SELECT "clients".* FROM "clients" + +Client.where(published: false).unscoped.all +# SELECT "clients".* FROM "clients" +``` + +`unscoped` can also accept a block. ```ruby Client.unscoped { @@ -1323,25 +1477,108 @@ Client.unscoped { Dynamic Finders --------------- -For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and methods. +For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` method. You can specify an exclamation point (`!`) on the end of the dynamic finders to get them to raise an `ActiveRecord::RecordNotFound` error if they do not return any records, like `Client.find_by_name!("Ryan")` If you want to find both by name and locked, you can chain these finders together by simply typing "`and`" between the fields. For example, `Client.find_by_first_name_and_locked("Ryan", true)`. +Enums +----- + +The `enum` macro maps an integer column to a set of possible values. + +```ruby +class Book < ApplicationRecord + enum availability: [:available, :unavailable] +end +``` + +This will automatically create the corresponding [scopes](#scopes) to query the +model. Methods to transition between states and query the current state are also +added. + +```ruby +# Both examples below query just available books. +Book.available +# or +Book.where(availability: :available) + +book = Book.new(availability: :available) +book.available? # => true +book.unavailable! # => true +book.available? # => false +``` + +Read the full documentation about enums +[in the Rails API docs](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html). + +Understanding The Method Chaining +--------------------------------- + +The Active Record pattern implements [Method Chaining](http://en.wikipedia.org/wiki/Method_chaining), +which allow us to use multiple Active Record methods together in a simple and straightforward way. + +You can chain methods in a statement when the previous method called returns an +`ActiveRecord::Relation`, like `all`, `where`, and `joins`. Methods that return +a single object (see [Retrieving a Single Object Section](#retrieving-a-single-object)) +have to be at the end of the statement. + +There are some examples below. This guide won't cover all the possibilities, just a few as examples. +When an Active Record method is called, the query is not immediately generated and sent to the database, +this just happens when the data is actually needed. So each example below generates a single query. + +### Retrieving filtered data from multiple tables + +```ruby +Person + .select('people.id, people.name, comments.text') + .joins(:comments) + .where('comments.created_at > ?', 1.week.ago) +``` + +The result should be something like this: + +```sql +SELECT people.id, people.name, comments.text +FROM people +INNER JOIN comments + ON comments.person_id = people.id +WHERE comments.created_at > '2015-01-01' +``` + +### Retrieving specific data from multiple tables + +```ruby +Person + .select('people.id, people.name, companies.name') + .joins(:company) + .find_by('people.name' => 'John') # this should be the last +``` + +The above should generate: + +```sql +SELECT people.id, people.name, companies.name +FROM people +INNER JOIN companies + ON companies.person_id = people.id +WHERE people.name = 'John' +LIMIT 1 +``` + +NOTE: Note that if a query matches multiple records, `find_by` will +fetch only the first one and ignore the others (see the `LIMIT 1` +statement above). + Find or Build a New Object -------------------------- -NOTE: Some dynamic finders have been deprecated in Rails 4.0 and will be -removed in Rails 4.1. The best practice is to use Active Record scopes -instead. You can find the deprecation gem at -https://github.com/rails/activerecord-deprecated_finders - It's common that you need to find a record or create it if it doesn't exist. You can do that with the `find_or_create_by` and `find_or_create_by!` methods. ### `find_or_create_by` -The `find_or_create_by` method checks whether a record with the attributes exists. If it doesn't, then `create` is called. Let's see an example. +The `find_or_create_by` method checks whether a record with the specified attributes exists. If it doesn't, then `create` is called. Let's see an example. Suppose you want to find a client named 'Andy', and if there's none, create one. You can do so by running: @@ -1410,7 +1647,7 @@ now want the client named 'Nick': ```ruby nick = Client.find_or_initialize_by(first_name: 'Nick') -# => +# => # nick.persisted? # => false @@ -1442,10 +1679,10 @@ Client.find_by_sql("SELECT * FROM clients INNER JOIN orders ON clients.id = orders.client_id ORDER BY clients.created_at desc") # => [ - #, - #, - # ... -] +# #, +# #, +# ... +# ] ``` `find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects. @@ -1457,14 +1694,14 @@ Client.find_by_sql("SELECT * FROM clients ```ruby Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'") # => [ - {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, - {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"} -] +# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, +# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"} +# ] ``` ### `pluck` -`pluck` can be used to query a single or multiple columns from the underlying table of a model. It accepts a list of column names as argument and returns an array of values of the specified columns with the corresponding data type. +`pluck` can be used to query single or multiple columns from the underlying table of a model. It accepts a list of column names as argument and returns an array of values of the specified columns with the corresponding data type. ```ruby Client.where(active: true).pluck(:id) @@ -1504,7 +1741,7 @@ a large or often-running query. However, any model method overrides will not be available. For example: ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord def name "I am #{super}" end @@ -1539,7 +1776,7 @@ Person.ids ``` ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord self.primary_key = "person_id" end @@ -1631,7 +1868,7 @@ Which will execute: ```sql SELECT count(DISTINCT clients.id) AS count_all FROM clients - LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE + LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE (clients.first_name = 'Ryan' AND orders.status = 'received') ``` @@ -1712,10 +1949,11 @@ EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.` 2 rows in set (0.00 sec) ``` -under MySQL. +under MySQL and MariaDB. -Active Record performs a pretty printing that emulates the one of the database -shells. So, the same query running with the PostgreSQL adapter would yield instead +Active Record performs a pretty printing that emulates that of the +corresponding database shell. So, the same query running with the +PostgreSQL adapter would yield instead ``` EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 @@ -1771,7 +2009,7 @@ EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN 1 row in set (0.00 sec) ``` -under MySQL. +under MySQL and MariaDB. ### Interpreting EXPLAIN @@ -1780,6 +2018,8 @@ following pointers may be helpful: * SQLite3: [EXPLAIN QUERY PLAN](http://www.sqlite.org/eqp.html) -* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.6/en/explain-output.html) +* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.7/en/explain-output.html) + +* MariaDB: [EXPLAIN](https://mariadb.com/kb/en/mariadb/explain/) * PostgreSQL: [Using EXPLAIN](http://www.postgresql.org/docs/current/static/using-explain.html) diff --git a/source/active_record_validations.md b/source/active_record_validations.md index 582bb24..5313361 100644 --- a/source/active_record_validations.md +++ b/source/active_record_validations.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Validations ========================= @@ -18,7 +20,7 @@ Validations Overview Here's an example of a very simple validation: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -45,7 +47,7 @@ built-in helpers for common needs, and allows you to create your own validation methods as well. There are several other ways to validate data before it is saved into your -database, including native database constraints, client-side validations, +database, including native database constraints, client-side validations and controller-level validations. Here's a summary of the pros and cons: * Database constraints and/or stored procedures make the validation mechanisms @@ -78,7 +80,7 @@ method to determine whether an object is already in the database or not. Consider the following simple Active Record class: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord end ``` @@ -120,7 +122,7 @@ database only if the object is valid: * `update!` The bang versions (e.g. `save!`) raise an exception if the record is invalid. -The non-bang versions don't, `save` and `update` return `false`, +The non-bang versions don't: `save` and `update` return `false`, and `create` just returns the object. ### Skipping Validations @@ -141,19 +143,21 @@ database regardless of its validity. They should be used with caution. * `update_counters` Note that `save` also has the ability to skip validations if passed `validate: -false` as argument. This technique should be used with caution. +false` as an argument. This technique should be used with caution. * `save(validate: false)` ### `valid?` and `invalid?` -To verify whether or not an object is valid, Rails uses the `valid?` method. -You can also use this method on your own. `valid?` triggers your validations +Before saving an Active Record object, Rails runs your validations. +If these validations produce any errors, Rails does not save the object. + +You can also run these validations on your own. `valid?` triggers your validations and returns true if no errors were found in the object, and false otherwise. As you saw above: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -166,11 +170,12 @@ through the `errors.messages` instance method, which returns a collection of err By definition, an object is valid if this collection is empty after running validations. -Note that an object instantiated with `new` will not report errors even if it's -technically invalid, because validations are not run when using `new`. +Note that an object instantiated with `new` will not report errors +even if it's technically invalid, because validations are automatically run +only when the object is saved, such as with the `create` or `save` methods. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -216,7 +221,7 @@ it doesn't verify the validity of the object as a whole. It only checks to see whether there are errors found on an individual attribute of the object. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -225,8 +230,26 @@ end ``` We'll cover validation errors in greater depth in the [Working with Validation -Errors](#working-with-validation-errors) section. For now, let's turn to the -built-in validation helpers that Rails provides by default. +Errors](#working-with-validation-errors) section. + +### `errors.details` + +To check which validations failed on an invalid attribute, you can use +`errors.details[:attribute]`. It returns an array of hashes with an `:error` +key to get the symbol of the validator: + +```ruby +class Person < ApplicationRecord + validates :name, presence: true +end + +>> person = Person.new +>> person.valid? +>> person.errors.details[:name] # => [{error: :blank}] +``` + +Using `details` with custom validators is covered in the [Working with +Validation Errors](#working-with-validation-errors) section. Validation Helpers ------------------ @@ -252,28 +275,42 @@ available helpers. This method validates that a checkbox on the user interface was checked when a form was submitted. This is typically used when the user needs to agree to your -application's terms of service, confirm reading some text, or any similar -concept. This validation is very specific to web applications and this -'acceptance' does not need to be recorded anywhere in your database (if you -don't have a field for it, the helper will just create a virtual attribute). +application's terms of service, confirm that some text is read, or any similar +concept. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :terms_of_service, acceptance: true end ``` +This check is performed only if `terms_of_service` is not `nil`. The default error message for this helper is _"must be accepted"_. +You can also pass custom message via the `message` option. + +```ruby +class Person < ApplicationRecord + validates :terms_of_service, acceptance: { message: 'must be abided' } +end +``` -It can receive an `:accept` option, which determines the value that will be -considered acceptance. It defaults to "1" and can be easily changed. +It can also receive an `:accept` option, which determines the allowed values +that will be considered as accepted. It defaults to `['1', true]` and can be +easily changed. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :terms_of_service, acceptance: { accept: 'yes' } + validates :eula, acceptance: { accept: ['TRUE', 'accepted'] } end ``` +This validation is very specific to web applications and this +'acceptance' does not need to be recorded anywhere in your database. If you +don't have a field for it, the helper will just create a virtual attribute. If +the field does exist in your database, the `accept` option must be set to +or include `true` or else the validation will not run. + ### `validates_associated` You should use this helper when your model has associations with other models @@ -281,7 +318,7 @@ and they also need to be validated. When you try to save your object, `valid?` will be called upon each one of the associated objects. ```ruby -class Library < ActiveRecord::Base +class Library < ApplicationRecord has_many :books validates_associated :books end @@ -304,7 +341,7 @@ or a password. This validation creates a virtual attribute whose name is the name of the field that has to be confirmed with "_confirmation" appended. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, confirmation: true end ``` @@ -318,15 +355,25 @@ In your view template you could use something like This check is performed only if `email_confirmation` is not `nil`. To require confirmation, make sure to add a presence check for the confirmation attribute -(we'll take a look at `presence` later on this guide): +(we'll take a look at `presence` later on in this guide): ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, confirmation: true validates :email_confirmation, presence: true end ``` +There is also a `:case_sensitive` option that you can use to define whether the +confirmation constraint will be case sensitive or not. This option defaults to +true. + +```ruby +class Person < ApplicationRecord + validates :email, confirmation: { case_sensitive: false } +end +``` + The default error message for this helper is _"doesn't match confirmation"_. ### `exclusion` @@ -335,7 +382,7 @@ This helper validates that the attributes' values are not included in a given set. In fact, this set can be any enumerable object. ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :subdomain, exclusion: { in: %w(www us ca jp), message: "%{value} is reserved." } end @@ -345,7 +392,8 @@ The `exclusion` helper has an option `:in` that receives the set of values that will not be accepted for the validated attributes. The `:in` option has an alias called `:within` that you can use for the same purpose, if you'd like to. This example uses the `:message` option to show how you can include the -attribute's value. +attribute's value. For full options to the message argument please see the +[message documentation](#message). The default error message is _"is reserved"_. @@ -355,12 +403,14 @@ This helper validates the attributes' values by testing whether they match a given regular expression, which is specified using the `:with` option. ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/, message: "only allows letters" } end ``` +Alternatively, you can require that the specified attribute does _not_ match the regular expression by using the `:without` option. + The default error message is _"is invalid"_. ### `inclusion` @@ -369,7 +419,7 @@ This helper validates that the attributes' values are included in a given set. In fact, this set can be any enumerable object. ```ruby -class Coffee < ActiveRecord::Base +class Coffee < ApplicationRecord validates :size, inclusion: { in: %w(small medium large), message: "%{value} is not a valid size" } end @@ -378,7 +428,8 @@ end The `inclusion` helper has an option `:in` that receives the set of values that will be accepted. The `:in` option has an alias called `:within` that you can use for the same purpose, if you'd like to. The previous example uses the -`:message` option to show how you can include the attribute's value. +`:message` option to show how you can include the attribute's value. For full +options please see the [message documentation](#message). The default error message for this helper is _"is not included in the list"_. @@ -388,7 +439,7 @@ This helper validates the length of the attributes' values. It provides a variety of options, so you can specify length constraints in different ways: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, length: { minimum: 2 } validates :bio, length: { maximum: 500 } validates :password, length: { in: 6..20 } @@ -411,27 +462,12 @@ number corresponding to the length constraint being used. You can still use the `:message` option to specify an error message. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :bio, length: { maximum: 1000, too_long: "%{count} characters is the maximum allowed" } end ``` -This helper counts characters by default, but you can split the value in a -different way using the `:tokenizer` option: - -```ruby -class Essay < ActiveRecord::Base - validates :content, length: { - minimum: 300, - maximum: 400, - tokenizer: lambda { |str| str.scan(/\w+/) }, - too_short: "must have at least %{count} words", - too_long: "must have at most %{count} words" - } -end -``` - Note that the default error messages are plural (e.g., "is too short (minimum is %{count} characters)"). For this reason, when `:minimum` is 1 you should provide a personalized message or use `presence: true` instead. When @@ -448,17 +484,14 @@ point number. To specify that only integral numbers are allowed set If you set `:only_integer` to `true`, then it will use the ```ruby -/\A[+-]?\d+\Z/ +/\A[+-]?\d+\z/ ``` regular expression to validate the attribute's value. Otherwise, it will try to convert the value to a number using `Float`. -WARNING. Note that the regular expression above allows a trailing newline -character. - ```ruby -class Player < ActiveRecord::Base +class Player < ApplicationRecord validates :points, numericality: true validates :games_played, numericality: { only_integer: true } end @@ -477,14 +510,18 @@ constraints to acceptable values: default error message for this option is _"must be equal to %{count}"_. * `:less_than` - Specifies the value must be less than the supplied value. The default error message for this option is _"must be less than %{count}"_. -* `:less_than_or_equal_to` - Specifies the value must be less than or equal the - supplied value. The default error message for this option is _"must be less - than or equal to %{count}"_. +* `:less_than_or_equal_to` - Specifies the value must be less than or equal to + the supplied value. The default error message for this option is _"must be + less than or equal to %{count}"_. +* `:other_than` - Specifies the value must be other than the supplied value. + The default error message for this option is _"must be other than %{count}"_. * `:odd` - Specifies the value must be an odd number if set to true. The default error message for this option is _"must be odd"_. * `:even` - Specifies the value must be an even number if set to true. The default error message for this option is _"must be even"_. +NOTE: By default, `numericality` doesn't allow `nil` values. You can use `allow_nil: true` option to permit it. + The default error message is _"is not a number"_. ### `presence` @@ -494,7 +531,7 @@ This helper validates that the specified attributes are not empty. It uses the is, a string that is either empty or consists of whitespace. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, :login, :email, presence: true end ``` @@ -504,7 +541,7 @@ whether the associated object itself is present, and not the foreign key used to map the association. ```ruby -class LineItem < ActiveRecord::Base +class LineItem < ApplicationRecord belongs_to :order validates :order, presence: true end @@ -514,7 +551,7 @@ In order to validate associated records whose presence is required, you must specify the `:inverse_of` option for the association: ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord has_many :line_items, inverse_of: :order end ``` @@ -524,9 +561,15 @@ If you validate the presence of an object associated via a `has_one` or `marked_for_destruction?`. Since `false.blank?` is true, if you want to validate the presence of a boolean -field you should use `validates :field_name, inclusion: { in: [true, false] }`. +field you should use one of the following validations: -The default error message is _"can't be blank"_. +```ruby +validates :boolean_field_name, inclusion: { in: [true, false] } +validates :boolean_field_name, exclusion: { in: [nil] } +``` + +By using one of these validations, you will ensure the value will NOT be `nil` +which would result in a `NULL` value in most cases. ### `absence` @@ -535,7 +578,7 @@ This helper validates that the specified attributes are absent. It uses the is, a string that is either empty or consists of whitespace. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, :login, :email, absence: true end ``` @@ -545,7 +588,7 @@ whether the associated object itself is absent, and not the foreign key used to map the association. ```ruby -class LineItem < ActiveRecord::Base +class LineItem < ApplicationRecord belongs_to :order validates :order, absence: true end @@ -555,7 +598,7 @@ In order to validate associated records whose absence is required, you must specify the `:inverse_of` option for the association: ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord has_many :line_items, inverse_of: :order end ``` @@ -575,12 +618,10 @@ This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, -you must create a unique index on both columns in your database. See -[the MySQL manual](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html) -for more details about multiple column indexes. +you must create a unique index on that column in your database. ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :email, uniqueness: true end ``` @@ -588,22 +629,23 @@ end The validation happens by performing an SQL query into the model's table, searching for an existing record with the same value in that attribute. -There is a `:scope` option that you can use to specify other attributes that +There is a `:scope` option that you can use to specify one or more attributes that are used to limit the uniqueness check: ```ruby -class Holiday < ActiveRecord::Base +class Holiday < ApplicationRecord validates :name, uniqueness: { scope: :year, message: "should happen once per year" } end ``` +Should you wish to create a database constraint to prevent possible violations of a uniqueness validation using the `:scope` option, you must create a unique index on both columns in your database. See [the MySQL manual](http://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html) for more details about multiple column indexes or [the PostgreSQL manual](http://www.postgresql.org/docs/current/static/ddl-constraints.html) for examples of unique constraints that refer to a group of columns. There is also a `:case_sensitive` option that you can use to define whether the uniqueness constraint will be case sensitive or not. This option defaults to true. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, uniqueness: { case_sensitive: false } end ``` @@ -626,7 +668,7 @@ class GoodnessValidator < ActiveModel::Validator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_with GoodnessValidator end ``` @@ -654,7 +696,7 @@ class GoodnessValidator < ActiveModel::Validator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_with GoodnessValidator, fields: [:first_name, :last_name] end ``` @@ -667,7 +709,7 @@ If your validator is complex enough that you want instance variables, you can easily use a plain old Ruby object instead: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validate do |person| GoodnessValidator.new(person).validate end @@ -696,9 +738,9 @@ passed to `validates_each` will be tested against it. In the following example, we don't want names and surnames to begin with lower case. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_each :name, :surname do |record, attr, value| - record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/ + record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/ end end ``` @@ -719,12 +761,15 @@ The `:allow_nil` option skips the validation when the value being validated is `nil`. ```ruby -class Coffee < ActiveRecord::Base +class Coffee < ApplicationRecord validates :size, inclusion: { in: %w(small medium large), message: "%{value} is not a valid size" }, allow_nil: true end ``` +For full options to the message argument please see the +[message documentation](#message). + ### `:allow_blank` The `:allow_blank` option is similar to the `:allow_nil` option. This option @@ -732,7 +777,7 @@ will let validation pass if the attribute's value is `blank?`, like `nil` or an empty string for example. ```ruby -class Topic < ActiveRecord::Base +class Topic < ApplicationRecord validates :title, length: { is: 5 }, allow_blank: true end @@ -745,7 +790,37 @@ Topic.create(title: nil).valid? # => true As you've already seen, the `:message` option lets you specify the message that will be added to the `errors` collection when validation fails. When this option is not used, Active Record will use the respective default error message -for each validation helper. +for each validation helper. The `:message` option accepts a `String` or `Proc`. + +A `String` `:message` value can optionally contain any/all of `%{value}`, +`%{attribute}`, and `%{model}` which will be dynamically replaced when +validation fails. This replacement is done using the I18n gem, and the +placeholders must match exactly, no spaces are allowed. + +A `Proc` `:message` value is given two arguments: the object being validated, and +a hash with `:model`, `:attribute`, and `:value` key-value pairs. + +```ruby +class Person < ApplicationRecord + # Hard-coded message + validates :name, presence: { message: "must be given please" } + + # Message with dynamic attribute value. %{value} will be replaced with + # the actual value of the attribute. %{attribute} and %{model} also + # available. + validates :age, numericality: { message: "%{value} seems wrong" } + + # Proc + validates :username, + uniqueness: { + # object = person object being validated + # data = { model: "Person", attribute: "Username", value: } + message: ->(object, data) do + "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}" + end + } +end +``` ### `:on` @@ -757,7 +832,7 @@ new record is created or `on: :update` to run the validation only when a record is updated. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord # it will be possible to update email with a duplicated value validates :email, uniqueness: true, on: :create @@ -769,6 +844,25 @@ class Person < ActiveRecord::Base end ``` +You can also use `on:` to define custom context. +Custom contexts need to be triggered explicitly +by passing name of the context to `valid?`, `invalid?` or `save`. + +```ruby +class Person < ApplicationRecord + validates :email, uniqueness: true, on: :account_setup + validates :age, numericality: true, on: :account_setup +end + +person = Person.new +``` + +`person.valid?(:account_setup)` executes both the validations +without saving the model. And `person.save(context: :account_setup)` +validates `person` in `account_setup` context before saving. +On explicit triggers, model is validated by +validations of only that context and validations without context. + Strict Validations ------------------ @@ -776,17 +870,17 @@ You can also specify validations to be strict and raise `ActiveModel::StrictValidationFailed` when the object is invalid. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: { strict: true } end Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank ``` -There is also an ability to pass custom exception to `:strict` option. +There is also the ability to pass a custom exception to the `:strict` option. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :token, presence: true, uniqueness: true, strict: TokenGenerationException end @@ -810,7 +904,7 @@ to the name of a method that will get called right before validation happens. This is the most commonly used option. ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord validates :card_number, presence: true, if: :paid_with_card? def paid_with_card? @@ -819,18 +913,6 @@ class Order < ActiveRecord::Base end ``` -### Using a String with `:if` and `:unless` - -You can also use a string that will be evaluated using `eval` and needs to -contain valid Ruby code. You should use this option only when the string -represents a really short condition. - -```ruby -class Person < ActiveRecord::Base - validates :surname, presence: true, if: "name.nil?" -end -``` - ### Using a Proc with `:if` and `:unless` Finally, it's possible to associate `:if` and `:unless` with a `Proc` object @@ -839,7 +921,7 @@ inline condition instead of a separate method. This option is best suited for one-liners. ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :password, confirmation: true, unless: Proc.new { |a| a.password.blank? } end @@ -847,11 +929,11 @@ end ### Grouping Conditional validations -Sometimes it is useful to have multiple validations use one condition, it can +Sometimes it is useful to have multiple validations use one condition. It can be easily achieved using `with_options`. ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord with_options if: :is_admin? do |admin| admin.validates :password, length: { minimum: 10 } admin.validates :email, presence: true @@ -859,8 +941,8 @@ class User < ActiveRecord::Base end ``` -All validations inside of `with_options` block will have automatically passed -the condition `if: :is_admin?` +All validations inside of the `with_options` block will have automatically +passed the condition `if: :is_admin?` ### Combining Validation Conditions @@ -869,7 +951,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and `:unless` to the same validation. ```ruby -class Computer < ActiveRecord::Base +class Computer < ApplicationRecord validates :mouse, presence: true, if: ["market.retail?", :desktop?], unless: Proc.new { |c| c.trackpad.present? } @@ -887,8 +969,8 @@ write your own validators or validation methods as you prefer. ### Custom Validators -Custom validators are classes that extend `ActiveModel::Validator`. These -classes must implement a `validate` method which takes a record as an argument +Custom validators are classes that inherit from `ActiveModel::Validator`. These +classes must implement the `validate` method which takes a record as an argument and performs the validation on it. The custom validator is called using the `validates_with` method. @@ -923,7 +1005,7 @@ class EmailValidator < ActiveModel::EachValidator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, presence: true, email: true end ``` @@ -935,14 +1017,19 @@ own custom validators. You can also create methods that verify the state of your models and add messages to the `errors` collection when they are invalid. You must then -register these methods by using the `validate` class method, passing in the -symbols for the validation methods' names. +register these methods by using the `validate` +([API](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate)) +class method, passing in the symbols for the validation methods' names. You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered. +The `valid?` method will verify that the errors collection is empty, +so your custom validation methods should add errors to it when you +wish validation to fail: + ```ruby -class Invoice < ActiveRecord::Base +class Invoice < ApplicationRecord validate :expiration_date_cannot_be_in_the_past, :discount_cannot_be_greater_than_total_value @@ -960,12 +1047,13 @@ class Invoice < ActiveRecord::Base end ``` -By default such validations will run every time you call `valid?`. It is also -possible to control when to run these custom validations by giving an `:on` -option to the `validate` method, with either: `:create` or `:update`. +By default, such validations will run every time you call `valid?` +or save the object. But it is also possible to control when to run these +custom validations by giving an `:on` option to the `validate` method, +with either: `:create` or `:update`. ```ruby -class Invoice < ActiveRecord::Base +class Invoice < ApplicationRecord validate :active_customer, on: :create def active_customer @@ -986,7 +1074,7 @@ The following is a list of the most commonly used methods. Please refer to the ` Returns an instance of the class `ActiveModel::Errors` containing all errors. Each key is the attribute name and the value is an array of strings with all errors. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -1005,7 +1093,7 @@ person.errors.messages # => {} `errors[]` is used when you want to check the error messages for a specific attribute. It returns an array of strings with all error messages for the given attribute, each string with one error message. If there are no errors related to the attribute, it returns an empty array. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -1025,10 +1113,12 @@ person.errors[:name] ### `errors.add` -The `add` method lets you manually add messages that are related to particular attributes. You can use the `errors.full_messages` or `errors.to_a` methods to view the messages in the form they might be displayed to a user. Those particular messages get the attribute name prepended (and capitalized). `add` receives the name of the attribute you want to add the message to, and the message itself. +The `add` method lets you add an error message related to a particular attribute. It takes as arguments the attribute and the error message. + +The `errors.full_messages` method (or its equivalent, `errors.to_a`) returns the error messages in a user-friendly format, with the capitalized attribute name prepended to each message, as shown in the examples below. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord def a_method_used_for_validation_purposes errors.add(:name, "cannot contain the characters !@#%*()_-+=") end @@ -1043,12 +1133,12 @@ person.errors.full_messages # => ["Name cannot contain the characters !@#%*()_-+="] ``` -Another way to do this is using `[]=` setter +An equivalent to `errors#add` is to use `<<` to append a message to the `errors.messages` array for an attribute: ```ruby - class Person < ActiveRecord::Base + class Person < ApplicationRecord def a_method_used_for_validation_purposes - errors[:name] = "cannot contain the characters !@#%*()_-+=" + errors.messages[:name] << "cannot contain the characters !@#%*()_-+=" end end @@ -1061,12 +1151,49 @@ Another way to do this is using `[]=` setter # => ["Name cannot contain the characters !@#%*()_-+="] ``` +### `errors.details` + +You can specify a validator type to the returned error details hash using the +`errors.add` method. + +```ruby +class Person < ApplicationRecord + def a_method_used_for_validation_purposes + errors.add(:name, :invalid_characters) + end +end + +person = Person.create(name: "!@#") + +person.errors.details[:name] +# => [{error: :invalid_characters}] +``` + +To improve the error details to contain the unallowed characters set for instance, +you can pass additional keys to `errors.add`. + +```ruby +class Person < ApplicationRecord + def a_method_used_for_validation_purposes + errors.add(:name, :invalid_characters, not_allowed: "!@#%*()_-+=") + end +end + +person = Person.create(name: "!@#") + +person.errors.details[:name] +# => [{error: :invalid_characters, not_allowed: "!@#%*()_-+="}] +``` + +All built in Rails validators populate the details hash with the corresponding +validator type. + ### `errors[:base]` You can add error messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. Since `errors[:base]` is an array, you can simply add a string to it and it will be used as an error message. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord def a_method_used_for_validation_purposes errors[:base] << "This person is invalid because ..." end @@ -1078,7 +1205,7 @@ end The `clear` method is used when you intentionally want to clear all the messages in the `errors` collection. Of course, calling `errors.clear` upon an invalid object won't actually make it valid: the `errors` collection will now be empty, but the next time you call `valid?` or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the `errors` collection will be filled again. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -1090,9 +1217,9 @@ person.errors[:name] person.errors.clear person.errors.empty? # => true -p.save # => false +person.save # => false -p.errors[:name] +person.errors[:name] # => ["can't be blank", "is too short (minimum is 3 characters)"] ``` @@ -1101,7 +1228,7 @@ p.errors[:name] The `size` method returns the total number of error messages for the object. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end diff --git a/source/active_support_core_extensions.md b/source/active_support_core_extensions.md index 5ed392d..67bed4c 100644 --- a/source/active_support_core_extensions.md +++ b/source/active_support_core_extensions.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Support Core Extensions ============================== @@ -133,36 +135,53 @@ NOTE: Defined in `active_support/core_ext/object/blank.rb`. ### `duplicable?` -A few fundamental objects in Ruby are singletons. For example, in the whole life of a program the integer 1 refers always to the same instance: +In Ruby 2.4 most objects can be duplicated via `dup` or `clone` except +methods and certain numbers. Though Ruby 2.2 and 2.3 can't duplicate `nil`, +`false`, `true`, and symbols as well as instances `Float`, `Fixnum`, +and `Bignum` instances. ```ruby -1.object_id # => 3 -Math.cos(0).to_i.object_id # => 3 +"foo".dup # => "foo" +"".dup # => "" +1.method(:+).dup # => TypeError: allocator undefined for Method +Complex(0).dup # => TypeError: can't copy Complex ``` -Hence, there's no way these objects can be duplicated through `dup` or `clone`: +Active Support provides `duplicable?` to query an object about this: ```ruby -true.dup # => TypeError: can't dup TrueClass +"foo".duplicable? # => true +"".duplicable? # => true +Rational(1).duplicable? # => false +Complex(1).duplicable? # => false +1.method(:+).duplicable? # => false ``` -Some numbers which are not singletons are not duplicable either: +`duplicable?` matches Ruby's `dup` according to the Ruby version. + +So in 2.4: ```ruby -0.0.clone # => allocator undefined for Float -(2**1024).clone # => allocator undefined for Bignum +nil.dup # => nil +:my_symbol.dup # => :my_symbol +1.dup # => 1 + +nil.duplicable? # => true +:my_symbol.duplicable? # => true +1.duplicable? # => true ``` -Active Support provides `duplicable?` to programmatically query an object about this property: +Whereas in 2.2 and 2.3: ```ruby -"foo".duplicable? # => true -"".duplicable? # => true -0.0.duplicable? # => false -false.duplicable? # => false -``` +nil.dup # => TypeError: can't dup NilClass +:my_symbol.dup # => TypeError: can't dup Symbol +1.dup # => TypeError: can't dup Fixnum -By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, and module objects. +nil.duplicable? # => false +:my_symbol.duplicable? # => false +1.duplicable? # => false +``` WARNING: Any class can disallow duplication by removing `dup` and `clone` or raising exceptions from them. Thus only `rescue` can tell whether a given arbitrary object is duplicable. `duplicable?` depends on the hard-coded list above, but it is much faster than `rescue`. Use it only if you know the hard-coded list is enough in your use case. @@ -170,7 +189,7 @@ NOTE: Defined in `active_support/core_ext/object/duplicable.rb`. ### `deep_dup` -The `deep_dup` method returns deep copy of a given object. Normally, when you `dup` an object that contains other objects, Ruby does not `dup` them, so it creates a shallow copy of the object. If you have an array with a string, for example, it will look like this: +The `deep_dup` method returns a deep copy of a given object. Normally, when you `dup` an object that contains other objects, Ruby does not `dup` them, so it creates a shallow copy of the object. If you have an array with a string, for example, it will look like this: ```ruby array = ['string'] @@ -246,6 +265,13 @@ end @person.try { |p| "#{p.first_name} #{p.last_name}" } ``` +Note that `try` will swallow no-method errors, returning nil instead. If you want to protect against typos, use `try!` instead: + +```ruby +@number.try(:nest) # => nil +@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer +``` + NOTE: Defined in `active_support/core_ext/object/try.rb`. ### `class_eval(*args, &block)` @@ -347,7 +373,7 @@ end we get: ```ruby -current_user.to_query('user') # => user=357-john-smith +current_user.to_query('user') # => "user=357-john-smith" ``` This method escapes whatever is needed, both for the key and the value: @@ -359,7 +385,7 @@ account.to_query('company[name]') so its output is ready to be used in a query string. -Arrays return the result of applying `to_query` to each element with `_key_[]` as key, and join the result with "&": +Arrays return the result of applying `to_query` to each element with `key[]` as key, and join the result with "&": ```ruby [3.4, -45.6].to_query('sample') @@ -388,7 +414,7 @@ The method `with_options` provides a way to factor out common options in a serie Given a default options hash, `with_options` yields a proxy object to a block. Within the block, methods called on the proxy are forwarded to the receiver with their options merged. For example, you get rid of the duplication in: ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord has_many :customers, dependent: :destroy has_many :products, dependent: :destroy has_many :invoices, dependent: :destroy @@ -399,7 +425,7 @@ end this way: ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord with_options dependent: :destroy do |assoc| assoc.has_many :customers assoc.has_many :products @@ -451,7 +477,7 @@ NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`. #### `instance_variable_names` -The method `instance_variable_names` returns an array. Each name includes the "@" sign. +The method `instance_variable_names` returns an array. Each name includes the "@" sign. ```ruby class C @@ -465,7 +491,7 @@ C.new(0, 1).instance_variable_names # => ["@x", "@y"] NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`. -### Silencing Warnings, Streams, and Exceptions +### Silencing Warnings and Exceptions The methods `silence_warnings` and `enable_warnings` change the value of `$VERBOSE` accordingly for the duration of their block, and reset it afterwards: @@ -473,26 +499,10 @@ The methods `silence_warnings` and `enable_warnings` change the value of `$VERBO silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } ``` -You can silence any stream while a block runs with `silence_stream`: - -```ruby -silence_stream(STDOUT) do - # STDOUT is silent here -end -``` - -The `quietly` method addresses the common use case where you want to silence STDOUT and STDERR, even in subprocesses: +Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is not captured: ```ruby -quietly { system 'bundle install' } -``` - -For example, the railties test suite uses that one in a few places to prevent command messages from being echoed intermixed with the progress status. - -Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is reraised: - -```ruby -# If the user is locked the increment is lost, no big deal. +# If the user is locked, the increment is lost, no big deal. suppress(ActiveRecord::StaleObjectError) do current_user.increment! :visits end @@ -518,56 +528,6 @@ NOTE: Defined in `active_support/core_ext/object/inclusion.rb`. Extensions to `Module` ---------------------- -### `alias_method_chain` - -Using plain Ruby you can wrap methods with other methods, that's called _alias chaining_. - -For example, let's say you'd like params to be strings in functional tests, as they are in real requests, but still want the convenience of assigning integers and other kind of values. To accomplish that you could wrap `ActionController::TestCase#process` this way in `test/test_helper.rb`: - -```ruby -ActionController::TestCase.class_eval do - # save a reference to the original process method - alias_method :original_process, :process - - # now redefine process and delegate to original_process - def process(action, params=nil, session=nil, flash=nil, http_method='GET') - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - original_process(action, params, session, flash, http_method) - end -end -``` - -That's the method `get`, `post`, etc., delegate the work to. - -That technique has a risk, it could be the case that `:original_process` was taken. To try to avoid collisions people choose some label that characterizes what the chaining is about: - -```ruby -ActionController::TestCase.class_eval do - def process_with_stringified_params(...) - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(action, params, session, flash, http_method) - end - alias_method :process_without_stringified_params, :process - alias_method :process, :process_with_stringified_params -end -``` - -The method `alias_method_chain` provides a shortcut for that pattern: - -```ruby -ActionController::TestCase.class_eval do - def process_with_stringified_params(...) - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(action, params, session, flash, http_method) - end - alias_method_chain :process, :stringified_params -end -``` - -Rails uses `alias_method_chain` all over the code base. For example validations are added to `ActiveRecord::Base#save` by wrapping the method that way in a separate module specialized in validations. - -NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. - ### Attributes #### `alias_attribute` @@ -575,7 +535,7 @@ NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (one mnemonic is that they go in the same order as if you did an assignment): ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord # You can refer to the email column as "login". # This can be meaningful for authentication code. alias_attribute :login, :email @@ -639,8 +599,6 @@ module ActiveSupport mattr_accessor :load_once_paths mattr_accessor :autoloaded_constants mattr_accessor :explicitly_unloadable_constants - mattr_accessor :logger - mattr_accessor :log_activity mattr_accessor :constant_watch_stack mattr_accessor :constant_watch_stack_mutex end @@ -716,87 +674,6 @@ M.parents # => [X::Y, X, Object] NOTE: Defined in `active_support/core_ext/module/introspection.rb`. -### Constants - -The method `local_constants` returns the names of the constants that have been -defined in the receiver module: - -```ruby -module X - X1 = 1 - X2 = 2 - module Y - Y1 = :y1 - X1 = :overrides_X1_above - end -end - -X.local_constants # => [:X1, :X2, :Y] -X::Y.local_constants # => [:Y1, :X1] -``` - -The names are returned as symbols. - -NOTE: Defined in `active_support/core_ext/module/introspection.rb`. - -#### Qualified Constant Names - -The standard methods `const_defined?`, `const_get` , and `const_set` accept -bare constant names. Active Support extends this API to be able to pass -relative qualified constant names. - -The new methods are `qualified_const_defined?`, `qualified_const_get`, and -`qualified_const_set`. Their arguments are assumed to be qualified constant -names relative to their receiver: - -```ruby -Object.qualified_const_defined?("Math::PI") # => true -Object.qualified_const_get("Math::PI") # => 3.141592653589793 -Object.qualified_const_set("Math::Phi", 1.618034) # => 1.618034 -``` - -Arguments may be bare constant names: - -```ruby -Math.qualified_const_get("E") # => 2.718281828459045 -``` - -These methods are analogous to their built-in counterparts. In particular, -`qualified_constant_defined?` accepts an optional second argument to be -able to say whether you want the predicate to look in the ancestors. -This flag is taken into account for each constant in the expression while -walking down the path. - -For example, given - -```ruby -module M - X = 1 -end - -module N - class C - include M - end -end -``` - -`qualified_const_defined?` behaves this way: - -```ruby -N.qualified_const_defined?("C::X", false) # => false -N.qualified_const_defined?("C::X", true) # => true -N.qualified_const_defined?("C::X") # => true -``` - -As the last example implies, the second argument defaults to true, -as in `const_defined?`. - -For coherence with the built-in methods only relative paths are accepted. -Absolute qualified constant names like `::Math::PI` raise `NameError`. - -NOTE: Defined in `active_support/core_ext/module/qualified_const.rb`. - ### Reachable A named module is reachable if it is stored in its corresponding constant. It means you can reach the module object via the constant. @@ -883,7 +760,7 @@ The macro `delegate` offers an easy way to forward methods. Let's imagine that users in some application have login information in the `User` model but name and other data in a separate `Profile` model: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile end ``` @@ -891,7 +768,7 @@ end With that configuration you get a user's name via their profile, `user.profile.name`, but it could be handy to still be able to access such attribute directly: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile def name @@ -903,7 +780,7 @@ end That is what `delegate` does for you: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile delegate :name, to: :profile @@ -1011,7 +888,7 @@ self.default_params = { }.freeze ``` -They can be also accessed and overridden at the instance level. +They can also be accessed and overridden at the instance level. ```ruby A.x = 1 @@ -1044,7 +921,8 @@ class A class_attribute :x, instance_reader: false end -A.new.x = 1 # NoMethodError +A.new.x = 1 +A.new.x # NoMethodError ``` For convenience `class_attribute` also defines an instance predicate which is the double negation of what the instance reader returns. In the examples above it would be called `x?`. @@ -1251,7 +1129,7 @@ Calling `dup` or `clone` on safe strings yields safe strings. The method `remove` will remove all occurrences of the pattern: ```ruby -"Hello World".remove(/Hello /) => "World" +"Hello World".remove(/Hello /) # => "World" ``` There's also the destructive version `String#remove!`. @@ -1268,7 +1146,7 @@ The method `squish` strips leading and trailing whitespace, and substitutes runs There's also the destructive version `String#squish!`. -Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E). +Note that it handles both ASCII and Unicode whitespace. NOTE: Defined in `active_support/core_ext/string/filters.rb`. @@ -1310,6 +1188,38 @@ In above examples "dear" gets cut first, but then `:separator` prevents it. NOTE: Defined in `active_support/core_ext/string/filters.rb`. +### `truncate_words` + +The method `truncate_words` returns a copy of its receiver truncated after a given number of words: + +```ruby +"Oh dear! Oh dear! I shall be late!".truncate_words(4) +# => "Oh dear! Oh dear!..." +``` + +Ellipsis can be customized with the `:omission` option: + +```ruby +"Oh dear! Oh dear! I shall be late!".truncate_words(4, omission: '…') +# => "Oh dear! Oh dear!…" +``` + +Pass a `:separator` to truncate the string at a natural break: + +```ruby +"Oh dear! Oh dear! I shall be late!".truncate_words(3, separator: '!') +# => "Oh dear! Oh dear! I shall be late..." +``` + +The option `:separator` can be a regexp: + +```ruby +"Oh dear! Oh dear! I shall be late!".truncate_words(4, separator: /\s/) +# => "Oh dear! Oh dear!..." +``` + +NOTE: Defined in `active_support/core_ext/string/filters.rb`. + ### `inquiry` The `inquiry` method converts a string into a `StringInquirer` object making equality checks prettier. @@ -1415,7 +1325,7 @@ Returns the substring of the string starting at position `position`: "hello".from(0) # => "hello" "hello".from(2) # => "llo" "hello".from(-2) # => "lo" -"hello".from(10) # => "" if < 1.9, nil in 1.9 +"hello".from(10) # => nil ``` NOTE: Defined in `active_support/core_ext/string/access.rb`. @@ -1661,19 +1571,6 @@ Given a string with a qualified constant reference expression, `deconstantize` r "Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel" ``` -Active Support for example uses this method in `Module#qualified_const_set`: - -```ruby -def qualified_const_set(path, value) - QualifiedConstUtils.raise_if_absolute(path) - - const_name = path.demodulize - mod_name = path.deconstantize - mod = mod_name.empty? ? self : qualified_const_get(mod_name) - mod.const_set(const_name, value) -end -``` - NOTE: Defined in `active_support/core_ext/string/inflections.rb`. #### `parameterize` @@ -1685,6 +1582,20 @@ The method `parameterize` normalizes its receiver in a way that can be used in p "Kurt Gödel".parameterize # => "kurt-godel" ``` +To preserve the case of the string, set the `preserve_case` argument to true. By default, `preserve_case` is set to false. + +```ruby +"John Smith".parameterize(preserve_case: true) # => "John-Smith" +"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel" +``` + +To use a custom separator, override the `separator` argument. + +```ruby +"John Smith".parameterize(separator: "_") # => "john\_smith" +"Kurt Gödel".parameterize(separator: "_") # => "kurt\_godel" +``` + In fact, the result string is wrapped in an instance of `ActiveSupport::Multibyte::Chars`. NOTE: Defined in `active_support/core_ext/string/inflections.rb`. @@ -1728,7 +1639,7 @@ NOTE: Defined in `active_support/core_ext/string/inflections.rb`. The method `constantize` resolves the constant reference expression in its receiver: ```ruby -"Fixnum".constantize # => Fixnum +"Integer".constantize # => Integer module M X = 1 @@ -1801,16 +1712,14 @@ attribute names: ```ruby def full_messages - full_messages = [] - - each do |attribute, messages| - ... - attr_name = attribute.to_s.gsub('.', '_').humanize - attr_name = @base.class.human_attribute_name(attribute, default: attr_name) - ... - end + map { |attribute, message| full_message(attribute, message) } +end - full_messages +def full_message + ... + attr_name = attribute.to_s.tr('.', '_').humanize + attr_name = @base.class.human_attribute_name(attribute, default: attr_name) + ... end ``` @@ -1849,15 +1758,15 @@ The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wr ```ruby "2010-07-27".to_date # => Tue, 27 Jul 2010 -"2010-07-27 23:37:00".to_time # => Tue Jul 27 23:37:00 UTC 2010 +"2010-07-27 23:37:00".to_time # => 2010-07-27 23:37:00 +0200 "2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000 ``` `to_time` receives an optional argument `:utc` or `:local`, to indicate which time zone you want the time in: ```ruby -"2010-07-27 23:42:00".to_time(:utc) # => Tue Jul 27 23:42:00 UTC 2010 -"2010-07-27 23:42:00".to_time(:local) # => Tue Jul 27 23:42:00 +0200 2010 +"2010-07-27 23:42:00".to_time(:utc) # => 2010-07-27 23:42:00 UTC +"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200 ``` Default is `:utc`. @@ -1920,23 +1829,7 @@ as well as adding or subtracting their results from a Time object. For example: (4.months + 5.years).from_now ``` -While these methods provide precise calculation when used as in the examples above, care -should be taken to note that this is not true if the result of `months', `years', etc is -converted before use: - -```ruby -# equivalent to 30.days.to_i.from_now -1.month.to_i.from_now - -# equivalent to 365.25.days.to_f.from_now -1.year.to_f.from_now -``` - -In such cases, Ruby's core [Date](http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html) and -[Time](http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html) should be used for precision -date and time arithmetic. - -NOTE: Defined in `active_support/core_ext/numeric/time.rb`. +NOTE: Defined in `active_support/core_ext/numeric/time.rb` ### Formatting @@ -2003,12 +1896,14 @@ Produce a string representation of a number rounded to a precision: Produce a string representation of a number as a human-readable number of bytes: ```ruby -123.to_s(:human_size) # => 123 Bytes -1234.to_s(:human_size) # => 1.21 KB -12345.to_s(:human_size) # => 12.1 KB -1234567.to_s(:human_size) # => 1.18 MB -1234567890.to_s(:human_size) # => 1.15 GB -1234567890123.to_s(:human_size) # => 1.12 TB +123.to_s(:human_size) # => 123 Bytes +1234.to_s(:human_size) # => 1.21 KB +12345.to_s(:human_size) # => 12.1 KB +1234567.to_s(:human_size) # => 1.18 MB +1234567890.to_s(:human_size) # => 1.15 GB +1234567890123.to_s(:human_size) # => 1.12 TB +1234567890123456.to_s(:human_size) # => 1.1 PB +1234567890123456789.to_s(:human_size) # => 1.07 EB ``` Produce a string representation of a number in human-readable words: @@ -2073,30 +1968,22 @@ Extensions to `BigDecimal` -------------------------- ### `to_s` -The method `to_s` is aliased to `to_formatted_s`. This provides a convenient way to display a BigDecimal value in floating-point notation: +The method `to_s` provides a default specifier of "F". This means that a simple call to `to_s` will result in floating point representation instead of engineering notation: ```ruby BigDecimal.new(5.00, 6).to_s # => "5.0" ``` -### `to_formatted_s` - -Te method `to_formatted_s` provides a default specifier of "F". This means that a simple call to `to_formatted_s` or `to_s` will result in floating point representation instead of engineering notation: - -```ruby -BigDecimal.new(5.00, 6).to_formatted_s # => "5.0" -``` - and that symbol specifiers are also supported: ```ruby -BigDecimal.new(5.00, 6).to_formatted_s(:db) # => "5.0" +BigDecimal.new(5.00, 6).to_s(:db) # => "5.0" ``` Engineering notation is still supported: ```ruby -BigDecimal.new(5.00, 6).to_formatted_s("e") # => "0.5E1" +BigDecimal.new(5.00, 6).to_s("e") # => "0.5E1" ``` Extensions to `Enumerable` @@ -2116,7 +2003,7 @@ Addition only assumes the elements respond to `+`: ```ruby [[1, 2], [2, 3], [3, 4]].sum # => [1, 2, 2, 3, 3, 4] %w(foo bar baz).sum # => "foobarbaz" -{a: 1, b: 2, c: 3}.sum # => [:b, 2, :c, 3, :a, 1] +{a: 1, b: 2, c: 3}.sum # => [:b, 2, :c, 3, :a, 1] ``` The sum of an empty collection is zero by default, but this is customizable: @@ -2184,6 +2071,27 @@ to_visit << node if visited.exclude?(node) NOTE: Defined in `active_support/core_ext/enumerable.rb`. +### `without` + +The method `without` returns a copy of an enumerable with the specified elements +removed: + +```ruby +["David", "Rafael", "Aaron", "Todd"].without("Aaron", "Todd") # => ["David", "Rafael"] +``` + +NOTE: Defined in `active_support/core_ext/enumerable.rb`. + +### `pluck` + +The method `pluck` returns an array based on the given key: + +```ruby +[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"] +``` + +NOTE: Defined in `active_support/core_ext/enumerable.rb`. + Extensions to `Array` --------------------- @@ -2192,22 +2100,22 @@ Extensions to `Array` Active Support augments the API of arrays to ease certain ways of accessing them. For example, `to` returns the subarray of elements up to the one at the passed index: ```ruby -%w(a b c d).to(2) # => %w(a b c) +%w(a b c d).to(2) # => ["a", "b", "c"] [].to(7) # => [] ``` Similarly, `from` returns the tail from the element at the passed index to the end. If the index is greater than the length of the array, it returns an empty array. ```ruby -%w(a b c d).from(2) # => %w(c d) +%w(a b c d).from(2) # => ["c", "d"] %w(a b c d).from(10) # => [] [].from(0) # => [] ``` -The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element (`first` is built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. +The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element, as do `second_to_last` and `third_to_last` (`first` and `last` are built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. ```ruby -%w(a b c d).third # => c +%w(a b c d).third # => "c" %w(a b c d).fifth # => nil ``` @@ -2220,7 +2128,7 @@ NOTE: Defined in `active_support/core_ext/array/access.rb`. This method is an alias of `Array#unshift`. ```ruby -%w(a b c d).prepend('e') # => %w(e a b c d) +%w(a b c d).prepend('e') # => ["e", "a", "b", "c", "d"] [].prepend(10) # => [10] ``` @@ -2231,8 +2139,8 @@ NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. This method is an alias of `Array#<<`. ```ruby -%w(a b c d).append('e') # => %w(a b c d e) -[].append([1,2]) # => [[1,2]] +%w(a b c d).append('e') # => ["a", "b", "c", "d", "e"] +[].append([1,2]) # => [[1, 2]] ``` NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. @@ -2338,7 +2246,7 @@ Contributor.limit(2).order(:rank).to_xml To do so it sends `to_xml` to every item in turn, and collects the results under a root node. All items must respond to `to_xml`, an exception is raised otherwise. -By default, the name of the root element is the underscorized and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with `is_a?`) and they are not hashes. In the example above that's "contributors". +By default, the name of the root element is the underscored and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with `is_a?`) and they are not hashes. In the example above that's "contributors". If there's any element that does not belong to the type of the first one the root node becomes "objects": @@ -2419,7 +2327,7 @@ The method `Array.wrap` wraps its argument in an array unless it is already an a Specifically: -* If the argument is `nil` an empty list is returned. +* If the argument is `nil` an empty array is returned. * Otherwise, if the argument responds to `to_ary` it is invoked, and if the value of `to_ary` is not `nil`, it is returned. * Otherwise, an array with the argument as its single element is returned. @@ -2431,9 +2339,9 @@ Array.wrap(0) # => [0] This method is similar in purpose to `Kernel#Array`, but there are some differences: -* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns `nil` right away. +* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns an array with the argument as its single element right away. * If the returned value from `to_ary` is neither `nil` nor an `Array` object, `Kernel#Array` raises an exception, while `Array.wrap` does not, it just returns the value. -* It does not call `to_a` on the argument, though special-cases `nil` to return an empty array. +* It does not call `to_a` on the argument, if the argument does not respond to +to_ary+ it returns an array with the argument as its single element. The last point is particularly worth comparing for some enumerables: @@ -2456,7 +2364,7 @@ NOTE: Defined in `active_support/core_ext/array/wrap.rb`. ### Duplicating -The method `Array.deep_dup` duplicates itself and all objects inside +The method `Array#deep_dup` duplicates itself and all objects inside recursively with Active Support method `Object#deep_dup`. It works like `Array#map` with sending `deep_dup` method to each object inside. ```ruby @@ -2600,8 +2508,7 @@ To do so, the method loops over the pairs and builds nodes that depend on the _v ```ruby XML_TYPE_NAMES = { "Symbol" => "symbol", - "Fixnum" => "integer", - "Bignum" => "integer", + "Integer" => "integer", "BigDecimal" => "decimal", "Float" => "float", "TrueClass" => "boolean", @@ -2678,7 +2585,7 @@ NOTE: Defined in `active_support/core_ext/hash/deep_merge.rb`. ### Deep duplicating -The method `Hash.deep_dup` duplicates itself and all keys and values +The method `Hash#deep_dup` duplicates itself and all keys and values inside recursively with Active Support method `Object#deep_dup`. It works like `Enumerator#each_with_object` with sending `deep_dup` method to each pair inside. ```ruby @@ -2721,7 +2628,7 @@ The method `transform_keys` accepts a block and returns a hash that has applied ```ruby {nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase } -# => {"" => nil, "A" => :a, "1" => 1} +# => {"" => nil, "1" => 1, "A" => :a} ``` In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: @@ -2763,7 +2670,7 @@ The method `stringify_keys` returns a hash that has a stringified version of the ```ruby {nil => nil, 1 => 1, a: :a}.stringify_keys -# => {"" => nil, "a" => :a, "1" => 1} +# => {"" => nil, "1" => 1, "a" => :a} ``` In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: @@ -2805,7 +2712,7 @@ The method `symbolize_keys` returns a hash that has a symbolized version of the ```ruby {nil => nil, 1 => 1, "a" => "a"}.symbolize_keys -# => {1=>1, nil=>nil, :a=>"a"} +# => {nil=>nil, 1=>1, :a=>"a"} ``` WARNING. Note in the previous example only one key was symbolized. @@ -2862,13 +2769,27 @@ Active Record does not accept unknown options when building associations, for ex NOTE: Defined in `active_support/core_ext/hash/keys.rb`. +### Working with Values + +#### `transform_values` && `transform_values!` + +The method `transform_values` accepts a block and returns a hash that has applied the block operations to each of the values in the receiver. + +```ruby +{ nil => nil, 1 => 1, :x => :a }.transform_values { |value| value.to_s.upcase } +# => {nil=>"", 1=>"1", :x=>"A"} +``` +There's also the bang variant `transform_values!` that applies the block operations to values in the very receiver. + +NOTE: Defined in `active_support/core_ext/hash/transform_values.rb`. + ### Slicing Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes: ```ruby {a: 1, b: 2, c: 3}.slice(:a, :c) -# => {:c=>3, :a=>1} +# => {:a=>1, :c=>3} {a: 1, b: 2, c: 3}.slice(:b, :X) # => {:b=>2} # non-existing keys are ignored @@ -2962,6 +2883,24 @@ end NOTE: Defined in `active_support/core_ext/regexp.rb`. +### `match?` + +Rails implements `Regexp#match?` for Ruby versions prior to 2.4: + +```ruby +/oo/.match?('foo') # => true +/oo/.match?('bar') # => false +/oo/.match?('foo', 1) # => true +``` + +The backport has the same interface and lack of side-effects in the caller like +not setting `$1` and friends, but it does not have the speed benefits. Its +purpose is to be able to write 2.4 compatible code. Rails itself uses this +predicate internally for example. + +Active Support defines `Regexp#match?` only if not present, so code running +under 2.4 or later does run the original one and gets the performance boost. + Extensions to `Range` --------------------- @@ -3017,53 +2956,6 @@ The method `Range#overlaps?` says whether any two given ranges have non-void int NOTE: Defined in `active_support/core_ext/range/overlaps.rb`. -Extensions to `Proc` --------------------- - -### `bind` - -As you surely know Ruby has an `UnboundMethod` class whose instances are methods that belong to the limbo of methods without a self. The method `Module#instance_method` returns an unbound method for example: - -```ruby -Hash.instance_method(:delete) # => # -``` - -An unbound method is not callable as is, you need to bind it first to an object with `bind`: - -```ruby -clear = Hash.instance_method(:clear) -clear.bind({a: 1}).call # => {} -``` - -Active Support defines `Proc#bind` with an analogous purpose: - -```ruby -Proc.new { size }.bind([]).call # => 0 -``` - -As you see that's callable and bound to the argument, the return value is indeed a `Method`. - -NOTE: To do so `Proc#bind` actually creates a method under the hood. If you ever see a method with a weird name like `__bind_1256598120_237302` in a stack trace you know now where it comes from. - -Action Pack uses this trick in `rescue_from` for example, which accepts the name of a method and also a proc as callbacks for a given rescued exception. It has to call them in either case, so a bound method is returned by `handler_for_rescue`, thus simplifying the code in the caller: - -```ruby -def handler_for_rescue(exception) - _, rescuer = Array(rescue_handlers).reverse.detect do |klass_name, handler| - ... - end - - case rescuer - when Symbol - method(rescuer) - when Proc - rescuer.bind(self) - end -end -``` - -NOTE: Defined in `active_support/core_ext/proc.rb`. - Extensions to `Date` -------------------- @@ -3075,7 +2967,7 @@ INFO: The following calculation methods have edge cases in October 1582, since d #### `Date.current` -Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Date.current`. +Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, `future?`, `on_weekday?` and `on_weekend?`, all of them relative to `Date.current`. When making Date comparisons using methods which honor the user time zone, make sure to use `Date.current` and not `Date.today`. There are cases where the user time zone might be in the future compared to the system time zone, which `Date.today` uses by default. This means `Date.today` may equal `Date.yesterday`. @@ -3464,6 +3356,8 @@ years_ago years_since prev_year (last_year) next_year +on_weekday? +on_weekend? ``` The following methods are reimplemented so you do **not** need to load `active_support/core_ext/date/calculations.rb` for these ones: @@ -3650,6 +3544,8 @@ years_ago years_since prev_year (last_year) next_year +on_weekday? +on_weekend? ``` They are analogous. Please refer to their documentation above and take into account the following differences: @@ -3785,50 +3681,6 @@ WARNING. If the argument is an `IO` it needs to respond to `rewind` to be able t NOTE: Defined in `active_support/core_ext/marshal.rb`. -Extensions to `Logger` ----------------------- - -### `around_[level]` - -Takes two arguments, a `before_message` and `after_message` and calls the current level method on the `Logger` instance, passing in the `before_message`, then the specified message, then the `after_message`: - -```ruby -logger = Logger.new("log/development.log") -logger.around_info("before", "after") { |logger| logger.info("during") } -``` - -### `silence` - -Silences every log level lesser to the specified one for the duration of the given block. Log level orders are: debug, info, error and fatal. - -```ruby -logger = Logger.new("log/development.log") -logger.silence(Logger::INFO) do - logger.debug("In space, no one can hear you scream.") - logger.info("Scream all you want, small mailman!") -end -``` - -### `datetime_format=` - -Modifies the datetime format output by the formatter class associated with this logger. If the formatter class does not have a `datetime_format` method then this is ignored. - -```ruby -class Logger::FormatWithTime < Logger::Formatter - cattr_accessor(:datetime_format) { "%Y%m%d%H%m%S" } - - def self.call(severity, timestamp, progname, msg) - "#{timestamp.strftime(datetime_format)} -- #{String === msg ? msg : msg.inspect}\n" - end -end - -logger = Logger.new("log/development.log") -logger.formatter = Logger::FormatWithTime -logger.info("<- is the current time") -``` - -NOTE: Defined in `active_support/core_ext/logger.rb`. - Extensions to `NameError` ------------------------- @@ -3845,7 +3697,7 @@ def default_helper_module! module_name = name.sub(/Controller$/, '') module_path = module_name.underscore helper module_path -rescue MissingSourceFile => e +rescue LoadError => e raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" @@ -3857,7 +3709,7 @@ NOTE: Defined in `active_support/core_ext/name_error.rb`. Extensions to `LoadError` ------------------------- -Active Support adds `is_missing?` to `LoadError`, and also assigns that class to the constant `MissingSourceFile` for backwards compatibility. +Active Support adds `is_missing?` to `LoadError`. Given a path name `is_missing?` tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension). @@ -3868,7 +3720,7 @@ def default_helper_module! module_name = name.sub(/Controller$/, '') module_path = module_name.underscore helper module_path -rescue MissingSourceFile => e +rescue LoadError => e raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" diff --git a/source/active_support_instrumentation.md b/source/active_support_instrumentation.md index 7033947..03c9183 100644 --- a/source/active_support_instrumentation.md +++ b/source/active_support_instrumentation.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Support Instrumentation ============================== @@ -17,7 +19,7 @@ After reading this guide, you will know: Introduction to instrumentation ------------------------------- -The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. +The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the [Rails framework](#rails-framework-hooks). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken. @@ -110,6 +112,7 @@ Action Controller | `:controller` | The controller name | | `:action` | The action | | `:params` | Hash of request parameters without any filtered parameter | +| `:headers` | Request headers | | `:format` | html/js/json/xml etc | | `:method` | HTTP request verb | | `:path` | Request path | @@ -119,6 +122,7 @@ Action Controller controller: "PostsController", action: "new", params: { "action" => "new", "controller" => "posts" }, + headers: #, format: :html, method: "GET", path: "/posts/new" @@ -132,16 +136,20 @@ Action Controller | `:controller` | The controller name | | `:action` | The action | | `:params` | Hash of request parameters without any filtered parameter | +| `:headers` | Request headers | | `:format` | html/js/json/xml etc | | `:method` | HTTP request verb | | `:path` | Request path | +| `:status` | HTTP status code | | `:view_runtime` | Amount spent in view in ms | +| `:db_runtime` | Amount spent executing database queries in ms | ```ruby { controller: "PostsController", action: "index", params: {"action" => "index", "controller" => "posts"}, + headers: #, format: :html, method: "GET", path: "/posts", @@ -214,7 +222,25 @@ Action View ```ruby { - identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb", + identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb" +} +``` + +### render_collection.action_view + +| Key | Value | +| ------------- | ------------------------------------- | +| `:identifier` | Full path to template | +| `:count` | Size of collection | +| `:cache_hits` | Number of partials fetched from cache | + +`:cache_hits` is only included if the collection is rendered with `cached: true`. + +```ruby +{ + identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb", + count: 3, + cache_hits: 0 } ``` @@ -223,11 +249,13 @@ Active Record ### sql.active_record -| Key | Value | -| ------------ | --------------------- | -| `:sql` | SQL statement | -| `:name` | Name of the operation | -| `:object_id` | `self.object_id` | +| Key | Value | +| ---------------- | ---------------------------------------- | +| `:sql` | SQL statement | +| `:name` | Name of the operation | +| `:connection_id` | `self.object_id` | +| `:binds` | Bind parameters | +| `:cached` | `true` is added when cached queries used | INFO. The adapters will add their own data as well. @@ -240,13 +268,19 @@ INFO. The adapters will add their own data as well. } ``` -### identity.active_record +### instantiation.active_record | Key | Value | | ---------------- | ----------------------------------------- | -| `:line` | Primary Key of object in the identity map | -| `:name` | Record's class | -| `:connection_id` | `self.object_id` | +| `:record_count` | Number of records that instantiated | +| `:class_name` | Record's class | + +```ruby +{ + record_count: 1, + class_name: "User" +} +``` Action Mailer ------------- @@ -303,17 +337,6 @@ Action Mailer } ``` -ActiveResource --------------- - -### request.active_resource - -| Key | Value | -| -------------- | -------------------- | -| `:method` | HTTP method | -| `:request_uri` | Complete URI | -| `:result` | HTTP response object | - Active Support -------------- @@ -396,6 +419,38 @@ INFO. Cache stores may add their own keys } ``` +Active Job +-------- + +### enqueue_at.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | + +### enqueue.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | + +### perform_start.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | + +### perform.active_job + +| Key | Value | +| ------------ | -------------------------------------- | +| `:adapter` | QueueAdapter object processing the job | +| `:job` | Job object | + + Railties -------- @@ -426,7 +481,7 @@ The block receives the following arguments: * The name of the event * Time when it started * Time when it finished -* An unique ID for this event +* A unique ID for this event * The payload (described in previous sections) ```ruby diff --git a/source/api_app.md b/source/api_app.md new file mode 100644 index 0000000..f373d31 --- /dev/null +++ b/source/api_app.md @@ -0,0 +1,424 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + + +Using Rails for API-only Applications +===================================== + +In this guide you will learn: + +* What Rails provides for API-only applications +* How to configure Rails to start without any browser features +* How to decide which middleware you will want to include +* How to decide which modules to use in your controller + +-------------------------------------------------------------------------------- + +What is an API Application? +--------------------------- + +Traditionally, when people said that they used Rails as an "API", they meant +providing a programmatically accessible API alongside their web application. +For example, GitHub provides [an API](http://developer.github.com) that you +can use from your own custom clients. + +With the advent of client-side frameworks, more developers are using Rails to +build a back-end that is shared between their web application and other native +applications. + +For example, Twitter uses its [public API](https://dev.twitter.com) in its web +application, which is built as a static site that consumes JSON resources. + +Instead of using Rails to generate HTML that communicates with the server +through forms and links, many developers are treating their web application as +just an API client delivered as HTML with JavaScript that consumes a JSON API. + +This guide covers building a Rails application that serves JSON resources to an +API client, including client-side frameworks. + +Why Use Rails for JSON APIs? +---------------------------- + +The first question a lot of people have when thinking about building a JSON API +using Rails is: "isn't using Rails to spit out some JSON overkill? Shouldn't I +just use something like Sinatra?". + +For very simple APIs, this may be true. However, even in very HTML-heavy +applications, most of an application's logic lives outside of the view +layer. + +The reason most people use Rails is that it provides a set of defaults that +allows developers to get up and running quickly, without having to make a lot of trivial +decisions. + +Let's take a look at some of the things that Rails provides out of the box that are +still applicable to API applications. + +Handled at the middleware layer: + +- Reloading: Rails applications support transparent reloading. This works even if + your application gets big and restarting the server for every request becomes + non-viable. +- Development Mode: Rails applications come with smart defaults for development, + making development pleasant without compromising production-time performance. +- Test Mode: Ditto development mode. +- Logging: Rails applications log every request, with a level of verbosity + appropriate for the current mode. Rails logs in development include information + about the request environment, database queries, and basic performance + information. +- Security: Rails detects and thwarts [IP spoofing + attacks](http://en.wikipedia.org/wiki/IP_address_spoofing) and handles + cryptographic signatures in a [timing + attack](http://en.wikipedia.org/wiki/Timing_attack) aware way. Don't know what + an IP spoofing attack or a timing attack is? Exactly. +- Parameter Parsing: Want to specify your parameters as JSON instead of as a + URL-encoded String? No problem. Rails will decode the JSON for you and make + it available in `params`. Want to use nested URL-encoded parameters? That + works too. +- Conditional GETs: Rails handles conditional `GET` (`ETag` and `Last-Modified`) + processing request headers and returning the correct response headers and status + code. All you need to do is use the + [`stale?`](http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F) + check in your controller, and Rails will handle all of the HTTP details for you. +- HEAD requests: Rails will transparently convert `HEAD` requests into `GET` ones, + and return just the headers on the way out. This makes `HEAD` work reliably in + all Rails APIs. + +While you could obviously build these up in terms of existing Rack middleware, +this list demonstrates that the default Rails middleware stack provides a lot +of value, even if you're "just generating JSON". + +Handled at the Action Pack layer: + +- Resourceful Routing: If you're building a RESTful JSON API, you want to be + using the Rails router. Clean and conventional mapping from HTTP to controllers + means not having to spend time thinking about how to model your API in terms + of HTTP. +- URL Generation: The flip side of routing is URL generation. A good API based + on HTTP includes URLs (see [the GitHub Gist API](http://developer.github.com/v3/gists/) + for an example). +- Header and Redirection Responses: `head :no_content` and + `redirect_to user_url(https://github.com/current_user)` come in handy. Sure, you could manually + add the response headers, but why? +- Caching: Rails provides page, action and fragment caching. Fragment caching + is especially helpful when building up a nested JSON object. +- Basic, Digest, and Token Authentication: Rails comes with out-of-the-box support + for three kinds of HTTP authentication. +- Instrumentation: Rails has an instrumentation API that triggers registered + handlers for a variety of events, such as action processing, sending a file or + data, redirection, and database queries. The payload of each event comes with + relevant information (for the action processing event, the payload includes + the controller, action, parameters, request format, request method and the + request's full path). +- Generators: It is often handy to generate a resource and get your model, + controller, test stubs, and routes created for you in a single command for + further tweaking. Same for migrations and others. +- Plugins: Many third-party libraries come with support for Rails that reduce + or eliminate the cost of setting up and gluing together the library and the + web framework. This includes things like overriding default generators, adding + Rake tasks, and honoring Rails choices (like the logger and cache back-end). + +Of course, the Rails boot process also glues together all registered components. +For example, the Rails boot process is what uses your `config/database.yml` file +when configuring Active Record. + +**The short version is**: you may not have thought about which parts of Rails +are still applicable even if you remove the view layer, but the answer turns out +to be most of it. + +The Basic Configuration +----------------------- + +If you're building a Rails application that will be an API server first and +foremost, you can start with a more limited subset of Rails and add in features +as needed. + +### Creating a new application + +You can generate a new api Rails app: + +```bash +$ rails new my_api --api +``` + +This will do three main things for you: + +- Configure your application to start with a more limited set of middleware + than normal. Specifically, it will not include any middleware primarily useful + for browser applications (like cookies support) by default. +- Make `ApplicationController` inherit from `ActionController::API` instead of + `ActionController::Base`. As with middleware, this will leave out any Action + Controller modules that provide functionalities primarily used by browser + applications. +- Configure the generators to skip generating views, helpers and assets when + you generate a new resource. + +### Changing an existing application + +If you want to take an existing application and make it an API one, read the +following steps. + +In `config/application.rb` add the following line at the top of the `Application` +class definition: + +```ruby +config.api_only = true +``` + +In `config/environments/development.rb`, set `config.debug_exception_response_format` +to configure the format used in responses when errors occur in development mode. + +To render an HTML page with debugging information, use the value `:default`. + +```ruby +config.debug_exception_response_format = :default +``` + +To render debugging information preserving the response format, use the value `:api`. + +```ruby +config.debug_exception_response_format = :api +``` + +By default, `config.debug_exception_response_format` is set to `:api`, when `config.api_only` is set to true. + +Finally, inside `app/controllers/application_controller.rb`, instead of: + +```ruby +class ApplicationController < ActionController::Base +end +``` + +do: + +```ruby +class ApplicationController < ActionController::API +end +``` + +Choosing Middleware +-------------------- + +An API application comes with the following middleware by default: + +- `Rack::Sendfile` +- `ActionDispatch::Static` +- `ActionDispatch::Executor` +- `ActiveSupport::Cache::Strategy::LocalCache::Middleware` +- `Rack::Runtime` +- `ActionDispatch::RequestId` +- `Rails::Rack::Logger` +- `ActionDispatch::ShowExceptions` +- `ActionDispatch::DebugExceptions` +- `ActionDispatch::RemoteIp` +- `ActionDispatch::Reloader` +- `ActionDispatch::Callbacks` +- `ActiveRecord::Migration::CheckPending` +- `Rack::Head` +- `Rack::ConditionalGet` +- `Rack::ETag` + +See the [internal middleware](rails_on_rack.html#internal-middleware-stack) +section of the Rack guide for further information on them. + +Other plugins, including Active Record, may add additional middleware. In +general, these middleware are agnostic to the type of application you are +building, and make sense in an API-only Rails application. + +You can get a list of all middleware in your application via: + +```bash +$ rails middleware +``` + +### Using the Cache Middleware + +By default, Rails will add a middleware that provides a cache store based on +the configuration of your application (memcache by default). This means that +the built-in HTTP cache will rely on it. + +For instance, using the `stale?` method: + +```ruby +def show + @post = Post.find(params[:id]) + + if stale?(last_modified: @post.updated_at) + render json: @post + end +end +``` + +The call to `stale?` will compare the `If-Modified-Since` header in the request +with `@post.updated_at`. If the header is newer than the last modified, this +action will return a "304 Not Modified" response. Otherwise, it will render the +response and include a `Last-Modified` header in it. + +Normally, this mechanism is used on a per-client basis. The cache middleware +allows us to share this caching mechanism across clients. We can enable +cross-client caching in the call to `stale?`: + +```ruby +def show + @post = Post.find(params[:id]) + + if stale?(last_modified: @post.updated_at, public: true) + render json: @post + end +end +``` + +This means that the cache middleware will store off the `Last-Modified` value +for a URL in the Rails cache, and add an `If-Modified-Since` header to any +subsequent inbound requests for the same URL. + +Think of it as page caching using HTTP semantics. + +### Using Rack::Sendfile + +When you use the `send_file` method inside a Rails controller, it sets the +`X-Sendfile` header. `Rack::Sendfile` is responsible for actually sending the +file. + +If your front-end server supports accelerated file sending, `Rack::Sendfile` +will offload the actual file sending work to the front-end server. + +You can configure the name of the header that your front-end server uses for +this purpose using `config.action_dispatch.x_sendfile_header` in the appropriate +environment's configuration file. + +You can learn more about how to use `Rack::Sendfile` with popular +front-ends in [the Rack::Sendfile +documentation](http://rubydoc.info/github/rack/rack/master/Rack/Sendfile). + +Here are some values for this header for some popular servers, once these servers are configured to support +accelerated file sending: + +```ruby +# Apache and lighttpd +config.action_dispatch.x_sendfile_header = "X-Sendfile" + +# Nginx +config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" +``` + +Make sure to configure your server to support these options following the +instructions in the `Rack::Sendfile` documentation. + +### Using ActionDispatch::Request + +`ActionDispatch::Request#params` will take parameters from the client in the JSON +format and make them available in your controller inside `params`. + +To use this, your client will need to make a request with JSON-encoded parameters +and specify the `Content-Type` as `application/json`. + +Here's an example in jQuery: + +```javascript +jQuery.ajax({ + type: 'POST', + url: '/people', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({ person: { firstName: "Yehuda", lastName: "Katz" } }), + success: function(json) { } +}); +``` + +`ActionDispatch::Request` will see the `Content-Type` and your parameters +will be: + +```ruby +{ :person => { :firstName => "Yehuda", :lastName => "Katz" } } +``` + +### Other Middleware + +Rails ships with a number of other middleware that you might want to use in an +API application, especially if one of your API clients is the browser: + +- `Rack::MethodOverride` +- `ActionDispatch::Cookies` +- `ActionDispatch::Flash` +- For session management + * `ActionDispatch::Session::CacheStore` + * `ActionDispatch::Session::CookieStore` + * `ActionDispatch::Session::MemCacheStore` + +Any of these middleware can be added via: + +```ruby +config.middleware.use Rack::MethodOverride +``` + +### Removing Middleware + +If you don't want to use a middleware that is included by default in the API-only +middleware set, you can remove it with: + +```ruby +config.middleware.delete ::Rack::Sendfile +``` + +Keep in mind that removing these middleware will remove support for certain +features in Action Controller. + +Choosing Controller Modules +--------------------------- + +An API application (using `ActionController::API`) comes with the following +controller modules by default: + +- `ActionController::UrlFor`: Makes `url_for` and similar helpers available. +- `ActionController::Redirecting`: Support for `redirect_to`. +- `AbstractController::Rendering` and `ActionController::ApiRendering`: Basic support for rendering. +- `ActionController::Renderers::All`: Support for `render :json` and friends. +- `ActionController::ConditionalGet`: Support for `stale?`. +- `ActionController::BasicImplicitRender`: Makes sure to return an empty response, if there isn't an explicit one. +- `ActionController::StrongParameters`: Support for parameters white-listing in combination with Active Model mass assignment. +- `ActionController::ForceSSL`: Support for `force_ssl`. +- `ActionController::DataStreaming`: Support for `send_file` and `send_data`. +- `AbstractController::Callbacks`: Support for `before_action` and + similar helpers. +- `ActionController::Rescue`: Support for `rescue_from`. +- `ActionController::Instrumentation`: Support for the instrumentation + hooks defined by Action Controller (see [the instrumentation + guide](active_support_instrumentation.html#action-controller) for +more information regarding this). +- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash, + so that you don't have to specify root elements sending POST requests for instance. + +Other plugins may add additional modules. You can get a list of all modules +included into `ActionController::API` in the rails console: + +```bash +$ bin/rails c +>> ActionController::API.ancestors - ActionController::Metal.ancestors +=> [ActionController::API, + ActiveRecord::Railties::ControllerRuntime, + ActionDispatch::Routing::RouteSet::MountedHelpers, + ActionController::ParamsWrapper, + ... , + AbstractController::Rendering, + ActionView::ViewPaths] +``` + +### Adding Other Modules + +All Action Controller modules know about their dependent modules, so you can feel +free to include any modules into your controllers, and all dependencies will be +included and set up as well. + +Some common modules you might want to add: + +- `AbstractController::Translation`: Support for the `l` and `t` localization + and translation methods. +- `ActionController::HttpAuthentication::Basic` (or `Digest` or `Token`): Support + for basic, digest or token HTTP authentication. +- `ActionView::Layouts`: Support for layouts when rendering. +- `ActionController::MimeResponds`: Support for `respond_to`. +- `ActionController::Cookies`: Support for `cookies`, which includes + support for signed and encrypted cookies. This requires the cookies middleware. + +The best place to add a module is in your `ApplicationController`, but you can +also add modules to individual controllers. diff --git a/source/api_documentation_guidelines.md b/source/api_documentation_guidelines.md index a2ebf55..3c61754 100644 --- a/source/api_documentation_guidelines.md +++ b/source/api_documentation_guidelines.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + API Documentation Guidelines ============================ @@ -14,7 +16,8 @@ RDoc ---- The [Rails API documentation](http://api.rubyonrails.org) is generated with -[RDoc](http://docs.seattlerb.org/rdoc/). +[RDoc](http://docs.seattlerb.org/rdoc/). To generate it, make sure you are +in the rails root directory, run `bundle install` and execute: ```bash bundle exec rake rdoc @@ -81,6 +84,12 @@ English Please use American English (*color*, *center*, *modularize*, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). +Oxford Comma +------------ + +Please use the [Oxford comma](http://en.wikipedia.org/wiki/Serial_comma) +("red, white, and blue", instead of "red, white and blue"). + Example Code ------------ @@ -111,7 +120,7 @@ On the other hand, big chunks of structured documentation may have a separate "E The results of expressions follow them and are introduced by "# => ", vertically aligned: ```ruby -# For checking if a fixnum is even or odd. +# For checking if an integer is even or odd. # # 1.even? # => false # 1.odd? # => true @@ -231,7 +240,7 @@ You can quickly test the RDoc output with the following command: ``` $ echo "+:to_param+" | rdoc --pipe -#=>

:to_param

+# =>

:to_param

``` ### Regular Font @@ -324,10 +333,6 @@ As a contributor, it's important to think about whether this API is meant for en A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly. -If you come across an existing `:nodoc:` you should tread lightly. Consider asking someone from the core team or author of the code before removing it. This should almost always happen through a pull request instead of the docrails project. - -A `:nodoc:` should never be added simply because a method or class is missing documentation. There may be an instance where an internal public method wasn't given a `:nodoc:` by mistake, for example when switching a method from private to public visibility. When this happens it should be discussed over a PR on a case-by-case basis and never committed directly to docrails. - To summarize, the Rails team uses `:nodoc:` to mark publicly visible methods and classes for internal use; changes to the visibility of API should be considered carefully and discussed over a pull request first. Regarding the Rails Stack diff --git a/source/asset_pipeline.md b/source/asset_pipeline.md index e31cefa..61b7112 100644 --- a/source/asset_pipeline.md +++ b/source/asset_pipeline.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + The Asset Pipeline ================== @@ -19,21 +21,20 @@ What is the Asset Pipeline? The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages and pre-processors such as CoffeeScript, Sass and ERB. +It allows assets in your application to be automatically combined with assets +from other gems. For example, jquery-rails includes a copy of jquery.js +and enables AJAX features in Rails. -The asset pipeline is technically no longer a core feature of Rails 4, it has -been extracted out of the framework into the -[sprockets-rails](https://github.com/rails/sprockets-rails) gem. - -The asset pipeline is enabled by default. - -You can disable the asset pipeline while creating a new application by +The asset pipeline is implemented by the +[sprockets-rails](https://github.com/rails/sprockets-rails) gem, +and is enabled by default. You can disable it while creating a new application by passing the `--skip-sprockets` option. ```bash rails new appname --skip-sprockets ``` -Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier` +Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier` gems to your Gemfile, which are used by Sprockets for asset compression: ```ruby @@ -42,8 +43,8 @@ gem 'uglifier' gem 'coffee-rails' ``` -Using the `--skip-sprockets` option will prevent Rails 4 from adding -`sass-rails` and `uglifier` to Gemfile, so if you later want to enable +Using the `--skip-sprockets` option will prevent Rails from adding +them to your Gemfile, so if you later want to enable the asset pipeline you will have to add those gems to your Gemfile. Also, creating an application with the `--skip-sprockets` option will generate a slightly different `config/application.rb` file, with a require statement @@ -64,7 +65,7 @@ config.assets.js_compressor = :uglifier ``` NOTE: The `sass-rails` gem is automatically used for CSS compression if included -in Gemfile and no `config.assets.css_compressor` option is set. +in the Gemfile and no `config.assets.css_compressor` option is set. ### Main Features @@ -77,9 +78,9 @@ requests can mean faster loading for your application. Sprockets concatenates all JavaScript files into one master `.js` file and all CSS files into one master `.css` file. As you'll learn later in this guide, you can customize this strategy to group files any way you like. In production, -Rails inserts an MD5 fingerprint into each filename so that the file is cached -by the web browser. You can invalidate the cache by altering this fingerprint, -which happens automatically whenever you change the file contents. +Rails inserts an SHA256 fingerprint into each filename so that the file is +cached by the web browser. You can invalidate the cache by altering this +fingerprint, which happens automatically whenever you change the file contents. The second feature of the asset pipeline is asset minification or compression. For CSS files, this is done by removing whitespace and comments. For JavaScript, @@ -105,7 +106,7 @@ or in web browsers) to keep their own copy of the content. When the content is updated, the fingerprint will change. This will cause the remote clients to request a new copy of the content. This is generally known as _cache busting_. -The technique sprockets uses for fingerprinting is to insert a hash of the +The technique Sprockets uses for fingerprinting is to insert a hash of the content into the name, usually at the end. For example a CSS file `global.css` ``` @@ -147,7 +148,7 @@ clients to fetch them again, even when the content of those assets has not chang Fingerprinting fixes these problems by avoiding query strings, and by ensuring that filenames are consistent based on their content. -Fingerprinting is enabled by default for production and disabled for all other +Fingerprinting is enabled by default for both the development and production environments. You can enable or disable it in your configuration through the `config.assets.digest` option. @@ -166,9 +167,9 @@ pipeline, the preferred location for these assets is now the `app/assets` directory. Files in this directory are served by the Sprockets middleware. Assets can still be placed in the `public` hierarchy. Any assets under `public` -will be served as static files by the application or web server. You should use -`app/assets` for files that must undergo some pre-processing before they are -served. +will be served as static files by the application or web server when +`config.public_file_server.enabled` is set to true. You should use `app/assets` for +files that must undergo some pre-processing before they are served. In production, Rails precompiles these files to `public/assets` by default. The precompiled copies are then served as static assets by the web server. The files @@ -180,12 +181,12 @@ When you generate a scaffold or a controller, Rails also generates a JavaScript file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`) for that controller. Additionally, when generating a scaffold, Rails generates -the file scaffolds.css (or scaffolds.css.scss if `sass-rails` is in the +the file scaffolds.css (or scaffolds.scss if `sass-rails` is in the `Gemfile`.) For example, if you generate a `ProjectsController`, Rails will also add a new -file at `app/assets/javascripts/projects.js.coffee` and another at -`app/assets/stylesheets/projects.css.scss`. By default these files will be ready +file at `app/assets/javascripts/projects.coffee` and another at +`app/assets/stylesheets/projects.scss`. By default these files will be ready to use by your application immediately using the `require_tree` directive. See [Manifest Files and Directives](#manifest-files-and-directives) for more details on require_tree. @@ -206,10 +207,8 @@ default .coffee and .scss files will not be precompiled on their own. See precompiling works. NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. -If you are using Mac OS X or Windows, you have a JavaScript runtime installed in -your operating system. Check -[ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all -supported JavaScript runtimes. +If you are using macOS or Windows, you have a JavaScript runtime installed in +your operating system. Check [ExecJS](https://github.com/rails/execjs#readme) documentation to know all supported JavaScript runtimes. You can also disable generation of controller specific asset files by adding the following to your `config/application.rb` configuration: @@ -232,7 +231,9 @@ images, JavaScript files or stylesheets. scope of the application or those libraries which are shared across applications. * `vendor/assets` is for assets that are owned by outside entities, such as -code for JavaScript plugins and CSS frameworks. +code for JavaScript plugins and CSS frameworks. Keep in mind that third party +code with references to other files also processed by the asset Pipeline (images, +stylesheets, etc.), will need to be rewritten to use helpers like `asset_path`. WARNING: If you are upgrading from Rails 3, please take into account that assets under `lib/assets` or `vendor/assets` are available for inclusion via the @@ -325,16 +326,16 @@ familiar `javascript_include_tag` and `stylesheet_link_tag`: <%= javascript_include_tag "application" %> ``` -If using the turbolinks gem, which is included by default in Rails 4, then +If using the turbolinks gem, which is included by default in Rails, then include the 'data-turbolinks-track' option which causes turbolinks to check if an asset has been updated and if so loads it into the page: ```erb -<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> -<%= javascript_include_tag "application", "data-turbolinks-track" => true %> +<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => "reload" %> +<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %> ``` -In regular views you can access images in the `public/assets/images` directory +In regular views you can access images in the `app/assets/images` directory like this: ```erb @@ -345,9 +346,9 @@ Provided that the pipeline is enabled within your application (and not disabled in the current environment context), this file is served by Sprockets. If a file exists at `public/assets/rails.png` it is served by the web server. -Alternatively, a request for a file with an MD5 hash such as -`public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same -way. How these hashes are generated is covered in the [In +Alternatively, a request for a file with an SHA256 hash such as +`public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png` +is treated the same way. How these hashes are generated is covered in the [In Production](#in-production) section later on in this guide. Sprockets will also look through the paths specified in `config.assets.paths`, @@ -401,13 +402,13 @@ When using the asset pipeline, paths to assets must be re-written and underscored in Ruby) for the following asset classes: image, font, video, audio, JavaScript and stylesheet. -* `image-url("https://github.com/rails.png")` becomes `url(https://github.com/assets/rails.png)` -* `image-path("rails.png")` becomes `"/assets/rails.png"`. +* `image-url("https://github.com/rails.png")` returns `url(https://github.com/assets/rails.png)` +* `image-path("rails.png")` returns `"/assets/rails.png"` The more generic form can also be used: -* `asset-url("https://github.com/rails.png")` becomes `url(https://github.com/assets/rails.png)` -* `asset-path("rails.png")` becomes `"/assets/rails.png"` +* `asset-url("https://github.com/rails.png")` returns `url(https://github.com/assets/rails.png)` +* `asset-path("rails.png")` returns `"/assets/rails.png"` #### JavaScript/CoffeeScript and ERB @@ -422,7 +423,7 @@ $('#logo').attr({ src: "<%= asset_path('logo.png') %>" }); This writes the path to the particular asset being referenced. Similarly, you can use the `asset_path` helper in CoffeeScript files with `erb` -extension (e.g., `application.js.coffee.erb`): +extension (e.g., `application.coffee.erb`): ```js $('#logo').attr src: "<%= asset_path('logo.png') %>" @@ -434,14 +435,14 @@ Sprockets uses manifest files to determine which assets to include and serve. These manifest files contain _directives_ - instructions that tell Sprockets which files to require in order to build a single CSS or JavaScript file. With these directives, Sprockets loads the files specified, processes them if -necessary, concatenates them into one single file and then compresses them (if -`Rails.application.config.assets.compress` is true). By serving one file rather -than many, the load time of pages can be greatly reduced because the browser -makes fewer requests. Compression also reduces file size, enabling the -browser to download them faster. +necessary, concatenates them into one single file and then compresses them +(based on value of `Rails.application.config.assets.js_compressor`). By serving +one file rather than many, the load time of pages can be greatly reduced because +the browser makes fewer requests. Compression also reduces file size, enabling +the browser to download them faster. -For example, a new Rails 4 application includes a default +For example, a new Rails application includes a default `app/assets/javascripts/application.js` file containing the following lines: ```js @@ -482,9 +483,9 @@ which contains these lines: */ ``` -Rails 4 creates both `app/assets/javascripts/application.js` and +Rails creates both `app/assets/javascripts/application.js` and `app/assets/stylesheets/application.css` regardless of whether the ---skip-sprockets option is used when creating a new rails application. This is +--skip-sprockets option is used when creating a new Rails application. This is so you can easily add asset pipelining later if you like. The directives that work in JavaScript files also work in stylesheets @@ -523,8 +524,8 @@ The file extensions used on an asset determine what preprocessing is applied. When a controller or a scaffold is generated with the default Rails gemset, a CoffeeScript file and a SCSS file are generated in place of a regular JavaScript and CSS file. The example used before was a controller called "projects", which -generated an `app/assets/javascripts/projects.js.coffee` and an -`app/assets/stylesheets/projects.css.scss` file. +generated an `app/assets/javascripts/projects.coffee` and an +`app/assets/stylesheets/projects.scss` file. In development mode, or if the asset pipeline is disabled, when these files are requested they are processed by the processors provided by the `coffee-script` @@ -536,13 +537,13 @@ web server. Additional layers of preprocessing can be requested by adding other extensions, where each extension is processed in a right-to-left manner. These should be used in the order the processing should be applied. For example, a stylesheet -called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB, +called `app/assets/stylesheets/projects.scss.erb` is first processed as ERB, then SCSS, and finally served as CSS. The same applies to a JavaScript file - -`app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then +`app/assets/javascripts/projects.coffee.erb` is processed as ERB, then CoffeeScript, and served as JavaScript. Keep in mind the order of these preprocessors is important. For example, if -you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee` +you called your JavaScript file `app/assets/javascripts/projects.erb.coffee` then it would be processed with the CoffeeScript interpreter first, which wouldn't understand ERB and therefore you would run into problems. @@ -585,6 +586,19 @@ in your application are included in the `config.assets.precompile` list. If `config.assets.digest` is also true, the asset pipeline will require that all requests for assets include digests. +### Raise an Error When an Asset is Not Found + +If you are using sprockets-rails >= 3.2.0 you can configure what happens +when an asset lookup is performed and nothing is found. If you turn off "asset fallback" +then an error will be raised when an asset cannot be found. + +```ruby +config.assets.unknown_asset_fallback = false +``` + +If "asset fallback" is enabled then when an asset cannot be found the path will be +output instead and no error raised. The asset fallback behavior is enabled by default. + ### Turning Digests Off You can turn off digests by updating `config/environments/development.rb` to @@ -640,8 +654,8 @@ In the production environment Sprockets uses the fingerprinting scheme outlined above. By default Rails assumes assets have been precompiled and will be served as static assets by your web server. -During the precompilation phase an MD5 is generated from the contents of the -compiled files, and inserted into the filenames as they are written to disc. +During the precompilation phase an SHA256 is generated from the contents of the +compiled files, and inserted into the filenames as they are written to disk. These fingerprinted names are used by the Rails helpers in place of the manifest name. @@ -660,13 +674,12 @@ generates something like this: rel="stylesheet" /> ``` -Note: with the Asset Pipeline the :cache and :concat options aren't used +NOTE: with the Asset Pipeline the `:cache` and `:concat` options aren't used anymore, delete these options from the `javascript_include_tag` and `stylesheet_link_tag`. The fingerprinting behavior is controlled by the `config.assets.digest` -initialization option (which defaults to `true` for production and `false` for -everything else). +initialization option (which defaults to `true`). NOTE: Under normal circumstances the default `config.assets.digest` option should not be changed. If there are no digests in the filenames, and far-future @@ -675,7 +688,7 @@ content changes. ### Precompiling Assets -Rails comes bundled with a rake task to compile the asset manifests and other +Rails comes bundled with a task to compile the asset manifests and other files in the pipeline. Compiled assets are written to the location specified in `config.assets.prefix`. @@ -685,10 +698,10 @@ You can call this task on the server during deployment to create compiled versions of your assets directly on the server. See the next section for information on compiling locally. -The rake task is: +The task is: ```bash -$ RAILS_ENV=production bin/rake assets:precompile +$ RAILS_ENV=production bin/rails assets:precompile ``` Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment. @@ -724,49 +737,30 @@ If you have other manifests or individual stylesheets and JavaScript files to include, you can add them to the `precompile` array in `config/initializers/assets.rb`: ```ruby -Rails.application.config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js'] -``` - -Or, you can opt to precompile all assets with something like this: - -```ruby -# config/initializers/assets.rb -Rails.application.config.assets.precompile << Proc.new do |path| - if path =~ /\.(css|js)\z/ - full_path = Rails.application.assets.resolve(path).to_path - app_assets_path = Rails.root.join('app', 'assets').to_path - if full_path.starts_with? app_assets_path - puts "including asset: " + full_path - true - else - puts "excluding asset: " + full_path - false - end - else - false - end -end +Rails.application.config.assets.precompile += %w( admin.js admin.css ) ``` NOTE. Always specify an expected compiled filename that ends with .js or .css, even if you want to add Sass or CoffeeScript files to the precompile array. -The rake task also generates a `manifest-md5hash.json` that contains a list with -all your assets and their respective fingerprints. This is used by the Rails -helper methods to avoid handing the mapping requests back to Sprockets. A -typical manifest file looks like: +The task also generates a `.sprockets-manifest-md5hash.json` (where `md5hash` is +an MD5 hash) that contains a list with all your assets and their respective +fingerprints. This is used by the Rails helper methods to avoid handing the +mapping requests back to Sprockets. A typical manifest file looks like: ```ruby -{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506, -"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560, -"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591, -"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406, -"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646, -"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets":{"application.js": -"application-723d1be6cc741a3aabb1cec24276d681.js","application.css": -"application-1c5752789588ac18d7e1a50b1f0fd4c2.css", -"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png": -"my_image-231a680f23887d9dd70710ea5efd3c62.png"}} +{"files":{"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383, +"digest":"aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b","integrity":"sha256-ruS+cfEogDeueLmX3ziDMu39JGRxtTPc7aqPn+FWRCs="}, +"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css":{"logical_path":"application.css","mtime":"2016-12-23T19:12:20-05:00","size":2994, +"digest":"86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18","integrity":"sha256-hqKStQcHk8N+LA5fOfc7s4dkTq6tp/lub8BAoCixbBg="}, +"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico":{"logical_path":"favicon.ico","mtime":"2016-12-23T20:11:00-05:00","size":8629, +"digest":"8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda","integrity":"sha256-jSOHuNTTLOzZP6OQDfDp/4nQGqzYT1DngMF8n2s9Dto="}, +"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png":{"logical_path":"my_image.png","mtime":"2016-12-23T20:10:54-05:00","size":23414, +"digest":"f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493","integrity":"sha256-9AKBVv1+ygNYTV8vwEcN8eDbxzaequY4sv8DP5iOxJM="}}, +"assets":{"application.js":"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js", +"application.css":"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css", +"favicon.ico":"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico", +"my_image.png":"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png"}} ``` The default location for the manifest is the root of the location specified in @@ -806,45 +800,9 @@ location ~ ^/assets/ { add_header Cache-Control public; add_header ETag ""; - break; -} -``` - -#### GZip Compression - -When files are precompiled, Sprockets also creates a -[gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web -servers are typically configured to use a moderate compression ratio as a -compromise, but since precompilation happens once, Sprockets uses the maximum -compression ratio, thus reducing the size of the data transfer to the minimum. -On the other hand, web servers can be configured to serve compressed content -directly from disk, rather than deflating non-compressed files themselves. - -NGINX is able to do this automatically enabling `gzip_static`: - -```nginx -location ~ ^/(assets)/ { - root /path/to/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; } ``` -This directive is available if the core module that provides this feature was -compiled with the web server. Ubuntu/Debian packages, even `nginx-light`, have -the module compiled. Otherwise, you may need to perform a manual compilation: - -```bash -./configure --with-http_gzip_static_module -``` - -If you're compiling NGINX with Phusion Passenger you'll need to pass that option -when prompted. - -A robust configuration for Apache is possible but tricky; please Google around. -(Or help update this Guide if you have a good configuration example for Apache.) - ### Local Precompilation There are several reasons why you might want to precompile your assets locally. @@ -894,7 +852,7 @@ config.assets.compile = true On the first request the assets are compiled and cached as outlined in development above, and the manifest names used in the helpers are altered to -include the MD5 hash. +include the SHA256 hash. Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This signals all caches between your server and the client browser that this content @@ -916,24 +874,209 @@ end ### CDNs -If your assets are being served by a CDN, ensure they don't stick around in your -cache forever. This can cause problems. If you use -`config.action_controller.perform_caching = true`, Rack::Cache will use -`Rails.cache` to store assets. This can cause your cache to fill up quickly. +CDN stands for [Content Delivery +Network](http://en.wikipedia.org/wiki/Content_delivery_network), they are +primarily designed to cache assets all over the world so that when a browser +requests the asset, a cached copy will be geographically close to that browser. +If you are serving assets directly from your Rails server in production, the +best practice is to use a CDN in front of your application. + +A common pattern for using a CDN is to set your production application as the +"origin" server. This means when a browser requests an asset from the CDN and +there is a cache miss, it will grab the file from your server on the fly and +then cache it. For example if you are running a Rails application on +`example.com` and have a CDN configured at `mycdnsubdomain.fictional-cdn.com`, +then when a request is made to `mycdnsubdomain.fictional- +cdn.com/assets/smile.png`, the CDN will query your server once at +`example.com/assets/smile.png` and cache the request. The next request to the +CDN that comes in to the same URL will hit the cached copy. When the CDN can +serve an asset directly the request never touches your Rails server. Since the +assets from a CDN are geographically closer to the browser, the request is +faster, and since your server doesn't need to spend time serving assets, it can +focus on serving application code as fast as possible. + +#### Set up a CDN to Serve Static Assets + +To set up your CDN you have to have your application running in production on +the internet at a publicly available URL, for example `example.com`. Next +you'll need to sign up for a CDN service from a cloud hosting provider. When you +do this you need to configure the "origin" of the CDN to point back at your +website `example.com`, check your provider for documentation on configuring the +origin server. + +The CDN you provisioned should give you a custom subdomain for your application +such as `mycdnsubdomain.fictional-cdn.com` (note fictional-cdn.com is not a +valid CDN provider at the time of this writing). Now that you have configured +your CDN server, you need to tell browsers to use your CDN to grab assets +instead of your Rails server directly. You can do this by configuring Rails to +set your CDN as the asset host instead of using a relative path. To set your +asset host in Rails, you need to set `config.action_controller.asset_host` in +`config/environments/production.rb`: + +```ruby +config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com' +``` + +NOTE: You only need to provide the "host", this is the subdomain and root +domain, you do not need to specify a protocol or "scheme" such as `http://` or +`https://`. When a web page is requested, the protocol in the link to your asset +that is generated will match how the webpage is accessed by default. + +You can also set this value through an [environment +variable](http://en.wikipedia.org/wiki/Environment_variable) to make running a +staging copy of your site easier: + +``` +config.action_controller.asset_host = ENV['CDN_HOST'] +``` + + + +Note: You would need to set `CDN_HOST` on your server to `mycdnsubdomain +.fictional-cdn.com` for this to work. + +Once you have configured your server and your CDN when you serve a webpage that +has an asset: + +```erb +<%= asset_path('smile.png') %> +``` + +Instead of returning a path such as `/assets/smile.png` (digests are left out +for readability). The URL generated will have the full path to your CDN. -Every cache is different, so evaluate how your CDN handles caching and make sure -that it plays nicely with the pipeline. You may find quirks related to your -specific set up, you may not. The defaults NGINX uses, for example, should give -you no problems when used as an HTTP cache. +``` +http://mycdnsubdomain.fictional-cdn.com/assets/smile.png +``` -If you want to serve only some assets from your CDN, you can use custom -`:host` option of `asset_url` helper, which overwrites value set in +If the CDN has a copy of `smile.png` it will serve it to the browser and your +server doesn't even know it was requested. If the CDN does not have a copy it +will try to find it at the "origin" `example.com/assets/smile.png` and then store +it for future use. + +If you want to serve only some assets from your CDN, you can use custom `:host` +option your asset helper, which overwrites value set in `config.action_controller.asset_host`. -```ruby -asset_url 'image.png', :host => 'http://cdn.example.com/' +```erb +<%= asset_path 'image.png', host: 'mycdnsubdomain.fictional-cdn.com' %> +``` + +#### Customize CDN Caching Behavior + +A CDN works by caching content. If the CDN has stale or bad content, then it is +hurting rather than helping your application. The purpose of this section is to +describe general caching behavior of most CDNs, your specific provider may +behave slightly differently. + +##### CDN Request Caching + +While a CDN is described as being good for caching assets, in reality caches the +entire request. This includes the body of the asset as well as any headers. The +most important one being `Cache-Control` which tells the CDN (and web browsers) +how to cache contents. This means that if someone requests an asset that does +not exist `/assets/i-dont-exist.png` and your Rails application returns a 404, +then your CDN will likely cache the 404 page if a valid `Cache-Control` header +is present. + +##### CDN Header Debugging + +One way to check the headers are cached properly in your CDN is by using [curl]( +http://explainshell.com/explain?cmd=curl+-I+http%3A%2F%2Fwww.example.com). You +can request the headers from both your server and your CDN to verify they are +the same: + +``` +$ curl -I http://www.example/assets/application- +d0e099e021c95eb0de3615fd1d8c4d83.css +HTTP/1.1 200 OK +Server: Cowboy +Date: Sun, 24 Aug 2014 20:27:50 GMT +Connection: keep-alive +Last-Modified: Thu, 08 May 2014 01:24:14 GMT +Content-Type: text/css +Cache-Control: public, max-age=2592000 +Content-Length: 126560 +Via: 1.1 vegur +``` + +Versus the CDN copy. + +``` +$ curl -I http://mycdnsubdomain.fictional-cdn.com/application- +d0e099e021c95eb0de3615fd1d8c4d83.css +HTTP/1.1 200 OK Server: Cowboy Last- +Modified: Thu, 08 May 2014 01:24:14 GMT Content-Type: text/css +Cache-Control: +public, max-age=2592000 +Via: 1.1 vegur +Content-Length: 126560 +Accept-Ranges: +bytes +Date: Sun, 24 Aug 2014 20:28:45 GMT +Via: 1.1 varnish +Age: 885814 +Connection: keep-alive +X-Served-By: cache-dfw1828-DFW +X-Cache: HIT +X-Cache-Hits: +68 +X-Timer: S1408912125.211638212,VS0,VE0 +``` + +Check your CDN documentation for any additional information they may provide +such as `X-Cache` or for any additional headers they may add. + +##### CDNs and the Cache-Control Header + +The [cache control +header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) is a W3C +specification that describes how a request can be cached. When no CDN is used, a +browser will use this information to cache contents. This is very helpful for +assets that are not modified so that a browser does not need to re-download a +website's CSS or JavaScript on every request. Generally we want our Rails server +to tell our CDN (and browser) that the asset is "public", that means any cache +can store the request. Also we commonly want to set `max-age` which is how long +the cache will store the object before invalidating the cache. The `max-age` +value is set to seconds with a maximum possible value of `31536000` which is one +year. You can do this in your Rails application by setting + +``` +config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=31536000' +} +``` + +Now when your application serves an asset in production, the CDN will store the +asset for up to a year. Since most CDNs also cache headers of the request, this +`Cache-Control` will be passed along to all future browsers seeking this asset, +the browser then knows that it can store this asset for a very long time before +needing to re-request it. + +##### CDNs and URL based Cache Invalidation + +Most CDNs will cache contents of an asset based on the complete URL. This means +that a request to + +``` +http://mycdnsubdomain.fictional-cdn.com/assets/smile-123.png +``` + +Will be a completely different cache from + +``` +http://mycdnsubdomain.fictional-cdn.com/assets/smile.png ``` +If you want to set far future `max-age` in your `Cache-Control` (and you do), +then make sure when you change your assets that your cache is invalidated. For +example when changing the smiley face in an image from yellow to blue, you want +all visitors of your site to get the new blue face. When using a CDN with the +Rails asset pipeline `config.assets.digest` is set to true by default so that +each asset will have a different file name when it is changed. This way you +don't have to ever manually invalidate any items in your cache. By using a +different unique asset name instead, your users get the latest asset. + Customizing the Pipeline ------------------------ @@ -973,15 +1116,21 @@ The following line invokes `uglifier` for JavaScript compression. config.assets.js_compressor = :uglifier ``` -NOTE: You will need an [ExecJS](https://github.com/sstephenson/execjs#readme) -supported runtime in order to use `uglifier`. If you are using Mac OS X or +NOTE: You will need an [ExecJS](https://github.com/rails/execjs#readme) +supported runtime in order to use `uglifier`. If you are using macOS or Windows you have a JavaScript runtime installed in your operating system. -NOTE: The `config.assets.compress` initialization option is no longer used in -Rails 4 to enable either CSS or JavaScript compression. Setting it will have no -effect on the application. Instead, setting `config.assets.css_compressor` and -`config.assets.js_compressor` will control compression of CSS and JavaScript -assets. + + +### Serving GZipped version of assets + +By default, gzipped version of compiled assets will be generated, along with +the non-gzipped version of assets. Gzipped assets help reduce the transmission +of data over the wire. You can configure this by setting the `gzip` flag. + +```ruby +config.assets.gzip = false # disable gzipped assets generation +``` ### Using Your Own Compressor @@ -1047,19 +1196,14 @@ TIP: For further details have a look at the docs of your production web server: Assets Cache Store ------------------ -The default Rails cache store will be used by Sprockets to cache assets in -development and production. This can be changed by setting -`config.assets.cache_store`: +By default, Sprockets caches assets in `tmp/cache/assets` in development +and production environments. This can be changed as follows: ```ruby -config.assets.cache_store = :memory_store -``` - -The options accepted by the assets cache store are the same as the application's -cache store. - -```ruby -config.assets.cache_store = :memory_store, { size: 32.megabytes } +config.assets.configure do |env| + env.cache = ActiveSupport::Cache.lookup_store(:memory_store, + { size: 32.megabytes }) +end ``` To disable the assets cache store: @@ -1085,35 +1229,25 @@ Sprockets. Making Your Library or Gem a Pre-Processor ------------------------------------------ -As Sprockets uses [Tilt](https://github.com/rtomayko/tilt) as a generic -interface to different templating engines, your gem should just implement the -Tilt template protocol. Normally, you would subclass `Tilt::Template` and -reimplement the `prepare` method, which initializes your template, and the -`evaluate` method, which returns the processed source. The original source is -stored in `data`. Have a look at -[`Tilt::Template`](https://github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb) -sources to learn more. +Sprockets uses Processors, Transformers, Compressors, and Exporters to extend +Sprockets functionality. Have a look at +[Extending Sprockets](https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md) +to learn more. Here we registered a preprocessor to add a comment to the end +of text/css (.css) files. ```ruby -module BangBang - class Template < ::Tilt::Template - def prepare - # Do any initialization here - end - - # Adds a "!" to original template. - def evaluate(scope, locals, &block) - "#{data}!" - end +module AddComment + def self.call(input) + { data: input[:data] + "/* Hello From my sprockets extension */" } end end ``` -Now that you have a `Template` class, it's time to associate it with an -extension for template files: +Now that you have a module that modifies the input data, it's time to register +it as a preprocessor for your mime type. ```ruby -Sprockets.register_engine '.bang', BangBang::Template +Sprockets.register_preprocessor 'text/css', AddComment ``` Upgrading from Old Versions of Rails @@ -1150,25 +1284,27 @@ config.assets.debug = true And in `production.rb`: ```ruby -# Choose the compressors to use (if any) config.assets.js_compressor = -# :uglifier config.assets.css_compressor = :yui +# Choose the compressors to use (if any) +config.assets.js_compressor = :uglifier +# config.assets.css_compressor = :yui # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = false -# Generate digests for assets URLs. This is planned for deprecation. +# Generate digests for assets URLs. config.assets.digest = true # Precompile additional assets (application.js, application.css, and all -# non-JS/CSS are already added) config.assets.precompile += %w( search.js ) +# non-JS/CSS are already added) +# config.assets.precompile += %w( admin.js admin.css ) ``` -Rails 4 no longer sets default config values for Sprockets in `test.rb`, so +Rails 4 and above no longer set default config values for Sprockets in `test.rb`, so `test.rb` now requires Sprockets configuration. The old defaults in the test -environment are: `config.assets.compile = true`, `config.assets.compress = -false`, `config.assets.debug = false` and `config.assets.digest = false`. +environment are: `config.assets.compile = true`, `config.assets.compress = false`, +`config.assets.debug = false` and `config.assets.digest = false`. -The following should also be added to `Gemfile`: +The following should also be added to your `Gemfile`: ```ruby gem 'sass-rails', "~> 3.2.3" diff --git a/source/association_basics.md b/source/association_basics.md index daf4113..5794bfa 100644 --- a/source/association_basics.md +++ b/source/association_basics.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record Associations ========================== @@ -14,54 +16,54 @@ After reading this guide, you will know: Why Associations? ----------------- -Why do we need associations between models? Because they make common operations simpler and easier in your code. For example, consider a simple Rails application that includes a model for customers and a model for orders. Each customer can have many orders. Without associations, the model declarations would look like this: +In Rails, an _association_ is a connection between two Active Record models. Why do we need associations between models? Because they make common operations simpler and easier in your code. For example, consider a simple Rails application that includes a model for authors and a model for books. Each author can have many books. Without associations, the model declarations would look like this: ```ruby -class Customer < ActiveRecord::Base +class Author < ApplicationRecord end -class Order < ActiveRecord::Base +class Book < ApplicationRecord end ``` -Now, suppose we wanted to add a new order for an existing customer. We'd need to do something like this: +Now, suppose we wanted to add a new book for an existing author. We'd need to do something like this: ```ruby -@order = Order.create(order_date: Time.now, customer_id: @customer.id) +@book = Book.create(published_at: Time.now, author_id: @author.id) ``` -Or consider deleting a customer, and ensuring that all of its orders get deleted as well: +Or consider deleting an author, and ensuring that all of its books get deleted as well: ```ruby -@orders = Order.where(customer_id: @customer.id) -@orders.each do |order| - order.destroy +@books = Book.where(author_id: @author.id) +@books.each do |book| + book.destroy end -@customer.destroy +@author.destroy ``` -With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders: +With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up authors and books: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, dependent: :destroy +class Author < ApplicationRecord + has_many :books, dependent: :destroy end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -With this change, creating a new order for a particular customer is easier: +With this change, creating a new book for a particular author is easier: ```ruby -@order = @customer.orders.create(order_date: Time.now) +@book = @author.books.create(published_at: Time.now) ``` -Deleting a customer and all of its orders is *much* easier: +Deleting an author and all of its books is *much* easier: ```ruby -@customer.destroy +@author.destroy ``` To learn more about the different types of associations, read the next section of this guide. That's followed by some tips and tricks for working with associations, and then by a complete reference to the methods and options for associations in Rails. @@ -69,7 +71,7 @@ To learn more about the different types of associations, read the next section o The Types of Associations ------------------------- -In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key-Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations: +Rails supports six types of associations: * `belongs_to` * `has_one` @@ -78,35 +80,37 @@ In Rails, an _association_ is a connection between two Active Record models. Ass * `has_one :through` * `has_and_belongs_to_many` +Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain [Primary Key](https://en.wikipedia.org/wiki/Unique_key)-[Foreign Key](https://en.wikipedia.org/wiki/Foreign_key) information between instances of the two models, and you also get a number of utility methods added to your model. + In the remainder of this guide, you'll learn how to declare and use the various forms of associations. But first, a quick introduction to the situations where each association type is appropriate. ### The `belongs_to` Association -A `belongs_to` association sets up a one-to-one connection with another model, such that each instance of the declaring model "belongs to" one instance of the other model. For example, if your application includes customers and orders, and each order can be assigned to exactly one customer, you'd declare the order model this way: +A `belongs_to` association sets up a one-to-one connection with another model, such that each instance of the declaring model "belongs to" one instance of the other model. For example, if your application includes authors and books, and each book can be assigned to exactly one author, you'd declare the book model this way: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` ![belongs_to Association Diagram](images/belongs_to.png) -NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `customer` association in the `Order` model, you would be told that there was an "uninitialized constant Order::Customers". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too. +NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too. The corresponding migration might look like this: ```ruby -class CreateOrders < ActiveRecord::Migration +class CreateBooks < ActiveRecord::Migration[5.0] def change - create_table :customers do |t| + create_table :authors do |t| t.string :name t.timestamps end - create_table :orders do |t| - t.belongs_to :customer, index: true - t.datetime :order_date + create_table :books do |t| + t.belongs_to :author, index: true + t.datetime :published_at t.timestamps end end @@ -118,7 +122,7 @@ end A `has_one` association also sets up a one-to-one connection with another model, but with somewhat different semantics (and consequences). This association indicates that each instance of a model contains or possesses one instance of another model. For example, if each supplier in your application has only one account, you'd declare the supplier model like this: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end ``` @@ -128,7 +132,7 @@ end The corresponding migration might look like this: ```ruby -class CreateSuppliers < ActiveRecord::Migration +class CreateSuppliers < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| t.string :name @@ -144,13 +148,24 @@ class CreateSuppliers < ActiveRecord::Migration end ``` +Depending on the use case, you might also need to create a unique index and/or +a foreign key constraint on the supplier column for the accounts table. In this +case, the column definition might look like this: + +```ruby +create_table :accounts do |t| + t.belongs_to :supplier, index: { unique: true }, foreign_key: true + # ... +end +``` + ### The `has_many` Association -A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing customers and orders, the customer model could be declared like this: +A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing authors and books, the author model could be declared like this: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` @@ -161,16 +176,16 @@ NOTE: The name of the other model is pluralized when declaring a `has_many` asso The corresponding migration might look like this: ```ruby -class CreateCustomers < ActiveRecord::Migration +class CreateAuthors < ActiveRecord::Migration[5.0] def change - create_table :customers do |t| + create_table :authors do |t| t.string :name t.timestamps end - create_table :orders do |t| - t.belongs_to :customer, index:true - t.datetime :order_date + create_table :books do |t| + t.belongs_to :author, index: true + t.datetime :published_at t.timestamps end end @@ -182,17 +197,17 @@ end A `has_many :through` association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding _through_ a third model. For example, consider a medical practice where patients make appointments to see physicians. The relevant association declarations could look like this: ```ruby -class Physician < ActiveRecord::Base +class Physician < ApplicationRecord has_many :appointments has_many :patients, through: :appointments end -class Appointment < ActiveRecord::Base +class Appointment < ApplicationRecord belongs_to :physician belongs_to :patient end -class Patient < ActiveRecord::Base +class Patient < ApplicationRecord has_many :appointments has_many :physicians, through: :appointments end @@ -203,7 +218,7 @@ end The corresponding migration might look like this: ```ruby -class CreateAppointments < ActiveRecord::Migration +class CreateAppointments < ActiveRecord::Migration[5.0] def change create_table :physicians do |t| t.string :name @@ -225,30 +240,32 @@ class CreateAppointments < ActiveRecord::Migration end ``` -The collection of join models can be managed via the API. For example, if you assign +The collection of join models can be managed via the [`has_many` association methods](#has-many-association-reference). +For example, if you assign: ```ruby physician.patients = patients ``` -new join models are created for newly associated objects, and if some are gone their rows are deleted. +Then new join models are automatically created for the newly associated objects. +If some that existed previously are now missing, then their join rows are automatically deleted. WARNING: Automatic deletion of join models is direct, no destroy callbacks are triggered. The `has_many :through` association is also useful for setting up "shortcuts" through nested `has_many` associations. For example, if a document has many sections, and a section has many paragraphs, you may sometimes want to get a simple collection of all paragraphs in the document. You could set that up this way: ```ruby -class Document < ActiveRecord::Base +class Document < ApplicationRecord has_many :sections has_many :paragraphs, through: :sections end -class Section < ActiveRecord::Base +class Section < ApplicationRecord belongs_to :document has_many :paragraphs end -class Paragraph < ActiveRecord::Base +class Paragraph < ApplicationRecord belongs_to :section end ``` @@ -267,17 +284,17 @@ For example, if each supplier has one account, and each account is associated wi supplier model could look like this: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account has_one :account_history, through: :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier has_one :account_history end -class AccountHistory < ActiveRecord::Base +class AccountHistory < ApplicationRecord belongs_to :account end ``` @@ -287,7 +304,7 @@ end The corresponding migration might look like this: ```ruby -class CreateAccountHistories < ActiveRecord::Migration +class CreateAccountHistories < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| t.string :name @@ -314,11 +331,11 @@ end A `has_and_belongs_to_many` association creates a direct many-to-many connection with another model, with no intervening model. For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` @@ -328,7 +345,7 @@ end The corresponding migration might look like this: ```ruby -class CreateAssembliesAndParts < ActiveRecord::Migration +class CreateAssembliesAndParts < ActiveRecord::Migration[5.0] def change create_table :assemblies do |t| t.string :name @@ -355,11 +372,11 @@ If you want to set up a one-to-one relationship between two models, you'll need The distinction is in where you place the foreign key (it goes on the table for the class declaring the `belongs_to` association), but you should give some thought to the actual meaning of the data as well. The `has_one` relationship says that one of something is yours - that is, that something points back to you. For example, it makes more sense to say that a supplier owns an account than that an account owns a supplier. This suggests that the correct relationships are like this: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier end ``` @@ -367,10 +384,10 @@ end The corresponding migration might look like this: ```ruby -class CreateSuppliers < ActiveRecord::Migration +class CreateSuppliers < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| - t.string :name + t.string :name t.timestamps end @@ -392,11 +409,11 @@ NOTE: Using `t.integer :supplier_id` makes the foreign key naming obvious and ex Rails offers two different ways to declare a many-to-many relationship between models. The simpler way is to use `has_and_belongs_to_many`, which allows you to make the association directly: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` @@ -404,17 +421,17 @@ end The second way to declare a many-to-many relationship is to use `has_many :through`. This makes the association indirectly, through a join model: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_many :manifests has_many :parts, through: :manifests end -class Manifest < ActiveRecord::Base +class Manifest < ApplicationRecord belongs_to :assembly belongs_to :part end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_many :manifests has_many :assemblies, through: :manifests end @@ -422,22 +439,22 @@ end The simplest rule of thumb is that you should set up a `has_many :through` relationship if you need to work with the relationship model as an independent entity. If you don't need to do anything with the relationship model, it may be simpler to set up a `has_and_belongs_to_many` relationship (though you'll need to remember to create the joining table in the database). -You should use `has_many :through` if you need validations, callbacks, or extra attributes on the join model. +You should use `has_many :through` if you need validations, callbacks or extra attributes on the join model. ### Polymorphic Associations A slightly more advanced twist on associations is the _polymorphic association_. With polymorphic associations, a model can belong to more than one other model, on a single association. For example, you might have a picture model that belongs to either an employee model or a product model. Here's how this could be declared: ```ruby -class Picture < ActiveRecord::Base +class Picture < ApplicationRecord belongs_to :imageable, polymorphic: true end -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord has_many :pictures, as: :imageable end -class Product < ActiveRecord::Base +class Product < ApplicationRecord has_many :pictures, as: :imageable end ``` @@ -449,7 +466,7 @@ Similarly, you can retrieve `@product.pictures`. If you have an instance of the `Picture` model, you can get to its parent via `@picture.imageable`. To make this work, you need to declare both a foreign key column and a type column in the model that declares the polymorphic interface: ```ruby -class CreatePictures < ActiveRecord::Migration +class CreatePictures < ActiveRecord::Migration[5.0] def change create_table :pictures do |t| t.string :name @@ -458,7 +475,7 @@ class CreatePictures < ActiveRecord::Migration t.timestamps end - add_index :pictures, :imageable_id + add_index :pictures, [:imageable_type, :imageable_id] end end ``` @@ -466,7 +483,7 @@ end This migration can be simplified by using the `t.references` form: ```ruby -class CreatePictures < ActiveRecord::Migration +class CreatePictures < ActiveRecord::Migration[5.0] def change create_table :pictures do |t| t.string :name @@ -484,7 +501,7 @@ end In designing a data model, you will sometimes find a model that should have a relation to itself. For example, you may want to store all employees in a single database model, but be able to trace relationships such as between manager and subordinates. This situation can be modeled with self-joining associations: ```ruby -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord has_many :subordinates, class_name: "Employee", foreign_key: "manager_id" @@ -497,7 +514,7 @@ With this setup, you can retrieve `@employee.subordinates` and `@employee.manage In your migrations/schema, you will add a references column to the model itself. ```ruby -class CreateEmployees < ActiveRecord::Migration +class CreateEmployees < ActiveRecord::Migration[5.0] def change create_table :employees do |t| t.references :manager, index: true @@ -523,18 +540,18 @@ Here are a few things you should know to make efficient use of Active Record ass All of the association methods are built around caching, which keeps the result of the most recent query available for further operations. The cache is even shared across methods. For example: ```ruby -customer.orders # retrieves orders from the database -customer.orders.size # uses the cached copy of orders -customer.orders.empty? # uses the cached copy of orders +author.books # retrieves books from the database +author.books.size # uses the cached copy of books +author.books.empty? # uses the cached copy of books ``` -But what if you want to reload the cache, because data might have been changed by some other part of the application? Just pass `true` to the association call: +But what if you want to reload the cache, because data might have been changed by some other part of the application? Just call `reload` on the association: ```ruby -customer.orders # retrieves orders from the database -customer.orders.size # uses the cached copy of orders -customer.orders(true).empty? # discards the cached copy of orders - # and goes back to the database +author.books # retrieves books from the database +author.books.size # uses the cached copy of books +author.books.reload.empty? # discards the cached copy of books + # and goes back to the database ``` ### Avoiding Name Collisions @@ -550,43 +567,59 @@ Associations are extremely useful, but they are not magic. You are responsible f When you declare a `belongs_to` association, you need to create foreign keys as appropriate. For example, consider this model: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -This declaration needs to be backed up by the proper foreign key declaration on the orders table: +This declaration needs to be backed up by the proper foreign key declaration on the books table: ```ruby -class CreateOrders < ActiveRecord::Migration +class CreateBooks < ActiveRecord::Migration[5.0] def change - create_table :orders do |t| - t.datetime :order_date - t.string :order_number - t.integer :customer_id + create_table :books do |t| + t.datetime :published_at + t.string :book_number + t.integer :author_id end - - add_index :orders, :customer_id end end ``` If you create an association some time after you build the underlying model, you need to remember to create an `add_column` migration to provide the necessary foreign key. +It's a good practice to add an index on the foreign key to improve queries +performance and a foreign key constraint to ensure referential data integrity: + +```ruby +class CreateBooks < ActiveRecord::Migration[5.0] + def change + create_table :books do |t| + t.datetime :published_at + t.string :book_number + t.integer :author_id + end + + add_index :books, :author_id + add_foreign_key :books, :authors + end +end +``` + #### Creating Join Tables for `has_and_belongs_to_many` Associations -If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical order of the class names. So a join between customer and order models will give the default join table name of "customers_orders" because "c" outranks "o" in lexical ordering. +If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical book of the class names. So a join between author and book models will give the default join table name of "authors_books" because "a" outranks "b" in lexical ordering. -WARNING: The precedence between model names is calculated using the `<` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '_' is lexicographically _less_ than 's' in common encodings). +WARNING: The precedence between model names is calculated using the `<=>` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '\_' is lexicographically _less_ than 's' in common encodings). Whatever the name, you must manually generate the join table with an appropriate migration. For example, consider these associations: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` @@ -594,7 +627,7 @@ end These need to be backed up by a migration to create the `assemblies_parts` table. This table should be created without a primary key: ```ruby -class CreateAssembliesPartsJoinTable < ActiveRecord::Migration +class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0] def change create_table :assemblies_parts, id: false do |t| t.integer :assembly_id @@ -607,7 +640,20 @@ class CreateAssembliesPartsJoinTable < ActiveRecord::Migration end ``` -We pass `id: false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled models IDs, or exceptions about conflicting IDs, chances are you forgot that bit. +We pass `id: false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled model IDs, or exceptions about conflicting IDs, chances are you forgot that bit. + +You can also use the method `create_join_table` + +```ruby +class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0] + def change + create_join_table :assemblies, :parts do |t| + t.index :assembly_id + t.index :part_id + end + end +end +``` ### Controlling Association Scope @@ -616,11 +662,11 @@ By default, associations look for objects only within the current module's scope ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account end - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier end end @@ -632,13 +678,13 @@ This will work fine, because both the `Supplier` and the `Account` class are def ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account end end module Billing - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier end end @@ -650,14 +696,14 @@ To associate a model with a model in a different namespace, you must specify the ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account, class_name: "MyApplication::Billing::Account" end end module Billing - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, class_name: "MyApplication::Business::Supplier" end @@ -670,64 +716,82 @@ end It's normal for associations to work in two directions, requiring declaration on two different models: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -By default, Active Record doesn't know about the connection between these associations. This can lead to two copies of an object getting out of sync: +Active Record will attempt to automatically identify that these two models share a bi-directional association based on the association name. In this way, Active Record will only load one copy of the `Author` object, making your application more efficient and preventing inconsistent data: ```ruby -c = Customer.first -o = c.orders.first -c.first_name == o.customer.first_name # => true -c.first_name = 'Manny' -c.first_name == o.customer.first_name # => false +a = Author.first +b = a.books.first +a.first_name == b.author.first_name # => true +a.first_name = 'David' +a.first_name == b.author.first_name # => true ``` -This happens because c and o.customer are two different in-memory representations of the same data, and neither one is automatically refreshed from changes to the other. Active Record provides the `:inverse_of` option so that you can inform it of these relations: +Active Record supports automatic identification for most associations with standard names. However, Active Record will not automatically identify bi-directional associations that contain any of the following options: + +* `:conditions` +* `:through` +* `:polymorphic` +* `:class_name` +* `:foreign_key` + +For example, consider the following model declarations: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' end ``` -With these changes, Active Record will only load one copy of the customer object, preventing inconsistencies and making your application more efficient: +Active Record will no longer automatically recognize the bi-directional association: ```ruby -c = Customer.first -o = c.orders.first -c.first_name == o.customer.first_name # => true -c.first_name = 'Manny' -c.first_name == o.customer.first_name # => true +a = Author.first +b = a.books.first +a.first_name == b.writer.first_name # => true +a.first_name = 'David' +a.first_name == b.writer.first_name # => false ``` -There are a few limitations to `inverse_of` support: +Active Record provides the `:inverse_of` option so you can explicitly declare bi-directional associations: + +```ruby +class Author < ApplicationRecord + has_many :books, inverse_of: 'writer' +end + +class Book < ApplicationRecord + belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' +end +``` + +By including the `:inverse_of` option in the `has_many` association declaration, Active Record will now recognize the bi-directional association: + +```ruby +a = Author.first +b = a.books.first +a.first_name == b.writer.first_name # => true +a.first_name = 'David' +a.first_name == b.writer.first_name # => true +``` + +There are a few limitations to `:inverse_of` support: * They do not work with `:through` associations. * They do not work with `:polymorphic` associations. * They do not work with `:as` associations. -* For `belongs_to` associations, `has_many` inverse associations are ignored. - -Every association will attempt to automatically find the inverse association -and set the `:inverse_of` option heuristically (based on the association name). -Most associations with standard names will be supported. However, associations -that contain the following options will not have their inverses set -automatically: - -* :conditions -* :through -* :polymorphic -* :foreign_key Detailed Association Reference ------------------------------ @@ -742,7 +806,7 @@ The `belongs_to` association creates a one-to-one match with another model. In d When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association: -* `association(force_reload = false)` +* `association` * `association=(associate)` * `build_association(attributes = {})` * `create_association(attributes = {})` @@ -751,39 +815,43 @@ When you declare a `belongs_to` association, the declaring class automatically g In all of these methods, `association` is replaced with the symbol passed as the first argument to `belongs_to`. For example, given the declaration: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -Each instance of the order model will have these methods: +Each instance of the `Book` model will have these methods: ```ruby -customer -customer= -build_customer -create_customer -create_customer! +author +author= +build_author +create_author +create_author! ``` NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix. -##### `association(force_reload = false)` +##### `association` The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`. ```ruby -@customer = @order.customer +@author = @book.author ``` -If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument. +If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object. + +```ruby +@author = @book.reload.author +``` ##### `association=(associate)` -The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from the associate object and setting this object's foreign key to the same value. +The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from the associated object and setting this object's foreign key to the same value. ```ruby -@order.customer = @customer +@book.author = @author ``` ##### `build_association(attributes = {})` @@ -791,8 +859,8 @@ The `association=` method assigns an associated object to this object. Behind th The `build_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through this object's foreign key will be set, but the associated object will _not_ yet be saved. ```ruby -@customer = @order.build_customer(customer_number: 123, - customer_name: "John Doe") +@author = @book.build_author(author_number: 123, + author_name: "John Doe") ``` ##### `create_association(attributes = {})` @@ -800,8 +868,8 @@ The `build_association` method returns a new object of the associated type. This The `create_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through this object's foreign key will be set, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. ```ruby -@customer = @order.create_customer(customer_number: 123, - customer_name: "John Doe") +@author = @book.create_author(author_number: 123, + author_name: "John Doe") ``` ##### `create_association!(attributes = {})` @@ -814,8 +882,8 @@ Does the same as `create_association` above, but raises `ActiveRecord::RecordInv While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `belongs_to` association reference. Such customizations can easily be accomplished by passing options and scope blocks when you create the association. For example, this association uses two such options: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, dependent: :destroy, +class Book < ApplicationRecord + belongs_to :author, dependent: :destroy, counter_cache: true end ``` @@ -827,10 +895,12 @@ The `belongs_to` association supports these options: * `:counter_cache` * `:dependent` * `:foreign_key` +* `:primary_key` * `:inverse_of` * `:polymorphic` * `:touch` * `:validate` +* `:optional` ##### `:autosave` @@ -838,11 +908,11 @@ If you set the `:autosave` option to `true`, Rails will save any loaded members ##### `:class_name` -If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if an order belongs to a customer, but the actual name of the model containing customers is `Patron`, you'd set things up this way: +If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a book belongs to an author, but the actual name of the model containing authors is `Patron`, you'd set things up this way: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, class_name: "Patron" +class Book < ApplicationRecord + belongs_to :author, class_name: "Patron" end ``` @@ -851,47 +921,58 @@ end The `:counter_cache` option can be used to make finding the number of belonging objects more efficient. Consider these models: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -With these declarations, asking for the value of `@customer.orders.size` requires making a call to the database to perform a `COUNT(*)` query. To avoid this call, you can add a counter cache to the _belonging_ model: +With these declarations, asking for the value of `@author.books.size` requires making a call to the database to perform a `COUNT(*)` query. To avoid this call, you can add a counter cache to the _belonging_ model: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, counter_cache: true +class Book < ApplicationRecord + belongs_to :author, counter_cache: true end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` With this declaration, Rails will keep the cache value up to date, and then return that value in response to the `size` method. -Although the `:counter_cache` option is specified on the model that includes the `belongs_to` declaration, the actual column must be added to the _associated_ model. In the case above, you would need to add a column named `orders_count` to the `Customer` model. You can override the default column name if you need to: +Although the `:counter_cache` option is specified on the model that includes +the `belongs_to` declaration, the actual column must be added to the +_associated_ (`has_many`) model. In the case above, you would need to add a +column named `books_count` to the `Author` model. + +You can override the default column name by specifying a custom column name in +the `counter_cache` declaration instead of `true`. For example, to use +`count_of_books` instead of `books_count`: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, counter_cache: :count_of_orders +class Book < ApplicationRecord + belongs_to :author, counter_cache: :count_of_books end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` +NOTE: You only need to specify the :counter_cache option on the `belongs_to` +side of the association. + Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`. ##### `:dependent` -If you set the `:dependent` option to: +Controls what happens to associated objects when their owner is destroyed: -* `:destroy`, when the object is destroyed, `destroy` will be called on its -associated objects. -* `:delete`, when the object is destroyed, all its associated objects will be -deleted directly from the database without calling their `destroy` method. +* `:destroy` causes the associated objects to also be destroyed. +* `:delete_all` causes the associated objects to be deleted directly from the database (callbacks are not executed). +* `:nullify` causes the foreign keys to be set to `NULL` (callbacks are not executed). +* `:restrict_with_exception` causes an exception to be raised if there are associated records. +* `:restrict_with_error` causes an error to be added to the owner if there are associated objects. WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database. @@ -900,25 +981,45 @@ WARNING: You should not specify this option on a `belongs_to` association that i By convention, Rails assumes that the column used to hold the foreign key on this model is the name of the association with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, class_name: "Patron", +class Book < ApplicationRecord + belongs_to :author, class_name: "Patron", foreign_key: "patron_id" end ``` TIP: In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations. +##### `:primary_key` + +By convention, Rails assumes that the `id` column is used to hold the primary key +of its tables. The `:primary_key` option allows you to specify a different column. + +For example, given we have a `users` table with `guid` as the primary key. If we want a separate `todos` table to hold the foreign key `user_id` in the `guid` column, then we can use `primary_key` to achieve this like so: + +```ruby +class User < ApplicationRecord + self.primary_key = 'guid' # primary key is guid and not id +end + +class Todo < ApplicationRecord + belongs_to :user, primary_key: 'guid' +end +``` + +When we execute `@user.todos.create` then the `@todo` record will have its +`user_id` value as the `guid` value of `@user`. + ##### `:inverse_of` The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options. ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books, inverse_of: :author end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :author, inverse_of: :books end ``` @@ -928,23 +1029,23 @@ Passing `true` to the `:polymorphic` option indicates that this is a polymorphic ##### `:touch` -If you set the `:touch` option to `:true`, then the `updated_at` or `updated_on` timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: +If you set the `:touch` option to `true`, then the `updated_at` or `updated_on` timestamp on the associated object will be set to the current time whenever this object is saved or destroyed: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, touch: true +class Book < ApplicationRecord + belongs_to :author, touch: true end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -In this case, saving or destroying an order will update the timestamp on the associated customer. You can also specify a particular timestamp attribute to update: +In this case, saving or destroying a book will update the timestamp on the associated author. You can also specify a particular timestamp attribute to update: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, touch: :orders_updated_at +class Book < ApplicationRecord + belongs_to :author, touch: :books_updated_at end ``` @@ -952,13 +1053,18 @@ end If you set the `:validate` option to `true`, then associated objects will be validated whenever you save this object. By default, this is `false`: associated objects will not be validated when this object is saved. +##### `:optional` + +If you set the `:optional` option to `true`, then the presence of the associated +object won't be validated. By default, this option is set to `false`. + #### Scopes for `belongs_to` There may be times when you wish to customize the query used by `belongs_to`. Such customizations can be achieved via a scope block. For example: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, -> { where active: true }, +class Book < ApplicationRecord + belongs_to :author, -> { where active: true }, dependent: :destroy end ``` @@ -975,8 +1081,8 @@ You can use any of the standard [querying methods](active_record_querying.html) The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, -> { where active: true } +class book < ApplicationRecord + belongs_to :author, -> { where active: true } end ``` @@ -985,38 +1091,38 @@ end You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: ```ruby -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -If you frequently retrieve customers directly from line items (`@line_item.order.customer`), then you can make your code somewhat more efficient by including customers in the association from line items to orders: +If you frequently retrieve authors directly from line items (`@line_item.book.author`), then you can make your code somewhat more efficient by including authors in the association from line items to books: ```ruby -class LineItem < ActiveRecord::Base - belongs_to :order, -> { includes :customer } +class LineItem < ApplicationRecord + belongs_to :book, -> { includes :author } end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -NOTE: There's no need to use `includes` for immediate associations - that is, if you have `Order belongs_to :customer`, then the customer is eager-loaded automatically when it's needed. +NOTE: There's no need to use `includes` for immediate associations - that is, if you have `Book belongs_to :author`, then the author is eager-loaded automatically when it's needed. ##### `readonly` @@ -1033,8 +1139,8 @@ TIP: If you use the `select` method on a `belongs_to` association, you should al You can see if any associated objects exist by using the `association.nil?` method: ```ruby -if @order.customer.nil? - @msg = "No customer found for this order" +if @book.author.nil? + @msg = "No author found for this book" end ``` @@ -1050,7 +1156,7 @@ The `has_one` association creates a one-to-one match with another model. In data When you declare a `has_one` association, the declaring class automatically gains five methods related to the association: -* `association(force_reload = false)` +* `association` * `association=(associate)` * `build_association(attributes = {})` * `create_association(attributes = {})` @@ -1059,7 +1165,7 @@ When you declare a `has_one` association, the declaring class automatically gain In all of these methods, `association` is replaced with the symbol passed as the first argument to `has_one`. For example, given the declaration: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end ``` @@ -1076,7 +1182,7 @@ create_account! NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix. -##### `association(force_reload = false)` +##### `association` The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`. @@ -1084,11 +1190,15 @@ The `association` method returns the associated object, if any. If no associated @account = @supplier.account ``` -If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument. +If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object. + +```ruby +@account = @supplier.reload.account +``` ##### `association=(associate)` -The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from this object and setting the associate object's foreign key to the same value. +The `association=` method assigns an associated object to this object. Behind the scenes, this means extracting the primary key from this object and setting the associated object's foreign key to the same value. ```ruby @supplier.account = @account @@ -1119,7 +1229,7 @@ Does the same as `create_association` above, but raises `ActiveRecord::RecordInv While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_one` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, class_name: "Billing", dependent: :nullify end ``` @@ -1151,7 +1261,7 @@ If you set the `:autosave` option to `true`, Rails will save any loaded members If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a supplier has an account, but the actual name of the model containing accounts is `Billing`, you'd set things up this way: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, class_name: "Billing" end ``` @@ -1169,15 +1279,15 @@ Controls what happens to the associated object when its owner is destroyed: It's necessary not to set or leave `:nullify` option for those associations that have `NOT NULL` database constraints. If you don't set `dependent` to destroy such associations you won't be able to change the associated object -because initial associated object foreign key will be set to unallowed `NULL` -value. +because the initial associated object's foreign key will be set to the +unallowed `NULL` value. ##### `:foreign_key` By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, foreign_key: "supp_id" end ``` @@ -1189,11 +1299,11 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options. ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, inverse_of: :supplier end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier, inverse_of: :account end ``` @@ -1223,7 +1333,7 @@ If you set the `:validate` option to `true`, then associated objects will be val There may be times when you wish to customize the query used by `has_one`. Such customizations can be achieved via a scope block. For example: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { where active: true } end ``` @@ -1240,7 +1350,7 @@ You can use any of the standard [querying methods](active_record_querying.html) The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { where "confirmed = 1" } end ``` @@ -1250,16 +1360,16 @@ end You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier belongs_to :representative end -class Representative < ActiveRecord::Base +class Representative < ApplicationRecord has_many :accounts end ``` @@ -1267,16 +1377,16 @@ end If you frequently retrieve representatives directly from suppliers (`@supplier.account.representative`), then you can make your code somewhat more efficient by including representatives in the association from suppliers to accounts: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { includes :representative } end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier belongs_to :representative end -class Representative < ActiveRecord::Base +class Representative < ApplicationRecord has_many :accounts end ``` @@ -1317,13 +1427,13 @@ The `has_many` association creates a one-to-many relationship with another model When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association: -* `collection(force_reload = false)` +* `collection` * `collection<<(object, ...)` * `collection.delete(object, ...)` * `collection.destroy(object, ...)` -* `collection=objects` +* `collection=(objects)` * `collection_singular_ids` -* `collection_singular_ids=ids` +* `collection_singular_ids=(ids)` * `collection.clear` * `collection.empty?` * `collection.size` @@ -1337,38 +1447,38 @@ When you declare a `has_many` association, the declaring class automatically gai In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -Each instance of the customer model will have these methods: +Each instance of the `Author` model will have these methods: ```ruby -orders(force_reload = false) -orders<<(object, ...) -orders.delete(object, ...) -orders.destroy(object, ...) -orders=objects -order_ids -order_ids=ids -orders.clear -orders.empty? -orders.size -orders.find(...) -orders.where(...) -orders.exists?(...) -orders.build(attributes = {}, ...) -orders.create(attributes = {}) -orders.create!(attributes = {}) +books +books<<(object, ...) +books.delete(object, ...) +books.destroy(object, ...) +books=(objects) +book_ids +book_ids=(ids) +books.clear +books.empty? +books.size +books.find(...) +books.where(...) +books.exists?(...) +books.build(attributes = {}, ...) +books.create(attributes = {}) +books.create!(attributes = {}) ``` -##### `collection(force_reload = false)` +##### `collection` The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. ```ruby -@orders = @customer.orders +@books = @author.books ``` ##### `collection<<(object, ...)` @@ -1376,7 +1486,7 @@ The `collection` method returns an array of all of the associated objects. If th The `collection<<` method adds one or more objects to the collection by setting their foreign keys to the primary key of the calling model. ```ruby -@customer.orders << @order1 +@author.books << @book1 ``` ##### `collection.delete(object, ...)` @@ -1384,7 +1494,7 @@ The `collection<<` method adds one or more objects to the collection by setting The `collection.delete` method removes one or more objects from the collection by setting their foreign keys to `NULL`. ```ruby -@customer.orders.delete(@order1) +@author.books.delete(@book1) ``` WARNING: Additionally, objects will be destroyed if they're associated with `dependent: :destroy`, and deleted if they're associated with `dependent: :delete_all`. @@ -1394,38 +1504,45 @@ WARNING: Additionally, objects will be destroyed if they're associated with `dep The `collection.destroy` method removes one or more objects from the collection by running `destroy` on each object. ```ruby -@customer.orders.destroy(@order1) +@author.books.destroy(@book1) ``` WARNING: Objects will _always_ be removed from the database, ignoring the `:dependent` option. -##### `collection=objects` +##### `collection=(objects)` -The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. +The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. The changes are persisted to the database. ##### `collection_singular_ids` The `collection_singular_ids` method returns an array of the ids of the objects in the collection. ```ruby -@order_ids = @customer.order_ids +@book_ids = @author.book_ids ``` -##### `collection_singular_ids=ids` +##### `collection_singular_ids=(ids)` -The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. +The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. The changes are persisted to the database. ##### `collection.clear` -The `collection.clear` method removes every object from the collection. This destroys the associated objects if they are associated with `dependent: :destroy`, deletes them directly from the database if `dependent: :delete_all`, and otherwise sets their foreign keys to `NULL`. +The `collection.clear` method removes all objects from the collection according to the strategy specified by the `dependent` option. If no option is given, it follows the default strategy. The default strategy for `has_many :through` associations is `delete_all`, and for `has_many` associations is to set the foreign keys to `NULL`. + +```ruby +@author.books.clear +``` + +WARNING: Objects will be deleted if they're associated with `dependent: :destroy`, +just like `dependent: :delete_all`. ##### `collection.empty?` The `collection.empty?` method returns `true` if the collection does not contain any associated objects. ```erb -<% if @customer.orders.empty? %> - No Orders Found +<% if @author.books.empty? %> + No Books Found <% end %> ``` @@ -1434,7 +1551,7 @@ The `collection.empty?` method returns `true` if the collection does not contain The `collection.size` method returns the number of objects in the collection. ```ruby -@order_count = @customer.orders.size +@book_count = @author.books.size ``` ##### `collection.find(...)` @@ -1442,7 +1559,7 @@ The `collection.size` method returns the number of objects in the collection. The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. ```ruby -@open_orders = @customer.orders.find(1) +@available_books = @author.books.find(1) ``` ##### `collection.where(...)` @@ -1450,30 +1567,42 @@ The `collection.find` method finds objects within the collection. It uses the sa The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed. ```ruby -@open_orders = @customer.orders.where(open: true) # No query yet -@open_order = @open_orders.first # Now the database will be queried +@available_books = @author.books.where(available: true) # No query yet +@available_book = @available_books.first # Now the database will be queried ``` ##### `collection.exists?(...)` -The `collection.exists?` method checks whether an object meeting the supplied conditions exists in the collection. It uses the same syntax and options as `ActiveRecord::Base.exists?`. +The `collection.exists?` method checks whether an object meeting the supplied +conditions exists in the collection. It uses the same syntax and options as +[`ActiveRecord::Base.exists?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F). ##### `collection.build(attributes = {}, ...)` -The `collection.build` method returns one or more new objects of the associated type. These objects will be instantiated from the passed attributes, and the link through their foreign key will be created, but the associated objects will _not_ yet be saved. +The `collection.build` method returns a single or array of new objects of the associated type. The object(s) will be instantiated from the passed attributes, and the link through their foreign key will be created, but the associated objects will _not_ yet be saved. ```ruby -@order = @customer.orders.build(order_date: Time.now, - order_number: "A12345") +@book = @author.books.build(published_at: Time.now, + book_number: "A12345") + +@books = @author.books.build([ + { published_at: Time.now, book_number: "A12346" }, + { published_at: Time.now, book_number: "A12347" } +]) ``` ##### `collection.create(attributes = {})` -The `collection.create` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. +The `collection.create` method returns a single or array of new objects of the associated type. The object(s) will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. ```ruby -@order = @customer.orders.create(order_date: Time.now, - order_number: "A12345") +@book = @author.books.create(published_at: Time.now, + book_number: "A12345") + +@books = @author.books.create([ + { published_at: Time.now, book_number: "A12346" }, + { published_at: Time.now, book_number: "A12347" } +]) ``` ##### `collection.create!(attributes = {})` @@ -1485,8 +1614,8 @@ Does the same as `collection.create` above, but raises `ActiveRecord::RecordInva While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, dependent: :delete_all, validate: :false +class Author < ApplicationRecord + has_many :books, dependent: :delete_all, validate: false end ``` @@ -1495,6 +1624,7 @@ The `has_many` association supports these options: * `:as` * `:autosave` * `:class_name` +* `:counter_cache` * `:dependent` * `:foreign_key` * `:inverse_of` @@ -1514,14 +1644,18 @@ If you set the `:autosave` option to `true`, Rails will save any loaded members ##### `:class_name` -If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a customer has many orders, but the actual name of the model containing orders is `Transaction`, you'd set things up this way: +If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if an author has many books, but the actual name of the model containing books is `Transaction`, you'd set things up this way: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, class_name: "Transaction" +class Author < ApplicationRecord + has_many :books, class_name: "Transaction" end ``` +##### `:counter_cache` + +This option can be used to configure a custom named `:counter_cache`. You only need this option when you customized the name of your `:counter_cache` on the [belongs_to association](#options-for-belongs-to). + ##### `:dependent` Controls what happens to the associated objects when their owner is destroyed: @@ -1532,15 +1666,13 @@ Controls what happens to the associated objects when their owner is destroyed: * `:restrict_with_exception` causes an exception to be raised if there are any associated records * `:restrict_with_error` causes an error to be added to the owner if there are any associated objects -NOTE: This option is ignored when you use the `:through` option on the association. - ##### `:foreign_key` By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, foreign_key: "cust_id" +class Author < ApplicationRecord + has_many :books, foreign_key: "cust_id" end ``` @@ -1551,12 +1683,12 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options. ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books, inverse_of: :author end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :author, inverse_of: :books end ``` @@ -1564,18 +1696,19 @@ end By convention, Rails assumes that the column used to hold the primary key of the association is `id`. You can override this and explicitly specify the primary key with the `:primary_key` option. -Let's say that `users` table has `id` as the primary_key but it also has -`guid` column. And the requirement is that `todos` table should hold -`guid` column value and not `id` value. This can be achieved like this +Let's say the `users` table has `id` as the primary_key but it also +has a `guid` column. The requirement is that the `todos` table should +hold the `guid` column value as the foreign key and not `id` +value. This can be achieved like this: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_many :todos, primary_key: :guid end ``` -Now if we execute `@user.todos.create` then `@todo` record will have -`user_id` value as the `guid` value of `@user`. +Now if we execute `@todo = @user.todos.create` then the `@todo` +record's `user_id` value will be the `guid` value of `@user`. ##### `:source` @@ -1599,8 +1732,8 @@ If you set the `:validate` option to `false`, then associated objects will not b There may be times when you wish to customize the query used by `has_many`. Such customizations can be achieved via a scope block. For example: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { where processed: true } +class Author < ApplicationRecord + has_many :books, -> { where processed: true } end ``` @@ -1615,29 +1748,29 @@ You can use any of the standard [querying methods](active_record_querying.html) * `order` * `readonly` * `select` -* `uniq` +* `distinct` ##### `where` The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class Customer < ActiveRecord::Base - has_many :confirmed_orders, -> { where "confirmed = 1" }, - class_name: "Order" +class Author < ApplicationRecord + has_many :confirmed_books, -> { where "confirmed = 1" }, + class_name: "Book" end ``` You can also set conditions via a hash: ```ruby -class Customer < ActiveRecord::Base - has_many :confirmed_orders, -> { where confirmed: true }, - class_name: "Order" +class Author < ApplicationRecord + has_many :confirmed_books, -> { where confirmed: true }, + class_name: "Book" end ``` -If you use a hash-style `where` option, then record creation via this association will be automatically scoped using the hash. In this case, using `@customer.confirmed_orders.create` or `@customer.confirmed_orders.build` will create orders where the confirmed column has the value `true`. +If you use a hash-style `where` option, then record creation via this association will be automatically scoped using the hash. In this case, using `@author.confirmed_books.create` or `@author.confirmed_books.build` will create books where the confirmed column has the value `true`. ##### `extending` @@ -1648,9 +1781,9 @@ The `extending` method specifies a named module to extend the association proxy. The `group` method supplies an attribute name to group the result set by, using a `GROUP BY` clause in the finder SQL. ```ruby -class Customer < ActiveRecord::Base - has_many :line_items, -> { group 'orders.id' }, - through: :orders +class Author < ApplicationRecord + has_many :line_items, -> { group 'books.id' }, + through: :books end ``` @@ -1659,34 +1792,34 @@ end You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end ``` -If you frequently retrieve line items directly from customers (`@customer.orders.line_items`), then you can make your code somewhat more efficient by including line items in the association from customers to orders: +If you frequently retrieve line items directly from authors (`@author.books.line_items`), then you can make your code somewhat more efficient by including line items in the association from authors to books: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { includes :line_items } +class Author < ApplicationRecord + has_many :books, -> { includes :line_items } end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end ``` @@ -1695,10 +1828,10 @@ end The `limit` method lets you restrict the total number of objects that will be fetched through an association. ```ruby -class Customer < ActiveRecord::Base - has_many :recent_orders, - -> { order('order_date desc').limit(100) }, - class_name: "Order", +class Author < ApplicationRecord + has_many :recent_books, + -> { order('published_at desc').limit(100) }, + class_name: "Book", end ``` @@ -1711,8 +1844,8 @@ The `offset` method lets you specify the starting offset for fetching objects vi The `order` method dictates the order in which associated objects will be received (in the syntax used by an SQL `ORDER BY` clause). ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { order "date_confirmed DESC" } +class Author < ApplicationRecord + has_many :books, -> { order "date_confirmed DESC" } end ``` @@ -1732,7 +1865,7 @@ Use the `distinct` method to keep the collection free of duplicates. This is mostly useful together with the `:through` option. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :readings has_many :articles, through: :readings end @@ -1742,7 +1875,7 @@ article = Article.create(name: 'a1') person.articles << article person.articles << article person.articles.inspect # => [#
, #
] -Reading.all.inspect # => [#, #] +Reading.all.inspect # => [#, #] ``` In the above case there are two readings and `person.articles` brings out both of @@ -1761,7 +1894,7 @@ article = Article.create(name: 'a1') person.articles << article person.articles << article person.articles.inspect # => [#
] -Reading.all.inspect # => [#, #] +Reading.all.inspect # => [#, #] ``` In the above case there are still two readings. However `person.articles` shows @@ -1771,11 +1904,21 @@ If you want to make sure that, upon insertion, all of the records in the persisted association are distinct (so that you can be sure that when you inspect the association that you will never find duplicate records), you should add a unique index on the table itself. For example, if you have a table named -`person_articles` and you want to make sure all the articles are unique, you could -add the following in a migration: +`readings` and you want to make sure the articles can only be added to a person once, +you could add the following in a migration: ```ruby -add_index :person_articles, :article, unique: true +add_index :readings, [:person_id, :article_id], unique: true +``` + +Once you have this unique index, attempting to add the article to a person twice +will raise an `ActiveRecord::RecordNotUnique` error: + +```ruby +person = Person.create(name: 'Honda') +article = Article.create(name: 'a1') +person.articles << article +person.articles << article # => ActiveRecord::RecordNotUnique ``` Note that checking for uniqueness using something like `include?` is subject @@ -1806,13 +1949,13 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association: -* `collection(force_reload = false)` +* `collection` * `collection<<(object, ...)` * `collection.delete(object, ...)` * `collection.destroy(object, ...)` -* `collection=objects` +* `collection=(objects)` * `collection_singular_ids` -* `collection_singular_ids=ids` +* `collection_singular_ids=(ids)` * `collection.clear` * `collection.empty?` * `collection.size` @@ -1826,21 +1969,21 @@ When you declare a `has_and_belongs_to_many` association, the declaring class au In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_and_belongs_to_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration: ```ruby -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` -Each instance of the part model will have these methods: +Each instance of the `Part` model will have these methods: ```ruby -assemblies(force_reload = false) +assemblies assemblies<<(object, ...) assemblies.delete(object, ...) assemblies.destroy(object, ...) -assemblies=objects +assemblies=(objects) assembly_ids -assembly_ids=ids +assembly_ids=(ids) assemblies.clear assemblies.empty? assemblies.size @@ -1859,7 +2002,7 @@ If the join table for a `has_and_belongs_to_many` association has additional col WARNING: The use of extra attributes on the join table in a `has_and_belongs_to_many` association is deprecated. If you require this sort of complex behavior on the table that joins two models in a many-to-many relationship, you should use a `has_many :through` association instead of `has_and_belongs_to_many`. -##### `collection(force_reload = false)` +##### `collection` The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. @@ -1885,19 +2028,17 @@ The `collection.delete` method removes one or more objects from the collection b @part.assemblies.delete(@assembly1) ``` -WARNING: This does not trigger callbacks on the join records. - ##### `collection.destroy(object, ...)` -The `collection.destroy` method removes one or more objects from the collection by running `destroy` on each record in the join table, including running callbacks. This does not destroy the objects. +The `collection.destroy` method removes one or more objects from the collection by deleting records in the join table. This does not destroy the objects. ```ruby @part.assemblies.destroy(@assembly1) ``` -##### `collection=objects` +##### `collection=(objects)` -The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. +The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate. The changes are persisted to the database. ##### `collection_singular_ids` @@ -1907,9 +2048,9 @@ The `collection_singular_ids` method returns an array of the ids of the objects @assembly_ids = @part.assembly_ids ``` -##### `collection_singular_ids=ids` +##### `collection_singular_ids=(ids)` -The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. +The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate. The changes are persisted to the database. ##### `collection.clear` @@ -1951,7 +2092,9 @@ The `collection.where` method finds objects within the collection based on the c ##### `collection.exists?(...)` -The `collection.exists?` method checks whether an object meeting the supplied conditions exists in the collection. It uses the same syntax and options as `ActiveRecord::Base.exists?`. +The `collection.exists?` method checks whether an object meeting the supplied +conditions exists in the collection. It uses the same syntax and options as +[`ActiveRecord::Base.exists?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F). ##### `collection.build(attributes = {})` @@ -1978,9 +2121,9 @@ Does the same as `collection.create`, but raises `ActiveRecord::RecordInvalid` i While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_and_belongs_to_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options: ```ruby -class Parts < ActiveRecord::Base - has_and_belongs_to_many :assemblies, autosave: true, - readonly: true +class Parts < ApplicationRecord + has_and_belongs_to_many :assemblies, -> { readonly }, + autosave: true end ``` @@ -1992,7 +2135,6 @@ The `has_and_belongs_to_many` association supports these options: * `:foreign_key` * `:join_table` * `:validate` -* `:readonly` ##### `:association_foreign_key` @@ -2001,7 +2143,7 @@ By convention, Rails assumes that the column in the join table used to hold the TIP: The `:foreign_key` and `:association_foreign_key` options are useful when setting up a many-to-many self-join. For example: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_and_belongs_to_many :friends, class_name: "User", foreign_key: "this_user_id", @@ -2018,7 +2160,7 @@ If you set the `:autosave` option to `true`, Rails will save any loaded members If the name of the other model cannot be derived from the association name, you can use the `:class_name` option to supply the model name. For example, if a part has many assemblies, but the actual name of the model containing assemblies is `Gadget`, you'd set things up this way: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, class_name: "Gadget" end ``` @@ -2028,7 +2170,7 @@ end By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to this model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_and_belongs_to_many :friends, class_name: "User", foreign_key: "this_user_id", @@ -2049,7 +2191,7 @@ If you set the `:validate` option to `false`, then associated objects will not b There may be times when you wish to customize the query used by `has_and_belongs_to_many`. Such customizations can be achieved via a scope block. For example: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where active: true } end ``` @@ -2065,14 +2207,14 @@ You can use any of the standard [querying methods](active_record_querying.html) * `order` * `readonly` * `select` -* `uniq` +* `distinct` ##### `where` The `where` method lets you specify the conditions that the associated object must meet. ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where "factory = 'Seattle'" } end @@ -2081,7 +2223,7 @@ end You can also set conditions via a hash: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where factory: 'Seattle' } end @@ -2098,7 +2240,7 @@ The `extending` method specifies a named module to extend the association proxy. The `group` method supplies an attribute name to group the result set by, using a `GROUP BY` clause in the finder SQL. ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { group "factory" } end ``` @@ -2112,7 +2254,7 @@ You can use the `includes` method to specify second-order associations that shou The `limit` method lets you restrict the total number of objects that will be fetched through an association. ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { order("created_at DESC").limit(50) } end @@ -2127,7 +2269,7 @@ The `offset` method lets you specify the starting offset for fetching objects vi The `order` method dictates the order in which associated objects will be received (in the syntax used by an SQL `ORDER BY` clause). ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { order "assembly_name ASC" } end @@ -2141,9 +2283,9 @@ If you use the `readonly` method, then the associated objects will be read-only The `select` method lets you override the SQL `SELECT` clause that is used to retrieve data about the associated objects. By default, Rails retrieves all columns. -##### `uniq` +##### `distinct` -Use the `uniq` method to remove duplicates from the collection. +Use the `distinct` method to remove duplicates from the collection. #### When are Objects Saved? @@ -2169,10 +2311,10 @@ Association callbacks are similar to normal callbacks, but they are triggered by You define association callbacks by adding options to the association declaration. For example: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, before_add: :check_credit_limit +class Author < ApplicationRecord + has_many :books, before_add: :check_credit_limit - def check_credit_limit(order) + def check_credit_limit(book) ... end end @@ -2183,15 +2325,15 @@ Rails passes the object being added or removed to the callback. You can stack callbacks on a single event by passing them as an array: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, +class Author < ApplicationRecord + has_many :books, before_add: [:check_credit_limit, :calculate_shipping_charges] - def check_credit_limit(order) + def check_credit_limit(book) ... end - def calculate_shipping_charges(order) + def calculate_shipping_charges(book) ... end end @@ -2204,10 +2346,10 @@ If a `before_add` callback throws an exception, the object does not get added to You're not limited to the functionality that Rails automatically builds into association proxy objects. You can also extend these objects through anonymous modules, adding new finders, creators, or other methods. For example: ```ruby -class Customer < ActiveRecord::Base - has_many :orders do - def find_by_order_prefix(order_number) - find_by(region_id: order_number[0..2]) +class Author < ApplicationRecord + has_many :books do + def find_by_book_prefix(book_number) + find_by(category_id: book_number[0..2]) end end end @@ -2222,11 +2364,11 @@ module FindRecentExtension end end -class Customer < ActiveRecord::Base - has_many :orders, -> { extending FindRecentExtension } +class Author < ApplicationRecord + has_many :books, -> { extending FindRecentExtension } end -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_many :deliveries, -> { extending FindRecentExtension } end ``` @@ -2236,3 +2378,67 @@ Extensions can refer to the internals of the association proxy using these three * `proxy_association.owner` returns the object that the association is a part of. * `proxy_association.reflection` returns the reflection object that describes the association. * `proxy_association.target` returns the associated object for `belongs_to` or `has_one`, or the collection of associated objects for `has_many` or `has_and_belongs_to_many`. + +Single Table Inheritance +------------------------ + +Sometimes, you may want to share fields and behavior between different models. +Let's say we have Car, Motorcycle and Bicycle models. We will want to share +the `color` and `price` fields and some methods for all of them, but having some +specific behavior for each, and separated controllers too. + +Rails makes this quite easy. First, let's generate the base Vehicle model: + +```bash +$ rails generate model vehicle type:string color:string price:decimal{10.2} +``` + +Did you note we are adding a "type" field? Since all models will be saved in a +single database table, Rails will save in this column the name of the model that +is being saved. In our example, this can be "Car", "Motorcycle" or "Bicycle." +STI won't work without a "type" field in the table. + +Next, we will generate the three models that inherit from Vehicle. For this, +we can use the `--parent=PARENT` option, which will generate a model that +inherits from the specified parent and without equivalent migration (since the +table already exists). + +For example, to generate the Car model: + +```bash +$ rails generate model car --parent=Vehicle +``` + +The generated model will look like this: + +```ruby +class Car < Vehicle +end +``` + +This means that all behavior added to Vehicle is available for Car too, as +associations, public methods, etc. + +Creating a car will save it in the `vehicles` table with "Car" as the `type` field: + +```ruby +Car.create(color: 'Red', price: 10000) +``` + +will generate the following SQL: + +```sql +INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000) +``` + +Querying car records will just search for vehicles that are cars: + +```ruby +Car.all +``` + +will run a query like: + +```sql +SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car') +``` diff --git a/source/autoloading_and_reloading_constants.md b/source/autoloading_and_reloading_constants.md new file mode 100644 index 0000000..05743ee --- /dev/null +++ b/source/autoloading_and_reloading_constants.md @@ -0,0 +1,1314 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Autoloading and Reloading Constants +=================================== + +This guide documents how constant autoloading and reloading works. + +After reading this guide, you will know: + +* Key aspects of Ruby constants +* What is `autoload_paths` +* How constant autoloading works +* What is `require_dependency` +* How constant reloading works +* Solutions to common autoloading gotchas + +-------------------------------------------------------------------------------- + + +Introduction +------------ + +Ruby on Rails allows applications to be written as if their code was preloaded. + +In a normal Ruby program classes need to load their dependencies: + +```ruby +require 'application_controller' +require 'post' + +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +Our Rubyist instinct quickly sees some redundancy in there: If classes were +defined in files matching their name, couldn't their loading be automated +somehow? We could save scanning the file for dependencies, which is brittle. + +Moreover, `Kernel#require` loads files once, but development is much more smooth +if code gets refreshed when it changes without restarting the server. It would +be nice to be able to use `Kernel#load` in development, and `Kernel#require` in +production. + +Indeed, those features are provided by Ruby on Rails, where we just write + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +This guide documents how that works. + + +Constants Refresher +------------------- + +While constants are trivial in most programming languages, they are a rich +topic in Ruby. + +It is beyond the scope of this guide to document Ruby constants, but we are +nevertheless going to highlight a few key topics. Truly grasping the following +sections is instrumental to understanding constant autoloading and reloading. + +### Nesting + +Class and module definitions can be nested to create namespaces: + +```ruby +module XML + class SAXParser + # (1) + end +end +``` + +The *nesting* at any given place is the collection of enclosing nested class and +module objects outwards. The nesting at any given place can be inspected with +`Module.nesting`. For example, in the previous example, the nesting at +(1) is + +```ruby +[XML::SAXParser, XML] +``` + +It is important to understand that the nesting is composed of class and module +*objects*, it has nothing to do with the constants used to access them, and is +also unrelated to their names. + +For instance, while this definition is similar to the previous one: + +```ruby +class XML::SAXParser + # (2) +end +``` + +the nesting in (2) is different: + +```ruby +[XML::SAXParser] +``` + +`XML` does not belong to it. + +We can see in this example that the name of a class or module that belongs to a +certain nesting does not necessarily correlate with the namespaces at the spot. + +Even more, they are totally independent, take for instance + +```ruby +module X + module Y + end +end + +module A + module B + end +end + +module X::Y + module A::B + # (3) + end +end +``` + +The nesting in (3) consists of two module objects: + +```ruby +[A::B, X::Y] +``` + +So, it not only doesn't end in `A`, which does not even belong to the nesting, +but it also contains `X::Y`, which is independent from `A::B`. + +The nesting is an internal stack maintained by the interpreter, and it gets +modified according to these rules: + +* The class object following a `class` keyword gets pushed when its body is +executed, and popped after it. + +* The module object following a `module` keyword gets pushed when its body is +executed, and popped after it. + +* A singleton class opened with `class << object` gets pushed, and popped later. + +* When `instance_eval` is called using a string argument, +the singleton class of the receiver is pushed to the nesting of the eval'ed +code. When `class_eval` or `module_eval` is called using a string argument, +the receiver is pushed to the nesting of the eval'ed code. + +* The nesting at the top-level of code interpreted by `Kernel#load` is empty +unless the `load` call receives a true value as second argument, in which case +a newly created anonymous module is pushed by Ruby. + +It is interesting to observe that blocks do not modify the stack. In particular +the blocks that may be passed to `Class.new` and `Module.new` do not get the +class or module being defined pushed to their nesting. That's one of the +differences between defining classes and modules in one way or another. + +### Class and Module Definitions are Constant Assignments + +Let's suppose the following snippet creates a class (rather than reopening it): + +```ruby +class C +end +``` + +Ruby creates a constant `C` in `Object` and stores in that constant a class +object. The name of the class instance is "C", a string, named after the +constant. + +That is, + +```ruby +class Project < ApplicationRecord +end +``` + +performs a constant assignment equivalent to + +```ruby +Project = Class.new(ApplicationRecord) +``` + +including setting the name of the class as a side-effect: + +```ruby +Project.name # => "Project" +``` + +Constant assignment has a special rule to make that happen: if the object +being assigned is an anonymous class or module, Ruby sets the object's name to +the name of the constant. + +INFO. From then on, what happens to the constant and the instance does not +matter. For example, the constant could be deleted, the class object could be +assigned to a different constant, be stored in no constant anymore, etc. Once +the name is set, it doesn't change. + +Similarly, module creation using the `module` keyword as in + +```ruby +module Admin +end +``` + +performs a constant assignment equivalent to + +```ruby +Admin = Module.new +``` + +including setting the name as a side-effect: + +```ruby +Admin.name # => "Admin" +``` + +WARNING. The execution context of a block passed to `Class.new` or `Module.new` +is not entirely equivalent to the one of the body of the definitions using the +`class` and `module` keywords. But both idioms result in the same constant +assignment. + +Thus, when one informally says "the `String` class", that really means: the +class object stored in the constant called "String" in the class object stored +in the `Object` constant. `String` is otherwise an ordinary Ruby constant and +everything related to constants such as resolution algorithms applies to it. + +Likewise, in the controller + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +`Post` is not syntax for a class. Rather, `Post` is a regular Ruby constant. If +all is good, the constant is evaluated to an object that responds to `all`. + +That is why we talk about *constant* autoloading, Rails has the ability to +load constants on the fly. + +### Constants are Stored in Modules + +Constants belong to modules in a very literal sense. Classes and modules have +a constant table; think of it as a hash table. + +Let's analyze an example to really understand what that means. While common +abuses of language like "the `String` class" are convenient, the exposition is +going to be precise here for didactic purposes. + +Let's consider the following module definition: + +```ruby +module Colors + RED = '0xff0000' +end +``` + +First, when the `module` keyword is processed, the interpreter creates a new +entry in the constant table of the class object stored in the `Object` constant. +Said entry associates the name "Colors" to a newly created module object. +Furthermore, the interpreter sets the name of the new module object to be the +string "Colors". + +Later, when the body of the module definition is interpreted, a new entry is +created in the constant table of the module object stored in the `Colors` +constant. That entry maps the name "RED" to the string "0xff0000". + +In particular, `Colors::RED` is totally unrelated to any other `RED` constant +that may live in any other class or module object. If there were any, they +would have separate entries in their respective constant tables. + +Pay special attention in the previous paragraphs to the distinction between +class and module objects, constant names, and value objects associated to them +in constant tables. + +### Resolution Algorithms + +#### Resolution Algorithm for Relative Constants + +At any given place in the code, let's define *cref* to be the first element of +the nesting if it is not empty, or `Object` otherwise. + +Without getting too much into the details, the resolution algorithm for relative +constant references goes like this: + +1. If the nesting is not empty the constant is looked up in its elements and in +order. The ancestors of those elements are ignored. + +2. If not found, then the algorithm walks up the ancestor chain of the cref. + +3. If not found and the cref is a module, the constant is looked up in `Object`. + +4. If not found, `const_missing` is invoked on the cref. The default +implementation of `const_missing` raises `NameError`, but it can be overridden. + +Rails autoloading **does not emulate this algorithm**, but its starting point is +the name of the constant to be autoloaded, and the cref. See more in [Relative +References](#autoloading-algorithms-relative-references). + +#### Resolution Algorithm for Qualified Constants + +Qualified constants look like this: + +```ruby +Billing::Invoice +``` + +`Billing::Invoice` is composed of two constants: `Billing` is relative and is +resolved using the algorithm of the previous section. + +INFO. Leading colons would make the first segment absolute rather than +relative: `::Billing::Invoice`. That would force `Billing` to be looked up +only as a top-level constant. + +`Invoice` on the other hand is qualified by `Billing` and we are going to see +its resolution next. Let's define *parent* to be that qualifying class or module +object, that is, `Billing` in the example above. The algorithm for qualified +constants goes like this: + +1. The constant is looked up in the parent and its ancestors. + +2. If the lookup fails, `const_missing` is invoked in the parent. The default +implementation of `const_missing` raises `NameError`, but it can be overridden. + +As you see, this algorithm is simpler than the one for relative constants. In +particular, the nesting plays no role here, and modules are not special-cased, +if neither they nor their ancestors have the constants, `Object` is **not** +checked. + +Rails autoloading **does not emulate this algorithm**, but its starting point is +the name of the constant to be autoloaded, and the parent. See more in +[Qualified References](#autoloading-algorithms-qualified-references). + + +Vocabulary +---------- + +### Parent Namespaces + +Given a string with a constant path we define its *parent namespace* to be the +string that results from removing its rightmost segment. + +For example, the parent namespace of the string "A::B::C" is the string "A::B", +the parent namespace of "A::B" is "A", and the parent namespace of "A" is "". + +The interpretation of a parent namespace when thinking about classes and modules +is tricky though. Let's consider a module M named "A::B": + +* The parent namespace, "A", may not reflect nesting at a given spot. + +* The constant `A` may no longer exist, some code could have removed it from +`Object`. + +* If `A` exists, the class or module that was originally in `A` may not be there +anymore. For example, if after a constant removal there was another constant +assignment there would generally be a different object in there. + +* In such case, it could even happen that the reassigned `A` held a new class or +module called also "A"! + +* In the previous scenarios M would no longer be reachable through `A::B` but +the module object itself could still be alive somewhere and its name would +still be "A::B". + +The idea of a parent namespace is at the core of the autoloading algorithms +and helps explain and understand their motivation intuitively, but as you see +that metaphor leaks easily. Given an edge case to reason about, take always into +account that by "parent namespace" the guide means exactly that specific string +derivation. + +### Loading Mechanism + +Rails autoloads files with `Kernel#load` when `config.cache_classes` is false, +the default in development mode, and with `Kernel#require` otherwise, the +default in production mode. + +`Kernel#load` allows Rails to execute files more than once if [constant +reloading](#constant-reloading) is enabled. + +This guide uses the word "load" freely to mean a given file is interpreted, but +the actual mechanism can be `Kernel#load` or `Kernel#require` depending on that +flag. + + +Autoloading Availability +------------------------ + +Rails is always able to autoload provided its environment is in place. For +example the `runner` command autoloads: + +``` +$ bin/rails runner 'p User.column_names' +["id", "email", "created_at", "updated_at"] +``` + +The console autoloads, the test suite autoloads, and of course the application +autoloads. + +By default, Rails eager loads the application files when it boots in production +mode, so most of the autoloading going on in development does not happen. But +autoloading may still be triggered during eager loading. + +For example, given + +```ruby +class BeachHouse < House +end +``` + +if `House` is still unknown when `app/models/beach_house.rb` is being eager +loaded, Rails autoloads it. + + +autoload_paths +-------------- + +As you probably know, when `require` gets a relative file name: + +```ruby +require 'erb' +``` + +Ruby looks for the file in the directories listed in `$LOAD_PATH`. That is, Ruby +iterates over all its directories and for each one of them checks whether they +have a file called "erb.rb", or "erb.so", or "erb.o", or "erb.dll". If it finds +any of them, the interpreter loads it and ends the search. Otherwise, it tries +again in the next directory of the list. If the list gets exhausted, `LoadError` +is raised. + +We are going to cover how constant autoloading works in more detail later, but +the idea is that when a constant like `Post` is hit and missing, if there's a +`post.rb` file for example in `app/models` Rails is going to find it, evaluate +it, and have `Post` defined as a side-effect. + +Alright, Rails has a collection of directories similar to `$LOAD_PATH` in which +to look up `post.rb`. That collection is called `autoload_paths` and by +default it contains: + +* All subdirectories of `app` in the application and engines present at boot + time. For example, `app/controllers`. They do not need to be the default + ones, any custom directories like `app/workers` belong automatically to + `autoload_paths`. + +* Any existing second level directories called `app/*/concerns` in the + application and engines. + +* The directory `test/mailers/previews`. + +Also, this collection is configurable via `config.autoload_paths`. For example, +`lib` was in the list years ago, but no longer is. An application can opt-in +by adding this to `config/application.rb`: + +```ruby +config.autoload_paths << "#{Rails.root}/lib" +``` + +`config.autoload_paths` is not changeable from environment-specific configuration files. + +The value of `autoload_paths` can be inspected. In a just generated application +it is (edited): + +``` +$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths' +.../app/assets +.../app/controllers +.../app/helpers +.../app/mailers +.../app/models +.../app/controllers/concerns +.../app/models/concerns +.../test/mailers/previews +``` + +INFO. `autoload_paths` is computed and cached during the initialization process. +The application needs to be restarted to reflect any changes in the directory +structure. + + +Autoloading Algorithms +---------------------- + +### Relative References + +A relative constant reference may appear in several places, for example, in + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +all three constant references are relative. + +#### Constants after the `class` and `module` Keywords + +Ruby performs a lookup for the constant that follows a `class` or `module` +keyword because it needs to know if the class or module is going to be created +or reopened. + +If the constant is not defined at that point it is not considered to be a +missing constant, autoloading is **not** triggered. + +So, in the previous example, if `PostsController` is not defined when the file +is interpreted Rails autoloading is not going to be triggered, Ruby will just +define the controller. + +#### Top-Level Constants + +On the contrary, if `ApplicationController` is unknown, the constant is +considered missing and an autoload is going to be attempted by Rails. + +In order to load `ApplicationController`, Rails iterates over `autoload_paths`. +First it checks if `app/assets/application_controller.rb` exists. If it does not, +which is normally the case, it continues and finds +`app/controllers/application_controller.rb`. + +If the file defines the constant `ApplicationController` all is fine, otherwise +`LoadError` is raised: + +``` +unable to autoload constant ApplicationController, expected + to define it (LoadError) +``` + +INFO. Rails does not require the value of autoloaded constants to be a class or +module object. For example, if the file `app/models/max_clients.rb` defines +`MAX_CLIENTS = 100` autoloading `MAX_CLIENTS` works just fine. + +#### Namespaces + +Autoloading `ApplicationController` looks directly under the directories of +`autoload_paths` because the nesting in that spot is empty. The situation of +`Post` is different, the nesting in that line is `[PostsController]` and support +for namespaces comes into play. + +The basic idea is that given + +```ruby +module Admin + class BaseController < ApplicationController + @@all_roles = Role.all + end +end +``` + +to autoload `Role` we are going to check if it is defined in the current or +parent namespaces, one at a time. So, conceptually we want to try to autoload +any of + +``` +Admin::BaseController::Role +Admin::Role +Role +``` + +in that order. That's the idea. To do so, Rails looks in `autoload_paths` +respectively for file names like these: + +``` +admin/base_controller/role.rb +admin/role.rb +role.rb +``` + +modulus some additional directory lookups we are going to cover soon. + +INFO. `'Constant::Name'.underscore` gives the relative path without extension of +the file name where `Constant::Name` is expected to be defined. + +Let's see how Rails autoloads the `Post` constant in the `PostsController` +above assuming the application has a `Post` model defined in +`app/models/post.rb`. + +First it checks for `posts_controller/post.rb` in `autoload_paths`: + +``` +app/assets/posts_controller/post.rb +app/controllers/posts_controller/post.rb +app/helpers/posts_controller/post.rb +... +test/mailers/previews/posts_controller/post.rb +``` + +Since the lookup is exhausted without success, a similar search for a directory +is performed, we are going to see why in the [next section](#automatic-modules): + +``` +app/assets/posts_controller/post +app/controllers/posts_controller/post +app/helpers/posts_controller/post +... +test/mailers/previews/posts_controller/post +``` + +If all those attempts fail, then Rails starts the lookup again in the parent +namespace. In this case only the top-level remains: + +``` +app/assets/post.rb +app/controllers/post.rb +app/helpers/post.rb +app/mailers/post.rb +app/models/post.rb +``` + +A matching file is found in `app/models/post.rb`. The lookup stops there and the +file is loaded. If the file actually defines `Post` all is fine, otherwise +`LoadError` is raised. + +### Qualified References + +When a qualified constant is missing Rails does not look for it in the parent +namespaces. But there is a caveat: when a constant is missing, Rails is +unable to tell if the trigger was a relative reference or a qualified one. + +For example, consider + +```ruby +module Admin + User +end +``` + +and + +```ruby +Admin::User +``` + +If `User` is missing, in either case all Rails knows is that a constant called +"User" was missing in a module called "Admin". + +If there is a top-level `User` Ruby would resolve it in the former example, but +wouldn't in the latter. In general, Rails does not emulate the Ruby constant +resolution algorithms, but in this case it tries using the following heuristic: + +> If none of the parent namespaces of the class or module has the missing +> constant then Rails assumes the reference is relative. Otherwise qualified. + +For example, if this code triggers autoloading + +```ruby +Admin::User +``` + +and the `User` constant is already present in `Object`, it is not possible that +the situation is + +```ruby +module Admin + User +end +``` + +because otherwise Ruby would have resolved `User` and no autoloading would have +been triggered in the first place. Thus, Rails assumes a qualified reference and +considers the file `admin/user.rb` and directory `admin/user` to be the only +valid options. + +In practice, this works quite well as long as the nesting matches all parent +namespaces respectively and the constants that make the rule apply are known at +that time. + +However, autoloading happens on demand. If by chance the top-level `User` was +not yet loaded, then Rails assumes a relative reference by contract. + +Naming conflicts of this kind are rare in practice, but if one occurs, +`require_dependency` provides a solution by ensuring that the constant needed +to trigger the heuristic is defined in the conflicting place. + +### Automatic Modules + +When a module acts as a namespace, Rails does not require the application to +define a file for it, a directory matching the namespace is enough. + +Suppose an application has a back office whose controllers are stored in +`app/controllers/admin`. If the `Admin` module is not yet loaded when +`Admin::UsersController` is hit, Rails needs first to autoload the constant +`Admin`. + +If `autoload_paths` has a file called `admin.rb` Rails is going to load that +one, but if there's no such file and a directory called `admin` is found, Rails +creates an empty module and assigns it to the `Admin` constant on the fly. + +### Generic Procedure + +Relative references are reported to be missing in the cref where they were hit, +and qualified references are reported to be missing in their parent (see +[Resolution Algorithm for Relative +Constants](#resolution-algorithm-for-relative-constants) at the beginning of +this guide for the definition of *cref*, and [Resolution Algorithm for Qualified +Constants](#resolution-algorithm-for-qualified-constants) for the definition of +*parent*). + +The procedure to autoload constant `C` in an arbitrary situation is as follows: + +``` +if the class or module in which C is missing is Object + let ns = '' +else + let M = the class or module in which C is missing + + if M is anonymous + let ns = '' + else + let ns = M.name + end +end + +loop do + # Look for a regular file. + for dir in autoload_paths + if the file "#{dir}/#{ns.underscore}/c.rb" exists + load/require "#{dir}/#{ns.underscore}/c.rb" + + if C is now defined + return + else + raise LoadError + end + end + end + + # Look for an automatic module. + for dir in autoload_paths + if the directory "#{dir}/#{ns.underscore}/c" exists + if ns is an empty string + let C = Module.new in Object and return + else + let C = Module.new in ns.constantize and return + end + end + end + + if ns is empty + # We reached the top-level without finding the constant. + raise NameError + else + if C exists in any of the parent namespaces + # Qualified constants heuristic. + raise NameError + else + # Try again in the parent namespace. + let ns = the parent namespace of ns and retry + end + end +end +``` + + +require_dependency +------------------ + +Constant autoloading is triggered on demand and therefore code that uses a +certain constant may have it already defined or may trigger an autoload. That +depends on the execution path and it may vary between runs. + +There are times, however, in which you want to make sure a certain constant is +known when the execution reaches some code. `require_dependency` provides a way +to load a file using the current [loading mechanism](#loading-mechanism), and +keeping track of constants defined in that file as if they were autoloaded to +have them reloaded as needed. + +`require_dependency` is rarely needed, but see a couple of use-cases in +[Autoloading and STI](#autoloading-and-sti) and [When Constants aren't +Triggered](#when-constants-aren-t-missed). + +WARNING. Unlike autoloading, `require_dependency` does not expect the file to +define any particular constant. Exploiting this behavior would be a bad practice +though, file and constant paths should match. + + +Constant Reloading +------------------ + +When `config.cache_classes` is false Rails is able to reload autoloaded +constants. + +For example, if you're in a console session and edit some file behind the +scenes, the code can be reloaded with the `reload!` command: + +``` +> reload! +``` + +When the application runs, code is reloaded when something relevant to this +logic changes. In order to do that, Rails monitors a number of things: + +* `config/routes.rb`. + +* Locales. + +* Ruby files under `autoload_paths`. + +* `db/schema.rb` and `db/structure.sql`. + +If anything in there changes, there is a middleware that detects it and reloads +the code. + +Autoloading keeps track of autoloaded constants. Reloading is implemented by +removing them all from their respective classes and modules using +`Module#remove_const`. That way, when the code goes on, those constants are +going to be unknown again, and files reloaded on demand. + +INFO. This is an all-or-nothing operation, Rails does not attempt to reload only +what changed since dependencies between classes makes that really tricky. +Instead, everything is wiped. + + +Module#autoload isn't Involved +------------------------------ + +`Module#autoload` provides a lazy way to load constants that is fully integrated +with the Ruby constant lookup algorithms, dynamic constant API, etc. It is quite +transparent. + +Rails internals make extensive use of it to defer as much work as possible from +the boot process. But constant autoloading in Rails is **not** implemented with +`Module#autoload`. + +One possible implementation based on `Module#autoload` would be to walk the +application tree and issue `autoload` calls that map existing file names to +their conventional constant name. + +There are a number of reasons that prevent Rails from using that implementation. + +For example, `Module#autoload` is only capable of loading files using `require`, +so reloading would not be possible. Not only that, it uses an internal `require` +which is not `Kernel#require`. + +Then, it provides no way to remove declarations in case a file is deleted. If a +constant gets removed with `Module#remove_const` its `autoload` is not triggered +again. Also, it doesn't support qualified names, so files with namespaces should +be interpreted during the walk tree to install their own `autoload` calls, but +those files could have constant references not yet configured. + +An implementation based on `Module#autoload` would be awesome but, as you see, +at least as of today it is not possible. Constant autoloading in Rails is +implemented with `Module#const_missing`, and that's why it has its own contract, +documented in this guide. + + +Common Gotchas +-------------- + +### Nesting and Qualified Constants + +Let's consider + +```ruby +module Admin + class UsersController < ApplicationController + def index + @users = User.all + end + end +end +``` + +and + +```ruby +class Admin::UsersController < ApplicationController + def index + @users = User.all + end +end +``` + +To resolve `User` Ruby checks `Admin` in the former case, but it does not in +the latter because it does not belong to the nesting (see [Nesting](#nesting) +and [Resolution Algorithms](#resolution-algorithms)). + +Unfortunately Rails autoloading does not know the nesting in the spot where the +constant was missing and so it is not able to act as Ruby would. In particular, +`Admin::User` will get autoloaded in either case. + +Albeit qualified constants with `class` and `module` keywords may technically +work with autoloading in some cases, it is preferable to use relative constants +instead: + +```ruby +module Admin + class UsersController < ApplicationController + def index + @users = User.all + end + end +end +``` + +### Autoloading and STI + +Single Table Inheritance (STI) is a feature of Active Record that enables +storing a hierarchy of models in one single table. The API of such models is +aware of the hierarchy and encapsulates some common needs. For example, given +these classes: + +```ruby +# app/models/polygon.rb +class Polygon < ApplicationRecord +end + +# app/models/triangle.rb +class Triangle < Polygon +end + +# app/models/rectangle.rb +class Rectangle < Polygon +end +``` + +`Triangle.create` creates a row that represents a triangle, and +`Rectangle.create` creates a row that represents a rectangle. If `id` is the +ID of an existing record, `Polygon.find(id)` returns an object of the correct +type. + +Methods that operate on collections are also aware of the hierarchy. For +example, `Polygon.all` returns all the records of the table, because all +rectangles and triangles are polygons. Active Record takes care of returning +instances of their corresponding class in the result set. + +Types are autoloaded as needed. For example, if `Polygon.first` is a rectangle +and `Rectangle` has not yet been loaded, Active Record autoloads it and the +record is correctly instantiated. + +All good, but if instead of performing queries based on the root class we need +to work on some subclass, things get interesting. + +While working with `Polygon` you do not need to be aware of all its descendants, +because anything in the table is by definition a polygon, but when working with +subclasses Active Record needs to be able to enumerate the types it is looking +for. Let’s see an example. + +`Rectangle.all` only loads rectangles by adding a type constraint to the query: + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle") +``` + +Let’s introduce now a subclass of `Rectangle`: + +```ruby +# app/models/square.rb +class Square < Rectangle +end +``` + +`Rectangle.all` should now return rectangles **and** squares: + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle", "Square") +``` + +But there’s a caveat here: How does Active Record know that the class `Square` +exists at all? + +Even if the file `app/models/square.rb` exists and defines the `Square` class, +if no code yet used that class, `Rectangle.all` issues the query + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle") +``` + +That is not a bug, the query includes all *known* descendants of `Rectangle`. + +A way to ensure this works correctly regardless of the order of execution is to +manually load the direct subclasses at the bottom of the file that defines each +intermediate class: + +```ruby +# app/models/rectangle.rb +class Rectangle < Polygon +end +require_dependency 'square' +``` + +This needs to happen for every intermediate (non-root and non-leaf) class. The +root class does not scope the query by type, and therefore does not necessarily +have to know all its descendants. + +### Autoloading and `require` + +Files defining constants to be autoloaded should never be `require`d: + +```ruby +require 'user' # DO NOT DO THIS + +class UsersController < ApplicationController + ... +end +``` + +There are two possible gotchas here in development mode: + +1. If `User` is autoloaded before reaching the `require`, `app/models/user.rb` +runs again because `load` does not update `$LOADED_FEATURES`. + +2. If the `require` runs first Rails does not mark `User` as an autoloaded +constant and changes to `app/models/user.rb` aren't reloaded. + +Just follow the flow and use constant autoloading always, never mix +autoloading and `require`. As a last resort, if some file absolutely needs to +load a certain file use `require_dependency` to play nice with constant +autoloading. This option is rarely needed in practice, though. + +Of course, using `require` in autoloaded files to load ordinary 3rd party +libraries is fine, and Rails is able to distinguish their constants, they are +not marked as autoloaded. + +### Autoloading and Initializers + +Consider this assignment in `config/initializers/set_auth_service.rb`: + +```ruby +AUTH_SERVICE = if Rails.env.production? + RealAuthService +else + MockedAuthService +end +``` + +The purpose of this setup would be that the application uses the class that +corresponds to the environment via `AUTH_SERVICE`. In development mode +`MockedAuthService` gets autoloaded when the initializer runs. Let’s suppose +we do some requests, change its implementation, and hit the application again. +To our surprise the changes are not reflected. Why? + +As [we saw earlier](#constant-reloading), Rails removes autoloaded constants, +but `AUTH_SERVICE` stores the original class object. Stale, non-reachable +using the original constant, but perfectly functional. + +The following code summarizes the situation: + +```ruby +class C + def quack + 'quack!' + end +end + +X = C +Object.instance_eval { remove_const(:C) } +X.new.quack # => quack! +X.name # => C +C # => uninitialized constant C (NameError) +``` + +Because of that, it is not a good idea to autoload constants on application +initialization. + +In the case above we could implement a dynamic access point: + +```ruby +# app/models/auth_service.rb +class AuthService + if Rails.env.production? + def self.instance + RealAuthService + end + else + def self.instance + MockedAuthService + end + end +end +``` + +and have the application use `AuthService.instance` instead. `AuthService` +would be loaded on demand and be autoload-friendly. + +### `require_dependency` and Initializers + +As we saw before, `require_dependency` loads files in an autoloading-friendly +way. Normally, though, such a call does not make sense in an initializer. + +One could think about doing some [`require_dependency`](#require-dependency) +calls in an initializer to make sure certain constants are loaded upfront, for +example as an attempt to address the [gotcha with STIs](#autoloading-and-sti). + +Problem is, in development mode [autoloaded constants are wiped](#constant-reloading) +if there is any relevant change in the file system. If that happens then +we are in the very same situation the initializer wanted to avoid! + +Calls to `require_dependency` have to be strategically written in autoloaded +spots. + +### When Constants aren't Missed + +#### Relative References + +Let's consider a flight simulator. The application has a default flight model + +```ruby +# app/models/flight_model.rb +class FlightModel +end +``` + +that can be overridden by each airplane, for instance + +```ruby +# app/models/bell_x1/flight_model.rb +module BellX1 + class FlightModel < FlightModel + end +end + +# app/models/bell_x1/aircraft.rb +module BellX1 + class Aircraft + def initialize + @flight_model = FlightModel.new + end + end +end +``` + +The initializer wants to create a `BellX1::FlightModel` and nesting has +`BellX1`, that looks good. But if the default flight model is loaded and the +one for the Bell-X1 is not, the interpreter is able to resolve the top-level +`FlightModel` and autoloading is thus not triggered for `BellX1::FlightModel`. + +That code depends on the execution path. + +These kind of ambiguities can often be resolved using qualified constants: + +```ruby +module BellX1 + class Plane + def flight_model + @flight_model ||= BellX1::FlightModel.new + end + end +end +``` + +Also, `require_dependency` is a solution: + +```ruby +require_dependency 'bell_x1/flight_model' + +module BellX1 + class Plane + def flight_model + @flight_model ||= FlightModel.new + end + end +end +``` + +#### Qualified References + +Given + +```ruby +# app/models/hotel.rb +class Hotel +end + +# app/models/image.rb +class Image +end + +# app/models/hotel/image.rb +class Hotel + class Image < Image + end +end +``` + +the expression `Hotel::Image` is ambiguous because it depends on the execution +path. + +As [we saw before](#resolution-algorithm-for-qualified-constants), Ruby looks +up the constant in `Hotel` and its ancestors. If `app/models/image.rb` has +been loaded but `app/models/hotel/image.rb` hasn't, Ruby does not find `Image` +in `Hotel`, but it does in `Object`: + +``` +$ bin/rails r 'Image; p Hotel::Image' 2>/dev/null +Image # NOT Hotel::Image! +``` + +The code evaluating `Hotel::Image` needs to make sure +`app/models/hotel/image.rb` has been loaded, possibly with +`require_dependency`. + +In these cases the interpreter issues a warning though: + +``` +warning: toplevel constant Image referenced by Hotel::Image +``` + +This surprising constant resolution can be observed with any qualifying class: + +``` +2.1.5 :001 > String::Array +(irb):1: warning: toplevel constant Array referenced by String::Array + => Array +``` + +WARNING. To find this gotcha the qualifying namespace has to be a class, +`Object` is not an ancestor of modules. + +### Autoloading within Singleton Classes + +Let's suppose we have these class definitions: + +```ruby +# app/models/hotel/services.rb +module Hotel + class Services + end +end + +# app/models/hotel/geo_location.rb +module Hotel + class GeoLocation + class << self + Services + end + end +end +``` + +If `Hotel::Services` is known by the time `app/models/hotel/geo_location.rb` +is being loaded, `Services` is resolved by Ruby because `Hotel` belongs to the +nesting when the singleton class of `Hotel::GeoLocation` is opened. + +But if `Hotel::Services` is not known, Rails is not able to autoload it, the +application raises `NameError`. + +The reason is that autoloading is triggered for the singleton class, which is +anonymous, and as [we saw before](#generic-procedure), Rails only checks the +top-level namespace in that edge case. + +An easy solution to this caveat is to qualify the constant: + +```ruby +module Hotel + class GeoLocation + class << self + Hotel::Services + end + end +end +``` + +### Autoloading in `BasicObject` + +Direct descendants of `BasicObject` do not have `Object` among their ancestors +and cannot resolve top-level constants: + +```ruby +class C < BasicObject + String # NameError: uninitialized constant C::String +end +``` + +When autoloading is involved that plot has a twist. Let's consider: + +```ruby +class C < BasicObject + def user + User # WRONG + end +end +``` + +Since Rails checks the top-level namespace `User` gets autoloaded just fine the +first time the `user` method is invoked. You only get the exception if the +`User` constant is known at that point, in particular in a *second* call to +`user`: + +```ruby +c = C.new +c.user # surprisingly fine, User +c.user # NameError: uninitialized constant C::User +``` + +because it detects that a parent namespace already has the constant (see [Qualified +References](#autoloading-algorithms-qualified-references)). + +As with pure Ruby, within the body of a direct descendant of `BasicObject` use +always absolute constant paths: + +```ruby +class C < BasicObject + ::String # RIGHT + + def user + ::User # RIGHT + end +end +``` diff --git a/source/caching_with_rails.md b/source/caching_with_rails.md index 0902e34..af4ef6a 100644 --- a/source/caching_with_rails.md +++ b/source/caching_with_rails.md @@ -1,12 +1,26 @@ -Caching with Rails: An overview +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Caching with Rails: An Overview =============================== -This guide will teach you what you need to know about avoiding that expensive round-trip to your database and returning what you need to return to the web clients in the shortest time possible. +This guide is an introduction to speeding up your Rails application with caching. + +Caching means to store content generated during the request-response cycle and +to reuse it when responding to similar requests. + +Caching is often the most effective way to boost an application's performance. +Through caching, web sites running on a single server with a single database +can sustain a load of thousands of concurrent users. + +Rails provides a set of caching features out of the box. This guide will teach +you the scope and purpose of each one of them. Master these techniques and your +Rails applications can serve millions of views without exorbitant response times +or server bills. After reading this guide, you will know: -* Page and action caching (moved to separate gems as of Rails 4). -* Fragment caching. +* Fragment and Russian doll caching. +* How to manage the caching dependencies. * Alternative cache stores. * Conditional GET support. @@ -16,21 +30,34 @@ Basic Caching ------------- This is an introduction to three types of caching techniques: page, action and -fragment caching. Rails provides by default fragment caching. In order to use -page and action caching, you will need to add `actionpack-page_caching` and +fragment caching. By default Rails provides fragment caching. In order to use +page and action caching you will need to add `actionpack-page_caching` and `actionpack-action_caching` to your Gemfile. -To start playing with caching you'll want to ensure that `config.action_controller.perform_caching` is set to `true`, if you're running in development mode. This flag is normally set in the corresponding `config/environments/*.rb` and caching is disabled by default for development and test, and enabled for production. +By default, caching is only enabled in your production environment. To play +around with caching locally you'll want to enable caching in your local +environment by setting `config.action_controller.perform_caching` to `true` in +the relevant `config/environments/*.rb` file: ```ruby config.action_controller.perform_caching = true ``` +NOTE: Changing the value of `config.action_controller.perform_caching` will +only have an effect on the caching provided by the Action Controller component. +For instance, it will not impact low-level caching, that we address +[below](#low-level-caching). + ### Page Caching -Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or NGINX), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with. +Page caching is a Rails mechanism which allows the request for a generated page +to be fulfilled by the webserver (i.e. Apache or NGINX) without having to go +through the entire Rails stack. While this is super fast it can't be applied to +every situation (such as pages that need authentication). Also, because the +webserver is serving a file directly from the filesystem you will need to +implement cache expiration. -INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). See [DHH's key-based cache expiration overview](http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method. +INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). ### Action Caching @@ -40,116 +67,215 @@ INFO: Action Caching has been removed from Rails 4. See the [actionpack-action_c ### Fragment Caching -Life would be perfect if we could get away with caching the entire contents of a page or action and serving it out to the world. Unfortunately, dynamic web applications usually build pages with a variety of components not all of which have the same caching characteristics. In order to address such a dynamically created page where different parts of the page need to be cached and expired differently, Rails provides a mechanism called Fragment Caching. +Dynamic web applications usually build pages with a variety of components not +all of which have the same caching characteristics. When different parts of the +page need to be cached and expired separately you can use Fragment Caching. Fragment Caching allows a fragment of view logic to be wrapped in a cache block and served out of the cache store when the next request comes in. -As an example, if you wanted to show all the orders placed on your website in real time and didn't want to cache that part of the page, but did want to cache the part of the page which lists all products available, you could use this piece of code: +For example, if you wanted to cache each product on a page, you could use this +code: ```html+erb -<% Order.find_recent.each do |o| %> - <%= o.buyer.name %> bought <%= o.product.name %> +<% @products.each do |product| %> + <% cache product do %> + <%= render product %> + <% end %> <% end %> +``` -<% cache do %> - All available products: - <% Product.all.each do |p| %> - <%= link_to p.name, product_url(https://github.com/p) %> - <% end %> +When your application receives its first request to this page, Rails will write +a new cache entry with a unique key. A key looks something like this: + +``` +views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901 +``` + +The number in the middle is the `product_id` followed by the timestamp value in +the `updated_at` attribute of the product record. Rails uses the timestamp value +to make sure it is not serving stale data. If the value of `updated_at` has +changed, a new key will be generated. Then Rails will write a new cache to that +key, and the old cache written to the old key will never be used again. This is +called key-based expiration. + +Cache fragments will also be expired when the view fragment changes (e.g., the +HTML in the view changes). The string of characters at the end of the key is a +template tree digest. It is an MD5 hash computed based on the contents of the +view fragment you are caching. If you change the view fragment, the MD5 hash +will change, expiring the existing file. + +TIP: Cache stores like Memcached will automatically delete old cache files. + +If you want to cache a fragment under certain conditions, you can use +`cache_if` or `cache_unless`: + +```erb +<% cache_if admin?, product do %> + <%= render product %> <% end %> ``` -The cache block in our example will bind to the action that called it and is written out to the same place as the Action Cache, which means that if you want to cache multiple fragments per action, you should provide an `action_suffix` to the cache call: +#### Collection caching + +The `render` helper can also cache individual templates rendered for a collection. +It can even one up the previous example with `each` by reading all cache +templates at once instead of one by one. This is done by passing `cached: true` when rendering the collection: ```html+erb -<% cache(action: 'recent', action_suffix: 'all_products') do %> - All available products: +<%= render partial: 'products/product', collection: @products, cached: true %> ``` -and you can expire it using the `expire_fragment` method, like so: +All cached templates from previous renders will be fetched at once with much +greater speed. Additionally, the templates that haven't yet been cached will be +written to cache and multi fetched on the next render. -```ruby -expire_fragment(controller: 'products', action: 'recent', action_suffix: 'all_products') + +### Russian Doll Caching + +You may want to nest cached fragments inside other cached fragments. This is +called Russian doll caching. + +The advantage of Russian doll caching is that if a single product is updated, +all the other inner fragments can be reused when regenerating the outer +fragment. + +As explained in the previous section, a cached file will expire if the value of +`updated_at` changes for a record on which the cached file directly depends. +However, this will not expire any cache the fragment is nested within. + +For example, take the following view: + +```erb +<% cache product do %> + <%= render product.games %> +<% end %> ``` -If you don't want the cache block to bind to the action that called it, you can also use globally keyed fragments by calling the `cache` method with a key: +Which in turn renders this view: ```erb -<% cache('all_available_products') do %> - All available products: +<% cache game do %> + <%= render game %> <% end %> ``` -This fragment is then available to all actions in the `ProductsController` using the key and can be expired the same way: +If any attribute of game is changed, the `updated_at` value will be set to the +current time, thereby expiring the cache. However, because `updated_at` +will not be changed for the product object, that cache will not be expired and +your app will serve stale data. To fix this, we tie the models together with +the `touch` method: ```ruby -expire_fragment('all_available_products') +class Product < ApplicationRecord + has_many :games +end + +class Game < ApplicationRecord + belongs_to :product, touch: true +end ``` -If you want to avoid expiring the fragment manually, whenever an action updates a product, you can define a helper method: + +With `touch` set to true, any action which changes `updated_at` for a game +record will also change it for the associated product, thereby expiring the +cache. + +### Managing dependencies + +In order to correctly invalidate the cache, you need to properly define the +caching dependencies. Rails is clever enough to handle common cases so you don't +have to specify anything. However, sometimes, when you're dealing with custom +helpers for instance, you need to explicitly define them. + +#### Implicit dependencies + +Most template dependencies can be derived from calls to `render` in the template +itself. Here are some examples of render calls that `ActionView::Digestor` knows +how to decode: ```ruby -module ProductsHelper - def cache_key_for_products - count = Product.count - max_updated_at = Product.maximum(:updated_at).try(:utc).try(:to_s, :number) - "products/all-#{count}-#{max_updated_at}" - end -end +render partial: "comments/comment", collection: commentable.comments +render "comments/comments" +render 'comments/comments' +render('comments/comments') + +render "header" translates to render("comments/header") + +render(@topic) translates to render("topics/topic") +render(topics) translates to render("topics/topic") +render(message.topics) translates to render("topics/topic") ``` -This method generates a cache key that depends on all products and can be used in the view: +On the other hand, some calls need to be changed to make caching work properly. +For instance, if you're passing a custom collection, you'll need to change: -```erb -<% cache(cache_key_for_products) do %> - All available products: -<% end %> +```ruby +render @project.documents.where(published: true) ``` -If you want to cache a fragment under certain condition you can use `cache_if` or `cache_unless` +to: -```erb -<% cache_if (condition, cache_key_for_products) do %> - All available products: -<% end %> +```ruby +render partial: "documents/document", collection: @project.documents.where(published: true) ``` -You can also use an Active Record model as the cache key: +#### Explicit dependencies -```erb -<% Product.all.each do |p| %> - <% cache(p) do %> - <%= link_to p.name, product_url(https://github.com/p) %> - <% end %> -<% end %> +Sometimes you'll have template dependencies that can't be derived at all. This +is typically the case when rendering happens in helpers. Here's an example: + +```html+erb +<%= render_sortable_todolists @project.todolists %> ``` -Behind the scenes, a method called `cache_key` will be invoked on the model and it returns a string like `products/23-20130109142513`. The cache key includes the model name, the id and finally the updated_at timestamp. Thus it will automatically generate a new fragment when the product is updated because the key changes. +You'll need to use a special comment format to call those out: -You can also combine the two schemes which is called "Russian Doll Caching": +```html+erb +<%# Template Dependency: todolists/todolist %> +<%= render_sortable_todolists @project.todolists %> +``` -```erb -<% cache(cache_key_for_products) do %> - All available products: - <% Product.all.each do |p| %> - <% cache(p) do %> - <%= link_to p.name, product_url(https://github.com/p) %> - <% end %> - <% end %> +In some cases, like a single table inheritance setup, you might have a bunch of +explicit dependencies. Instead of writing every template out, you can use a +wildcard to match any template in a directory: + +```html+erb +<%# Template Dependency: events/* %> +<%= render_categorizable_events @person.events %> +``` + +As for collection caching, if the partial template doesn't start with a clean +cache call, you can still benefit from collection caching by adding a special +comment format anywhere in the template, like: + +```html+erb +<%# Template Collection: notification %> +<% my_helper_that_calls_cache(some_arg, notification) do %> + <%= notification.name %> <% end %> ``` -It's called "Russian Doll Caching" because it nests multiple fragments. The advantage is that if a single product is updated, all the other inner fragments can be reused when regenerating the outer fragment. +#### External dependencies + +If you use a helper method, for example, inside a cached block and you then update +that helper, you'll have to bump the cache as well. It doesn't really matter how +you do it, but the MD5 of the template file must change. One recommendation is to +simply be explicit in a comment, like: + +```html+erb +<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %> +<%= some_helper_method(person) %> +``` ### Low-Level Caching -Sometimes you need to cache a particular value or query result, instead of caching view fragments. Rails caching mechanism works great for storing __any__ kind of information. +Sometimes you need to cache a particular value or query result instead of caching view fragments. Rails' caching mechanism works great for storing __any__ kind of information. -The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, the result of the block will be cached to the given key and the result is returned. +The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, that block will be executed in the event of a cache miss. The return value of the block will be written to the cache under the given cache key, and that return value will be returned. In case of cache hit, the cached value will be returned without executing the block. Consider the following example. An application has a `Product` model with an instance method that looks up the product’s price on a competing website. The data returned by this method would be perfect for low-level caching: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord def competing_price Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do Competitor::API.find_price(id) @@ -158,11 +284,14 @@ class Product < ActiveRecord::Base end ``` -NOTE: Notice that in this example we used `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. +NOTE: Notice that in this example we used the `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. ### SQL Caching -Query caching is a Rails feature that caches the result set returned by each query so that if Rails encounters the same query again for that request, it will use the cached result set as opposed to running the query against the database again. +Query caching is a Rails feature that caches the result set returned by each +query. If Rails encounters the same query again for that request, it will use +the cached result set as opposed to running the query against the database +again. For example: @@ -182,19 +311,27 @@ class ProductsController < ApplicationController end ``` +The second time the same query is run against the database, it's not actually going to hit the database. The first time the result is returned from the query it is stored in the query cache (in memory) and the second time it's pulled from memory. + +However, it's important to note that query caches are created at the start of +an action and destroyed at the end of that action and thus persist only for the +duration of the action. If you'd like to store query results in a more +persistent fashion, you can with low level caching. + Cache Stores ------------ -Rails provides different stores for the cached data created by **action** and **fragment** caches. - -TIP: Page caches are always stored on disk. +Rails provides different stores for the cached data (apart from SQL and page +caching). ### Configuration -You can set up your application's default cache store by calling `config.cache_store=` in the Application definition inside your `config/application.rb` file or in an Application.configure block in an environment specific configuration file (i.e. `config/environments/*.rb`). The first argument will be the cache store to use and the rest of the argument will be passed as arguments to the cache store constructor. +You can set up your application's default cache store by setting the +`config.cache_store` configuration option. Other parameters can be passed as +arguments to the cache store's constructor: ```ruby -config.cache_store = :memory_store +config.cache_store = :memory_store, { size: 64.megabytes } ``` NOTE: Alternatively, you can call `ActionController::Base.cache_store` outside of a configuration block. @@ -213,21 +350,42 @@ There are some common options used by all cache implementations. These can be pa * `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network. -* `:compress_threshold` - This options is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. +* `:compress_threshold` - This option is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. * `:expires_in` - This option sets an expiration time in seconds for the cache entry when it will be automatically removed from the cache. * `:race_condition_ttl` - This option is used in conjunction with the `:expires_in` option. It will prevent race conditions when cache entries expire by preventing multiple processes from simultaneously regenerating the same entry (also known as the dog pile effect). This option sets the number of seconds that an expired entry can be reused while a new value is being regenerated. It's a good practice to set this value if you use the `:expires_in` option. +#### Custom Cache Stores + +You can create your own custom cache store by simply extending +`ActiveSupport::Cache::Store` and implementing the appropriate methods. This way, +you can swap in any number of caching technologies into your Rails application. + +To use a custom cache store, simply set the cache store to a new instance of your +custom class. + +```ruby +config.cache_store = MyCacheStore.new +``` + ### ActiveSupport::Cache::MemoryStore -This cache store keeps entries in memory in the same Ruby process. The cache store has a bounded size specified by the `:size` options to the initializer (default is 32Mb). When the cache exceeds the allotted size, a cleanup will occur and the least recently used entries will be removed. +This cache store keeps entries in memory in the same Ruby process. The cache +store has a bounded size specified by sending the `:size` option to the +initializer (default is 32Mb). When the cache exceeds the allotted size, a +cleanup will occur and the least recently used entries will be removed. ```ruby config.cache_store = :memory_store, { size: 64.megabytes } ``` -If you're running multiple Ruby on Rails server processes (which is the case if you're using mongrel_cluster or Phusion Passenger), then your Rails server process instances won't be able to share cache data with each other. This cache store is not appropriate for large application deployments, but can work well for small, low traffic sites with only a couple of server processes or for development and test environments. +If you're running multiple Ruby on Rails server processes (which is the case +if you're using Phusion Passenger or puma clustered mode), then your Rails server +process instances won't be able to share cache data with each other. This cache +store is not appropriate for large application deployments. However, it can +work well for small, low traffic sites with only a couple of server processes, +as well as development and test environments. ### ActiveSupport::Cache::FileStore @@ -237,9 +395,13 @@ This cache store uses the file system to store entries. The path to the director config.cache_store = :file_store, "/path/to/cache/directory" ``` -With this cache store, multiple server processes on the same host can share a cache. Servers processes running on different hosts could share a cache by using a shared file system, but that set up would not be ideal and is not recommended. The cache store is appropriate for low to medium traffic sites that are served off one or two hosts. +With this cache store, multiple server processes on the same host can share a +cache. This cache store is appropriate for low to medium traffic sites that are +served off one or two hosts. Server processes running on different hosts could +share a cache by using a shared file system, but that setup is not recommended. -Note that the cache will grow until the disk is full unless you periodically clear out old entries. +As the cache will grow until the disk is full, it is recommended to +periodically clear out old entries. This is the default cache store implementation. @@ -247,65 +409,32 @@ This is the default cache store implementation. This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very high performance and redundancy. -When initializing the cache, you need to specify the addresses for all memcached servers in your cluster. If none is specified, it will assume memcached is running on the local host on the default port, but this is not an ideal set up for larger sites. +When initializing the cache, you need to specify the addresses for all +memcached servers in your cluster. If none are specified, it will assume +memcached is running on localhost on the default port, but this is not an ideal +setup for larger sites. -The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operation like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry. +The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operations like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry. ```ruby config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" ``` -### ActiveSupport::Cache::EhcacheStore - -If you are using JRuby you can use Terracotta's Ehcache as the cache store for your application. Ehcache is an open source Java cache that also offers an enterprise version with increased scalability, management, and commercial support. You must first install the jruby-ehcache-rails3 gem (version 1.1.0 or later) to use this cache store. - -```ruby -config.cache_store = :ehcache_store -``` - -When initializing the cache, you may use the `:ehcache_config` option to specify the Ehcache config file to use (where the default is "ehcache.xml" in your Rails config directory), and the :cache_name option to provide a custom name for your cache (the default is rails_cache). - -In addition to the standard `:expires_in` option, the `write` method on this cache can also accept the additional `:unless_exist` option, which will cause the cache store to use Ehcache's `putIfAbsent` method instead of `put`, and therefore will not overwrite an existing entry. Additionally, the `write` method supports all of the properties exposed by the [Ehcache Element class](http://ehcache.org/apidocs/net/sf/ehcache/Element.html) , including: - -| Property | Argument Type | Description | -| --------------------------- | ------------------- | ----------------------------------------------------------- | -| elementEvictionData | ElementEvictionData | Sets this element's eviction data instance. | -| eternal | boolean | Sets whether the element is eternal. | -| timeToIdle, tti | int | Sets time to idle | -| timeToLive, ttl, expires_in | int | Sets time to Live | -| version | long | Sets the version attribute of the ElementAttributes object. | - -These options are passed to the `write` method as Hash options using either camelCase or underscore notation, as in the following examples: - -```ruby -Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds) -caches_action :index, expires_in: 60.seconds, unless_exist: true -``` - -For more information about Ehcache, see [http://ehcache.org/](http://ehcache.org/) . -For more information about Ehcache for JRuby and Rails, see [http://ehcache.org/documentation/jruby.html](http://ehcache.org/documentation/jruby.html) - ### ActiveSupport::Cache::NullStore -This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache`, but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss. +This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss. ```ruby config.cache_store = :null_store ``` -### Custom Cache Stores - -You can create your own custom cache store by simply extending `ActiveSupport::Cache::Store` and implementing the appropriate methods. In this way, you can swap in any number of caching technologies into your Rails application. - -To use a custom cache store, simple set the cache store to a new instance of the class. - -```ruby -config.cache_store = MyCacheStore.new -``` - -### Cache Keys +Cache Keys +---------- -The keys used in a cache can be any object that responds to either `:cache_key` or to `:to_param`. You can implement the `:cache_key` method on your classes if you need to generate custom keys. Active Record will generate keys based on the class name and record id. +The keys used in a cache can be any object that responds to either `cache_key` or +`to_param`. You can implement the `cache_key` method on your classes if you need +to generate custom keys. Active Record will generate keys based on the class name +and record id. You can use Hashes and Arrays of values as cache keys. @@ -314,7 +443,12 @@ You can use Hashes and Arrays of values as cache keys. Rails.cache.read(site: "mysite", owners: [owner_1, owner_2]) ``` -The keys you use on `Rails.cache` will not be the same as those actually used with the storage engine. They may be modified with a namespace or altered to fit technology backend constraints. This means, for instance, that you can't save values with `Rails.cache` and then try to pull them out with the `memcache-client` gem. However, you also don't need to worry about exceeding the memcached size limit or violating syntax rules. +The keys you use on `Rails.cache` will not be the same as those actually used with +the storage engine. They may be modified with a namespace or altered to fit +technology backend constraints. This means, for instance, that you can't save +values with `Rails.cache` and then try to pull them out with the `dalli` gem. +However, you also don't need to worry about exceeding the memcached size limit or +violating syntax rules. Conditional GET support ----------------------- @@ -347,18 +481,23 @@ class ProductsController < ApplicationController end ``` -Instead of an options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: +Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: ```ruby class ProductsController < ApplicationController def show @product = Product.find(params[:id]) - respond_with(@product) if stale?(@product) + + if stale?(@product) + respond_to do |wants| + # ... normal response processing + end + end end end ``` -If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using respond_to or calling render yourself) then you've got an easy helper in fresh_when: +If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using `respond_to` or calling render yourself) then you've got an easy helper in `fresh_when`: ```ruby class ProductsController < ApplicationController @@ -372,3 +511,81 @@ class ProductsController < ApplicationController end end ``` + +Sometimes we want to cache response, for example a static page, that never gets +expired. To achieve this, we can use `http_cache_forever` helper and by doing +so browser and proxies will cache it indefinitely. + +By default cached responses will be private, cached only on the user's web +browser. To allow proxies to cache the response, set `public: true` to indicate +that they can serve the cached response to all users. + +Using this helper, `last_modified` header is set to `Time.new(2011, 1, 1).utc` +and `expires` header is set to a 100 years. + +WARNING: Use this method carefully as browser/proxy won't be able to invalidate +the cached response unless browser cache is forcefully cleared. + +```ruby +class HomeController < ApplicationController + def index + http_cache_forever(public: true) do + render + end + end +end +``` + +### Strong v/s Weak ETags + +Rails generates weak ETags by default. Weak ETags allow semantically equivalent +responses to have the same ETags, even if their bodies do not match exactly. +This is useful when we don't want the page to be regenerated for minor changes in +response body. + +Weak ETags have a leading `W/` to differentiate them from strong ETags. + +``` + W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag + "618bbc92e2d35ea1945008b42799b0e7" → Strong ETag +``` + +Unlike weak ETag, strong ETag implies that response should be exactly the same +and byte by byte identical. Useful when doing Range requests within a +large video or PDF file. Some CDNs support only strong ETags, like Akamai. +If you absolutely need to generate a strong ETag, it can be done as follows. + +```ruby + class ProductsController < ApplicationController + def show + @product = Product.find(params[:id]) + fresh_when last_modified: @product.published_at.utc, strong_etag: @product + end + end +``` + +You can also set the strong ETag directly on the response. + +```ruby + response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7" +``` + +Caching in Development +---------------------- + +It's common to want to test the caching strategy of your application +in developement mode. Rails provides the rake task `dev:cache` to +easily toggle caching on/off. + +```bash +$ bin/rails dev:cache +Development mode is now being cached. +$ bin/rails dev:cache +Development mode is no longer being cached. +``` + +References +---------- + +* [DHH's article on key-based expiration](https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) +* [Ryan Bates' Railscast on cache digests](http://railscasts.com/episodes/387-cache-digests) diff --git a/source/command_line.md b/source/command_line.md index a074b84..3360496 100644 --- a/source/command_line.md +++ b/source/command_line.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + The Rails Command Line ====================== @@ -19,12 +21,12 @@ There are a few commands that are absolutely critical to your everyday usage of * `rails console` * `rails server` -* `rake` +* `bin/rails` * `rails generate` * `rails dbconsole` * `rails new app_name` -All commands can run with ```-h or --help``` to list more information. +All commands can run with `-h` or `--help` to list more information. Let's create a simple Rails application to step through each of these commands in context. @@ -37,7 +39,7 @@ INFO: You can install the rails gem by typing `gem install rails`, if you don't ```bash $ rails new commandsapp create - create README.rdoc + create README.md create Rakefile create config.ru create .gitignore @@ -53,20 +55,22 @@ Rails will set you up with what seems like a huge amount of stuff for such a tin ### `rails server` -The `rails server` command launches a small web server named WEBrick which comes bundled with Ruby. You'll use this any time you want to access your application through a web browser. +The `rails server` command launches a web server named Puma which comes bundled with Rails. You'll use this any time you want to access your application through a web browser. With no further work, `rails server` will run our new shiny Rails app: ```bash $ cd commandsapp $ bin/rails server -=> Booting WEBrick -=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 -=> Call with -d to detach -=> Ctrl-C to shutdown server -[2013-08-07 02:00:01] INFO WEBrick 1.3.1 -[2013-08-07 02:00:01] INFO ruby 2.0.0 (2013-06-27) [x86_64-darwin11.2.0] -[2013-08-07 02:00:01] INFO WEBrick::HTTPServer#start: pid=69680 port=3000 +=> Booting Puma +=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000 +=> Run `rails server -h` for more startup options +Puma starting in single mode... +* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas +* Min threads: 5, max threads: 5 +* Environment: development +* Listening on tcp://localhost:3000 +Use Ctrl-C to stop ``` With just three commands we whipped up a Rails server listening on port 3000. Go to your browser and open [http://localhost:3000](http://localhost:3000), you will see a basic Rails app running. @@ -79,7 +83,7 @@ The server can be run on a different port using the `-p` option. The default dev $ bin/rails server -e production -p 4000 ``` -The `-b` option binds Rails to the specified IP, by default it is 0.0.0.0. You can run a server as a daemon by passing a `-d` option. +The `-b` option binds Rails to the specified IP, by default it is localhost. You can run a server as a daemon by passing a `-d` option. ### `rails generate` @@ -130,7 +134,7 @@ Example: `rails generate controller CreditCards open debit credit close` Credit card controller with URLs like /credit_cards/debit. - Controller: app/controllers/credit_card_controller.rb + Controller: app/controllers/credit_cards_controller.rb Test: test/controllers/credit_cards_controller_test.rb Views: app/views/credit_cards/debit.html.erb [...] Helper: app/helpers/credit_cards_helper.rb @@ -151,9 +155,9 @@ $ bin/rails generate controller Greetings hello create app/helpers/greetings_helper.rb invoke assets invoke coffee - create app/assets/javascripts/greetings.js.coffee + create app/assets/javascripts/greetings.coffee invoke scss - create app/assets/stylesheets/greetings.css.scss + create app/assets/stylesheets/greetings.scss ``` What all did this generate? It made sure a bunch of directories were in our application, and created a controller file, a view file, a functional test file, a helper for the view, a JavaScript file and a stylesheet file. @@ -179,7 +183,7 @@ Fire up your server using `rails server`. ```bash $ bin/rails server -=> Booting WEBrick... +=> Booting Puma... ``` The URL will be [http://localhost:3000/greetings/hello](http://localhost:3000/greetings/hello). @@ -205,7 +209,7 @@ Description: Create rails files for model generator. ``` -NOTE: For a list of available field types, refer to the [API documentation](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html#method-i-column) for the column method for the `TableDefinition` class. +NOTE: For a list of available field types for the `type` parameter, refer to the [API documentation](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_column) for the add_column method for the `SchemaStatements` module. The `index` parameter generates a corresponding index for the column. But instead of generating a model directly (which we'll be doing later), let's set up a scaffold. A **scaffold** in Rails is a full set of model, database migration for that model, controller to manipulate it, views to view and manipulate the data, and a test suite for each of the above. @@ -239,26 +243,32 @@ $ bin/rails generate scaffold HighScore game:string score:integer create app/views/high_scores/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/high_scores.js.coffee + create app/assets/javascripts/high_scores.coffee invoke scss - create app/assets/stylesheets/high_scores.css.scss + create app/assets/stylesheets/high_scores.scss invoke scss - identical app/assets/stylesheets/scaffolds.css.scss + identical app/assets/stylesheets/scaffolds.scss ``` The generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the **resource**, and new tests for everything. -The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The SQLite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while. +The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The SQLite3 database that Rails will create for you when we run the `bin/rails db:migrate` command. We'll talk more about bin/rails in-depth in a little while. ```bash -$ bin/rake db:migrate +$ bin/rails db:migrate == CreateHighScores: migrating =============================================== -- create_table(:high_scores) -> 0.0017s == CreateHighScores: migrated (0.0019s) ====================================== ``` -INFO: Let's talk about unit tests. Unit tests are code that tests and makes assertions about code. In unit testing, we take a little part of code, say a method of a model, and test its inputs and outputs. Unit tests are your friend. The sooner you make peace with the fact that your quality of life will drastically increase when you unit test your code, the better. Seriously. We'll make one in a moment. +INFO: Let's talk about unit tests. Unit tests are code that tests and makes assertions +about code. In unit testing, we take a little part of code, say a method of a model, +and test its inputs and outputs. Unit tests are your friend. The sooner you make +peace with the fact that your quality of life will drastically increase when you unit +test your code, the better. Seriously. Please visit +[the testing guide](http://guides.rubyonrails.org/testing.html) for an in-depth +look at unit testing. Let's see the interface Rails created for us. @@ -284,7 +294,7 @@ If you wish to test out some code without changing any data, you can do that by ```bash $ bin/rails console --sandbox -Loading development environment in sandbox (Rails 4.2.0) +Loading development environment in sandbox (Rails 5.1.0) Any modifications you make will be rolled back on exit irb(main):001:0> ``` @@ -316,7 +326,7 @@ With the `helper` method it is possible to access Rails and your application's h ### `rails dbconsole` -`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3. +`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL (including MariaDB), PostgreSQL and SQLite3. INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`. @@ -336,6 +346,12 @@ You can specify the environment in which the `runner` command should operate usi $ bin/rails runner -e staging "Model.long_running_method" ``` +You can even execute ruby code written in a file with runner. + +```bash +$ bin/rails runner lib/code_to_be_run.rb +``` + ### `rails destroy` Think of `destroy` as the opposite of `generate`. It'll figure out what generate did, and undo it. @@ -361,50 +377,63 @@ $ bin/rails destroy model Oops remove test/fixtures/oops.yml ``` -Rake ----- +bin/rails +--------- -Rake is Ruby Make, a standalone Ruby utility that replaces the Unix utility 'make', and uses a 'Rakefile' and `.rake` files to build up a list of tasks. In Rails, Rake is used for common administration tasks, especially sophisticated ones that build off of each other. +Since Rails 5.0+ has rake commands built into the rails executable, `bin/rails` is the new default for running commands. -You can get a list of Rake tasks available to you, which will often depend on your current directory, by typing `rake --tasks`. Each task has a description, and should help you find the thing you need. - -To get the full backtrace for running rake task you can pass the option -```--trace``` to command line, for example ```rake db:create --trace```. +You can get a list of bin/rails tasks available to you, which will often depend on your current directory, by typing `bin/rails --help`. Each task has a description, and should help you find the thing you need. ```bash -$ bin/rake --tasks -rake about # List versions of all Rails frameworks and the environment -rake assets:clean # Remove old compiled assets -rake assets:clobber # Remove compiled assets -rake assets:precompile # Compile all the assets named in config.assets.precompile -rake db:create # Create the database from config/database.yml for the current Rails.env +$ bin/rails --help +Usage: rails COMMAND [ARGS] + +The most common rails commands are: +generate Generate new code (short-cut alias: "g") +console Start the Rails console (short-cut alias: "c") +server Start the Rails server (short-cut alias: "s") ... -rake log:clear # Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development) -rake middleware # Prints out your Rack middleware stack + +All commands can be run with -h (or --help) for more information. + +In addition to those commands, there are: +about List versions of all Rails ... +assets:clean[keep] Remove old compiled assets +assets:clobber Remove compiled assets +assets:environment Load asset compile environment +assets:precompile Compile all the assets ... ... -rake tmp:clear # Clear session, cache, and socket files from tmp/ (narrow w/ tmp:sessions:clear, tmp:cache:clear, tmp:sockets:clear) -rake tmp:create # Creates tmp directories for sessions, cache, sockets, and pids +db:fixtures:load Loads fixtures into the ... +db:migrate Migrate the database ... +db:migrate:status Display status of migrations +db:rollback Rolls the schema back to ... +db:schema:cache:clear Clears a db/schema_cache.yml file +db:schema:cache:dump Creates a db/schema_cache.yml file +db:schema:dump Creates a db/schema.rb file ... +db:schema:load Loads a schema.rb file ... +db:seed Loads the seed data ... +db:structure:dump Dumps the database structure ... +db:structure:load Recreates the databases ... +db:version Retrieves the current schema ... +... +restart Restart app by touching ... +tmp:create Creates tmp directories ... ``` -INFO: You can also use ```rake -T``` to get the list of tasks. +INFO: You can also use `bin/rails -T` to get the list of tasks. ### `about` -`rake about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation. +`bin/rails about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation. ```bash -$ bin/rake about +$ bin/rails about About your application's environment -Ruby version 1.9.3 (x86_64-linux) -RubyGems version 1.3.6 -Rack version 1.3 -Rails version 4.2.0 +Rails version 5.1.0 +Ruby version 2.2.2 (x86_64-linux) +RubyGems version 2.4.6 +Rack version 2.0.1 JavaScript Runtime Node.js (V8) -Active Record version 4.2.0 -Action Pack version 4.2.0 -Action View version 4.2.0 -Action Mailer version 4.2.0 -Active Support version 4.2.0 -Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag +Middleware: Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActiveSupport::Cache::Strategy::LocalCache::Middleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Sprockets::Rails::QuietAssets, Rails::Rack::Logger, ActionDispatch::ShowExceptions, WebConsole::Middleware, ActionDispatch::DebugExceptions, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development Database adapter sqlite3 @@ -413,33 +442,22 @@ Database schema version 20110805173523 ### `assets` -You can precompile the assets in `app/assets` using `rake assets:precompile`, -and remove older compiled assets using `rake assets:clean`. The `assets:clean` -task allows for rolling deploys that may still be linking to an old asset while -the new assets are being built. +You can precompile the assets in `app/assets` using `bin/rails assets:precompile`, and remove older compiled assets using `bin/rails assets:clean`. The `assets:clean` task allows for rolling deploys that may still be linking to an old asset while the new assets are being built. -If you want to clear `public/assets` completely, you can use `rake assets:clobber`. +If you want to clear `public/assets` completely, you can use `bin/rails assets:clobber`. ### `db` -The most common tasks of the `db:` Rake namespace are `migrate` and `create`, and it will pay off to try out all of the migration rake tasks (`up`, `down`, `redo`, `reset`). `rake db:version` is useful when troubleshooting, telling you the current version of the database. - -More information about migrations can be found in the [Migrations](migrations.html) guide. - -### `doc` - -The `doc:` namespace has the tools to generate documentation for your app, API documentation, guides. Documentation can also be stripped which is mainly useful for slimming your codebase, like if you're writing a Rails application for an embedded platform. +The most common tasks of the `db:` bin/rails namespace are `migrate` and `create`, and it will pay off to try out all of the migration bin/rails tasks (`up`, `down`, `redo`, `reset`). `bin/rails db:version` is useful when troubleshooting, telling you the current version of the database. -* `rake doc:app` generates documentation for your application in `doc/app`. -* `rake doc:guides` generates Rails guides in `doc/guides`. -* `rake doc:rails` generates API documentation for Rails in `doc/api`. +More information about migrations can be found in the [Migrations](active_record_migrations.html) guide. ### `notes` -`rake notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js` and `.erb` for both default and custom annotations. +`bin/rails notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js` and `.erb` for both default and custom annotations. ```bash -$ bin/rake notes +$ bin/rails notes (in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [ 20] [TODO] any other way to do this? @@ -456,10 +474,10 @@ You can add support for new file extensions using `config.annotations.register_e config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } ``` -If you are looking for a specific annotation, say FIXME, you can use `rake notes:fixme`. Note that you have to lower case the annotation's name. +If you are looking for a specific annotation, say FIXME, you can use `bin/rails notes:fixme`. Note that you have to lower case the annotation's name. ```bash -$ bin/rake notes:fixme +$ bin/rails notes:fixme (in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [132] high priority for next deploy @@ -468,10 +486,10 @@ app/models/school.rb: * [ 17] ``` -You can also use custom annotations in your code and list them using `rake notes:custom` by specifying the annotation using an environment variable `ANNOTATION`. +You can also use custom annotations in your code and list them using `bin/rails notes:custom` by specifying the annotation using an environment variable `ANNOTATION`. ```bash -$ bin/rake notes:custom ANNOTATION=BUG +$ bin/rails notes:custom ANNOTATION=BUG (in /home/foobar/commandsapp) app/models/article.rb: * [ 23] Have to fix this one before pushing! @@ -479,11 +497,17 @@ app/models/article.rb: NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. -By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`. +By default, `rails notes` will look in the `app`, `config`, `db`, `lib` and `test` directories. If you would like to search other directories, you can configure them using `config.annotations.register_directories` option. + +```ruby +config.annotations.register_directories("spec", "vendor") +``` + +You can also provide them as a comma separated list in the environment variable `SOURCE_ANNOTATION_DIRECTORIES`. ```bash $ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor' -$ bin/rake notes +$ bin/rails notes (in /home/foobar/commandsapp) app/models/user.rb: * [ 35] [FIXME] User should have a subscription at this point @@ -493,7 +517,7 @@ spec/models/user_spec.rb: ### `routes` -`rake routes` will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with. +`rails routes` will list all of your defined routes, which is useful for tracking down routing problems in your app, or giving you a good overview of the URLs in an app you're trying to get familiar with. ### `test` @@ -503,21 +527,20 @@ Rails comes with a test suite called Minitest. Rails owes its stability to the u ### `tmp` -The `Rails.root/tmp` directory is, like the *nix /tmp directory, the holding place for temporary files like sessions (if you're using a file store for files), process id files, and cached actions. +The `Rails.root/tmp` directory is, like the *nix /tmp directory, the holding place for temporary files like process id files and cached actions. The `tmp:` namespaced tasks will help you clear and create the `Rails.root/tmp` directory: -* `rake tmp:cache:clear` clears `tmp/cache`. -* `rake tmp:sessions:clear` clears `tmp/sessions`. -* `rake tmp:sockets:clear` clears `tmp/sockets`. -* `rake tmp:clear` clears all the three: cache, sessions and sockets. -* `rake tmp:create` creates tmp directories for sessions, cache, sockets, and pids. +* `rails tmp:cache:clear` clears `tmp/cache`. +* `rails tmp:sockets:clear` clears `tmp/sockets`. +* `rails tmp:clear` clears all cache and sockets files. +* `rails tmp:create` creates tmp directories for cache, sockets and pids. ### Miscellaneous -* `rake stats` is great for looking at statistics on your code, displaying things like KLOCs (thousands of lines of code) and your code to test ratio. -* `rake secret` will give you a pseudo-random key to use for your session secret. -* `rake time:zones:all` lists all the timezones Rails knows about. +* `rails stats` is great for looking at statistics on your code, displaying things like KLOCs (thousands of lines of code) and your code to test ratio. +* `rails secret` will give you a pseudo-random key to use for your session secret. +* `rails time:zones:all` lists all the timezones Rails knows about. ### Custom Rake Tasks @@ -536,8 +559,8 @@ end To pass arguments to your custom rake task: ```ruby -task :task_name, [:arg_1] => [:pre_1, :pre_2] do |t, args| - # You can use args from here +task :task_name, [:arg_1] => [:prerequisite_1, :prerequisite_2] do |task, args| + argument_1 = args.arg_1 end ``` @@ -555,9 +578,9 @@ end Invocation of the tasks will look like: ```bash -$ bin/rake task_name -$ bin/rake "task_name[value 1]" # entire argument string should be quoted -$ bin/rake db:nothing +$ bin/rails task_name +$ bin/rails "task_name[value 1]" # entire argument string should be quoted +$ bin/rails db:nothing ``` NOTE: If your need to interact with your application models, perform database queries and so on, your task should depend on the `environment` task, which will load your application code. @@ -588,8 +611,8 @@ $ rails new . --git --database=postgresql create tmp/pids create Rakefile add 'Rakefile' - create README.rdoc -add 'README.rdoc' + create README.md +add 'README.md' create app/controllers/application_controller.rb add 'app/controllers/application_controller.rb' create app/helpers/application_helper.rb @@ -602,7 +625,7 @@ We had to create the **gitapp** directory and initialize an empty git repository ```bash $ cat config/database.yml -# PostgreSQL. Versions 8.2 and up are supported. +# PostgreSQL. Versions 9.1 and up are supported. # # Install the pg driver: # gem install pg diff --git a/source/configuring.md b/source/configuring.md index 801cef5..3746e1a 100644 --- a/source/configuring.md +++ b/source/configuring.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Configuring Rails Applications ============================== @@ -33,7 +35,7 @@ In general, the work of configuring Rails means configuring the components of Ra For example, the `config/application.rb` file includes this setting: ```ruby -config.autoload_paths += %W(#{config.root}/extras) +config.time_zone = 'Central Time (US & Canada)' ``` This is a setting for Rails itself. If you want to pass settings to individual Rails components, you can do so via the same `config` object in `config/application.rb`: @@ -58,22 +60,22 @@ These configuration methods are to be called on a `Rails::Railtie` object, such * `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints built-in in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`. -* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is false, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array. +* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is `false`, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array. * `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`. -* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to false in development mode, and true in test and production modes. Can also be enabled with `threadsafe!`. +* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to `false` in development mode, and `true` in test and production modes. * `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`. * `config.beginning_of_week` sets the default beginning of week for the application. Accepts a valid week day symbol (e.g. `:monday`). -* `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, or an object that implements the cache API. Defaults to `:file_store` if the directory `tmp/cache` exists, and to `:memory_store` otherwise. +* `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, or an object that implements the cache API. Defaults to `:file_store`. -* `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to true. +* `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to `true`. -* `config.consider_all_requests_local` is a flag. If true then any error will cause detailed debugging information to be dumped in the HTTP response, and the `Rails::Info` controller will show the application runtime context in `/rails/info/properties`. True by default in development and test environments, and false in production mode. For finer-grained control, set this to false and implement `local_request?` in controllers to specify which requests should provide debugging information on errors. +* `config.consider_all_requests_local` is a flag. If `true` then any error will cause detailed debugging information to be dumped in the HTTP response, and the `Rails::Info` controller will show the application runtime context in `/rails/info/properties`. `true` by default in development and test environments, and `false` in production mode. For finer-grained control, set this to `false` and implement `local_request?` in controllers to specify which requests should provide debugging information on errors. * `config.console` allows you to set class that will be used as console you run `rails console`. It's best to run it in `console` block: @@ -86,43 +88,61 @@ application. Accepts a valid week day symbol (e.g. `:monday`). end ``` -* `config.dependency_loading` is a flag that allows you to disable constant autoloading setting it to false. It only has effect if `config.cache_classes` is true, which it is by default in production mode. This flag is set to false by `config.threadsafe!`. - -* `config.eager_load` when true, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace. +* `config.eager_load` when `true`, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace. -* `config.eager_load_namespaces` registers namespaces that are eager loaded when `config.eager_load` is true. All namespaces in the list must respond to the `eager_load!` method. +* `config.eager_load_namespaces` registers namespaces that are eager loaded when `config.eager_load` is `true`. All namespaces in the list must respond to the `eager_load!` method. * `config.eager_load_paths` accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. Defaults to every folder in the `app` directory of the application. +* `config.enable_dependency_loading`: when true, enables autoloading, even if the application is eager loaded and `config.cache_classes` is set as true. Defaults to false. + * `config.encoding` sets up the application-wide encoding. Defaults to UTF-8. * `config.exceptions_app` sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to `ActionDispatch::PublicExceptions.new(Rails.public_path)`. -* `config.file_watcher` the class used to detect file updates in the filesystem when `config.reload_classes_only_on_change` is true. Must conform to `ActiveSupport::FileUpdateChecker` API. +* `config.debug_exception_response_format` sets the format used in responses when errors occur in development mode. Defaults to `:api` for API only apps and `:default` for normal apps. + +* `config.file_watcher` is the class used to detect file updates in the file system when `config.reload_classes_only_on_change` is `true`. Rails ships with `ActiveSupport::FileUpdateChecker`, the default, and `ActiveSupport::EventedFileUpdateChecker` (this one depends on the [listen](https://github.com/guard/listen) gem). Custom classes must conform to the `ActiveSupport::FileUpdateChecker` API. * `config.filter_parameters` used for filtering out the parameters that you don't want shown in the logs, such as passwords or credit card -numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`. +numbers. By default, Rails filters out passwords by adding `Rails.application.config.filter_parameters += [:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression. + +* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://api.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. -* `config.force_ssl` forces all requests to be under HTTPS protocol by using `ActionDispatch::SSL` middleware. +* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes. If you are setting a value for `config.logger` you must manually pass the value of your formatter to your logger before it is wrapped in an `ActiveSupport::TaggedLogging` instance, Rails will not do it for you. -* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`. +* `config.log_level` defines the verbosity of the Rails logger. This option +defaults to `:debug` for all environments. The available log levels are: `:debug`, +`:info`, `:warn`, `:error`, `:fatal`, and `:unknown`. -* `config.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all modes except production, where it defaults to `:info`. +* `config.log_tags` accepts a list of: methods that the `request` object responds to, a `Proc` that accepts the `request` object, or something that responds to `to_s`. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications. -* `config.log_tags` accepts a list of methods that the `request` object responds to. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications. +* `config.logger` is the logger that will be used for `Rails.logger` and any related Rails logging such as `ActiveRecord::Base.logger`. It defaults to an instance of `ActiveSupport::TaggedLogging` that wraps an instance of `ActiveSupport::Logger` which outputs a log to the `log/` directory. You can supply a custom logger, to get full compatibility you must follow these guidelines: + * To support a formatter, you must manually assign a formatter from the `config.log_formatter` value to the logger. + * To support tagged logs, the log instance must be wrapped with `ActiveSupport::TaggedLogging`. + * To support silencing, the logger must include `LoggerSilence` and `ActiveSupport::LoggerThreadSafeLevel` modules. The `ActiveSupport::Logger` class already includes these modules. -* `config.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::Logger`, with auto flushing off in production mode. + ```ruby + class MyLogger < ::Logger + include ActiveSupport::LoggerThreadSafeLevel + include LoggerSilence + end + + mylogger = MyLogger.new(STDOUT) + mylogger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(mylogger) + ``` * `config.middleware` allows you to configure the application's middleware. This is covered in depth in the [Configuring Middleware](#configuring-middleware) section below. -* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If `config.cache_classes` is true, this option is ignored. +* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to `true`. If `config.cache_classes` is `true`, this option is ignored. * `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`. -* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. NGINX or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app. +* `config.public_file_server.enabled` configures Rails to serve static files from the public directory. This option defaults to `true`, but in the production environment it is set to `false` because the server software (e.g. NGINX or Apache) used to run the application should serve static files instead. If you are running or testing your app in production mode using WEBrick (it is not recommended to use WEBrick in production) set the option to `true.` Otherwise, you won't be able to use page caching and request for files that exist under the public directory. -* `config.session_store` is usually set up in `config/initializers/session_store.rb` and specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Custom session stores can also be specified: +* `config.session_store` specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Defaults to a cookie store with application name as the session key. Custom session stores can also be specified: ```ruby config.session_store :my_custom_store @@ -135,35 +155,37 @@ numbers. New applications filter out passwords by adding the following `config.f ### Configuring Assets * `config.assets.enabled` a flag that controls whether the asset -pipeline is enabled. It is set to true by default. +pipeline is enabled. It is set to `true` by default. -* `config.assets.raise_runtime_errors`* Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. - -* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/environments/production.rb`. +* `config.assets.raise_runtime_errors` Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. * `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem. * `config.assets.js_compressor` defines the JavaScript compressor to use. Possible values are `:closure`, `:uglifier` and `:yui` which require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems respectively. +* `config.assets.gzip` a flag that enables the creation of gzipped version of compiled assets, along with non-gzipped assets. Set to `true` by default. + * `config.assets.paths` contains the paths which are used to look for assets. Appending paths to this configuration option will cause those paths to be used in the search for assets. * `config.assets.precompile` allows you to specify additional assets (other than `application.css` and `application.js`) which are to be precompiled when `rake assets:precompile` is run. +* `config.assets.unknown_asset_fallback` allows you to modify the behavior of the asset pipeline when an asset is not in the pipeline, if you use sprockets-rails 3.2.0 or newer. Defaults to `true`. + * `config.assets.prefix` defines the prefix where assets are served from. Defaults to `/assets`. * `config.assets.manifest` defines the full path to be used for the asset precompiler's manifest file. Defaults to a file named `manifest-.json` in the `config.assets.prefix` directory within the public folder. -* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb`. +* `config.assets.digest` enables the use of SHA256 fingerprints in asset names. Set to `true` by default. * `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`. -* `config.assets.cache_store` defines the cache store that Sprockets will use. The default is the Rails file store. - -* `config.assets.version` is an option string that is used in MD5 hash generation. This can be changed to force all files to be recompiled. +* `config.assets.version` is an option string that is used in SHA256 hash generation. This can be changed to force all files to be recompiled. * `config.assets.compile` is a boolean that can be used to turn on live Sprockets compilation in production. -* `config.assets.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to the same configured at `config.logger`. Setting `config.assets.logger` to false will turn off served assets logging. +* `config.assets.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to the same configured at `config.logger`. Setting `config.assets.logger` to `false` will turn off served assets logging. + +* `config.assets.quiet` disables logging of assets requests. Set to `true` by default in `development.rb`. ### Configuring Generators @@ -181,24 +203,27 @@ The full set of methods that can be used in this block are as follows: * `assets` allows to create assets on generating a scaffold. Defaults to `true`. * `force_plural` allows pluralized model names. Defaults to `false`. * `helper` defines whether or not to generate helpers. Defaults to `true`. -* `integration_tool` defines which integration tool to use. Defaults to `nil`. +* `integration_tool` defines which integration tool to use to generate integration tests. Defaults to `:test_unit`. * `javascripts` turns on the hook for JavaScript files in generators. Used in Rails for when the `scaffold` generator is run. Defaults to `true`. -* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `nil`. +* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `:js`. * `orm` defines which orm to use. Defaults to `false` and will use Active Record by default. * `resource_controller` defines which generator to use for generating a controller when using `rails generate resource`. Defaults to `:controller`. +* `resource_route` defines whether a resource route definition should be generated + or not. Defaults to `true`. * `scaffold_controller` different from `resource_controller`, defines which generator to use for generating a _scaffolded_ controller when using `rails generate scaffold`. Defaults to `:scaffold_controller`. * `stylesheets` turns on the hook for stylesheets in generators. Used in Rails for when the `scaffold` generator is run, but this hook can be used in other generates as well. Defaults to `true`. * `stylesheet_engine` configures the stylesheet engine (for eg. sass) to be used when generating assets. Defaults to `:css`. -* `test_framework` defines which test framework to use. Defaults to `false` and will use Test::Unit by default. +* `scaffold_stylesheet` creates `scaffold.css` when generating a scaffolded resource. Defaults to `true`. +* `test_framework` defines which test framework to use. Defaults to `false` and will use Minitest by default. * `template_engine` defines which template engine to use, such as ERB or Haml. Defaults to `:erb`. ### Configuring Middleware Every Rails application comes with a standard set of middleware which it uses in this order in the development environment: -* `ActionDispatch::SSL` forces every request to be under HTTPS protocol. Will be available if `config.force_ssl` is set to `true`. Options passed to this can be configured by using `config.ssl_options`. -* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.serve_static_assets` is `false`. -* `Rack::Lock` wraps the app in mutex so it can only be called by a single thread at a time. Only enabled when `config.cache_classes` is `false`. +* `ActionDispatch::SSL` forces every request to be served using HTTPS. Enabled if `config.force_ssl` is set to `true`. Options passed to this can be configured by setting `config.ssl_options`. +* `ActionDispatch::Static` is used to serve static assets. Disabled if `config.public_file_server.enabled` is `false`. Set `config.public_file_server.index_name` if you need to serve a static directory index file that is not named `index`. For example, to serve `main.html` instead of `index.html` for directory requests, set `config.public_file_server.index_name` to `"main"`. +* `ActionDispatch::Executor` allows thread safe code reloading. Disabled if `config.allow_concurrency` is `false`, which causes `Rack::Lock` to be loaded. `Rack::Lock` wraps the app in mutex so it can only be called by a single thread at a time. * `ActiveSupport::Cache::Strategy::LocalCache` serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread. * `Rack::Runtime` sets an `X-Runtime` header, containing the time (in seconds) taken to execute the request. * `Rails::Rack::Logger` notifies the logs that the request has begun. After request is complete, flushes all the logs. @@ -207,14 +232,11 @@ Every Rails application comes with a standard set of middleware which it uses in * `ActionDispatch::RemoteIp` checks for IP spoofing attacks and gets valid `client_ip` from request headers. Configurable with the `config.action_dispatch.ip_spoofing_check`, and `config.action_dispatch.trusted_proxies` options. * `Rack::Sendfile` intercepts responses whose body is being served from a file and replaces it with a server specific X-Sendfile header. Configurable with `config.action_dispatch.x_sendfile_header`. * `ActionDispatch::Callbacks` runs the prepare callbacks before serving the request. -* `ActiveRecord::ConnectionAdapters::ConnectionManagement` cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`. -* `ActiveRecord::QueryCache` caches all SELECT queries generated in a request. If any INSERT or UPDATE takes place then the cache is cleaned. * `ActionDispatch::Cookies` sets cookies for the request. * `ActionDispatch::Session::CookieStore` is responsible for storing the session in cookies. An alternate middleware can be used for this by changing the `config.action_controller.session_store` to an alternate value. Additionally, options passed to this can be configured by using `config.action_controller.session_options`. * `ActionDispatch::Flash` sets up the `flash` keys. Only available if `config.action_controller.session_store` is set to a value. -* `ActionDispatch::ParamsParser` parses out parameters from the request into `params`. * `Rack::MethodOverride` allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PATCH, PUT, and DELETE HTTP method types. -* `ActionDispatch::Head` converts HEAD requests to GET requests and serves them as so. +* `Rack::Head` converts HEAD requests to GET requests and serves them as so. Besides these usual middleware, you can add your own by using the `config.middleware.use` method: @@ -225,13 +247,19 @@ config.middleware.use Magical::Unicorns This will put the `Magical::Unicorns` middleware on the end of the stack. You can use `insert_before` if you wish to add a middleware before another. ```ruby -config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns +config.middleware.insert_before Rack::Head, Magical::Unicorns +``` + +Or you can insert a middleware to exact position by using indexes. For example, if you want to insert `Magical::Unicorns` middleware on top of the stack, you can do it, like so: + +```ruby +config.middleware.insert_before 0, Magical::Unicorns ``` There's also `insert_after` which will insert a middleware after another: ```ruby -config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns +config.middleware.insert_after Rack::Head, Magical::Unicorns ``` Middlewares can also be completely swapped out and replaced with others: @@ -243,7 +271,7 @@ config.middleware.swap ActionController::Failsafe, Lifo::Failsafe They can also be removed from the stack completely: ```ruby -config.middleware.delete "Rack::MethodOverride" +config.middleware.delete Rack::MethodOverride ``` ### Configuring i18n @@ -258,6 +286,28 @@ All these configuration options are delegated to the `I18n` library. * `config.i18n.load_path` sets the path Rails uses to look for locale files. Defaults to `config/locales/*.{yml,rb}`. +* `config.i18n.fallbacks` sets fallback behavior for missing translations. Here are 3 usage examples for this option: + + * You can set the option to `true` for using default locale as fallback, like so: + + ```ruby + config.i18n.fallbacks = true + ``` + + * Or you can set an array of locales as fallback, like so: + + ```ruby + config.i18n.fallbacks = [:tr, :en] + ``` + + * Or you can set different fallbacks for locales individually. For example, if you want to use `:tr` for `:az` and `:de`, `:en` for `:da` as fallbacks, you can do it, like so: + + ```ruby + config.i18n.fallbacks = { az: :tr, da: [:de, :en] } + #or + config.i18n.fallbacks.map = { az: :tr, da: [:de, :en] } + ``` + ### Configuring Active Record `config.active_record` includes a variety of configuration options: @@ -265,8 +315,8 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then passed on to any new database connections made. You can retrieve this logger by calling `logger` on either an Active Record model class or an Active Record model instance. Set to `nil` to disable logging. * `config.active_record.primary_key_prefix_type` lets you adjust the naming for primary key columns. By default, Rails assumes that primary key columns are named `id` (and this configuration option doesn't need to be set.) There are two other choices: -** `:table_name` would make the primary key for the Customer class `customerid` -** `:table_name_with_underscore` would make the primary key for the Customer class `customer_id` + * `:table_name` would make the primary key for the Customer class `customerid`. + * `:table_name_with_underscore` would make the primary key for the Customer class `customer_id`. * `config.active_record.table_name_prefix` lets you set a global string to be prepended to table names. If you set this to `northwest_`, then the Customer class will look for `northwest_customers` as its table. The default is an empty string. @@ -274,35 +324,58 @@ All these configuration options are delegated to the `I18n` library. * `config.active_record.schema_migrations_table_name` lets you set a string to be used as the name of the schema migrations table. -* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to true (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table. +* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to `true` (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table. * `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc`. * `config.active_record.schema_format` controls the format for dumping the database schema to a file. The options are `:ruby` (the default) for a database-independent version that depends on migrations, or `:sql` for a set of (potentially database-dependent) SQL statements. -* `config.active_record.timestamped_migrations` controls whether migrations are numbered with serial integers or with timestamps. The default is true, to use timestamps, which are preferred if there are multiple developers working on the same application. +* `config.active_record.error_on_ignored_order` specifies if an error should be raised if the order of a query is ignored during a batch query. The options are `true` (raise error) or `false` (warn). Default is `false`. -* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is true by default. +* `config.active_record.timestamped_migrations` controls whether migrations are numbered with serial integers or with timestamps. The default is `true`, to use timestamps, which are preferred if there are multiple developers working on the same application. -* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:number`. +* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is `true` by default. + +* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:nsec`. * `config.active_record.record_timestamps` is a boolean value which controls whether or not timestamping of `create` and `update` operations on a model occur. The default value is `true`. * `config.active_record.partial_writes` is a boolean value and controls whether or not partial writes are used (i.e. whether updates only set attributes that are dirty). Note that when using partial writes, you should also use optimistic locking `config.active_record.lock_optimistically` since concurrent updates may write attributes based on a possibly stale read state. The default value is `true`. -* `config.active_record.attribute_types_cached_by_default` sets the attribute types that `ActiveRecord::AttributeMethods` will cache by default on reads. The default is `[:datetime, :timestamp, :time, :date]`. - -* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is true. +* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is `true`. * `config.active_record.dump_schema_after_migration` is a flag which controls whether or not schema dump should happen (`db/schema.rb` or - `db/structure.sql`) when you run migrations. This is set to false in + `db/structure.sql`) when you run migrations. This is set to `false` in `config/environments/production.rb` which is generated by Rails. The - default value is true if this configuration is not set. + default value is `true` if this configuration is not set. + +* `config.active_record.dump_schemas` controls which database schemas will be dumped when calling `db:structure:dump`. + The options are `:schema_search_path` (the default) which dumps any schemas listed in `schema_search_path`, + `:all` which always dumps all schemas regardless of the `schema_search_path`, + or a string of comma separated schemas. + +* `config.active_record.belongs_to_required_by_default` is a boolean value and + controls whether a record fails validation if `belongs_to` association is not + present. + +* `config.active_record.warn_on_records_fetched_greater_than` allows setting a + warning threshold for query result size. If the number of records returned + by a query exceeds the threshold, a warning is logged. This can be used to + identify queries which might be causing a memory bloat. + +* `config.active_record.index_nested_attribute_errors` allows errors for nested + `has_many` relationships to be displayed with an index as well as the error. + Defaults to `false`. + +* `config.active_record.use_schema_cache_dump` enables users to get schema cache information + from `db/schema_cache.yml` (generated by `bin/rails db:schema:cache:dump`), instead of + having to send a query to the database to get this information. + Defaults to `true`. The MySQL adapter adds one additional configuration option: -* `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default. +* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`. The schema dumper adds one additional configuration option: @@ -314,11 +387,11 @@ The schema dumper adds one additional configuration option: * `config.action_controller.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets rather than the application server itself. -* `config.action_controller.perform_caching` configures whether the application should perform caching or not. Set to false in development mode, true in production. +* `config.action_controller.perform_caching` configures whether the application should perform the caching features provided by the Action Controller component or not. Set to `false` in development mode, `true` in production. * `config.action_controller.default_static_extension` configures the extension used for cached pages. Defaults to `.html`. -* `config.action_controller.default_charset` specifies the default character set for all renders. The default is "utf-8". +* `config.action_controller.include_all_helpers` configures whether all view helpers are available everywhere or are scoped to the corresponding controller. If set to `false`, `UsersHelper` methods are only available for views rendered as part of `UsersController`. If `true`, `UsersHelper` methods are available everywhere. The default configuration behavior (when this option is not explicitly set to `true` or `false`) is that all view helpers are available to each controller. * `config.action_controller.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action Controller. Set to `nil` to disable logging. @@ -326,6 +399,10 @@ The schema dumper adds one additional configuration option: * `config.action_controller.allow_forgery_protection` enables or disables CSRF protection. By default this is `false` in test mode and `true` in all other modes. +* `config.action_controller.forgery_protection_origin_check` configures whether the HTTP `Origin` header should be checked against the site's origin as an additional CSRF defense. + +* `config.action_controller.per_form_csrf_tokens` configures whether CSRF tokens are only valid for the method/action they were generated for. + * `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`. * `config.action_controller.permit_all_parameters` sets all the parameters for mass assignment to be permitted by default. The default value is `false`. @@ -334,6 +411,22 @@ The schema dumper adds one additional configuration option: * `config.action_controller.always_permitted_parameters` sets a list of whitelisted parameters that are permitted by default. The default values are `['controller', 'action']`. +* `config.action_controller.enable_fragment_cache_logging` determines whether to log fragment cache reads and writes in verbose format as follows: + + ``` + Read fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/d0bdf2974e1ef6d31685c3b392ad0b74 (0.6ms) + Rendered messages/_message.html.erb in 1.2 ms [cache hit] + Write fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/3b4e249ac9d168c617e32e84b99218b5 (1.1ms) + Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss] + ``` + + By default it is set to `false` which results in following output: + + ``` + Rendered messages/_message.html.erb in 1.2 ms [cache hit] + Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss] + ``` + ### Configuring Action Dispatch * `config.action_dispatch.session_store` sets the name of the store for session data. The default is `:cookie_store`; other valid options include `:active_record_store`, `:mem_cache_store` or the name of your own custom class. @@ -348,8 +441,14 @@ The schema dumper adds one additional configuration option: } ``` +* `config.action_dispatch.default_charset` specifies the default character set for all renders. Defaults to `nil`. + * `config.action_dispatch.tld_length` sets the TLD (top-level domain) length for the application. Defaults to `1`. +* `config.action_dispatch.ignore_accept_header` is used to determine whether to ignore accept headers from a request. Defaults to `false`. + +* `config.action_dispatch.x_sendfile_header` specifies server specific X-Sendfile header. This is useful for accelerated file sending from server. For example it can be set to 'X-Sendfile' for Apache. + * `config.action_dispatch.http_auth_salt` sets the HTTP Auth salt value. Defaults to `'http authentication'`. @@ -364,7 +463,33 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) - for more information. It defaults to true. + for more information. It defaults to `true`. + +* `config.action_dispatch.rescue_responses` configures what exceptions are assigned to an HTTP status. It accepts a hash and you can specify pairs of exception/status. By default, this is defined as: + + ```ruby + config.action_dispatch.rescue_responses = { + 'ActionController::RoutingError' => :not_found, + 'AbstractController::ActionNotFound' => :not_found, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::UnknownHttpMethod' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::UnknownFormat' => :not_acceptable, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, + 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, + 'ActionDispatch::Http::Parameters::ParseError' => :bad_request, + 'ActionController::BadRequest' => :bad_request, + 'ActionController::ParameterMissing' => :bad_request, + 'Rack::QueryParser::ParameterTypeError' => :bad_request, + 'Rack::QueryParser::InvalidParameterError' => :bad_request, + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity + } + ``` + + Any exceptions that are not configured will be mapped to 500 Internal Server Error. * `ActionDispatch::Callbacks.before` takes a block of code to run before the request. @@ -376,7 +501,7 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. `config.action_view` includes a small number of configuration settings: -* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Record. The default is +* `config.action_view.field_error_proc` provides an HTML generator for displaying errors that come from Active Model. The default is ```ruby Proc.new do |html_tag, instance| @@ -384,13 +509,23 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. end ``` -* `config.action_view.default_form_builder` tells Rails which form builder to use by default. The default is `ActionView::Helpers::FormBuilder`. If you want your form builder class to be loaded after initialization (so it's reloaded on each request in development), you can pass it as a `String` +* `config.action_view.default_form_builder` tells Rails which form builder to + use by default. The default is `ActionView::Helpers::FormBuilder`. If you + want your form builder class to be loaded after initialization (so it's + reloaded on each request in development), you can pass it as a `String`. * `config.action_view.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action View. Set to `nil` to disable logging. * `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`, which turns on trimming of tail spaces and newline when using `<%= -%>` or `<%= =%>`. See the [Erubis documentation](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces) for more information. -* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true` +* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to + set the default behavior for `authenticity_token` in forms with `remote: + true`. By default it's set to `false`, which means that remote forms will not + include `authenticity_token`, which is helpful when you're fragment-caching + the form. Remote forms get the authenticity from the `meta` tag, so embedding + is unnecessary unless you support browsers without JavaScript. In such case + you can either pass `authenticity_token: true` as a form option or set this + config setting to `true`. * `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::ArticlesController` which renders this template: @@ -400,7 +535,15 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`. -* `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations +* `config.action_view.raise_on_missing_translations` determines whether an + error should be raised for missing translations. + +* `config.action_view.automatically_disable_submit_tag` determines whether + submit_tag should automatically disable on click, this defaults to `true`. + +* `config.action_view.debug_missing_translation` determines whether to wrap the missing translations key in a `` tag or not. This defaults to `true`. + +* `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`. ### Configuring Action Mailer @@ -415,16 +558,19 @@ There are a number of settings available on `config.action_mailer`: * `:user_name` - If your mail server requires authentication, set the username in this setting. * `:password` - If your mail server requires authentication, set the password in this setting. * `:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`. + * `:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. It defaults to `true`. + * `:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is useful if you need to validate a self-signed and/or a wildcard certificate. This can be one of the OpenSSL verify constants, `:none` or `:peer` -- or the constant directly `OpenSSL::SSL::VERIFY_NONE` or `OpenSSL::SSL::VERIFY_PEER`, respectively. + * `:ssl/:tls` - Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection). * `config.action_mailer.sendmail_settings` allows detailed configuration for the `sendmail` delivery method. It accepts a hash of options, which can include any of these options: * `:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`. - * `:arguments` - The command line arguments. Defaults to `-i -t`. + * `:arguments` - The command line arguments. Defaults to `-i`. -* `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to true. +* `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to `true`. -* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) for more info. +* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](action_mailer_basics.html#action-mailer-configuration) for more info. -* `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to false for testing. +* `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to `false` for testing. * `config.action_mailer.default_options` configures Action Mailer defaults. Use to set options like `from` or `reply_to` for every mailer. These default to: @@ -467,13 +613,21 @@ There are a number of settings available on `config.action_mailer`: config.action_mailer.show_previews = false ``` +* `config.action_mailer.deliver_later_queue_name` specifies the queue name for + mailers. By default this is `mailers`. + +* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is `false` in all environments. + + ### Configuring Active Support There are a few configuration options available in Active Support: * `config.active_support.bare` enables or disables the loading of `active_support/all` when booting Rails. Defaults to `nil`, which means `active_support/all` is loaded. -* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `false`. +* `config.active_support.test_order` sets the order in which the test cases are executed. Possible values are `:random` and `:sorted`. Defaults to `:random`. + +* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `true`. * `config.active_support.use_standard_json_time_format` enables or disables serializing dates to ISO 8601 format. Defaults to `true`. @@ -489,6 +643,69 @@ There are a few configuration options available in Active Support: * `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. +### Configuring Active Job + +`config.active_job` provides the following configuration options: + +* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). + + ```ruby + # Be sure to have the adapter's gem in your Gemfile + # and follow the adapter's specific installation + # and deployment instructions. + config.active_job.queue_adapter = :sidekiq + ``` + +* `config.active_job.default_queue_name` can be used to change the default queue name. By default this is `"default"`. + + ```ruby + config.active_job.default_queue_name = :medium_priority + ``` + +* `config.active_job.queue_name_prefix` allows you to set an optional, non-blank, queue name prefix for all jobs. By default it is blank and not used. + + The following configuration would queue the given job on the `production_high_priority` queue when run in production: + + ```ruby + config.active_job.queue_name_prefix = Rails.env + ``` + + ```ruby + class GuestsCleanupJob < ActiveJob::Base + queue_as :high_priority + #.... + end + ``` + +* `config.active_job.queue_name_delimiter` has a default value of `'_'`. If `queue_name_prefix` is set, then `queue_name_delimiter` joins the prefix and the non-prefixed queue name. + + The following configuration would queue the provided job on the `video_server.low_priority` queue: + + ```ruby + # prefix must be set for delimiter to be used + config.active_job.queue_name_prefix = 'video_server' + config.active_job.queue_name_delimiter = '.' + ``` + + ```ruby + class EncoderJob < ActiveJob::Base + queue_as :low_priority + #.... + end + ``` + +* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging. + +### Configuring Action Cable + +* `config.action_cable.url` accepts a string for the URL for where + you are hosting your Action Cable server. You would use this option +if you are running Action Cable servers that are separated from your +main application. +* `config.action_cable.mount_path` accepts a string for where to mount Action + Cable, as part of the main server process. Defaults to `/cable`. +You can set this as nil to not mount Action Cable as part of your +normal Rails server. ### Configuring a Database @@ -531,7 +748,7 @@ TIP: You don't have to update the database configurations manually. If you look ### Connection Preference -Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact. +Since there are two ways to configure your connection (using `config/database.yml` or using an environment variable) it is important to understand how they can interact. If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable: @@ -631,11 +848,11 @@ development: timeout: 5000 ``` -NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it. +NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL (including MariaDB) and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it. -#### Configuring a MySQL Database +#### Configuring a MySQL or MariaDB Database -If you choose to use MySQL instead of the shipped SQLite3 database, your `config/database.yml` will look a little different. Here's the development section: +If you choose to use MySQL or MariaDB instead of the shipped SQLite3 database, your `config/database.yml` will look a little different. Here's the development section: ```yaml development: @@ -648,7 +865,7 @@ development: socket: /tmp/mysql.sock ``` -If your development computer's MySQL installation includes a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate. +If your development database has a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate. #### Configuring a PostgreSQL Database @@ -662,7 +879,7 @@ development: pool: 5 ``` -Prepared Statements are enabled by default on PostgreSQL. You can be disable prepared statements by setting `prepared_statements` to `false`: +Prepared Statements are enabled by default on PostgreSQL. You can disable prepared statements by setting `prepared_statements` to `false`: ```yaml production: @@ -690,9 +907,9 @@ development: database: db/development.sqlite3 ``` -#### Configuring a MySQL Database for JRuby Platform +#### Configuring a MySQL or MariaDB Database for JRuby Platform -If you choose to use MySQL and are using JRuby, your `config/database.yml` will look a little different. Here's the development section: +If you choose to use MySQL or MariaDB and are using JRuby, your `config/database.yml` will look a little different. Here's the development section: ```yaml development: @@ -745,7 +962,7 @@ Rails will now prepend "/app1" when generating links. #### Using Passenger -Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri). +Passenger makes it easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](https://www.phusionpassenger.com/library/deploy/apache/deploy/ruby/#deploying-an-app-to-a-sub-uri-or-subdirectory). #### Using a Reverse Proxy @@ -787,15 +1004,6 @@ server { Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. -#### Considerations when deploying to a subdirectory - -Deploying to a subdirectory in production has implications on various parts of -Rails. - -* development environment: -* testing environment: -* serving static assets: -* asset pipeline: Rails Environment Settings -------------------------- @@ -879,95 +1087,110 @@ Because `Rails::Application` inherits from `Rails::Railtie` (indirectly), you ca Below is a comprehensive list of all the initializers found in Rails in the order that they are defined (and therefore run in, unless otherwise stated). -* `load_environment_hook` Serves as a placeholder so that `:load_environment_config` can be defined to run before it. +* `load_environment_hook`: Serves as a placeholder so that `:load_environment_config` can be defined to run before it. + +* `load_active_support`: Requires `active_support/dependencies` which sets up the basis for Active Support. Optionally requires `active_support/all` if `config.active_support.bare` is un-truthful, which is the default. -* `load_active_support` Requires `active_support/dependencies` which sets up the basis for Active Support. Optionally requires `active_support/all` if `config.active_support.bare` is un-truthful, which is the default. +* `initialize_logger`: Initializes the logger (an `ActiveSupport::Logger` object) for the application and makes it accessible at `Rails.logger`, provided that no initializer inserted before this point has defined `Rails.logger`. -* `initialize_logger` Initializes the logger (an `ActiveSupport::Logger` object) for the application and makes it accessible at `Rails.logger`, provided that no initializer inserted before this point has defined `Rails.logger`. +* `initialize_cache`: If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack. -* `initialize_cache` If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack. +* `set_clear_dependencies_hook`: This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request. -* `set_clear_dependencies_hook` Provides a hook for `active_record.set_dispatch_hooks` to use, which will run before this initializer. This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request. +* `initialize_dependency_mechanism`: If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them. -* `initialize_dependency_mechanism` If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them. +* `bootstrap_hook`: Runs all configured `before_initialize` blocks. -* `bootstrap_hook` Runs all configured `before_initialize` blocks. +* `i18n.callbacks`: In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request. -* `i18n.callbacks` In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request. +* `active_support.deprecation_behavior`: Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values. -* `active_support.deprecation_behavior` Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values. +* `active_support.initialize_time_zone`: Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC". -* `active_support.initialize_time_zone` Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC". +* `active_support.initialize_beginning_of_week`: Sets the default beginning of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`. -* `active_support.initialize_beginning_of_week` Sets the default beginning of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`. +* `active_support.set_configs`: Sets up Active Support by using the settings in `config.active_support` by `send`'ing the method names as setters to `ActiveSupport` and passing the values through. -* `action_dispatch.configure` Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`. +* `action_dispatch.configure`: Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`. -* `action_view.set_configs` Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through. +* `action_view.set_configs`: Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through. -* `action_controller.logger` Sets `ActionController::Base.logger` - if it's not already set - to `Rails.logger`. +* `action_controller.assets_config`: Initializes the `config.actions_controller.assets_dir` to the app's public directory if not explicitly configured. -* `action_controller.initialize_framework_caches` Sets `ActionController::Base.cache_store` - if it's not already set - to `Rails.cache`. +* `action_controller.set_helpers_path`: Sets Action Controller's `helpers_path` to the application's `helpers_path`. -* `action_controller.set_configs` Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through. +* `action_controller.parameters_config`: Configures strong parameters options for `ActionController::Parameters`. -* `action_controller.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access. +* `action_controller.set_configs`: Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through. -* `active_record.initialize_timezone` Sets `ActiveRecord::Base.time_zone_aware_attributes` to true, as well as setting `ActiveRecord::Base.default_timezone` to UTC. When attributes are read from the database, they will be converted into the time zone specified by `Time.zone`. +* `action_controller.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access. -* `active_record.logger` Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`. +* `active_record.initialize_timezone`: Sets `ActiveRecord::Base.time_zone_aware_attributes` to `true`, as well as setting `ActiveRecord::Base.default_timezone` to UTC. When attributes are read from the database, they will be converted into the time zone specified by `Time.zone`. -* `active_record.set_configs` Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through. +* `active_record.logger`: Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`. -* `active_record.initialize_database` Loads the database configuration (by default) from `config/database.yml` and establishes a connection for the current environment. +* `active_record.migration_error`: Configures middleware to check for pending migrations. -* `active_record.log_runtime` Includes `ActiveRecord::Railties::ControllerRuntime` which is responsible for reporting the time taken by Active Record calls for the request back to the logger. +* `active_record.check_schema_cache_dump`: Loads the schema cache dump if configured and available. -* `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. +* `active_record.warn_on_records_fetched_greater_than`: Enables warnings when queries return large numbers of records. -* `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. +* `active_record.set_configs`: Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through. -* `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through. +* `active_record.initialize_database`: Loads the database configuration (by default) from `config/database.yml` and establishes a connection for the current environment. -* `action_mailer.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access. +* `active_record.log_runtime`: Includes `ActiveRecord::Railties::ControllerRuntime` which is responsible for reporting the time taken by Active Record calls for the request back to the logger. -* `set_load_path` This initializer runs before `bootstrap_hook`. Adds the `vendor`, `lib`, all directories of `app` and any paths specified by `config.load_paths` to `$LOAD_PATH`. +* `active_record.set_reloader_hooks`: Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. -* `set_autoload_paths` This initializer runs before `bootstrap_hook`. Adds all sub-directories of `app` and paths specified by `config.autoload_paths` to `ActiveSupport::Dependencies.autoload_paths`. +* `active_record.add_watchable_files`: Adds `schema.rb` and `structure.sql` files to watchable files. -* `add_routing_paths` Loads (by default) all `config/routes.rb` files (in the application and railties, including engines) and sets up the routes for the application. +* `active_job.logger`: Sets `ActiveJob::Base.logger` - if it's not already set - + to `Rails.logger`. -* `add_locales` Adds the files in `config/locales` (from the application, railties and engines) to `I18n.load_path`, making available the translations in these files. +* `active_job.set_configs`: Sets up Active Job by using the settings in `config.active_job` by `send`'ing the method names as setters to `ActiveJob::Base` and passing the values through. -* `add_view_paths` Adds the directory `app/views` from the application, railties and engines to the lookup path for view files for the application. +* `action_mailer.logger`: Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. -* `load_environment_config` Loads the `config/environments` file for the current environment. +* `action_mailer.set_configs`: Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through. -* `append_asset_paths` Finds asset paths for the application and all attached railties and keeps a track of the available directories in `config.static_asset_paths`. +* `action_mailer.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access. -* `prepend_helpers_path` Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application. +* `set_load_path`: This initializer runs before `bootstrap_hook`. Adds paths specified by `config.load_paths` and all autoload paths to `$LOAD_PATH`. -* `load_config_initializers` Loads all Ruby files from `config/initializers` in the application, railties and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded. +* `set_autoload_paths`: This initializer runs before `bootstrap_hook`. Adds all sub-directories of `app` and paths specified by `config.autoload_paths`, `config.eager_load_paths` and `config.autoload_once_paths` to `ActiveSupport::Dependencies.autoload_paths`. -* `engines_blank_point` Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are run. +* `add_routing_paths`: Loads (by default) all `config/routes.rb` files (in the application and railties, including engines) and sets up the routes for the application. -* `add_generator_templates` Finds templates for generators at `lib/templates` for the application, railties and engines and adds these to the `config.generators.templates` setting, which will make the templates available for all generators to reference. +* `add_locales`: Adds the files in `config/locales` (from the application, railties and engines) to `I18n.load_path`, making available the translations in these files. -* `ensure_autoload_once_paths_as_subset` Ensures that the `config.autoload_once_paths` only contains paths from `config.autoload_paths`. If it contains extra paths, then an exception will be raised. +* `add_view_paths`: Adds the directory `app/views` from the application, railties and engines to the lookup path for view files for the application. -* `add_to_prepare_blocks` The block for every `config.to_prepare` call in the application, a railtie or engine is added to the `to_prepare` callbacks for Action Dispatch which will be run per request in development, or before the first request in production. +* `load_environment_config`: Loads the `config/environments` file for the current environment. -* `add_builtin_route` If the application is running under the development environment then this will append the route for `rails/info/properties` to the application routes. This route provides the detailed information such as Rails and Ruby version for `public/index.html` in a default Rails application. +* `prepend_helpers_path`: Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application. -* `build_middleware_stack` Builds the middleware stack for the application, returning an object which has a `call` method which takes a Rack environment object for the request. +* `load_config_initializers`: Loads all Ruby files from `config/initializers` in the application, railties and engines. The files in this directory can be used to hold configuration settings that should be made after all of the frameworks are loaded. -* `eager_load!` If `config.eager_load` is true, runs the `config.before_eager_load` hooks and then calls `eager_load!` which will load all `config.eager_load_namespaces`. +* `engines_blank_point`: Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are run. -* `finisher_hook` Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines. +* `add_generator_templates`: Finds templates for generators at `lib/templates` for the application, railties and engines and adds these to the `config.generators.templates` setting, which will make the templates available for all generators to reference. -* `set_routes_reloader` Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`. +* `ensure_autoload_once_paths_as_subset`: Ensures that the `config.autoload_once_paths` only contains paths from `config.autoload_paths`. If it contains extra paths, then an exception will be raised. -* `disable_dependency_loading` Disables the automatic dependency loading if the `config.eager_load` is set to true. +* `add_to_prepare_blocks`: The block for every `config.to_prepare` call in the application, a railtie or engine is added to the `to_prepare` callbacks for Action Dispatch which will be run per request in development, or before the first request in production. + +* `add_builtin_route`: If the application is running under the development environment then this will append the route for `rails/info/properties` to the application routes. This route provides the detailed information such as Rails and Ruby version for `public/index.html` in a default Rails application. + +* `build_middleware_stack`: Builds the middleware stack for the application, returning an object which has a `call` method which takes a Rack environment object for the request. + +* `eager_load!`: If `config.eager_load` is `true`, runs the `config.before_eager_load` hooks and then calls `eager_load!` which will load all `config.eager_load_namespaces`. + +* `finisher_hook`: Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines. + +* `set_routes_reloader_hook`: Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`. + +* `disable_dependency_loading`: Disables the automatic dependency loading if the `config.eager_load` is set to `true`. Database pooling ---------------- @@ -982,33 +1205,37 @@ development: timeout: 5000 ``` -Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, mongrel, Unicorn etc.) should behave the same. Initially, the database connection pool is empty and it will create additional connections as the demand for them increases, until it reaches the connection pool limit. +Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, Puma, Unicorn etc.) should behave the same. The database connection pool is initially empty. As demand for connections increases it will create them until it reaches the connection pool limit. -Any one request will check out a connection the first time it requires access to the database, after which it will check the connection back in, at the end of the request, meaning that the additional connection slot will be available again for the next request in the queue. +Any one request will check out a connection the first time it requires access to the database. At the end of the request it will check the connection back in. This means that the additional connection slot will be available again for the next request in the queue. If you try to use more connections than are available, Active Record will block -and wait for a connection from the pool. When it cannot get connection, a timeout -error similar to given below will be thrown. +you and wait for a connection from the pool. If it cannot get a connection, a +timeout error similar to that given below will be thrown. ```ruby -ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it: +ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5.000 seconds (waited 5.000 seconds) ``` -If you get the above error, you might want to increase the size of connection -pool by incrementing the `pool` option in `database.yml` +If you get the above error, you might want to increase the size of the +connection pool by incrementing the `pool` option in `database.yml` -NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. +NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited number of connections. Custom configuration -------------------- -You can configure your own code through the Rails configuration object with custom configuration. It works like this: +You can configure your own code through the Rails configuration object with +custom configuration under either the `config.x` namespace, or `config` directly. +The key difference between these two is that you should be using `config.x` if you +are defining _nested_ configuration (ex: `config.x.nested.nested.hi`), and just +`config` for _single level_ configuration (ex: `config.hello`). ```ruby config.x.payment_processing.schedule = :daily config.x.payment_processing.retries = 3 - config.x.super_debugger = true + config.super_debugger = true ``` These configuration points are then available through the configuration object: @@ -1016,6 +1243,75 @@ These configuration points are then available through the configuration object: ```ruby Rails.configuration.x.payment_processing.schedule # => :daily Rails.configuration.x.payment_processing.retries # => 3 - Rails.configuration.x.super_debugger # => true - Rails.configuration.x.super_debugger.not_set # => nil + Rails.configuration.x.payment_processing.not_set # => nil + Rails.configuration.super_debugger # => true + ``` + +You can also use `Rails::Application.config_for` to load whole configuration files: + + ```ruby + # config/payment.yml: + production: + environment: production + merchant_id: production_merchant_id + public_key: production_public_key + private_key: production_private_key + development: + environment: sandbox + merchant_id: development_merchant_id + public_key: development_public_key + private_key: development_private_key + + # config/application.rb + module MyApp + class Application < Rails::Application + config.payment = config_for(:payment) + end + end ``` + + ```ruby + Rails.configuration.payment['merchant_id'] # => production_merchant_id or development_merchant_id + ``` + +Search Engines Indexing +----------------------- + +Sometimes, you may want to prevent some pages of your application to be visible +on search sites like Google, Bing, Yahoo or Duck Duck Go. The robots that index +these sites will first analyze the `http://your-site.com/robots.txt` file to +know which pages it is allowed to index. + +Rails creates this file for you inside the `/public` folder. By default, it allows +search engines to index all pages of your application. If you want to block +indexing on all pages of you application, use this: + +``` +User-agent: * +Disallow: / +``` + +To block just specific pages, it's necessary to use a more complex syntax. Learn +it on the [official documentation](http://www.robotstxt.org/robotstxt.html). + +Evented File System Monitor +--------------------------- + +If the [listen gem](https://github.com/guard/listen) is loaded Rails uses an +evented file system monitor to detect changes when `config.cache_classes` is +`false`: + +```ruby +group :development do + gem 'listen', '>= 3.0.5', '< 3.2' +end +``` + +Otherwise, in every request Rails walks the application tree to check if +anything has changed. + +On Linux and macOS no additional gems are needed, but some are required +[for *BSD](https://github.com/guard/listen#on-bsd) and +[for Windows](https://github.com/guard/listen#on-windows). + +Note that [some setups are unsupported](https://github.com/guard/listen#issues--limitations). diff --git a/source/contributing_to_ruby_on_rails.md b/source/contributing_to_ruby_on_rails.md index 0b05725..9166b9b 100644 --- a/source/contributing_to_ruby_on_rails.md +++ b/source/contributing_to_ruby_on_rails.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Contributing to Ruby on Rails ============================= @@ -13,6 +15,9 @@ After reading this guide, you will know: Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches. +As mentioned in [Rails +README](https://github.com/rails/rails/blob/master/README.md), everyone interacting in Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the Rails [code of conduct](http://rubyonrails.org/conduct/). + -------------------------------------------------------------------------------- Reporting an Issue @@ -20,25 +25,31 @@ Reporting an Issue Ruby on Rails uses [GitHub Issue Tracking](https://github.com/rails/rails/issues) to track issues (primarily bugs and contributions of new code). If you've found a bug in Ruby on Rails, this is the place to start. You'll need to create a (free) GitHub account in order to submit an issue, to comment on them or to create pull requests. -NOTE: Bugs in the most recent released version of Ruby on Rails are likely to get the most attention. Also, the Rails core team is always interested in feedback from those who can take the time to test _edge Rails_ (the code for the version of Rails that is currently under development). Later in this guide you'll find out how to get edge Rails for testing. +NOTE: Bugs in the most recent released version of Ruby on Rails are likely to get the most attention. Also, the Rails core team is always interested in feedback from those who can take the time to test _edge Rails_ (the code for the version of Rails that is currently under development). Later in this guide, you'll find out how to get edge Rails for testing. ### Creating a Bug Report -If you've found a problem in Ruby on Rails which is not a security risk, do a search in GitHub under [Issues](https://github.com/rails/rails/issues) in case it has already been reported. If you do not find any issue addressing it you may proceed to [open a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.) +If you've found a problem in Ruby on Rails which is not a security risk, do a search on GitHub under [Issues](https://github.com/rails/rails/issues) in case it has already been reported. If you are unable to find any open GitHub issues addressing the problem you found, your next step will be to [open a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.) -Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to replicate the bug and figure out a fix. +Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to reproduce the bug and figure out a fix. Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment. -### Create a Self-Contained gist for Active Record and Action Controller Issues +### Create an Executable Test Case + +Having a way to reproduce your issue will be very helpful for others to help confirm, investigate and ultimately fix your issue. You can do this by providing an executable test case. To make this process easier, we have prepared several bug report templates for you to use as a starting point: + +* Template for Active Record (models, database) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) +* Template for testing Active Record (migration) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_migrations_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_migrations_master.rb) +* Template for Action Pack (controllers, routing) issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) +* Template for Active Job issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_job_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_job_master.rb) +* Generic template for other issues: [gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb) + +These templates include the boilerplate code to set up a test case against either a released version of Rails (`*_gem.rb`) or edge Rails (`*_master.rb`). + +Simply copy the content of the appropriate template into a `.rb` file and make the necessary changes to demonstrate the issue. You can execute it by running `ruby the_file.rb` in your terminal. If all goes well, you should see your test case failing. -If you are filing a bug report, please use -[Active Record template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) or -[Action Controller template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) -if the bug is found in a published gem, and -[Active Record template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) or -[Action Controller template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) -if the bug happens in the master branch. +You can then share your executable test case as a [gist](https://gist.github.com), or simply paste the content into the issue description. ### Special Treatment for Security Issues @@ -49,18 +60,18 @@ WARNING: Please do not report security vulnerabilities with public GitHub issue Please don't put "feature request" items into GitHub Issues. If there's a new feature that you want to see added to Ruby on Rails, you'll need to write the code yourself - or convince someone else to partner with you to write the code. -Later in this guide you'll find detailed instructions for proposing a patch to +Later in this guide, you'll find detailed instructions for proposing a patch to Ruby on Rails. If you enter a wish list item in GitHub Issues with no code, you can expect it to be marked "invalid" as soon as it's reviewed. Sometimes, the line between 'bug' and 'feature' is a hard one to draw. Generally, a feature is anything that adds new behavior, while a bug is -anything that fixes already existing behavior that is misbehaving. Sometimes, -the core team will have to make a judgement call. That said, the distinction +anything that causes incorrect behavior. Sometimes, +the core team will have to make a judgment call. That said, the distinction generally just affects which release your patch will get in to; we love feature submissions! They just won't get backported to maintenance branches. -If you'd like feedback on an idea for a feature before doing the work for make +If you'd like feedback on an idea for a feature before doing the work to make a patch, please send an email to the [rails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You might get no response, which means that everyone is indifferent. You might find @@ -73,17 +84,17 @@ discussions new features require. Helping to Resolve Existing Issues ---------------------------------- -As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [Everyone's Issues](https://github.com/rails/rails/issues) list in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: +As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [issues list](https://github.com/rails/rails/issues) in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: ### Verifying Bug Reports For starters, it helps just to verify bug reports. Can you reproduce the reported issue on your own computer? If so, you can add a comment to the issue saying that you're seeing the same thing. -If something is very vague, can you help squash it down into something specific? Maybe you can provide additional information to help reproduce a bug, or help by eliminating needless steps that aren't required to demonstrate the problem. +If an issue is very vague, can you help narrow it down to something more specific? Maybe you can provide additional information to help reproduce a bug, or help by eliminating needless steps that aren't required to demonstrate the problem. If you find a bug report without a test, it's very useful to contribute a failing test. This is also a great way to get started exploring the source code: looking at the existing test files will teach you how to write more tests. New tests are best contributed in the form of a patch, as explained later on in the "Contributing to the Rails Code" section. -Anything you can do to make bug reports more succinct or easier to reproduce is a help to folks trying to write code to fix those bugs - whether you end up writing the code yourself or not. +Anything you can do to make bug reports more succinct or easier to reproduce helps folks trying to write code to fix those bugs - whether you end up writing the code yourself or not. ### Testing Patches @@ -96,7 +107,7 @@ $ git checkout -b testing_branch Then you can use their remote branch to update your codebase. For example, let's say the GitHub user JohnSmith has forked and pushed to a topic branch "orange" located at https://github.com/JohnSmith/rails. ```bash -$ git remote add JohnSmith git://github.com/JohnSmith/rails.git +$ git remote add JohnSmith https://github.com/JohnSmith/rails.git $ git pull JohnSmith orange ``` @@ -111,7 +122,7 @@ Once you're happy that the pull request contains a good change, comment on the G >I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. -If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. +If your comment simply reads "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. Contributing to the Rails Documentation --------------------------------------- @@ -119,32 +130,56 @@ Contributing to the Rails Documentation Ruby on Rails has two main sets of documentation: the guides, which help you learn about Ruby on Rails, and the API, which serves as a reference. -You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/rails/docrails/translating-rails-guides). +You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing them up to date with the latest edge Rails. -You can either open a pull request to [Rails](http://github.com/rails/rails) or -ask the [Rails core team](http://rubyonrails.org/core) for commit access on -[docrails](http://github.com/rails/docrails) if you contribute regularly. -Please do not open pull requests in docrails, if you'd like to get feedback on your -change, ask for it in [Rails](http://github.com/rails/rails) instead. +To do so, open a pull request to [Rails](https://github.com/rails/rails) on GitHub. -Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation. +When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html). -If you are unsure of the documentation changes, you can create an issue in the [Rails](https://github.com/rails/rails/issues) issues tracker on GitHub. +NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes. -When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html). +Translating Rails Guides +------------------------ -NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements. +We are happy to have people volunteer to translate the Rails guides into their own language. +If you want to translate the Rails guides in your own language, follows these steps: -NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes. +* Fork the project (rails/rails). +* Add a source folder for your own language, for example: *guides/source/it-IT* for Italian. +* Copy the contents of *guides/source* into your own language directory and translate them. +* Do NOT translate the HTML files, as they are automatically generated. + +To generate the guides in HTML format cd into the *guides* directory then run (eg. for it-IT): -WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails. +```bash +$ bundle install +$ bundle exec rake guides:generate:html GUIDES_LANGUAGE=it-IT +``` + +This will generate the guides in an *output* directory. + +NOTE: The instructions are for Rails > 4. The Redcarpet Gem doesn't work with JRuby. + +Translation efforts we know about (various versions): + +* **Italian**: [https://github.com/rixlabs/docrails](https://github.com/rixlabs/docrails) +* **Spanish**: [http://wiki.github.com/gramos/docrails](http://wiki.github.com/gramos/docrails) +* **Polish**: [https://github.com/apohllo/docrails/tree/master](https://github.com/apohllo/docrails/tree/master) +* **French** : [https://github.com/railsfrance/docrails](https://github.com/railsfrance/docrails) +* **Czech** : [https://github.com/rubyonrails-cz/docrails/tree/czech](https://github.com/rubyonrails-cz/docrails/tree/czech) +* **Turkish** : [https://github.com/ujk/docrails/tree/master](https://github.com/ujk/docrails/tree/master) +* **Korean** : [https://github.com/rorlakr/rails-guides](https://github.com/rorlakr/rails-guides) +* **Simplified Chinese** : [https://github.com/ruby-china/guides](https://github.com/ruby-china/guides) +* **Traditional Chinese** : [https://github.com/docrails-tw/guides](https://github.com/docrails-tw/guides) +* **Russian** : [https://github.com/morsbox/rusrails](https://github.com/morsbox/rusrails) +* **Japanese** : [https://github.com/yasslab/railsguides.jp](https://github.com/yasslab/railsguides.jp) Contributing to the Rails Code ------------------------------ ### Setting Up a Development Environment -To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to setup the tests on your own computer. +To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide, you'll learn how to setup the tests on your own computer. #### The Easy Way @@ -159,7 +194,7 @@ In case you can't use the Rails development box, see [this other guide](developm To be able to contribute code, you need to clone the Rails repository: ```bash -$ git clone git://github.com/rails/rails.git +$ git clone https://github.com/rails/rails.git ``` and create a dedicated branch: @@ -171,6 +206,14 @@ $ git checkout -b my_new_branch It doesn't matter much what name you use, because this branch will only exist on your local computer and your personal repository on GitHub. It won't be part of the Rails Git repository. +### Bundle install + +Install the required gems. + +```bash +$ bundle install +``` + ### Running an Application Against Your Local Branch In case you need a dummy Rails app to test changes, the `--dev` flag of `rails new` generates an application that uses your local branch: @@ -193,7 +236,7 @@ Now get busy and add/edit code. You're on your branch now, so you can write what * Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution. -TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted. +TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted (read more about [our rationales behind this decision](https://github.com/rails/rails/pull/13771#issuecomment-32746700)). #### Follow the Coding Conventions @@ -205,7 +248,7 @@ Rails follows a simple set of coding style conventions: * Use Ruby >= 1.9 syntax for hashes. Prefer `{ a: :b }` over `{ :a => :b }`. * Prefer `&&`/`||` over `and`/`or`. * Prefer class << self over self.method for class methods. -* Use `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. +* Use `my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. * Use `a = b` and not `a=b`. * Use assert_not methods instead of refute. * Prefer `method { do_stuff }` instead of `method{do_stuff}` for single-line blocks. @@ -215,40 +258,31 @@ The above are guidelines - please use your best judgment in using them. ### Benchmark Your Code -If your change has an impact on the performance of Rails, please use the -[benchmark-ips](https://github.com/evanphx/benchmark-ips) gem to provide -benchmark results for comparison. - -Here's an example of using benchmark-ips: - -```ruby -require 'benchmark/ips' - -Benchmark.ips do |x| - x.report('addition') { 1 + 2 } - x.report('addition with send') { 1.send(:+, 2) } -end -``` - -This will generate a report with the following information: - -``` -Calculating ------------------------------------- - addition 69114 i/100ms - addition with send 64062 i/100ms -------------------------------------------------- - addition 5307644.4 (±3.5%) i/s - 26539776 in 5.007219s - addition with send 3702897.9 (±3.5%) i/s - 18513918 in 5.006723s -``` - -Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information. +For changes that might have an impact on performance, please benchmark your +code and measure the impact. Please share the benchmark script you used as well +as the results. You should consider including this information in your commit +message, which allows future contributors to easily verify your findings and +determine if they are still relevant. (For example, future optimizations in the +Ruby VM might render certain optimizations unnecessary.) + +It is very easy to make an optimization that improves performance for a +specific scenario you care about but regresses on other common cases. +Therefore, you should test your change against a list of representative +scenarios. Ideally, they should be based on real-world scenarios extracted +from production applications. + +You can use the [benchmark template](https://github.com/rails/rails/blob/master/guides/bug_report_templates/benchmark.rb) +as a starting point. It includes the boilerplate code to setup a benchmark +using the [benchmark-ips](https://github.com/evanphx/benchmark-ips) gem. The +template is designed for testing relatively self-contained changes that can be +inlined into the script. ### Running Tests It is not customary in Rails to run the full test suite before pushing -changes. The railties test suite in particular takes a long time, and even -more if the source code is mounted in `/vagrant` as happens in the recommended -workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). +changes. The railties test suite in particular takes a long time, and takes an +especially long time if the source code is mounted in `/vagrant` as happens in +the recommended workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). As a compromise, test what your code obviously affects, and if the change is not in railties, run the whole test suite of the affected component. If all @@ -281,13 +315,20 @@ You can run a single test through ruby. For instance: ```bash $ cd actionmailer -$ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout +$ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout ``` The `-n` option allows you to run a single method instead of the whole file. -##### Testing Active Record +#### Testing Active Record + +First, create the databases you'll need. You can find a list of the required +table names, usernames, and passwords in `activerecord/test/config.example.yml`. + +For MySQL and PostgreSQL, running the SQL statements `create database +activerecord_unittest` and `create database activerecord_unittest2` is +sufficient. This is not necessary for SQLite3. This is how you run the Active Record test suite only for SQLite3: @@ -296,10 +337,9 @@ $ cd activerecord $ bundle exec rake test:sqlite3 ``` -You can now run the tests as you did for `sqlite3`. The tasks are respectively +You can now run the tests as you did for `sqlite3`. The tasks are respectively: ```bash -test:mysql test:mysql2 test:postgresql ``` @@ -310,12 +350,12 @@ Finally, $ bundle exec rake test ``` -will now run the four of them in turn. +will now run the three of them in turn. You can also run any single test separately: ```bash -$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb +$ ARCONN=sqlite3 bundle exec ruby -Itest test/cases/associations/has_many_associations_test.rb ``` To run a single test against all adapters, use: @@ -340,9 +380,9 @@ $ RUBYOPT=-W0 bundle exec rake test The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version. -You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. +You should add an entry **to the top** of the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. -A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: +A CHANGELOG entry should summarize what was changed and should end with the author's name. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: ``` * Summary of a change that briefly describes what was changed. You can use multiple @@ -359,17 +399,12 @@ A CHANGELOG entry should summarize what was changed and should end with author's *Your Name* ``` -Your name can be added directly after the last word if you don't provide any code examples or don't need multiple paragraphs. Otherwise, it's best to make as a new paragraph. +Your name can be added directly after the last word if there are no code +examples or multiple paragraphs. Otherwise, it's best to make a new paragraph. -### Sanity Check +### Updating the Gemfile.lock -You should not be the only person who looks at the code before you submit it. -If you know someone else who uses Rails, try asking them if they'll check out -your work. If you don't know anyone else using Rails, try hopping into the IRC -room or posting about your idea to the rails-core mailing list. Doing this in -private before you push a patch out publicly is the "smoke test" for a patch: -if you can't convince one other developer of the beauty of your code, you’re -unlikely to convince the core team either. +Some changes require the dependencies to be upgraded. In these cases make sure you run `bundle update` to get the right version of the dependency and commit the `Gemfile.lock` file within your changes. ### Commit Your Changes @@ -379,37 +414,45 @@ When you're happy with the code on your computer, you need to commit the changes $ git commit -a ``` -At this point, your editor should be fired up and you can write a message for this commit. Well formatted and descriptive commit messages are extremely helpful for the others, especially when figuring out why given change was made, so please take the time to write it. +This should fire up your editor to write a commit message. When you have +finished, save and close to continue. -Good commit message should be formatted according to the following example: +A well-formatted and descriptive commit message is very helpful to others for +understanding why the change was made, so please take the time to write it. + +A good commit message looks like this: ``` Short summary (ideally 50 characters or less) -More detailed description, if necessary. It should be wrapped to 72 -characters. Try to be as descriptive as you can, even if you think that -the commit content is obvious, it may not be obvious to others. You -should add such description also if it's already present in bug tracker, -it should not be necessary to visit a webpage to check the history. +More detailed description, if necessary. It should be wrapped to +72 characters. Try to be as descriptive as you can. Even if you +think that the commit content is obvious, it may not be obvious +to others. Add any description that is already present in the +relevant issues; it should not be necessary to visit a webpage +to check the history. + +The description section can have multiple paragraphs. -Description can have multiple paragraphs and you can use code examples -inside, just indent it with 4 spaces: +Code examples can be embedded by indenting them with 4 spaces: class ArticlesController def index - respond_with Article.limit(10) + render json: Article.limit(10) end end You can also add bullet points: -- you can use dashes or asterisks +- make a bullet point by starting a line with either a dash (-) + or an asterisk (*) -- also, try to indent next line of a point for readability, if it's too - long to fit in 72 characters +- wrap lines at 72 characters, and indent any additional lines + with 2 spaces for readability ``` -TIP. Please squash your commits into a single commit when appropriate. This simplifies future cherry picks, and also keeps the git log clean. +TIP. Please squash your commits into a single commit when appropriate. This +simplifies future cherry picks and keeps the git log clean. ### Update Your Branch @@ -436,7 +479,7 @@ Navigate to the Rails [GitHub repository](https://github.com/rails/rails) and pr Add the new remote to your local repository on your local machine: ```bash -$ git remote add mine git@github.com:/rails.git +$ git remote add mine https://github.com:/rails.git ``` Push to your remote: @@ -450,7 +493,7 @@ You might have cloned your forked repository into your machine and might want to In the directory you cloned your fork: ```bash -$ git remote add rails git://github.com/rails/rails.git +$ git remote add rails https://github.com/rails/rails.git ``` Download new commits and branches from the official repository: @@ -500,7 +543,7 @@ pull request". The Rails core team will be notified about your submission. Most pull requests will go through a few iterations before they get merged. Different contributors will sometimes have different opinions, and often -patches will need revised before they can get merged. +patches will need to be revised before they can get merged. Some contributors to Rails have email notifications from GitHub turned on, but others do not. Furthermore, (almost) everyone who works on Rails is a @@ -547,8 +590,7 @@ following: ```bash $ git fetch upstream $ git checkout my_pull_request -$ git rebase upstream/master -$ git rebase -i +$ git rebase -i upstream/master < Choose 'squash' for all of your commits except the first one. > < Edit the commit message to make sense, and describe all your changes. > @@ -559,6 +601,23 @@ $ git push origin my_pull_request -f You should be able to refresh the pull request on GitHub and see that it has been updated. +#### Updating pull request + +Sometimes you will be asked to make some changes to the code you have +already committed. This can include amending existing commits. In this +case Git will not allow you to push the changes as the pushed branch +and local branch do not match. Instead of opening a new pull request, +you can force push to your branch on GitHub as described earlier in +squashing commits section: + +```bash +$ git push origin my_pull_request -f +``` + +This will update the branch and pull request on GitHub with your new code. Do +note that using force push may result in commits being lost on the remote branch; use it with care. + + ### Older Versions of Ruby on Rails If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 4-0-stable branch: @@ -576,7 +635,7 @@ Changes that are merged into master are intended for the next major release of R For simple fixes, the easiest way to backport your changes is to [extract a diff from your changes in master and apply them to the target branch](http://ariejan.net/2009/10/26/how-to-create-and-apply-a-patch-with-git). -First make sure your changes are the only difference between your current branch and master: +First, make sure your changes are the only difference between your current branch and master: ```bash $ git log master..HEAD @@ -591,7 +650,7 @@ $ git format-patch master --stdout > ~/my_changes.patch Switch over to the target branch and apply your changes: ```bash -$ git checkout -b my_backport_branch 3-2-stable +$ git checkout -b my_backport_branch 4-2-stable $ git apply ~/my_changes.patch ``` @@ -604,4 +663,4 @@ And then... think about your next contribution! Rails Contributors ------------------ -All contributions, either via master or docrails, get credit in [Rails Contributors](http://contributors.rubyonrails.org). +All contributions get credit in [Rails Contributors](http://contributors.rubyonrails.org). diff --git a/source/credits.html.erb b/source/credits.html.erb index 8767fbe..5adbd12 100644 --- a/source/credits.html.erb +++ b/source/credits.html.erb @@ -22,17 +22,17 @@ Ruby on Rails Guides: Credits

Rails Guides Designers

<%= author('Jason Zimdars', 'jz') do %> - Jason Zimdars is an experienced creative director and web designer who has lead UI and UX design for numerous websites and web applications. You can see more of his design and writing at Thinkcage.com or follow him on Twitter. + Jason Zimdars is an experienced creative director and web designer who has lead UI and UX design for numerous websites and web applications. You can see more of his design and writing at Thinkcage.com or follow him on Twitter. <% end %>

Rails Guides Authors

<%= author('Ryan Bigg', 'radar', 'radar.png') do %> - Ryan Bigg works as the Community Manager at Spree Commerce and has been working with Rails since 2006. He's the author of Multi Tenancy With Rails and co-author of Rails 4 in Action. He's written many gems which can be seen on his GitHub page and he also tweets prolifically as @ryanbigg. + Ryan Bigg works as a Rails developer at Marketplacer and has been working with Rails since 2006. He's the author of Multi Tenancy With Rails and co-author of Rails 4 in Action. He's written many gems which can be seen on his GitHub page and he also tweets prolifically as @ryanbigg. <% end %> <%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %> -Oscar Del Ben is a software engineer at Wildfire. He's a regular open source contributor (GitHub account) and tweets regularly at @oscardelben. +Oscar Del Ben is a software engineer at Wildfire. He's a regular open source contributor (GitHub account) and tweets regularly at @oscardelben. <% end %> <%= author('Frederick Cheung', 'fcheung') do %> @@ -40,7 +40,7 @@ Oscar Del Ben is a software engineer at Wi <% end %> <%= author('Tore Darell', 'toretore') do %> - Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. His home on the Internet is his blog Sneaky Abstractions. + Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. You can follow him on Twitter. <% end %> <%= author('Jeff Dean', 'zilkey') do %> @@ -64,7 +64,7 @@ Oscar Del Ben is a software engineer at Wi <% end %> <%= author('Pratik Naik', 'lifo') do %> - Pratik Naik is a Ruby on Rails developer at Basecamp and also a member of the Rails core team. He maintains a blog at has_many :bugs, :through => :rails and has a semi-active twitter account. + Pratik Naik is a Ruby on Rails developer at Basecamp and maintains a blog at has_many :bugs, :through => :rails. He also has a semi-active twitter account. <% end %> <%= author('Emilio Tagua', 'miloops') do %> diff --git a/source/debugging_rails_applications.md b/source/debugging_rails_applications.md index 53b8566..58aab77 100644 --- a/source/debugging_rails_applications.md +++ b/source/debugging_rails_applications.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Debugging Rails Applications ============================ @@ -15,7 +17,7 @@ After reading this guide, you will know: View Helpers for Debugging -------------------------- -One common task is to inspect the contents of a variable. In Rails, you can do this with three methods: +One common task is to inspect the contents of a variable. Rails provides three different ways to do this: * `debug` * `to_yaml` @@ -52,7 +54,7 @@ Title: Rails debugging guide ### `to_yaml` -Displaying an instance variable, or any other object or method, in YAML format can be achieved this way: +Alternatively, calling `to_yaml` on any object converts it to YAML. You can pass this converted object into the `simple_format` helper method to format the output. This is how `debug` does its magic. ```html+erb <%= simple_format @article.to_yaml %> @@ -62,9 +64,7 @@ Displaying an instance variable, or any other object or method, in YAML format c

``` -The `to_yaml` method converts the method to YAML format leaving it more readable, and then the `simple_format` helper is used to render each line as in the console. This is how `debug` method does its magic. - -As a result of this, you will have something like this in your view: +The above code will render something like this: ```yaml --- !ruby/object Article @@ -92,7 +92,7 @@ Another useful method for displaying object values is `inspect`, especially when

``` -Will be rendered as follows: +Will render: ``` [1, 2, 3, 4, 5] @@ -107,38 +107,43 @@ It can also be useful to save information to log files at runtime. Rails maintai ### What is the Logger? -Rails makes use of the `ActiveSupport::Logger` class to write log information. You can also substitute another logger such as `Log4r` if you wish. +Rails makes use of the `ActiveSupport::Logger` class to write log information. Other loggers, such as `Log4r`, may also be substituted. -You can specify an alternative logger in your `environment.rb` or any environment file: +You can specify an alternative logger in `config/application.rb` or any other environment file, for example: ```ruby -Rails.logger = Logger.new(STDOUT) -Rails.logger = Log4r::Logger.new("Application Log") +config.logger = Logger.new(STDOUT) +config.logger = Log4r::Logger.new("Application Log") ``` Or in the `Initializer` section, add _any_ of the following ```ruby -config.logger = Logger.new(STDOUT) -config.logger = Log4r::Logger.new("Application Log") +Rails.logger = Logger.new(STDOUT) +Rails.logger = Log4r::Logger.new("Application Log") ``` TIP: By default, each log is created under `Rails.root/log/` and the log file is named after the environment in which the application is running. ### Log Levels -When something is logged it's printed into the corresponding log if the log level of the message is equal or higher than the configured log level. If you want to know the current log level you can call the `Rails.logger.level` method. +When something is logged, it's printed into the corresponding log if the log +level of the message is equal to or higher than the configured log level. If you +want to know the current log level, you can call the `Rails.logger.level` +method. -The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`, corresponding to the log level numbers from 0 up to 5 respectively. To change the default log level, use +The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, +and `:unknown`, corresponding to the log level numbers from 0 up to 5, +respectively. To change the default log level, use ```ruby config.log_level = :warn # In any environment initializer, or Rails.logger.level = 0 # at any time ``` -This is useful when you want to log under development or staging, but you don't want to flood your production log with unnecessary information. +This is useful when you want to log under development or staging without flooding your production log with unnecessary information. -TIP: The default Rails log level is `info` in production mode and `debug` in development and test mode. +TIP: The default Rails log level is `debug` in all environments. ### Sending Messages @@ -200,7 +205,7 @@ Adding extra logging like this makes it easy to search for unexpected or unusual When running multi-user, multi-account applications, it's often useful to be able to filter the logs using some custom rules. `TaggedLogging` -in Active Support helps in doing exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications. +in Active Support helps you do exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications. ```ruby logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) @@ -210,34 +215,33 @@ logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs " ``` ### Impact of Logs on Performance -Logging will always have a small impact on performance of your rails app, - particularly when logging to disk.However, there are a few subtleties: +Logging will always have a small impact on the performance of your Rails app, + particularly when logging to disk. Additionally, there are a few subtleties: Using the `:debug` level will have a greater performance penalty than `:fatal`, as a far greater number of strings are being evaluated and written to the log output (e.g. disk). -Another potential pitfall is that if you have many calls to `Logger` like this - in your code: +Another potential pitfall is too many calls to `Logger` in your code: ```ruby logger.debug "Person attributes hash: #{@person.attributes.inspect}" ``` -In the above example, There will be a performance impact even if the allowed +In the above example, there will be a performance impact even if the allowed output level doesn't include debug. The reason is that Ruby has to evaluate these strings, which includes instantiating the somewhat heavy `String` object -and interpolating the variables, and which takes time. +and interpolating the variables. Therefore, it's recommended to pass blocks to the logger methods, as these are -only evaluated if the output level is the same or included in the allowed level +only evaluated if the output level is the same as — or included in — the allowed level (i.e. lazy loading). The same code rewritten would be: ```ruby logger.debug {"Person attributes hash: #{@person.attributes.inspect}"} ``` -The contents of the block, and therefore the string interpolation, is only -evaluated if debug is enabled. This performance savings is only really +The contents of the block, and therefore the string interpolation, are only +evaluated if debug is enabled. This performance savings are only really noticeable with large amounts of logging, but it's a good practice to employ. Debugging with the `byebug` gem @@ -251,8 +255,8 @@ is your best companion. The debugger can also help you if you want to learn about the Rails source code but don't know where to start. Just debug any request to your application and -use this guide to learn how to move from the code you have written deeper into -Rails code. +use this guide to learn how to move from the code you have written into the +underlying Rails code. ### Setup @@ -283,7 +287,7 @@ As soon as your application calls the `byebug` method, the debugger will be started in a debugger shell inside the terminal window where you launched your application server, and you will be placed at the debugger's prompt `(byebug)`. Before the prompt, the code around the line that is about to be run will be -displayed and the current line will be marked by '=>'. Like this: +displayed and the current line will be marked by '=>', like this: ``` [1, 10] in /PathTo/project/app/controllers/articles_controller.rb @@ -308,16 +312,15 @@ processing the entire request. For example: ```bash -=> Booting WEBrick -=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 +=> Booting Puma +=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options -=> Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option) -=> Ctrl-C to shutdown server -[2014-04-11 13:11:47] INFO WEBrick 1.3.1 -[2014-04-11 13:11:47] INFO ruby 2.1.1 (2014-02-24) [i686-linux] -[2014-04-11 13:11:47] INFO WEBrick::HTTPServer#start: pid=6370 port=3000 - - +Puma starting in single mode... +* Version 3.4.0 (ruby 2.3.1-p112), codename: Owl Bowl Brawl +* Min threads: 5, max threads: 5 +* Environment: development +* Listening on tcp://localhost:3000 +Use Ctrl-C to stop Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200 ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations" Processing by ArticlesController#index as HTML @@ -333,34 +336,57 @@ Processing by ArticlesController#index as HTML 10: respond_to do |format| 11: format.html # index.html.erb 12: format.json { render json: @articles } - (byebug) ``` -Now it's time to explore and dig into your application. A good place to start is +Now it's time to explore your application. A good place to start is by asking the debugger for help. Type: `help` ``` (byebug) help -byebug 2.7.0 - -Type 'help ' for help on a specific command + break -- Sets breakpoints in the source code + catch -- Handles exception catchpoints + condition -- Sets conditions on breakpoints + continue -- Runs until program ends, hits a breakpoint or reaches a line + debug -- Spawns a subdebugger + delete -- Deletes breakpoints + disable -- Disables breakpoints or displays + display -- Evaluates expressions every time the debugger stops + down -- Moves to a lower frame in the stack trace + edit -- Edits source files + enable -- Enables breakpoints or displays + finish -- Runs the program until frame returns + frame -- Moves to a frame in the call stack + help -- Helps you using byebug + history -- Shows byebug's history of commands + info -- Shows several informations about the program being debugged + interrupt -- Interrupts the program + irb -- Starts an IRB session + kill -- Sends a signal to the current process + list -- Lists lines of source code + method -- Shows methods of an object, class or module + next -- Runs one or more lines of code + pry -- Starts a Pry session + quit -- Exits byebug + restart -- Restarts the debugged program + save -- Saves current byebug session to a file + set -- Modifies byebug settings + show -- Shows byebug settings + source -- Restores a previously saved byebug session + step -- Steps into blocks or methods one or more times + thread -- Commands to manipulate threads + tracevar -- Enables tracing of a global variable + undisplay -- Stops displaying all or some expressions when program stops + untracevar -- Stops tracing a global variable + up -- Moves to a higher frame in the stack trace + var -- Shows variables and its values + where -- Displays the backtrace -Available commands: -backtrace delete enable help list pry next restart source up -break disable eval info method ps save step var -catch display exit interrupt next putl set thread -condition down finish irb p quit show trace -continue edit frame kill pp reload skip undisplay +(byebug) ``` -TIP: To view the help menu for any command use `help ` at the -debugger prompt. For example: _`help list`_. You can abbreviate any debugging -command by supplying just enough letters to distinguish them from other -commands, so you can also use `l` for the `list` command, for example. - -To see the previous ten lines you should type `list-` (or `l-`) +To see the previous ten lines you should type `list-` (or `l-`). ``` (byebug) l- @@ -376,12 +402,11 @@ To see the previous ten lines you should type `list-` (or `l-`) 8 @articles = Article.find_recent 9 10 respond_to do |format| - ``` -This way you can move inside the file, being able to see the code above and over -the line where you added the `byebug` call. Finally, to see where you are in -the code again you can type `list=` +This way you can move inside the file and see the code above the line where you +added the `byebug` call. Finally, to see where you are in the code again you can +type `list=` ``` (byebug) list= @@ -397,7 +422,6 @@ the code again you can type `list=` 10: respond_to do |format| 11: format.html # index.html.erb 12: format.json { render json: @articles } - (byebug) ``` @@ -409,8 +433,7 @@ contexts as you go through the different parts of the stack. The debugger creates a context when a stopping point or an event is reached. The context has information about the suspended program which enables the debugger to inspect the frame stack, evaluate variables from the perspective of the -debugged program, and contains information about the place where the debugged -program is stopped. +debugged program, and know the place where the debugged program is stopped. At any time you can call the `backtrace` command (or its alias `where`) to print the backtrace of the application. This can be very helpful to know how you got @@ -420,46 +443,45 @@ then `backtrace` will supply the answer. ``` (byebug) where --> #0 ArticlesController.index - at /PathTo/project/test_app/app/controllers/articles_controller.rb:8 - #1 ActionController::ImplicitRender.send_action(method#String, *args#Array) - at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/implicit_render.rb:4 + at /PathToProject/app/controllers/articles_controller.rb:8 + #1 ActionController::BasicImplicitRender.send_action(method#String, *args#Array) + at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/basic_implicit_render.rb:4 #2 AbstractController::Base.process_action(action#NilClass, *args#Array) - at /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb:189 - #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass) - at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/rendering.rb:10 + at /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb:181 + #3 ActionController::Rendering.process_action(action, *args) + at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/rendering.rb:30 ... ``` The current frame is marked with `-->`. You can move anywhere you want in this -trace (thus changing the context) by using the `frame _n_` command, where _n_ is +trace (thus changing the context) by using the `frame n` command, where _n_ is the specified frame number. If you do that, `byebug` will display your new context. ``` (byebug) frame 2 -[184, 193] in /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb - 184: # is the intended way to override action dispatching. - 185: # - 186: # Notice that the first argument is the method to be dispatched - 187: # which is *not* necessarily the same as the action name. - 188: def process_action(method_name, *args) -=> 189: send_action(method_name, *args) - 190: end - 191: - 192: # Actually call the method associated with the action. Override - 193: # this method if you wish to change how action methods are called, - +[176, 185] in /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb + 176: # is the intended way to override action dispatching. + 177: # + 178: # Notice that the first argument is the method to be dispatched + 179: # which is *not* necessarily the same as the action name. + 180: def process_action(method_name, *args) +=> 181: send_action(method_name, *args) + 182: end + 183: + 184: # Actually call the method associated with the action. Override + 185: # this method if you wish to change how action methods are called, (byebug) ``` The available variables are the same as if you were running the code line by line. After all, that's what debugging is. -You can also use `up [n]` (`u` for abbreviated) and `down [n]` commands in order -to change the context _n_ frames up or down the stack respectively. _n_ defaults -to one. Up in this case is towards higher-numbered stack frames, and down is -towards lower-numbered stack frames. +You can also use `up [n]` and `down [n]` commands in order to change the context +_n_ frames up or down the stack respectively. _n_ defaults to one. Up in this +case is towards higher-numbered stack frames, and down is towards lower-numbered +stack frames. ### Threads @@ -467,16 +489,15 @@ The debugger can list, stop, resume and switch between running threads by using the `thread` command (or the abbreviated `th`). This command has a handful of options: -* `thread` shows the current thread. -* `thread list` is used to list all threads and their statuses. The plus + -character and the number indicates the current thread of execution. -* `thread stop _n_` stop thread _n_. -* `thread resume _n_` resumes thread _n_. -* `thread switch _n_` switches the current thread context to _n_. +* `thread`: shows the current thread. +* `thread list`: is used to list all threads and their statuses. The current +thread is marked with a plus (+) sign. +* `thread stop n`: stops thread _n_. +* `thread resume n`: resumes thread _n_. +* `thread switch n`: switches the current thread context to _n_. -This command is very helpful, among other occasions, when you are debugging -concurrent threads and need to verify that there are no race conditions in your -code. +This command is very helpful when you are debugging concurrent threads and need +to verify that there are no race conditions in your code. ### Inspecting Variables @@ -500,9 +521,9 @@ current context: 12: format.json { render json: @articles } (byebug) instance_variables -[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request, - :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name, - :@_response_body, :@marked_for_same_origin_verification, :@_config] +[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context, + :@_action_name, :@_response_body, :@marked_for_same_origin_verification, + :@_config] ``` As you may have figured out, all of the variables that you can access from a @@ -512,6 +533,7 @@ command later in this guide). ``` (byebug) next + [5, 14] in /PathTo/project/app/controllers/articles_controller.rb 5 # GET /articles.json 6 def index @@ -530,31 +552,40 @@ command later in this guide). And then ask again for the instance_variables: ``` -(byebug) instance_variables.include? "@articles" -true +(byebug) instance_variables +[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context, + :@_action_name, :@_response_body, :@marked_for_same_origin_verification, + :@_config, :@articles] ``` -Now `@articles` is included in the instance variables, because the line defining it -was executed. +Now `@articles` is included in the instance variables, because the line defining +it was executed. TIP: You can also step into **irb** mode with the command `irb` (of course!). -This way an irb session will be started within the context you invoked it. But -be warned: this is an experimental feature. +This will start an irb session within the context you invoked it. The `var` method is the most convenient way to show variables and their values. -Let's let `byebug` to help us with it. +Let's have `byebug` help us with it. ``` (byebug) help var -v[ar] cl[ass] show class variables of self -v[ar] const show constants of object -v[ar] g[lobal] show global variables -v[ar] i[nstance] show instance variables of object -v[ar] l[ocal] show local variables + + [v]ar + + Shows variables and its values + + + var all -- Shows local, global and instance variables of self. + var args -- Information about arguments of the current scope + var const -- Shows constants of an object. + var global -- Shows global variables. + var instance -- Shows instance variables of self or a specific object. + var local -- Shows local variables in current scope. + ``` This is a great way to inspect the values of the current context variables. For -example, to check that we have no local variables currently defined. +example, to check that we have no local variables currently defined: ``` (byebug) var local @@ -568,16 +599,16 @@ You can also inspect for an object method this way: @_start_transaction_state = {} @aggregation_cache = {} @association_cache = {} -@attributes = {"id"=>nil, "created_at"=>nil, "updated_at"=>nil} -@attributes_cache = {} -@changed_attributes = nil -... +@attributes = ## 5: where('created_at > ?', 1.week.ago).limit(limit) - 6: end - 7: - 8: end +[1, 6] in /PathToProject/app/models/article.rb + 1: class Article < ApplicationRecord + 2: def self.find_recent(limit = 10) + 3: byebug +=> 4: where('created_at > ?', 1.week.ago).limit(limit) + 5: end + 6: end (byebug) ``` -If we use `next`, we want go deep inside method calls. Instead, byebug will go -to the next line within the same context. In this case, this is the last line of -the method, so `byebug` will jump to next next line of the previous frame. +If we use `next`, we won't go deep inside method calls. Instead, `byebug` will +go to the next line within the same context. In this case, it is the last line +of the current method, so `byebug` will return to the next line of the caller +method. ``` (byebug) next -Next went up a frame because previous frame finished - -[4, 13] in /PathTo/project/test_app/app/controllers/articles_controller.rb +[4, 13] in /PathToProject/app/controllers/articles_controller.rb 4: # GET /articles 5: # GET /articles.json 6: def index @@ -649,29 +670,29 @@ Next went up a frame because previous frame finished (byebug) ``` -If we use `step` in the same situation, we will literally go the next ruby -instruction to be executed. In this case, the activesupport's `week` method. +If we use `step` in the same situation, `byebug` will literally go to the next +Ruby instruction to be executed -- in this case, Active Support's `week` method. ``` (byebug) step -[50, 59] in /PathToGems/activesupport-4.2.0/lib/active_support/core_ext/numeric/time.rb - 50: ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) - 51: end - 52: alias :day :days - 53: - 54: def weeks -=> 55: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) - 56: end - 57: alias :week :weeks - 58: - 59: def fortnights - +[49, 58] in /PathToGems/activesupport-5.1.0/lib/active_support/core_ext/numeric/time.rb + 49: + 50: # Returns a Duration instance matching the number of weeks provided. + 51: # + 52: # 2.weeks # => 14 days + 53: def weeks +=> 54: ActiveSupport::Duration.weeks(self) + 55: end + 56: alias :week :weeks + 57: + 58: # Returns a Duration instance matching the number of fortnights provided. (byebug) ``` -This is one of the best ways to find bugs in your code, or perhaps in Ruby on -Rails. +This is one of the best ways to find bugs in your code. + +TIP: You can also use `step n` or `next n` to move forward `n` steps at once. ### Breakpoints @@ -681,19 +702,18 @@ is reached. The debugger shell is invoked in that line. You can add breakpoints dynamically with the command `break` (or just `b`). There are 3 possible ways of adding breakpoints manually: -* `break line`: set breakpoint in the _line_ in the current source file. -* `break file:line [if expression]`: set breakpoint in the _line_ number inside -the _file_. If an _expression_ is given it must evaluated to _true_ to fire up -the debugger. +* `break n`: set breakpoint in line number _n_ in the current source file. +* `break file:n [if expression]`: set breakpoint in line number _n_ inside +file named _file_. If an _expression_ is given it must evaluated to _true_ to +fire up the debugger. * `break class(.|\#)method [if expression]`: set breakpoint in _method_ (. and \# for class and instance method respectively) defined in _class_. The -_expression_ works the same way as with file:line. - +_expression_ works the same way as with file:n. For example, in the previous situation ``` -[4, 13] in /PathTo/project/app/controllers/articles_controller.rb +[4, 13] in /PathToProject/app/controllers/articles_controller.rb 4: # GET /articles 5: # GET /articles.json 6: def index @@ -706,20 +726,20 @@ For example, in the previous situation 13: end (byebug) break 11 -Created breakpoint 1 at /PathTo/project/app/controllers/articles_controller.rb:11 +Successfully created breakpoint with id 1 ``` -Use `info breakpoints _n_` or `info break _n_` to list breakpoints. If you -supply a number, it lists that breakpoint. Otherwise it lists all breakpoints. +Use `info breakpoints` to list breakpoints. If you supply a number, it lists +that breakpoint. Otherwise it lists all breakpoints. ``` (byebug) info breakpoints Num Enb What -1 y at /PathTo/project/app/controllers/articles_controller.rb:11 +1 y at /PathToProject/app/controllers/articles_controller.rb:11 ``` -To delete breakpoints: use the command `delete _n_` to remove the breakpoint +To delete breakpoints: use the command `delete n` to remove the breakpoint number _n_. If no number is specified, it deletes all breakpoints that are currently active. @@ -731,10 +751,11 @@ No breakpoints. You can also enable or disable breakpoints: -* `enable breakpoints`: allow a _breakpoints_ list or all of them if no list is -specified, to stop your program. This is the default state when you create a +* `enable breakpoints [n [m [...]]]`: allows a specific breakpoint list or all +breakpoints to stop your program. This is the default state when you create a breakpoint. -* `disable breakpoints`: the _breakpoints_ will have no effect on your program. +* `disable breakpoints [n [m [...]]]`: make certain (or all) breakpoints have +no effect on your program. ### Catching Exceptions @@ -749,59 +770,136 @@ To list all active catchpoints use `catch`. There are two ways to resume execution of an application that is stopped in the debugger: -* `continue` [line-specification] \(or `c`): resume program execution, at the -address where your script last stopped; any breakpoints set at that address are -bypassed. The optional argument line-specification allows you to specify a line -number to set a one-time breakpoint which is deleted when that breakpoint is -reached. -* `finish` [frame-number] \(or `fin`): execute until the selected stack frame -returns. If no frame number is given, the application will run until the -currently selected frame returns. The currently selected frame starts out the -most-recent frame or 0 if no frame positioning (e.g up, down or frame) has been -performed. If a frame number is given it will run until the specified frame -returns. +* `continue [n]`: resumes program execution at the address where your script last +stopped; any breakpoints set at that address are bypassed. The optional argument +`n` allows you to specify a line number to set a one-time breakpoint which is +deleted when that breakpoint is reached. +* `finish [n]`: execute until the selected stack frame returns. If no frame +number is given, the application will run until the currently selected frame +returns. The currently selected frame starts out the most-recent frame or 0 if +no frame positioning (e.g up, down or frame) has been performed. If a frame +number is given it will run until the specified frame returns. ### Editing Two commands allow you to open code from the debugger into an editor: -* `edit [file:line]`: edit _file_ using the editor specified by the EDITOR -environment variable. A specific _line_ can also be given. +* `edit [file:n]`: edit file named _file_ using the editor specified by the +EDITOR environment variable. A specific line _n_ can also be given. ### Quitting -To exit the debugger, use the `quit` command (abbreviated `q`), or its alias -`exit`. +To exit the debugger, use the `quit` command (abbreviated to `q`). Or, type `q!` +to bypass the `Really quit? (y/n)` prompt and exit unconditionally. A simple quit tries to terminate all threads in effect. Therefore your server will be stopped and you will have to start it again. ### Settings -`byebug` has a few available options to tweak its behaviour: +`byebug` has a few available options to tweak its behavior: + +``` +(byebug) help set + + set -* `set autoreload`: Reload source code when changed (default: true). -* `set autolist`: Execute `list` command on every breakpoint (default: true). -* `set listsize _n_`: Set number of source lines to list by default to _n_ -(default: 10) -* `set forcestep`: Make sure the `next` and `step` commands always move to a new -line. + Modifies byebug settings -You can see the full list by using `help set`. Use `help set _subcommand_` to -learn about a particular `set` command. + Boolean values take "on", "off", "true", "false", "1" or "0". If you + don't specify a value, the boolean setting will be enabled. Conversely, + you can use "set no" to disable them. + + You can see these environment settings with the "show" command. + + List of supported settings: + + autosave -- Automatically save command history record on exit + autolist -- Invoke list command on every stop + width -- Number of characters per line in byebug's output + autoirb -- Invoke IRB on every stop + basename -- : information after every stop uses short paths + linetrace -- Enable line execution tracing + autopry -- Invoke Pry on every stop + stack_on_error -- Display stack trace when `eval` raises an exception + fullpath -- Display full file names in backtraces + histfile -- File where cmd history is saved to. Default: ./.byebug_history + listsize -- Set number of source lines to list by default + post_mortem -- Enable/disable post-mortem mode + callstyle -- Set how you want method call parameters to be displayed + histsize -- Maximum number of commands that can be stored in byebug history + savefile -- File where settings are saved to. Default: ~/.byebug_save +``` TIP: You can save these settings in an `.byebugrc` file in your home directory. The debugger reads these global settings when it starts. For example: ```bash -set forcestep +set callstyle short set listsize 25 ``` +Debugging with the `web-console` gem +------------------------------------ + +Web Console is a bit like `byebug`, but it runs in the browser. In any page you +are developing, you can request a console in the context of a view or a +controller. The console would be rendered next to your HTML content. + +### Console + +Inside any controller action or view, you can invoke the console by +calling the `console` method. + +For example, in a controller: + +```ruby +class PostsController < ApplicationController + def new + console + @post = Post.new + end +end +``` + +Or in a view: + +```html+erb +<% console %> + +

New Post

+``` + +This will render a console inside your view. You don't need to care about the +location of the `console` call; it won't be rendered on the spot of its +invocation but next to your HTML content. + +The console executes pure Ruby code: You can define and instantiate +custom classes, create new models and inspect variables. + +NOTE: Only one console can be rendered per request. Otherwise `web-console` +will raise an error on the second `console` invocation. + +### Inspecting Variables + +You can invoke `instance_variables` to list all the instance variables +available in your context. If you want to list all the local variables, you can +do that with `local_variables`. + +### Settings + +* `config.web_console.whitelisted_ips`: Authorized list of IPv4 or IPv6 +addresses and networks (defaults: `127.0.0.1/8, ::1`). +* `config.web_console.whiny_requests`: Log a message when a console rendering +is prevented (defaults: `true`). + +Since `web-console` evaluates plain Ruby code remotely on the server, don't try +to use it in production. + Debugging Memory Leaks ---------------------- -A Ruby application (on Rails or not), can leak memory - either in the Ruby code +A Ruby application (on Rails or not), can leak memory — either in the Ruby code or at the C code level. In this section, you will learn how to find and fix such leaks by using tool @@ -809,8 +907,8 @@ such as Valgrind. ### Valgrind -[Valgrind](http://valgrind.org/) is a Linux-only application for detecting -C-based memory leaks and race conditions. +[Valgrind](http://valgrind.org/) is an application for detecting C-based memory +leaks and race conditions. There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail. For example, if a C @@ -830,9 +928,9 @@ application. Here is a list of useful plugins for debugging: * [Footnotes](https://github.com/josevalim/rails-footnotes) Every Rails page has footnotes that give request information and link back to your source via TextMate. -* [Query Trace](https://github.com/ntalbott/query_trace/tree/master) Adds query +* [Query Trace](https://github.com/ruckus/active-record-query-trace/tree/master) Adds query origin tracing to your logs. -* [Query Reviewer](https://github.com/nesquena/query_reviewer) This rails plugin +* [Query Reviewer](https://github.com/nesquena/query_reviewer) This Rails plugin not only runs "EXPLAIN" before each of your select queries in development, but provides a small DIV in the rendered output of each page with the summary of warnings for each query that it analyzed. @@ -844,18 +942,13 @@ standard Rails error page with a new one containing more contextual information, like source code and variable inspection. * [RailsPanel](https://github.com/dejan/rails_panel) Chrome extension for Rails development that will end your tailing of development.log. Have all information -about your Rails app requests in the browser - in the Developer Tools panel. +about your Rails app requests in the browser — in the Developer Tools panel. Provides insight to db/rendering/total times, parameter list, rendered views and more. +* [Pry](https://github.com/pry/pry) An IRB alternative and runtime developer console. References ---------- -* [ruby-debug Homepage](http://bashdb.sourceforge.net/ruby-debug/home-page.html) -* [debugger Homepage](https://github.com/cldwalker/debugger) * [byebug Homepage](https://github.com/deivid-rodriguez/byebug) -* [Article: Debugging a Rails application with ruby-debug](http://www.sitepoint.com/debug-rails-app-ruby-debug/) -* [Ryan Bates' debugging ruby (revised) screencast](http://railscasts.com/episodes/54-debugging-ruby-revised) -* [Ryan Bates' stack trace screencast](http://railscasts.com/episodes/24-the-stack-trace) -* [Ryan Bates' logger screencast](http://railscasts.com/episodes/56-the-logger) -* [Debugging with ruby-debug](http://bashdb.sourceforge.net/ruby-debug.html) +* [web-console Homepage](https://github.com/rails/web-console) diff --git a/source/development_dependencies_install.md b/source/development_dependencies_install.md index b134c9d..7ec038e 100644 --- a/source/development_dependencies_install.md +++ b/source/development_dependencies_install.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Development Dependencies Install ================================ @@ -7,7 +9,7 @@ After reading this guide, you will know: * How to set up your machine for Rails development * How to run specific groups of unit tests from the Rails test suite -* How the ActiveRecord portion of the Rails test suite operates +* How the Active Record portion of the Rails test suite operates -------------------------------------------------------------------------------- @@ -19,16 +21,15 @@ The easiest and recommended way to get a development environment ready to hack i The Hard Way ------------ -In case you can't use the Rails development box, see section above, these are the steps to manually build a development box for Ruby on Rails core development. +In case you can't use the Rails development box, see section below, these are the steps to manually build a development box for Ruby on Rails core development. ### Install Git Ruby on Rails uses Git for source code control. The [Git homepage](http://git-scm.com/) has installation instructions. There are a variety of resources on the net that will help you get familiar with Git: * [Try Git course](http://try.github.io/) is an interactive course that will teach you the basics. -* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git +* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git. * [Everyday Git](http://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by. -* The [PeepCode screencast](https://peepcode.com/products/git) on Git is easier to follow. * [GitHub](http://help.github.com) offers links to a variety of Git resources. * [Pro Git](http://git-scm.com/book) is an entire book about Git with a Creative Commons license. @@ -45,42 +46,20 @@ $ cd rails The test suite must pass with any submitted code. No matter whether you are writing a new patch, or evaluating someone else's, you need to be able to run the tests. -Install first libxml2 and libxslt together with their development files for Nokogiri. In Ubuntu that's - -```bash -$ sudo apt-get install libxml2 libxml2-dev libxslt1-dev -``` - -If you are on Fedora or CentOS, you can run - -```bash -$ sudo yum install libxml2 libxml2-devel libxslt libxslt-devel -``` - -If you are running Arch Linux, you're done with: +Install first SQLite3 and its development files for the `sqlite3` gem. On macOS +users are done with: ```bash -$ sudo pacman -S libxml2 libxslt +$ brew install sqlite3 ``` -On FreeBSD, you just have to run: - -```bash -# pkg_add -r libxml2 libxslt -``` - -Alternatively, you can install the `textproc/libxml2` and `textproc/libxslt` -ports. - -If you have any problems with these libraries, you can install them manually by compiling the source code. Just follow the instructions at the [Red Hat/CentOS section of the Nokogiri tutorials](http://nokogiri.org/tutorials/installing_nokogiri.html#red_hat__centos) . - -Also, SQLite3 and its development files for the `sqlite3-ruby` gem - in Ubuntu you're done with just +In Ubuntu you're done with just: ```bash $ sudo apt-get install sqlite3 libsqlite3-dev ``` -And if you are on Fedora or CentOS, you're done with +If you are on Fedora or CentOS, you're done with ```bash $ sudo yum install sqlite3 sqlite3-devel @@ -95,12 +74,12 @@ $ sudo pacman -S sqlite For FreeBSD users, you're done with: ```bash -# pkg_add -r sqlite3 +# pkg install sqlite3 ``` Or compile the `databases/sqlite3` port. -Get a recent version of [Bundler](http://gembundler.com/) +Get a recent version of [Bundler](http://bundler.io/) ```bash $ gem install bundler @@ -117,7 +96,7 @@ This command will install all dependencies except the MySQL and PostgreSQL Ruby NOTE: If you would like to run the tests that use memcached, you need to ensure that you have it installed and running. -You can use [Homebrew](http://brew.sh/) to install memcached on OSX: +You can use [Homebrew](http://brew.sh/) to install memcached on OS X: ```bash $ brew install memcached @@ -135,6 +114,20 @@ Or use yum on Fedora or CentOS: $ sudo yum install memcached ``` +If you are running on Arch Linux: + +```bash +$ sudo pacman -S memcached +``` + +For FreeBSD users, you're done with: + +```bash +# pkg install memcached +``` + +Alternatively, you can compile the `databases/memcached` port. + With the dependencies now installed, you can run the test suite with: ```bash @@ -169,9 +162,13 @@ $ cd actionpack $ bundle exec ruby -Itest path/to/test.rb -n test_name ``` +### Railties Setup + +Some Railties tests depend on a JavaScript runtime environment, such as having [Node.js](https://nodejs.org/) installed. + ### Active Record Setup -The test suite of Active Record attempts to run four times: once for SQLite3, once for each of the two MySQL gems (`mysql` and `mysql2`), and once for PostgreSQL. We are going to see now how to set up the environment for them. +Active Record's test suite runs three times: once for SQLite3, once for MySQL, and once for PostgreSQL. We are going to see now how to set up the environment for them. WARNING: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. @@ -181,10 +178,22 @@ The Active Record test suite requires a custom config file: `activerecord/test/c #### MySQL and PostgreSQL -To be able to run the suite for MySQL and PostgreSQL we need their gems. Install first the servers, their client libraries, and their development files. In Ubuntu just run +To be able to run the suite for MySQL and PostgreSQL we need their gems. Install +first the servers, their client libraries, and their development files. + +On OS X, you can run: + +```bash +$ brew install mysql +$ brew install postgresql +``` + +Follow the instructions given by Homebrew to start these. + +In Ubuntu just run: ```bash -$ sudo apt-get install mysql-server libmysqlclient15-dev +$ sudo apt-get install mysql-server libmysqlclient-dev $ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev ``` @@ -206,18 +215,10 @@ $ sudo pacman -S postgresql postgresql-libs FreeBSD users will have to run the following: ```bash -# pkg_add -r mysql56-client mysql56-server -# pkg_add -r postgresql92-client postgresql92-server +# pkg install mysql56-client mysql56-server +# pkg install postgresql94-client postgresql94-server ``` -You can use [Homebrew](http://brew.sh/) to install MySQL and PostgreSQL on OSX: - -```bash -$ brew install mysql -$ brew install postgresql -``` -Follow instructions given by [Homebrew](http://brew.sh/) to start these. - Or install them through ports (they are located under the `databases` folder). If you run into troubles during the installation of MySQL, please see [the MySQL documentation](http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html). @@ -252,18 +253,20 @@ $ cd activerecord $ bundle exec rake db:mysql:build ``` -PostgreSQL's authentication works differently. A simple way to set up the development environment for example is to run with your development account -This is not needed when installed via [Homebrew](http://brew.sh). +PostgreSQL's authentication works differently. To setup the development environment +with your development account, on Linux or BSD, you just have to run: ```bash $ sudo -u postgres createuser --superuser $USER ``` -And for OS X (when installed via [Homebrew](http://brew.sh)) + +and for OS X: + ```bash $ createuser --superuser $USER ``` -and then create the test databases with +Then you need to create the test databases with ```bash $ cd activerecord @@ -289,3 +292,46 @@ NOTE: Using the rake task to create the test databases ensures they have the cor NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator". If you're using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails. + +### Action Cable Setup + +Action Cable uses Redis as its default subscriptions adapter ([read more](action_cable_overview.html#broadcasting)). Thus, in order to have Action Cable's tests passing you need to install and have Redis running. + +#### Install Redis From Source + +Redis' documentation discourage installations with package managers as those are usually outdated. Installing from source and bringing the server up is straight forward and well documented on [Redis' documentation](http://redis.io/download#installation). + +#### Install Redis From Package Manager + +On OS X, you can run: + +```bash +$ brew install redis +``` + +Follow the instructions given by Homebrew to start these. + +In Ubuntu just run: + +```bash +$ sudo apt-get install redis-server +``` + +On Fedora or CentOS (requires EPEL enabled), just run: + +```bash +$ sudo yum install redis +``` + +If you are running Arch Linux just run: + +```bash +$ sudo pacman -S redis +$ sudo systemctl start redis +``` + +FreeBSD users will have to run the following: + +```bash +# portmaster databases/redis +``` diff --git a/source/documents.yaml b/source/documents.yaml index 82e248e..2afef57 100644 --- a/source/documents.yaml +++ b/source/documents.yaml @@ -11,7 +11,7 @@ - name: Active Record Basics url: active_record_basics.html - description: This guide will get you started with models, persistence to database and the Active Record pattern and library. + description: This guide will get you started with models, persistence to database, and the Active Record pattern and library. - name: Active Record Migrations url: active_record_migrations.html @@ -19,7 +19,7 @@ - name: Active Record Validations url: active_record_validations.html - description: This guide covers how you can use Active Record validations + description: This guide covers how you can use Active Record validations. - name: Active Record Callbacks url: active_record_callbacks.html @@ -32,6 +32,11 @@ name: Active Record Query Interface url: active_record_querying.html description: This guide covers the database query interface provided by Active Record. + - + name: Active Model Basics + url: active_model_basics.html + description: This guide covers the use of model classes without Active Record. + work_in_progress: true - name: Views documents: @@ -69,16 +74,19 @@ - name: Rails Internationalization API url: i18n.html - description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on. + description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country, and so on. - name: Action Mailer Basics url: action_mailer_basics.html description: This guide describes how to use Action Mailer to send and receive emails. + - + name: Active Job Basics + url: active_job_basics.html + description: This guide provides you with all you need to get started creating, enqueuing, and executing background jobs. - name: Testing Rails Applications url: testing.html - work_in_progress: true - description: This is a rather comprehensive guide to doing both unit and functional tests in Rails. It covers everything from 'What is a test?' to the testing APIs. Enjoy. + description: This is a rather comprehensive guide to the various testing facilities in Rails. It covers everything from 'What is a test?' to Integration Testing. Enjoy. - name: Securing Rails Applications url: security.html @@ -92,9 +100,9 @@ url: configuring.html description: This guide covers the basic configuration settings for a Rails application. - - name: Rails Command Line Tools and Rake Tasks + name: The Rails Command Line url: command_line.html - description: This guide covers the command line tools and rake tasks provided by Rails. + description: This guide covers the command line tools provided by Rails. - name: Asset Pipeline url: asset_pipeline.html @@ -103,16 +111,38 @@ name: Working with JavaScript in Rails url: working_with_javascript_in_rails.html description: This guide covers the built-in Ajax/JavaScript functionality of Rails. - - - name: Getting Started with Engines - url: engines.html - description: This guide explains how to write a mountable engine. - work_in_progress: true - name: The Rails Initialization Process work_in_progress: true url: initialization.html - description: This guide explains the internals of the Rails initialization process as of Rails 4 + description: This guide explains the internals of the Rails initialization process. + - + name: Autoloading and Reloading Constants + url: autoloading_and_reloading_constants.html + description: This guide documents how autoloading and reloading constants work. + - + name: "Caching with Rails: An Overview" + url: caching_with_rails.html + description: This guide is an introduction to speeding up your Rails application with caching. + - + name: Active Support Instrumentation + work_in_progress: true + url: active_support_instrumentation.html + description: This guide explains how to use the instrumentation API inside of Active Support to measure events inside of Rails and other Ruby code. + - + name: Profiling Rails Applications + work_in_progress: true + url: profiling.html + description: This guide explains how to profile your Rails applications to improve performance. + - + name: Using Rails for API-only Applications + url: api_app.html + description: This guide explains how to effectively use Rails to develop a JSON API application. + - + name: Action Cable Overview + url: action_cable_overview.html + description: This guide explains how Action Cable works, and how to use WebSockets to create real-time features. + - name: Extending Rails documents: @@ -129,6 +159,11 @@ name: Creating and Customizing Rails Generators url: generators.html description: This guide covers the process of adding a brand new generator to your extension or providing an alternative to an element of a built-in Rails generator (such as providing alternative test stubs for the scaffold generator). + - + name: Getting Started with Engines + url: engines.html + description: This guide explains how to write a mountable engine. + work_in_progress: true - name: Contributing to Ruby on Rails documents: @@ -158,6 +193,18 @@ name: Upgrading Ruby on Rails url: upgrading_ruby_on_rails.html description: This guide helps in upgrading applications to latest Ruby on Rails versions. + - + name: Ruby on Rails 5.1 Release Notes + url: 5_1_release_notes.html + description: Release notes for Rails 5.1. + - + name: Ruby on Rails 5.0 Release Notes + url: 5_0_release_notes.html + description: Release notes for Rails 5.0. + - + name: Ruby on Rails 4.2 Release Notes + url: 4_2_release_notes.html + description: Release notes for Rails 4.2. - name: Ruby on Rails 4.1 Release Notes url: 4_1_release_notes.html diff --git a/source/documents_zh-CN.yaml b/source/documents_zh-CN.yaml deleted file mode 100644 index e7d1c56..0000000 --- a/source/documents_zh-CN.yaml +++ /dev/null @@ -1,218 +0,0 @@ -- - name: 入门 - documents: - - - name: Rails 入门 - url: getting_started.html - description: 从安装到建立第一个应用程序所需知道的一切。 -- - name: 模型 - documents: - - - name: Active Record 基础 - url: active_record_basics.html - description: 本篇介绍 Models、数据库持久性以及 Active Record 模式。 - - - name: Active Record 数据库迁移 - url: active_record_migrations.html - description: 本篇介绍如何有条有理地使用 Active Record 来修改数据库。 - - - name: Active Record 数据验证 - url: active_record_validations.html - description: 本篇介绍如何使用 Active Record 验证功能。 - - - name: Active Record 回调 - url: active_record_callbacks.html - description: 本篇介绍如何使用 Active Record 回调功能。 - - - name: Active Record 关联 - url: association_basics.html - description: 本篇介绍如何使用 Active Record 的关联功能。 - - - name: Active Record 查询 - url: active_record_querying.html - description: 本篇介绍如何使用 Active Record 的数据库查询功能。 -- - name: 视图 - documents: - - - name: Action View 基础 - url: action_view_overview.html - description: 本篇介绍 Action View 辅助方法。 - work_in_progress: true - - - name: Rails 布局和视图渲染 - url: layouts_and_rendering.html - description: 本篇介绍 Action Controller 与 Action View 基本的版型功能,包含了渲染、重定向、使用 `content_for` 区块、以及。 - - - name: Action View 表单帮助方法 - url: form_helpers.html - description: 本篇介绍 Action View 的表单帮助方法。 -- - name: 控制器 - documents: - - - name: Action Controller 简介 - url: action_controller_overview.html - description: 本篇介绍 Controller 的工作原理,Controller 在请求周期所扮演的角色。内容包含 Session、滤动器、Cookies、资料串流以及如何处理由请求所发起的异常。 - - - name: Rails 路由全解 - url: routing.html - description: 本篇介绍与使用者息息相关的路由功能。想了解如何使用 Rails 的路由,从这里开始。 -- - name: 深入 - documents: - - - name: Active Support 核心扩展 - url: active_support_core_extensions.html - description: 本篇介绍由 Active Support 定义的核心扩展功能。 - needs_translation: true - - - name: Rails 国际化 API - url: i18n.html - description: 本篇介绍如何国际化应用程序。将应用程序翻译成多种语言、更改单复数规则、对不同的国家使用正确的日期格式等。 - - - name: Action Mailer 基础 - url: action_mailer_basics.html - description: 本篇介绍如何使用 Action Mailer 来收发信件。 - - - name: Rails 程序测试指南 - url: testing.html - work_in_progress: true - description: 本篇介绍 Rails 里的单元与功能性测试,从什么是测试,解说到如何测试 API。 - - - name: Rails 安全指南 - url: security.html - description: 本篇介绍网路应用程序常见的安全问题,如何在 Rails 里处理这些问题。 - - - name: 调试 Rails 程序 - url: debugging_rails_applications.html - description: 本篇介绍如何给 Rails 应用程式除错。包含了多种除错技巧、如何理解与了解程式码背后究竟发生了什么事。 - - - name: 设置 Rails 程序 - url: configuring.html - description: 本篇介绍 Rails 应用程序的基本设置选项。 - - - name: Rails 命令行 - url: command_line.html - description: 本篇介绍 Rails 提供的 Rake 任务与命令行功能。 - - - name: Rails 缓存简介 - work_in_progress: true - url: caching_with_rails.html - description: 本篇介绍 Rails 提供的多种缓存技巧。 - - - name: Asset Pipeline - url: asset_pipeline.html - description: 本篇介绍 Asset Pipeline。 - - - name: 在 Rails 中使用 JavaScript - url: working_with_javascript_in_rails.html - description: 本篇介绍 Rails 内置的 Ajax 与 JavaScript 功能。 - - - name: Engine 入门 - url: engines.html - description: 本篇介绍如何撰写可嵌入至应用程序的 Engine。 - work_in_progress: true - needs_translation: true - - - name: Rails 启动过程 - work_in_progress: true - url: initialization.html - description: 本篇介绍 Rails 内部的启动过程。 -- - name: 扩展 Rails - documents: - - - name: 新建 Rails Plugins 的基础 - work_in_progress: true - url: plugins.html - description: 本篇介绍如何开发 Plugin 来扩展 Rails 的功能。 - work_in_progress: true - needs_translation: true - - - name: Rails on Rack - url: rails_on_rack.html - description: 本篇介绍 Rack 如何与 Rails 整合,以及如何与其他 Rack 组件互动。 - - - name: 客制与新建 Rails 产生器 - url: generators.html - description: 本篇介绍如何加入新的产生器、修改 Rails 内建的产生器。 - needs_translation: true - - - name: Rails 应用程式模版 - url: rails_application_templates.html - description: 本篇介绍如何使用应用程式模版。 - needs_translation: true -- - name: 贡献 Ruby on Rails - documents: - - - name: 贡献 Ruby on Rails - url: contributing_to_ruby_on_rails.html - description: Rails 不是“某个人”独有的框架。本篇介绍如何参与 Rails 开发。 - needs_translation: true - - - name: API 文件准则 - url: api_documentation_guidelines.html - description: 本篇记录了撰写 API 文件的准则。 - needs_translation: true - - - name: Ruby on Rails 指南准则 - url: ruby_on_rails_guides_guidelines.html - description: 本篇记录了撰写 Ruby on Rails 指南的准则。 - needs_translation: true -- - name: 维护方针 - documents: - - - name: 维护方针 - url: maintenance_policy.html - description: Ruby on Rails 目前官方支持的版本、何时发布新版本。 -- - name: 发布记 - documents: - - - name: 升级 Ruby on Rails - url: upgrading_ruby_on_rails.html - description: 本篇帮助您将程序升级至最新版本。 - needs_translation: true - - - name: Ruby on Rails 4.2 发布记 - url: 4_2_release_notes.html - description: Rails 4.2 的发布记。 - work_in_progress: true - - - name: Ruby on Rails 4.1 发布记 - url: 4_1_release_notes.html - description: Rails 4.1 的发布记。 - - - name: Ruby on Rails 4.0 发布记 - url: 4_0_release_notes.html - description: Rails 4.0 的发布记。 - needs_translation: true - - - name: Ruby on Rails 3.2 发布记 - url: 3_2_release_notes.html - description: Rails 3.2 的发布记。 - needs_translation: true - - - name: Ruby on Rails 3.1 发布记 - url: 3_1_release_notes.html - description: Rails 3.1 的发布记。 - needs_translation: true - - - name: Ruby on Rails 3.0 发布记 - url: 3_0_release_notes.html - description: Rails 3.0 的发布记。 - needs_translation: true - - - name: Ruby on Rails 2.3 发布记 - url: 2_3_release_notes.html - description: Rails 2.3 的发布记。 - needs_translation: true - - - name: Ruby on Rails 2.2 发布记 - url: 2_2_release_notes.html - description: Rails 2.2 的发布记。 - needs_translation: true diff --git a/source/engines.md b/source/engines.md index 24548a5..2276f34 100644 --- a/source/engines.md +++ b/source/engines.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Getting Started with Engines ============================ @@ -9,9 +11,10 @@ After reading this guide, you will know: * What makes an engine. * How to generate an engine. -* Building features for the engine. -* Hooking the engine into an application. -* Overriding engine functionality in the application. +* How to build features for the engine. +* How to hook the engine into an application. +* How to override engine functionality in the application. +* Avoid loading Rails frameworks with Load and Configuration Hooks -------------------------------------------------------------------------------- @@ -23,7 +26,7 @@ their host applications. A Rails application is actually just a "supercharged" engine, with the `Rails::Application` class inheriting a lot of its behavior from `Rails::Engine`. -Therefore, engines and applications can be thought of almost the same thing, +Therefore, engines and applications can be thought of as almost the same thing, just with subtle differences, as you'll see throughout this guide. Engines and applications also share a common structure. @@ -32,7 +35,7 @@ directory structure, and are both generated using the `rails plugin new` generator. The difference is that an engine is considered a "full plugin" by Rails (as indicated by the `--full` option that's passed to the generator command). We'll actually be using the `--mountable` option here, which includes -all the features of `--full`, and then some. This guide will refer to these +all the features of `--full`, and then some. This guide will refer to these "full plugins" simply as "engines" throughout. An engine **can** be a plugin, and a plugin **can** be an engine. @@ -44,7 +47,7 @@ see how to hook it into an application. Engines can also be isolated from their host applications. This means that an application is able to have a path provided by a routing helper such as -`articles_path` and use an engine also that provides a path also called +`articles_path` and use an engine that also provides a path also called `articles_path`, and the two would not clash. Along with this, controllers, models and table names are also namespaced. You'll see how to do this later in this guide. @@ -57,7 +60,7 @@ only be enhancing it, rather than changing it drastically. To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or -[Forem](https://github.com/radar/forem), an engine that provides forum +[Thredded](https://github.com/thredded/thredded), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. @@ -74,13 +77,13 @@ options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal: ```bash -$ bin/rails plugin new blorgh --mountable +$ rails plugin new blorgh --mountable ``` The full list of options for the plugin generator may be seen by typing: ```bash -$ bin/rails plugin --help +$ rails plugin --help ``` The `--mountable` option tells the generator that you want to create a @@ -148,7 +151,7 @@ When you include the engine into an application later on, you will do so with this line in the Rails application's `Gemfile`: ```ruby -gem 'blorgh', path: "vendor/engines/blorgh" +gem 'blorgh', path: 'engines/blorgh' ``` Don't forget to run `bundle install` as usual. By specifying it as a gem within @@ -182,7 +185,7 @@ end By inheriting from the `Rails::Engine` class, this gem notifies Rails that there's an engine at the specified path, and will correctly mount the engine inside the application, performing tasks such as adding the `app` directory of -the engine to the load path for models, mailers, controllers and views. +the engine to the load path for models, mailers, controllers, and views. The `isolate_namespace` method here deserves special notice. This call is responsible for isolating the controllers, models, routes and other things into @@ -237,6 +240,27 @@ NOTE: The `ApplicationController` class inside an engine is named just like a Rails application in order to make it easier for you to convert your applications into engines. +NOTE: Because of the way that Ruby does constant lookup you may run into a situation +where your engine controller is inheriting from the main application controller and +not your engine's application controller. Ruby is able to resolve the `ApplicationController` constant, and therefore the autoloading mechanism is not triggered. See the section [When Constants Aren't Missed](autoloading_and_reloading_constants.html#when-constants-aren-t-missed) of the [Autoloading and Reloading Constants](autoloading_and_reloading_constants.html) guide for further details. The best way to prevent this from +happening is to use `require_dependency` to ensure that the engine's application +controller is loaded. For example: + +``` ruby +# app/controllers/blorgh/articles_controller.rb: +require_dependency "blorgh/application_controller" + +module Blorgh + class ArticlesController < ApplicationController + ... + end +end +``` + +WARNING: Don't use `require` because it will break the automatic reloading of classes +in the development environment - using `require_dependency` ensures that classes are +loaded and unloaded in the correct manner. + Lastly, the `app/views` directory contains a `layouts` folder, which contains a file at `blorgh/application.html.erb`. This file allows you to specify a layout for the engine. If this engine is to be used as a stand-alone engine, then you @@ -366,7 +390,7 @@ called `Blorgh::ArticlesController` (at `app/controllers/blorgh/articles_controller.rb`) and its related views at `app/views/blorgh/articles`. This generator also generates a test for the controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper -(`app/helpers/blorgh/articles_controller.rb`). +(`app/helpers/blorgh/articles_helper.rb`). Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module: @@ -379,8 +403,8 @@ module Blorgh end ``` -NOTE: The `ApplicationController` class being inherited from here is the -`Blorgh::ApplicationController`, not an application's `ApplicationController`. +NOTE: The `ArticlesController` class inherits from +`Blorgh::ApplicationController`, not the application's `ApplicationController`. The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced: @@ -400,16 +424,7 @@ Finally, the assets for this resource are generated in two files: `app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little later. -By default, the scaffold styling is not applied to the engine because the -engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't -load it. To make the scaffold styling apply, insert this line into the `` -tag of this layout: - -```erb -<%= stylesheet_link_tag "scaffold" %> -``` - -You can see what the engine has so far by running `rake db:migrate` at the root +You can see what the engine has so far by running `bin/rails db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open `http://localhost:3000/blorgh/articles` you will see the default scaffold that has @@ -447,7 +462,7 @@ model, a comment controller and then modify the articles scaffold to display comments and allow people to create new ones. From the application root, run the model generator. Tell it to generate a -`Comment` model, with the related table having two columns: a `article_id` integer +`Comment` model, with the related table having two columns: an `article_id` integer and `text` text column. ```bash @@ -471,7 +486,7 @@ called `Blorgh::Comment`. Now run the migration to create our blorgh_comments table: ```bash -$ rake db:migrate +$ bin/rails db:migrate ``` To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and @@ -494,7 +509,7 @@ Turning the model into this: ```ruby module Blorgh - class Article < ActiveRecord::Base + class Article < ApplicationRecord has_many :comments end end @@ -589,7 +604,7 @@ the comments, however, is not quite right yet. If you were to create a comment right now, you would see this error: ``` -Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], +Missing partial blorgh/comments/_comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in: * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * "/Users/ryan/Sites/side_projects/blorgh/app/views" @@ -598,7 +613,7 @@ Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], The engine is unable to find the partial required for rendering the comments. Rails looks first in the application's (`test/dummy`) `app/views` directory and then in the engine's `app/views` directory. When it can't find it, it will throw -this error. The engine knows to look for `blorgh/comments/comment` because the +this error. The engine knows to look for `blorgh/comments/_comment` because the model object it is receiving is from the `Blorgh::Comment` class. This partial will be responsible for rendering just the comment text, for now. @@ -646,7 +661,7 @@ However, because you are developing the `blorgh` engine on your local machine, you will need to specify the `:path` option in your `Gemfile`: ```ruby -gem 'blorgh', path: "/path/to/blorgh" +gem 'blorgh', path: 'engines/blorgh' ``` Then run `bundle` to install the gem. @@ -677,17 +692,17 @@ pre-defined path which may be customizable. The engine contains migrations for the `blorgh_articles` and `blorgh_comments` table which need to be created in the application's database so that the engine's models can query them correctly. To copy these migrations into the -application use this command: +application run the following command from the `test/dummy` directory of your Rails engine: ```bash -$ rake blorgh:install:migrations +$ bin/rails blorgh:install:migrations ``` If you have multiple engines that need migrations copied over, use `railties:install:migrations` instead: ```bash -$ rake railties:install:migrations +$ bin/rails railties:install:migrations ``` This command, when run for the first time, will copy over all the migrations @@ -696,8 +711,8 @@ haven't been copied over already. The first run for this command will output something such as this: ```bash -Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh -Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh +Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh +Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh ``` The first timestamp (`[timestamp_1]`) will be the current time, and the second @@ -705,7 +720,7 @@ timestamp (`[timestamp_2]`) will be the current time plus a second. The reason for this is so that the migrations for the engine are run after any existing migrations in the application. -To run these migrations within the context of the application, simply run `rake +To run these migrations within the context of the application, simply run `bin/rails db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the articles will be empty. This is because the table created inside the application is different from the one created within the engine. Go ahead, play around with the @@ -716,14 +731,14 @@ If you would like to run migrations only from one engine, you can do it by specifying `SCOPE`: ```bash -rake db:migrate SCOPE=blorgh +bin/rails db:migrate SCOPE=blorgh ``` This may be useful if you want to revert engine's migrations before removing it. To revert all migrations from blorgh engine you can run code such as: ```bash -rake db:migrate SCOPE=blorgh VERSION=0 +bin/rails db:migrate SCOPE=blorgh VERSION=0 ``` ### Using a Class Provided by the Application @@ -750,7 +765,7 @@ application: rails g model user name:string ``` -The `rake db:migrate` command needs to be run here to ensure that our +The `bin/rails db:migrate` command needs to be run here to ensure that our application has the `users` table for future use. Also, to keep it simple, the articles form will have a new text field called @@ -785,7 +800,7 @@ before the article is saved. It will also need to have an `attr_accessor` set up for this field, so that the setter and getter methods are defined for it. To do all this, you'll need to add the `attr_accessor` for `author_name`, the -association for the author and the `before_save` call into +association for the author and the `before_validation` call into `app/models/blorgh/article.rb`. The `author` association will be hard-coded to the `User` class for the time being. @@ -793,7 +808,7 @@ association for the author and the `before_save` call into attr_accessor :author_name belongs_to :author, class_name: "User" -before_save :set_author +before_validation :set_author private def set_author @@ -822,24 +837,22 @@ This migration will need to be run on the application. To do that, it must first be copied using this command: ```bash -$ rake blorgh:install:migrations +$ bin/rails blorgh:install:migrations ``` Notice that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run. ``` -NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been -skipped. Migration with the same name already exists. NOTE Migration -[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration -with the same name already exists. Copied migration -[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh +NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists. +NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists. +Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh ``` Run the migration using: ```bash -$ rake db:migrate +$ bin/rails db:migrate ``` Now with all the pieces in place, an action will take place that will associate @@ -852,28 +865,10 @@ above the "Title" output inside `app/views/blorgh/articles/show.html.erb`: ```html+erb

Author: - <%= @article.author %> + <%= @article.author.name %>

``` -By outputting `@article.author` using the `<%=` tag, the `to_s` method will be -called on the object. By default, this will look quite ugly: - -``` -# -``` - -This is undesirable. It would be much better to have the user's name there. To -do this, add a `to_s` method to the `User` class within the application: - -```ruby -def to_s - name -end -``` - -Now instead of the ugly Ruby object output, the author's name will be displayed. - #### Using a Controller Provided by the Application Because Rails controllers generally share code for things like authentication @@ -888,7 +883,9 @@ engine this would be done by changing `app/controllers/blorgh/application_controller.rb` to look like: ```ruby -class Blorgh::ApplicationController < ApplicationController +module Blorgh + class ApplicationController < ::ApplicationController + end end ``` @@ -1036,31 +1033,46 @@ functionality, especially controllers. This means that if you were to make a typical `GET` to a controller in a controller's functional test like this: ```ruby -get :index +module Blorgh + class FooControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + + def test_index + get foos_url + ... + end + end +end ``` It may not function correctly. This is because the application doesn't know how to route these requests to the engine unless you explicitly tell it **how**. To -do this, you must also pass the `:use_route` option as a parameter on these -requests: +do this, you must set the `@routes` instance variable to the engine's route set +in your setup code: ```ruby -get :index, use_route: :blorgh +module Blorgh + class FooControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + + setup do + @routes = Engine.routes + end + + def test_index + get foos_url + ... + end + end +end ``` This tells the application that you still want to perform a `GET` request to the `index` action of this controller, but you want to use the engine's route to get there, rather than the application's one. -Another way to do this is to assign the `@routes` instance variable to `Engine.routes` in your test setup: - -```ruby -setup do - @routes = Engine.routes -end -``` - -This will also ensure url helpers for the engine will work as expected in your tests. +This also ensures that the engine's URL helpers will work as expected in your +tests. Improving engine functionality ------------------------------ @@ -1122,7 +1134,7 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments end ``` @@ -1143,7 +1155,7 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments def summary "#{title}" @@ -1155,7 +1167,7 @@ end Using `Class#class_eval` is great for simple adjustments, but for more complex class modifications, you might want to consider using [`ActiveSupport::Concern`] -(http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). +(http://api.rubyonrails.org/classes/ActiveSupport/Concern.html). ActiveSupport::Concern manages load order of interlinked dependent modules and classes at run time allowing you to significantly modularize your code. @@ -1164,7 +1176,7 @@ classes at run time allowing you to significantly modularize your code. ```ruby # MyApp/app/models/blorgh/article.rb -class Blorgh::Article < ActiveRecord::Base +class Blorgh::Article < ApplicationRecord include Blorgh::Concerns::Models::Article def time_since_created @@ -1180,13 +1192,13 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord include Blorgh::Concerns::Models::Article end ``` ```ruby -# Blorgh/lib/concerns/models/article +# Blorgh/lib/concerns/models/article.rb module Blorgh::Concerns::Models::Article extend ActiveSupport::Concern @@ -1198,7 +1210,7 @@ module Blorgh::Concerns::Models::Article attr_accessor :author_name belongs_to :author, class_name: "User" - before_save :set_author + before_validation :set_author private def set_author @@ -1347,13 +1359,13 @@ need to require `admin.css` or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorgh/admin.css"` in its stylesheets. In this situation, you should explicitly define these assets for precompilation. This tells sprockets to add -your engine assets when `rake assets:precompile` is triggered. +your engine assets when `bin/rails assets:precompile` is triggered. You can define assets for precompilation in `engine.rb`: ```ruby initializer "blorgh.assets.precompile" do |app| - app.config.assets.precompile += %w(admin.css admin.js) + app.config.assets.precompile += %w( admin.js admin.css ) end ``` @@ -1399,3 +1411,114 @@ module MyEngine end end ``` + +Active Support On Load Hooks +---------------------------- + +Active Support is the Ruby on Rails component responsible for providing Ruby language extensions, utilities, and other transversal utilities. + +Rails code can often be referenced on load of an application. Rails is responsible for the load order of these frameworks, so when you load frameworks, such as `ActiveRecord::Base`, prematurely you are violating an implicit contract your application has with Rails. Moreover, by loading code such as `ActiveRecord::Base` on boot of your application you are loading entire frameworks which may slow down your boot time and could cause conflicts with load order and boot of your application. + +On Load hooks are the API that allow you to hook into this initialization process without violating the load contract with Rails. This will also mitigate boot performance degradation and avoid conflicts. + +## What are `on_load` hooks? + +Since Ruby is a dynamic language, some code will cause different Rails frameworks to load. Take this snippet for instance: + +```ruby +ActiveRecord::Base.include(MyActiveRecordHelper) +``` + +This snippet means that when this file is loaded, it will encounter `ActiveRecord::Base`. This encounter causes Ruby to look for the definition of that constant and will require it. This causes the entire Active Record framework to be loaded on boot. + +`ActiveSupport.on_load` is a mechanism that can be used to defer the loading of code until it is actually needed. The snippet above can be changed to: + +```ruby +ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } +``` + +This new snippet will only include `MyActiveRecordHelper` when `ActiveRecord::Base` is loaded. + +## How does it work? + +In the Rails framework these hooks are called when a specific library is loaded. For example, when `ActionController::Base` is loaded, the `:action_controller_base` hook is called. This means that all `ActiveSupport.on_load` calls with `:action_controller_base` hooks will be called in the context of `ActionController::Base` (that means `self` will be an `ActionController::Base`). + +## Modifying code to use `on_load` hooks + +Modifying code is generally straightforward. If you have a line of code that refers to a Rails framework such as `ActiveRecord::Base` you can wrap that code in an `on_load` hook. + +### Example 1 + +```ruby +ActiveRecord::Base.include(MyActiveRecordHelper) +``` + +becomes + +```ruby +ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } # self refers to ActiveRecord::Base here, so we can simply #include +``` + +### Example 2 + +```ruby +ActionController::Base.prepend(MyActionControllerHelper) +``` + +becomes + +```ruby +ActiveSupport.on_load(:action_controller_base) { prepend MyActionControllerHelper } # self refers to ActionController::Base here, so we can simply #prepend +``` + +### Example 3 + +```ruby +ActiveRecord::Base.include_root_in_json = true +``` + +becomes + +```ruby +ActiveSupport.on_load(:active_record) { self.include_root_in_json = true } # self refers to ActiveRecord::Base here +``` + +## Available Hooks + +These are the hooks you can use in your own code. + +To hook into the initialization process of one of the following classes use the available hook. + +| Class | Available Hooks | +| --------------------------------- | ------------------------------------ | +| `ActionCable` | `action_cable` | +| `ActionController::API` | `action_controller_api` | +| `ActionController::API` | `action_controller` | +| `ActionController::Base` | `action_controller_base` | +| `ActionController::Base` | `action_controller` | +| `ActionController::TestCase` | `action_controller_test_case` | +| `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` | +| `ActionMailer::Base` | `action_mailer` | +| `ActionMailer::TestCase` | `action_mailer_test_case` | +| `ActionView::Base` | `action_view` | +| `ActionView::TestCase` | `action_view_test_case` | +| `ActiveJob::Base` | `active_job` | +| `ActiveJob::TestCase` | `active_job_test_case` | +| `ActiveRecord::Base` | `active_record` | +| `ActiveSupport::TestCase` | `active_support_test_case` | +| `i18n` | `i18n` | + +## Configuration hooks + +These are the available configuration hooks. They do not hook into any particular framework, instead they run in context of the entire application. + +| Hook | Use Case | +| ---------------------- | ------------------------------------------------------------------------------------- | +| `before_configuration` | First configurable block to run. Called before any initializers are run. | +| `before_initialize` | Second configurable block to run. Called before frameworks initialize. | +| `before_eager_load` | Third configurable block to run. Does not run if `config.cache_classes` set to false. | +| `after_initialize` | Last configurable block to run. Called after frameworks initialize. | + +### Example + +`config.before_configuration { puts 'I am called before any initializers' }` diff --git a/source/form_helpers.md b/source/form_helpers.md index 048eb9a..ba6e158 100644 --- a/source/form_helpers.md +++ b/source/form_helpers.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Form Helpers ============ @@ -38,7 +40,9 @@ When called without arguments like this, it creates a `` tag which, when s ``` -You'll notice that the HTML contains `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element has name attribute of `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf). +You'll notice that the HTML contains an `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element with the name `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their action is "GET" or "POST". + +The second input element with the name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf). ### A Generic Search Form @@ -96,7 +100,15 @@ form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_ ### Helpers for Generating Form Elements -Rails provides a series of helpers for generating form elements such as checkboxes, text fields, and radio buttons. These basic helpers, with names ending in "_tag" (such as `text_field_tag` and `check_box_tag`), generate just a single `` element. The first parameter to these is always the name of the input. When the form is submitted, the name will be passed along with the form data, and will make its way to the `params` hash in the controller with the value entered by the user for that field. For example, if the form contains `<%= text_field_tag(:query) %>`, then you would be able to get the value of this field in the controller with `params[:query]`. +Rails provides a series of helpers for generating form elements such as +checkboxes, text fields, and radio buttons. These basic helpers, with names +ending in `_tag` (such as `text_field_tag` and `check_box_tag`), generate just a +single `` element. The first parameter to these is always the name of the +input. When the form is submitted, the name will be passed along with the form +data, and will make its way to the `params` in the controller with the +value entered by the user for that field. For example, if the form contains +`<%= text_field_tag(:query) %>`, then you would be able to get the value of this +field in the controller with `params[:query]`. When naming inputs, Rails uses certain conventions that make it possible to submit parameters with non-scalar values such as arrays or hashes, which will also be accessible in `params`. You can read more about them in [chapter 7 of this guide](#understanding-parameter-naming-conventions). For details on the precise usage of these helpers, please refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html). @@ -152,7 +164,7 @@ make it easier for users to click the inputs. Other form controls worth mentioning are textareas, password fields, hidden fields, search fields, telephone fields, date fields, time fields, -color fields, datetime fields, datetime-local fields, month fields, week fields, +color fields, datetime-local fields, month fields, week fields, URL fields, email fields, number fields and range fields: ```erb @@ -162,7 +174,6 @@ URL fields, email fields, number fields and range fields: <%= search_field(:user, :name) %> <%= telephone_field(:user, :phone) %> <%= date_field(:user, :born_on) %> -<%= datetime_field(:user, :meeting_time) %> <%= datetime_local_field(:user, :graduation_day) %> <%= month_field(:user, :birthday_month) %> <%= week_field(:user, :birthday_week) %> @@ -183,7 +194,6 @@ Output: - @@ -201,9 +211,8 @@ IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local, month, week, URL, email, number and range inputs are HTML5 controls. If you require your app to have a consistent experience in older browsers, you will need an HTML5 polyfill (provided by CSS and/or JavaScript). -There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are -[Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/), -which provide a simple way to add functionality based on the presence of +There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a popular tool at the moment is +[Modernizr](https://modernizr.com/), which provides a simple way to add functionality based on the presence of detected HTML5 features. TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Security Guide](security.html#logging). @@ -231,7 +240,7 @@ Upon form submission the value entered by the user will be stored in `params[:pe WARNING: You must pass the name of an instance variable, i.e. `:person` or `"person"`, not an actual instance of your model object. -Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](./active_record_validations.html#displaying-validation-errors-in-views) guide. +Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](active_record_validations.html#displaying-validation-errors-in-views) guide. ### Binding a Form to an Object @@ -265,24 +274,24 @@ There are a few things to note here: The resulting HTML is: ```html -
+
``` -The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the parameter_names section. +The name passed to `form_for` controls the key used in `params` to access the form's values. Here the name is `article` and so all the inputs have names of the form `article[attribute_name]`. Accordingly, in the `create` action `params[:article]` will be a hash with keys `:title` and `:body`. You can read more about the significance of input names in the [parameter_names section](#understanding-parameter-naming-conventions). The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder. -You can create a similar binding without actually creating `
` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example if you had a `Person` model with an associated `ContactDetail` model you could create a form for creating both like so: +You can create a similar binding without actually creating `` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example, if you had a `Person` model with an associated `ContactDetail` model, you could create a form for creating both like so: ```erb <%= form_for @person, url: {action: "create"} do |person_form| %> <%= person_form.text_field :name %> - <%= fields_for @person.contact_detail do |contact_details_form| %> - <%= contact_details_form.text_field :phone_number %> + <%= fields_for @person.contact_detail do |contact_detail_form| %> + <%= contact_detail_form.text_field :phone_number %> <% end %> <% end %> ``` @@ -290,7 +299,7 @@ You can create a similar binding without actually creating `` tags with th which produces the following output: ```html - +
@@ -306,7 +315,7 @@ The Article model is directly available to users of the application, so - follow resources :articles ``` -TIP: Declaring a resource has a number of side-affects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources. +TIP: Declaring a resource has a number of side effects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources. When dealing with RESTful resources, calls to `form_for` can get significantly easier if you rely on **record identification**. In short, you can just pass the model instance and have Rails figure out model name and the rest: @@ -367,7 +376,7 @@ output: ``` -When parsing POSTed data, Rails will take into account the special `_method` parameter and acts as if the HTTP method was the one specified inside it ("PATCH" in this example). +When parsing POSTed data, Rails will take into account the special `_method` parameter and act as if the HTTP method was the one specified inside it ("PATCH" in this example). Making Select Boxes with Ease ----------------------------- @@ -429,9 +438,7 @@ output: Whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option. -TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer `2` you cannot pass `"2"` to `options_for_select` - you must pass `2`. Be aware of values extracted from the `params` hash as they are all strings. - -WARNING: when `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true. +WARNING: When `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true. You can add arbitrary attributes to the options using hashes: @@ -506,6 +513,12 @@ As the name implies, this only generates option tags. To generate a working sele <%= collection_select(:person, :city_id, City.all, :id, :name) %> ``` +As with other helpers, if you were to use the `collection_select` helper on a form builder scoped to the `@person` object, the syntax would be: + +```erb +<%= f.collection_select(:city_id, City.all, :id, :name) %> +``` + To recap, `options_from_collection_for_select` is to `collection_select` what `options_for_select` is to `select`. NOTE: Pairs passed to `options_for_select` should have the name first and the id second, however with `options_from_collection_for_select` the first argument is the value method and the second the text method. @@ -518,7 +531,7 @@ To leverage time zone support in Rails, you have to ask your users what time zon <%= time_zone_select(:person, :time_zone) %> ``` -There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the API documentation to learn about the possible arguments for these two methods. +There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-time_zone_options_for_select) to learn about the possible arguments for these two methods. Rails _used_ to have a `country_select` helper for choosing countries, but this has been extracted to the [country_select plugin](https://github.com/stefanpenner/country_select). When using this, be aware that the exclusion or inclusion of certain names from the list can be somewhat controversial (and was the reason this functionality was extracted from Rails). @@ -534,7 +547,7 @@ Both of these families of helpers will create a series of select boxes for the d ### Barebones Helpers -The `select_*` family of helpers take as their first argument an instance of `Date`, `Time` or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example +The `select_*` family of helpers take as their first argument an instance of `Date`, `Time` or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example: ```erb <%= select_date Date.today, prefix: :start_date %> @@ -548,7 +561,7 @@ outputs (with actual option values omitted for brevity) ``` -The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time` or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example +The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time` or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example: ```ruby Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i) @@ -591,9 +604,9 @@ NOTE: In many cases the built-in date pickers are clumsy as they do not aid the ### Individual Components -Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component `select_year`, `select_month`, `select_day`, `select_hour`, `select_minute`, `select_second`. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example "year" for `select_year`, "month" for `select_month` etc.) although this can be overridden with the `:field_name` option. The `:prefix` option works in the same way that it does for `select_date` and `select_time` and has the same default value. +Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component `select_year`, `select_month`, `select_day`, `select_hour`, `select_minute`, `select_second`. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example, "year" for `select_year`, "month" for `select_month` etc.) although this can be overridden with the `:field_name` option. The `:prefix` option works in the same way that it does for `select_date` and `select_time` and has the same default value. -The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time` or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example +The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time` or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example: ```erb <%= select_year(2009) %> @@ -623,7 +636,7 @@ Rails provides the usual pair of helpers: the barebones `file_field_tag` and the ### What Gets Uploaded -The object in the `params` hash is an instance of a subclass of `IO`. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of `File` backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example). +The object in the `params` hash is an instance of a subclass of `IO`. Depending on the size of the uploaded file it may in fact be a `StringIO` or an instance of `File` backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example). ```ruby def upload @@ -640,12 +653,12 @@ NOTE: If the user has not selected a file the corresponding parameter will be an ### Dealing with Ajax -Unlike other forms making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission. +Unlike other forms, making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission. Customizing Form Builders ------------------------- -As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of `FormBuilder` (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass `FormBuilder` and add the helpers there. For example +As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of `FormBuilder` (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass `FormBuilder` and add the helpers there. For example: ```erb <%= form_for @person do |f| %> @@ -671,7 +684,14 @@ class LabellingFormBuilder < ActionView::Helpers::FormBuilder end ``` -If you reuse this frequently you could define a `labeled_form_for` helper that automatically applies the `builder: LabellingFormBuilder` option. +If you reuse this frequently you could define a `labeled_form_for` helper that automatically applies the `builder: LabellingFormBuilder` option: + +```ruby +def labeled_form_for(record, options = {}, &block) + options.merge! builder: LabellingFormBuilder + form_for record, options, &block +end +``` The form builder used also determines what happens when you do @@ -684,21 +704,14 @@ If `f` is an instance of `FormBuilder` then this will render the `form` partial, Understanding Parameter Naming Conventions ------------------------------------------ -As you've seen in the previous sections, values from forms can be at the top level of the `params` hash or nested in another hash. For example in a standard `create` +As you've seen in the previous sections, values from forms can be at the top level of the `params` hash or nested in another hash. For example, in a standard `create` action for a Person model, `params[:person]` would usually be a hash of all the attributes for the person to create. The `params` hash can also contain arrays, arrays of hashes and so on. Fundamentally HTML forms don't know about any sort of structured data, all they generate is name-value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses. -TIP: You may find you can try out examples in this section faster by using the console to directly invoke Racks' parameter parser. For example, - -```ruby -Rack::Utils.parse_query "name=fred&phone=0123456789" -# => {"name"=>"fred", "phone"=>"0123456789"} -``` - ### Basic Structures -The two basic structures are arrays and hashes. Hashes mirror the syntax used for accessing the value in `params`. For example if a form contains +The two basic structures are arrays and hashes. Hashes mirror the syntax used for accessing the value in `params`. For example, if a form contains: ```html @@ -706,13 +719,13 @@ The two basic structures are arrays and hashes. Hashes mirror the syntax used fo the `params` hash will contain -```erb +```ruby {'person' => {'name' => 'Henry'}} ``` and `params[:person][:name]` will retrieve the submitted value in the controller. -Hashes can be nested as many levels as required, for example +Hashes can be nested as many levels as required, for example: ```html @@ -724,7 +737,7 @@ will result in the `params` hash being {'person' => {'address' => {'city' => 'New York'}}} ``` -Normally Rails ignores duplicate parameter names. If the parameter name contains an empty set of square brackets [] then they will be accumulated in an array. If you wanted people to be able to input multiple phone numbers, you could place this in the form: +Normally Rails ignores duplicate parameter names. If the parameter name contains an empty set of square brackets `[]` then they will be accumulated in an array. If you wanted users to be able to input multiple phone numbers, you could place this in the form: ```html @@ -732,11 +745,11 @@ Normally Rails ignores duplicate parameter names. If the parameter name contains ``` -This would result in `params[:person][:phone_number]` being an array. +This would result in `params[:person][:phone_number]` being an array containing the inputted phone numbers. ### Combining Them -We can mix and match these two concepts. For example, one element of a hash might be an array as in the previous example, or you can have an array of hashes. For example a form might let you create any number of addresses by repeating the following form fragment +We can mix and match these two concepts. One element of a hash might be an array as in the previous example, or you can have an array of hashes. For example, a form might let you create any number of addresses by repeating the following form fragment ```html @@ -746,7 +759,7 @@ We can mix and match these two concepts. For example, one element of a hash migh This would result in `params[:addresses]` being an array of hashes with keys `line1`, `line2` and `city`. Rails decides to start accumulating values in a new hash whenever it encounters an input name that already exists in the current hash. -There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can be usually replaced by hashes, for example instead of having an array of model objects one can have a hash of model objects keyed by their id, an array index or some other parameter. +There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can usually be replaced by hashes; for example, instead of having an array of model objects, one can have a hash of model objects keyed by their id, an array index or some other parameter. WARNING: Array parameters do not play well with the `check_box` helper. According to the HTML specification unchecked checkboxes submit no value. However it is often convenient for a checkbox to always submit a value. The `check_box` helper fakes this by creating an auxiliary hidden input with the same name. If the checkbox is unchecked only the hidden input is submitted and if it is checked then both are submitted but the value submitted by the checkbox takes precedence. When working with array parameters this duplicate submission will confuse Rails since duplicate input names are how it decides when to start a new array element. It is preferable to either use `check_box_tag` or to use hashes instead of arrays. @@ -856,19 +869,19 @@ Or if you don't want to render an `authenticity_token` field: Building Complex Forms ---------------------- -Many apps grow beyond simple forms editing a single object. For example when creating a `Person` you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary. +Many apps grow beyond simple forms editing a single object. For example, when creating a `Person` you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary. ### Configuring the Model Active Record provides model level support via the `accepts_nested_attributes_for` method: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses end -class Address < ActiveRecord::Base +class Address < ApplicationRecord belongs_to :person end ``` @@ -908,7 +921,7 @@ end ``` The `fields_for` yields a form builder. The parameters' name will be what -`accepts_nested_attributes_for` expects. For example when creating a user with +`accepts_nested_attributes_for` expects. For example, when creating a user with 2 addresses, the submitted parameters would look like: ```ruby @@ -956,7 +969,7 @@ private You can allow users to delete associated objects by passing `allow_destroy: true` to `accepts_nested_attributes_for` ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses, allow_destroy: true end @@ -997,7 +1010,7 @@ end It is often useful to ignore sets of fields that the user has not filled in. You can control this by passing a `:reject_if` proc to `accepts_nested_attributes_for`. This proc will be called with each hash of attributes submitted by the form. If the proc returns `false` then Active Record will not build an associated object for that hash. The example below only tries to build an address if the `kind` attribute is set. ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?} end diff --git a/source/generators.md b/source/generators.md index 5e88fa0..d0b6cef 100644 --- a/source/generators.md +++ b/source/generators.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Creating and Customizing Rails Generators & Templates ===================================================== @@ -8,6 +10,7 @@ After reading this guide, you will know: * How to see which generators are available in your application. * How to create a generator using templates. * How Rails searches for generators before invoking them. +* How Rails internally generates Rails code from the templates. * How to customize your scaffold by creating new generators. * How to customize your scaffold by changing generator templates. * How to use fallbacks to avoid overwriting a huge set of generators. @@ -196,16 +199,24 @@ $ bin/rails generate scaffold User name:string create app/views/users/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/users.js.coffee + create app/assets/javascripts/users.coffee invoke scss - create app/assets/stylesheets/users.css.scss + create app/assets/stylesheets/users.scss invoke scss - create app/assets/stylesheets/scaffolds.css.scss + create app/assets/stylesheets/scaffolds.scss ``` Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. -Our first customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds. We can achieve that by changing our configuration to the following: +If we want to avoid generating the default `app/assets/stylesheets/scaffolds.scss` file when scaffolding a new resource we can disable `scaffold_stylesheet`: + +```ruby + config.generators do |g| + g.scaffold_stylesheet false + end +``` + +The next customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following: ```ruby config.generators do |g| @@ -340,6 +351,22 @@ end If you generate another resource, you can see that we get exactly the same result! This is useful if you want to customize your scaffold templates and/or layout by just creating `edit.html.erb`, `index.html.erb` and so on inside `lib/templates/erb/scaffold`. +Scaffold templates in Rails frequently use ERB tags; these tags need to be +escaped so that the generated output is valid ERB code. + +For example, the following escaped ERB tag would be needed in the template +(note the extra `%`)... + +```ruby +<%%= stylesheet_include_tag :application %> +``` + +...to generate the following output: + +```ruby +<%= stylesheet_include_tag :application %> +``` + Adding Generators Fallbacks --------------------------- @@ -390,7 +417,7 @@ $ bin/rails generate scaffold Comment body:text create app/views/comments/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/comments.js.coffee + create app/assets/javascripts/comments.coffee invoke scss ``` @@ -432,6 +459,26 @@ $ rails new thud -m https://gist.github.com/radar/722911/raw/ Whilst the final section of this guide doesn't cover how to generate the most awesome template known to man, it will take you through the methods available at your disposal so that you can develop it yourself. These same methods are also available for generators. +Adding Command Line Arguments +----------------------------- +Rails generators can be easily modified to accept custom command line arguments. This functionality comes from [Thor](http://www.rubydoc.info/github/erikhuda/thor/master/Thor/Base/ClassMethods#class_option-instance_method): + +``` +class_option :scope, type: :string, default: 'read_products' +``` + +Now our generator can be invoked as follows: + +```bash +rails generate initializer --scope write_products +``` + +The command line arguments are accessed through the `options` method inside the generator class. e.g: + +```ruby +@scope = options['scope'] +``` + Generator methods ----------------- @@ -484,6 +531,14 @@ Adds a specified source to `Gemfile`: add_source "http://gems.github.com/" ``` +This method also takes a block: + +```ruby +add_source "http://gems.github.com/" do + gem "rspec-rails" +end +``` + ### `inject_into_file` Injects a block of code into a defined position in your file. diff --git a/source/getting_started.md b/source/getting_started.md index 1f91352..0681148 100644 --- a/source/getting_started.md +++ b/source/getting_started.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Getting Started with Rails ========================== @@ -21,10 +23,13 @@ application from scratch. It does not assume that you have any prior experience with Rails. However, to get the most out of it, you need to have some prerequisites installed: -* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer. -* The [RubyGems](http://rubygems.org) packaging system, which is installed with Ruby - versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org). -* A working installation of the [SQLite3 Database](http://www.sqlite.org). +* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer. +* Right version of [Development Kit](http://rubyinstaller.org/downloads/), if you + are using Windows. +* The [RubyGems](https://rubygems.org) packaging system, which is installed with + Ruby by default. To learn more about RubyGems, please read the + [RubyGems Guides](http://guides.rubygems.org). +* A working installation of the [SQLite3 Database](https://www.sqlite.org). Rails is a web application framework running on the Ruby programming language. If you have no prior experience with Ruby, you will find a very steep learning @@ -32,7 +37,7 @@ curve diving straight into Rails. There are several curated lists of online reso for learning Ruby: * [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/) -* [reSRC's List of Free Programming Books](http://resrc.io/list/10/list-of-free-programming-books/#ruby) +* [List of Free Programming Books](https://github.com/vhf/free-programming-books/blob/master/free-programming-books.md#ruby) Be aware that some resources, while still excellent, cover versions of Ruby as old as 1.6, and commonly 1.8, and will not include some syntax that you will see in day-to-day @@ -48,7 +53,7 @@ code while accomplishing more than many other languages and frameworks. Experienced Rails developers also report that it makes web application development more fun. -Rails is opinionated software. It makes the assumption that there is the "best" +Rails is opinionated software. It makes the assumption that there is a "best" way to do things, and it's designed to encourage that way - and in some cases to discourage alternatives. If you learn "The Rails Way" you'll probably discover a tremendous increase in productivity. If you persist in bringing old habits from @@ -63,14 +68,13 @@ The Rails philosophy includes two major guiding principles: again, our code is more maintainable, more extensible, and less buggy. * **Convention Over Configuration:** Rails has opinions about the best way to do many things in a web application, and defaults to this set of conventions, rather than - require that you specify every minutiae through endless configuration files. + require that you specify minutiae through endless configuration files. Creating a New Rails Project ---------------------------- - -The best way to use this guide is to follow each step as it happens, no code or -step needed to make this example application has been left out, so you can -literally follow along step by step. +The best way to read this guide is to follow it step by step. All steps are +essential to run this example application and no additional code or steps are +needed. By following along with this guide, you'll create a Rails project called `blog`, a (very) simple weblog. Before you can start building the application, @@ -82,26 +86,26 @@ your prompt will look something like `c:\source_code>` ### Installing Rails -Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose +Open up a command line prompt. On macOS open Terminal.app, on Windows choose "Run" from your Start menu and type 'cmd.exe'. Any commands prefaced with a dollar sign `$` should be run in the command line. Verify that you have a current version of Ruby installed: -TIP: A number of tools exist to help you quickly install Ruby and Ruby -on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), -while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp). - ```bash $ ruby -v -ruby 2.0.0p353 +ruby 2.3.1p112 ``` -If you don't have Ruby installed have a look at -[ruby-lang.org](https://www.ruby-lang.org/en/installation/) for possible ways to -install Ruby on your platform. - -Many popular UNIX-like OSes ship with an acceptable version of SQLite3. Windows -users and others can find installation instructions at [the SQLite3 website](http://www.sqlite.org). +TIP: A number of tools exist to help you quickly install Ruby and Ruby +on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), +while macOS users can use [Tokaido](https://github.com/tokaido/tokaidoapp). +For more installation methods for most Operating Systems take a look at +[ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/). + +Many popular UNIX-like OSes ship with an acceptable version of SQLite3. +On Windows, if you installed Rails through Rails Installer, you +already have SQLite installed. Others can find installation instructions +at the [SQLite3 website](https://www.sqlite.org). Verify that it is correctly installed and in your PATH: ```bash @@ -123,7 +127,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 4.2.0", you are ready to continue. +If it says something like "Rails 5.1.0", you are ready to continue. ### Creating the Blog Application @@ -144,6 +148,10 @@ This will create a Rails application called Blog in a `blog` directory and install the gem dependencies that are already mentioned in `Gemfile` using `bundle install`. +NOTE: If you're using Windows Subsystem for Linux then there are currently some +limitations on file system notifications that mean you should disable the `spring` +and `listen` gems which you can do by running `rails new blog --skip-spring --skip-listen`. + TIP: You can see all of the command line options that the Rails application builder accepts by running `rails new -h`. @@ -160,20 +168,21 @@ of the files and folders that Rails created by default: | File/Folder | Purpose | | ----------- | ------- | -|app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.| -|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, deploy or run your application.| +|app/|Contains the controllers, models, views, helpers, mailers, channels, jobs and assets for your application. You'll focus on this folder for the remainder of this guide.| +|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy or run your application.| |config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).| |config.ru|Rack configuration for Rack based servers used to start the application.| |db/|Contains your current database schema, as well as the database migrations.| -|Gemfile
Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://bundler.io).| +|Gemfile
Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see the [Bundler website](http://bundler.io).| |lib/|Extended modules for your application.| |log/|Application log files.| |public/|The only folder seen by the world as-is. Contains static files and compiled assets.| |Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.| -|README.rdoc|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.| +|README.md|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.| |test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).| -|tmp/|Temporary files (like cache, pid, and session files).| +|tmp/|Temporary files (like cache and pid files).| |vendor/|A place for all third-party code. In a typical Rails application this includes vendored gems.| +|.gitignore|This file tells git which files (or patterns) it should ignore. See [Github - Ignoring files](https://help.github.com/articles/ignoring-files) for more info about ignoring files. Hello, Rails! ------------- @@ -191,17 +200,20 @@ following in the `blog` directory: $ bin/rails server ``` +TIP: If you are using Windows, you have to pass the scripts under the `bin` +folder directly to the Ruby interpreter e.g. `ruby bin\rails server`. + TIP: Compiling CoffeeScript and JavaScript asset compression requires you have a JavaScript runtime available on your system, in the absence of a runtime you will see an `execjs` error during asset compilation. -Usually Mac OS X and Windows come with a JavaScript runtime installed. -Rails adds the `therubyracer` gem to the generated `Gemfile` in a -commented line for new apps and you can uncomment if you need it. +Usually macOS and Windows come with a JavaScript runtime installed. +Rails adds the `therubyracer` gem to the generated `Gemfile` in a +commented line for new apps and you can uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby users and is added by default to the `Gemfile` in apps generated under JRuby. You can investigate -about all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme). +all the supported runtimes at [ExecJS](https://github.com/rails/execjs#readme). -This will fire up WEBrick, a web server distributed with Ruby by default. To see +This will fire up Puma, a web server distributed with Rails by default. To see your application in action, open a browser window and navigate to . You should see the Rails default information page: @@ -209,15 +221,14 @@ your application in action, open a browser window and navigate to TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. To verify the server has stopped you should see your command prompt -cursor again. For most UNIX-like systems including Mac OS X this will be a +cursor again. For most UNIX-like systems including macOS this will be a dollar sign `$`. In development mode, Rails does not generally require you to restart the server; changes you make in files will be automatically picked up by the server. The "Welcome aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a -page. You can also click on the _About your application's environment_ link to -see a summary of your application's environment. +page. ### Say "Hello", Rails @@ -238,11 +249,11 @@ Ruby) which is processed by the request cycle in Rails before being sent to the user. To create a new controller, you will need to run the "controller" generator and -tell it you want a controller called "welcome" with an action called "index", +tell it you want a controller called "Welcome" with an action called "index", just like this: ```bash -$ bin/rails generate controller welcome index +$ bin/rails generate controller Welcome index ``` Rails will create several files and a route for you. @@ -257,11 +268,12 @@ invoke test_unit create test/controllers/welcome_controller_test.rb invoke helper create app/helpers/welcome_helper.rb +invoke test_unit invoke assets invoke coffee -create app/assets/javascripts/welcome.js.coffee +create app/assets/javascripts/welcome.coffee invoke scss -create app/assets/stylesheets/welcome.css.scss +create app/assets/stylesheets/welcome.scss ``` Most important of these are of course the controller, located at @@ -291,33 +303,32 @@ Open the file `config/routes.rb` in your editor. Rails.application.routes.draw do get 'welcome/index' - # The priority is based upon order of creation: - # first created -> highest priority. - # - # You can have the root of your site routed with "root" - # root 'welcome#index' - # - # ... + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html +end ``` -This is your application's _routing file_ which holds entries in a special DSL -(domain-specific language) that tells Rails how to connect incoming requests to -controllers and actions. This file contains many sample routes on commented -lines, and one of them actually shows you how to connect the root of your site -to a specific controller and action. Find the line beginning with `root` and -uncomment it. It should look something like the following: +This is your application's _routing file_ which holds entries in a special +[DSL (domain-specific language)](http://en.wikipedia.org/wiki/Domain-specific_language) +that tells Rails how to connect incoming requests to +controllers and actions. +Edit this file by adding the line of code `root 'welcome#index'`. +It should look something like the following: ```ruby -root 'welcome#index' +Rails.application.routes.draw do + get 'welcome/index' + + root 'welcome#index' +end ``` `root 'welcome#index'` tells Rails to map requests to the root of the application to the welcome controller's index action and `get 'welcome/index'` tells Rails to map requests to to the welcome controller's index action. This was created earlier when you ran the -controller generator (`rails generate controller welcome index`). +controller generator (`bin/rails generate controller Welcome index`). -Launch the web server again if you stopped it to generate the controller (`rails +Launch the web server again if you stopped it to generate the controller (`bin/rails server`) and navigate to in your browser. You'll see the "Hello, Rails!" message you put into `app/views/welcome/index.html.erb`, indicating that this new route is indeed going to `WelcomeController`'s `index` @@ -338,11 +349,12 @@ You can create, read, update and destroy items for a resource and these operations are referred to as _CRUD_ operations. Rails provides a `resources` method which can be used to declare a standard REST -resource. Here's what `config/routes.rb` should look like after the -_article resource_ is declared. +resource. You need to add the _article resource_ to the +`config/routes.rb` so the file will look as follows: ```ruby Rails.application.routes.draw do + get 'welcome/index' resources :articles @@ -350,13 +362,13 @@ Rails.application.routes.draw do end ``` -If you run `rake routes`, you'll see that it has defined routes for all the +If you run `bin/rails routes`, you'll see that it has defined routes for all the standard RESTful actions. The meaning of the prefix column (and other columns) will be seen later, but for now notice that Rails has inferred the singular form `article` and makes meaningful use of the distinction. ```bash -$ bin/rake routes +$ bin/rails routes Prefix Verb URI Pattern Controller#Action articles GET /articles(.:format) articles#index POST /articles(.:format) articles#create @@ -371,14 +383,14 @@ edit_article GET /articles/:id/edit(.:format) articles#edit In the next section, you will add the ability to create new articles in your application and be able to view them. This is the "C" and the "R" from CRUD: -creation and reading. The form for doing this will look like this: +create and read. The form for doing this will look like this: ![The new article form](images/getting_started/new_article.png) It will look a little basic for now, but that's ok. We'll look at improving the styling for it afterwards. -### Laying down the ground work +### Laying down the groundwork Firstly, you need a place within the application to create a new article. A great place for that would be at `/articles/new`. With the route already @@ -394,7 +406,7 @@ a controller called `ArticlesController`. You can do this by running this command: ```bash -$ bin/rails g controller articles +$ bin/rails generate controller Articles ``` If you open up the newly generated `app/controllers/articles_controller.rb` @@ -422,12 +434,12 @@ If you refresh now, you'll get a new error: This error indicates that Rails cannot find the `new` action inside the `ArticlesController` that you just generated. This is because when controllers are generated in Rails they are empty by default, unless you tell it -your wanted actions during the generation process. +your desired actions during the generation process. To manually define an action inside a controller, all you need to do is to define a new method inside the controller. Open `app/controllers/articles_controller.rb` and inside the `ArticlesController` -class, define a `new` method so that the controller now looks like this: +class, define the `new` method so that your controller now looks like this: ```ruby class ArticlesController < ApplicationController @@ -444,43 +456,42 @@ With the `new` method defined in `ArticlesController`, if you refresh You're getting this error now because Rails expects plain actions like this one to have views associated with them to display their information. With no view -available, Rails errors out. +available, Rails will raise an exception. In the above image, the bottom line has been truncated. Let's see what the full -thing looks like: +error message looks like: ->Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" +>ArticlesController#new is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot. That's quite a lot of text! Let's quickly go through and understand what each -part of it does. +part of it means. -The first part identifies what template is missing. In this case, it's the +The first part identifies which template is missing. In this case, it's the `articles/new` template. Rails will first look for this template. If not found, then it will attempt to load a template called `application/new`. It looks for one here because the `ArticlesController` inherits from `ApplicationController`. -The next part of the message contains a hash. The `:locale` key in this hash -simply indicates what spoken language template should be retrieved. By default, -this is the English - or "en" - template. The next key, `:formats` specifies the -format of template to be served in response. The default format is `:html`, and -so Rails is looking for an HTML template. The final key, `:handlers`, is telling -us what _template handlers_ could be used to render our template. `:erb` is most -commonly used for HTML templates, `:builder` is used for XML templates, and -`:coffee` uses CoffeeScript to build JavaScript templates. - -The final part of this message tells us where Rails has looked for the templates. -Templates within a basic Rails application like this are kept in a single -location, but in more complex applications it could be many different paths. +The next part of the message contains `request.formats` which specifies +the format of template to be served in response. It is set to `text/html` as we +requested this page via browser, so Rails is looking for an HTML template. +`request.variant` specifies what kind of physical devices would be served by +the response and helps Rails determine which template to use in the response. +It is empty because no information has been provided. The simplest template that would work in this case would be one located at -`app/views/articles/new.html.erb`. The extension of this file name is key: the -first extension is the _format_ of the template, and the second extension is the -_handler_ that will be used. Rails is attempting to find a template called -`articles/new` within `app/views` for the application. The format for this -template can only be `html` and the handler must be one of `erb`, `builder` or -`coffee`. Because you want to create a new HTML form, you will be using the `ERB` -language. Therefore the file should be called `articles/new.html.erb` and needs -to be located inside the `app/views` directory of the application. +`app/views/articles/new.html.erb`. The extension of this file name is important: +the first extension is the _format_ of the template, and the second extension +is the _handler_ that will be used to render the template. Rails is attempting +to find a template called `articles/new` within `app/views` for the +application. The format for this template can only be `html` and the default +handler for HTML is `erb`. Rails uses other handlers for other formats. +`builder` handler is used to build XML templates and `coffee` handler uses +CoffeeScript to build JavaScript templates. Since you want to create a new +HTML form, you will be using the `ERB` language which is designed to embed Ruby +in HTML. + +Therefore the file should be called `articles/new.html.erb` and needs to be +located inside the `app/views` directory of the application. Go ahead now and create a new file at `app/views/articles/new.html.erb` and write this content in it: @@ -518,7 +529,7 @@ method called `form_for`. To use this method, add this code into <% end %> ``` -If you refresh the page now, you'll see the exact same form as in the example. +If you refresh the page now, you'll see the exact same form from our example above. Building forms in Rails is really just that easy! When you call `form_for`, you pass it an identifying object for this @@ -548,10 +559,10 @@ this: In this example, the `articles_path` helper is passed to the `:url` option. To see what Rails will do with this, we look back at the output of -`rake routes`: +`bin/rails routes`: ```bash -$ bin/rake routes +$ bin/rails routes Prefix Verb URI Pattern Controller#Action articles GET /articles(.:format) articles#index POST /articles(.:format) articles#create @@ -596,9 +607,11 @@ class ArticlesController < ApplicationController end ``` -If you re-submit the form now, you'll see another familiar error: a template is -missing. That's ok, we can ignore that for now. What the `create` action should -be doing is saving our new article to the database. +If you re-submit the form now, you may not see any change on the page. Don't worry! +This is because Rails by default returns `204 No Content` response for an action if +we don't specify what the response should be. We just added the `create` action +but didn't specify anything about how the response should be. In this case, the +`create` action should save our new article to the database. When a form is submitted, the fields of the form are sent to Rails as _parameters_. These parameters can then be referenced inside the controller @@ -611,20 +624,19 @@ def create end ``` -The `render` method here is taking a very simple hash with a key of `plain` and +The `render` method here is taking a very simple hash with a key of `:plain` and value of `params[:article].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` -method returns an `ActiveSupport::HashWithIndifferentAccess` object, which +method returns an `ActionController::Parameters` object, which allows you to access the keys of the hash using either strings or symbols. In this situation, the only parameters that matter are the ones from the form. TIP: Ensure you have a firm grasp of the `params` method, as you'll use it fairly regularly. Let's consider an example URL: **http://www.example.com/?username=dhh&email=dhh@email.com**. In this URL, `params[:username]` would equal "dhh" and `params[:email]` would equal "dhh@email.com". -If you re-submit the form one more time you'll now no longer get the missing -template error. Instead, you'll see something that looks like the following: +If you re-submit the form one more time, you'll see something that looks like the following: ```ruby -{"title"=>"First article!", "text"=>"This is my first article."} +"First Article!", "text"=>"This is my first article."} permitted: false> ``` This action is now displaying the parameters for the article that are coming in @@ -642,7 +654,7 @@ run this command in your terminal: $ bin/rails generate model Article title:string text:text ``` -With that command we told Rails that we want a `Article` model, together +With that command we told Rails that we want an `Article` model, together with a _title_ attribute of type string, and a _text_ attribute of type text. Those attributes are automatically added to the `articles` table in the database and mapped to the `Article` model. @@ -658,18 +670,18 @@ models, as that will be done automatically by Active Record. ### Running a Migration -As we've just seen, `rails generate model` created a _database migration_ file +As we've just seen, `bin/rails generate model` created a _database migration_ file inside the `db/migrate` directory. Migrations are Ruby classes that are designed to make it simple to create and modify database tables. Rails uses rake commands to run migrations, and it's possible to undo a migration after it's been applied to your database. Migration filenames include a timestamp to ensure that they're processed in the order that they were created. -If you look in the `db/migrate/20140120191729_create_articles.rb` file (remember, -yours will have a slightly different name), here's what you'll find: +If you look in the `db/migrate/YYYYMMDDHHMMSS_create_articles.rb` file +(remember, yours will have a slightly different name), here's what you'll find: ```ruby -class CreateArticles < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration[5.0] def change create_table :articles do |t| t.string :title @@ -688,13 +700,13 @@ in case you want to reverse it later. When you run this migration it will create an `articles` table with one string column and a text column. It also creates two timestamp fields to allow Rails to track article creation and update times. -TIP: For more information about migrations, refer to [Rails Database Migrations] -(migrations.html). +TIP: For more information about migrations, refer to [Active Record Migrations] +(active_record_migrations.html). -At this point, you can use a rake command to run the migration: +At this point, you can use a bin/rails command to run the migration: ```bash -$ bin/rake db:migrate +$ bin/rails db:migrate ``` Rails will execute this migration command and tell you it created the Articles @@ -711,7 +723,7 @@ NOTE. Because you're working in the development environment by default, this command will apply to the database defined in the `development` section of your `config/database.yml` file. If you would like to execute migrations in another environment, for instance in production, you must explicitly pass it when -invoking the command: `rake db:migrate RAILS_ENV=production`. +invoking the command: `bin/rails db:migrate RAILS_ENV=production`. ### Saving data in the controller @@ -736,7 +748,7 @@ database columns. In the first line we do just that (remember that `@article.save` is responsible for saving the model in the database. Finally, we redirect the user to the `show` action, which we'll define later. -TIP: You might be wondering why the `A` in `Article.new` is capitalized above, whereas most other references to articles in this guide have used lowercase. In this context, we are referring to the class named `Article` that is defined in `\models\article.rb`. Class names in Ruby must begin with a capital letter. +TIP: You might be wondering why the `A` in `Article.new` is capitalized above, whereas most other references to articles in this guide have used lowercase. In this context, we are referring to the class named `Article` that is defined in `app/models/article.rb`. Class names in Ruby must begin with a capital letter. TIP: As we'll see later, `@article.save` returns a boolean indicating whether the article was saved or not. @@ -748,7 +760,7 @@ to create an article. Try it! You should get an error that looks like this: (images/getting_started/forbidden_attributes_for_new_article.png) Rails has several security features that help you write secure applications, -and you're running into one of them now. This one is called [strong parameters](http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters), +and you're running into one of them now. This one is called [strong parameters](action_controller_overview.html#strong-parameters), which requires us to tell Rails exactly which parameters are allowed into our controller actions. @@ -756,7 +768,7 @@ Why do you have to bother? The ability to grab and automatically assign all controller parameters to your model in one shot makes the programmer's job easier, but this convenience also allows malicious use. What if a request to the server was crafted to look like a new article form submit but also included -extra fields with values that violated your applications integrity? They would +extra fields with values that violated your application's integrity? They would be 'mass assigned' into your model and then into the database along with the good stuff - potentially breaking your application or worse. @@ -798,7 +810,7 @@ If you submit the form again now, Rails will complain about not finding the `show` action. That's not very useful though, so let's add the `show` action before proceeding. -As we have seen in the output of `rake routes`, the route for `show` action is +As we have seen in the output of `bin/rails routes`, the route for `show` action is as follows: ``` @@ -815,7 +827,7 @@ NOTE: A frequent practice is to place the standard CRUD actions in each controller in the following order: `index`, `show`, `new`, `edit`, `create`, `update` and `destroy`. You may use any order you choose, but keep in mind that these are public methods; as mentioned earlier in this guide, they must be placed -before any private or protected method in the controller in order to work. +before declaring `private` visibility in the controller. Given that, let's add the `show` action, as follows: @@ -828,12 +840,12 @@ class ArticlesController < ApplicationController def new end - # snipped for brevity + # snippet for brevity ``` A couple of things to note. We use `Article.find` to find the article we're interested in, passing in `params[:id]` to get the `:id` parameter from the -request. We also use an instance variable (prefixed by `@`) to hold a +request. We also use an instance variable (prefixed with `@`) to hold a reference to the article object. We do this because Rails will pass all instance variables to the view. @@ -860,7 +872,7 @@ Visit and give it a try! ### Listing all articles We still need a way to list all our articles, so let's do that. -The route for this as per output of `rake routes` is: +The route for this as per output of `bin/rails routes` is: ``` articles GET /articles(.:format) articles#index @@ -884,7 +896,7 @@ class ArticlesController < ApplicationController def new end - # snipped for brevity + # snippet for brevity ``` And then finally, add the view for this action, located at @@ -903,6 +915,7 @@ And then finally, add the view for this action, located at <%= article.title %> <%= article.text %> + <%= link_to 'Show', article_path(article) %> <% end %> @@ -979,21 +992,22 @@ and restart the web server when a change is made. The model file, `app/models/article.rb` is about as simple as it can get: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord end ``` There isn't much to this file - but note that the `Article` class inherits from -`ActiveRecord::Base`. Active Record supplies a great deal of functionality to -your Rails models for free, including basic database CRUD (Create, Read, Update, -Destroy) operations, data validation, as well as sophisticated search support -and the ability to relate multiple models to one another. +`ApplicationRecord`. `ApplicationRecord` inherits from `ActiveRecord::Base` +which supplies a great deal of functionality to your Rails models for free, +including basic database CRUD (Create, Read, Update, Destroy) operations, data +validation, as well as sophisticated search support and the ability to relate +multiple models to one another. Rails includes methods to help you validate the data that you send to models. Open the `app/models/article.rb` file and edit it: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord validates :title, presence: true, length: { minimum: 5 } end @@ -1141,9 +1155,9 @@ new articles. Create a file called `app/views/articles/edit.html.erb` and make it look as follows: ```html+erb -

Editing article

+

Edit article

-<%= form_for :article, url: article_path(@article), method: :patch do |f| %> +<%= form_for(@article) do |f| %> <% if @article.errors.any? %>
@@ -1181,14 +1195,15 @@ it look as follows: This time we point the form to the `update` action, which is not defined yet but will be very soon. -The `method: :patch` option tells Rails that we want this form to be submitted +Passing the article object to the method, will automagically create url for submitting the edited article form. +This option tells Rails that we want this form to be submitted via the `PATCH` HTTP method which is the HTTP method you're expected to use to **update** resources according to the REST protocol. The first parameter of `form_for` can be an object, say, `@article` which would cause the helper to fill in the form with the fields of the object. Passing in a symbol (`:article`) with the same name as the instance variable (`@article`) -also automagically leads to the same behavior. This is what is happening here. +also automagically leads to the same behavior. More details can be found in [form_for documentation] (http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for). @@ -1231,10 +1246,9 @@ article we want to show the form back to the user. We reuse the `article_params` method that we defined earlier for the create action. -TIP: You don't need to pass all attributes to `update`. For -example, if you'd call `@article.update(title: 'A new title')` -Rails would only update the `title` attribute, leaving all other -attributes untouched. +TIP: It is not necessary to pass all the attributes to `update`. For example, +if `@article.update(title: 'A new title')` was called, Rails would only update +the `title` attribute, leaving all other attributes untouched. Finally, we want to show a link to the `edit` action in the list of all the articles, so let's add that now to `app/views/articles/index.html.erb` to make @@ -1266,8 +1280,8 @@ bottom of the template: ```html+erb ... -<%= link_to 'Back', articles_path %> | -<%= link_to 'Edit', edit_article_path(@article) %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` And here's how our app looks so far: @@ -1279,7 +1293,7 @@ And here's how our app looks so far: Our `edit` page looks very similar to the `new` page; in fact, they both share the same code for displaying the form. Let's remove this duplication by using a view partial. By convention, partial files are -prefixed by an underscore. +prefixed with an underscore. TIP: You can read more about partials in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide. @@ -1354,7 +1368,7 @@ Then do the same for the `app/views/articles/edit.html.erb` view: We're now ready to cover the "D" part of CRUD, deleting articles from the database. Following the REST convention, the route for -deleting articles as per output of `rake routes` is: +deleting articles as per output of `bin/rails routes` is: ```ruby DELETE /articles/:id(.:format) articles#destroy @@ -1470,16 +1484,20 @@ Finally, add a 'Destroy' link to your `index` action template ``` Here we're using `link_to` in a different way. We pass the named route as the -second argument, and then the options as another argument. The `:method` and -`:'data-confirm'` options are used as HTML5 attributes so that when the link is -clicked, Rails will first show a confirm dialog to the user, and then submit the -link with method `delete`. This is done via the JavaScript file `jquery_ujs` -which is automatically included into your application's layout -(`app/views/layouts/application.html.erb`) when you generated the application. -Without this file, the confirmation dialog box wouldn't appear. +second argument, and then the options as another argument. The `method: :delete` +and `data: { confirm: 'Are you sure?' }` options are used as HTML5 attributes so +that when the link is clicked, Rails will first show a confirm dialog to the +user, and then submit the link with method `delete`. This is done via the +JavaScript file `jquery_ujs` which is automatically included in your +application's layout (`app/views/layouts/application.html.erb`) when you +generated the application. Without this file, the confirmation dialog box won't +appear. ![Confirm Dialog](images/getting_started/confirm_dialog.png) +TIP: Learn more about jQuery Unobtrusive Adapter (jQuery UJS) on +[Working With JavaScript in Rails](working_with_javascript_in_rails.html) guide. + Congratulations, you can now create, show, list, update and destroy articles. @@ -1497,7 +1515,7 @@ comments on articles. We're going to see the same generator that we used before when creating the `Article` model. This time we'll create a `Comment` model to hold -reference of article comments. Run this command in your terminal: +reference to an article. Run this command in your terminal: ```bash $ bin/rails generate model Comment commenter:string body:text article:references @@ -1509,13 +1527,13 @@ This command will generate four files: | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | db/migrate/20140120201010_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) | | app/models/comment.rb | The Comment model | -| test/models/comment_test.rb | Testing harness for the comments model | +| test/models/comment_test.rb | Testing harness for the comment model | | test/fixtures/comments.yml | Sample comments for use in testing | First, take a look at `app/models/comment.rb`: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :article end ``` @@ -1524,18 +1542,21 @@ This is very similar to the `Article` model that you saw earlier. The difference is the line `belongs_to :article`, which sets up an Active Record _association_. You'll learn a little about associations in the next section of this guide. +The (`:references`) keyword used in the bash command is a special data type for models. +It creates a new column on your database table with the provided model name appended with an `_id` +that can hold integer values. You can get a better understanding after analyzing the +`db/schema.rb` file below. + In addition to the model, Rails has also made a migration to create the corresponding database table: ```ruby -class CreateComments < ActiveRecord::Migration +class CreateComments < ActiveRecord::Migration[5.0] def change create_table :comments do |t| t.string :commenter t.text :body - - # this line adds an integer column called `article_id`. - t.references :article, index: true + t.references :article, foreign_key: true t.timestamps end @@ -1543,12 +1564,12 @@ class CreateComments < ActiveRecord::Migration end ``` -The `t.references` line sets up a foreign key column for the association between -the two models. An index for this association is also created on this column. -Go ahead and run the migration: +The `t.references` line creates an integer column called `article_id`, an index +for it, and a foreign key constraint that points to the `id` column of the `articles` +table. Go ahead and run the migration: ```bash -$ bin/rake db:migrate +$ bin/rails db:migrate ``` Rails is smart enough to only execute the migrations that have not already been @@ -1575,7 +1596,7 @@ association. You've already seen the line of code inside the `Comment` model (app/models/comment.rb) that makes each comment belong to an Article: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :article end ``` @@ -1584,7 +1605,7 @@ You'll need to edit `app/models/article.rb` to add the other side of the association: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments validates :title, presence: true, length: { minimum: 5 } @@ -1627,7 +1648,7 @@ controller. Again, we'll use the same generator we used before: $ bin/rails generate controller Comments ``` -This creates six files and one empty directory: +This creates five files and one empty directory: | File/Directory | Purpose | | -------------------------------------------- | ---------------------------------------- | @@ -1635,8 +1656,8 @@ This creates six files and one empty directory: | app/views/comments/ | Views of the controller are stored here | | test/controllers/comments_controller_test.rb | The test for the controller | | app/helpers/comments_helper.rb | A view helper file | -| app/assets/javascripts/comment.js.coffee | CoffeeScript for the controller | -| app/assets/stylesheets/comment.css.scss | Cascading style sheet for the controller | +| app/assets/javascripts/comments.coffee | CoffeeScript for the controller | +| app/assets/stylesheets/comments.scss | Cascading style sheet for the controller | Like with any blog, our readers will create their comments directly after reading the article, and once they have added their comment, will be sent back @@ -1673,8 +1694,8 @@ So first, we'll wire up the Article show template

<% end %> -<%= link_to 'Back', articles_path %> | -<%= link_to 'Edit', edit_article_path(@article) %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` This adds a form on the `Article` show page that creates a new comment by @@ -1754,8 +1775,8 @@ add that to the `app/views/articles/show.html.erb`.

<% end %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` Now you can add articles and comments to your blog and have them show up in the @@ -1820,8 +1841,8 @@ following:

<% end %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` This will now render the partial in `app/views/comments/_comment.html.erb` once @@ -1870,8 +1891,8 @@ Then you make the `app/views/articles/show.html.erb` look like the following:

Add a comment:

<%= render 'comments/form' %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` The second render just defines the partial template we want to render, @@ -1950,7 +1971,7 @@ you to use the `dependent` option of an association to achieve this. Modify the Article model, `app/models/article.rb`, as follows: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments, dependent: :destroy validates :title, presence: true, length: { minimum: 5 } @@ -1987,7 +2008,7 @@ class ArticlesController < ApplicationController @articles = Article.all end - # snipped for brevity + # snippet for brevity ``` We also want to allow only authenticated users to delete comments, so in the @@ -2003,7 +2024,7 @@ class CommentsController < ApplicationController # ... end - # snipped for brevity + # snippet for brevity ``` Now if you try to create a new article, you will be greeted with a basic HTTP @@ -2029,28 +2050,17 @@ What's Next? ------------ Now that you've seen your first Rails application, you should feel free to -update it and experiment on your own. But you don't have to do everything -without help. As you need assistance getting up and running with Rails, feel -free to consult these support resources: +update it and experiment on your own. + +Remember you don't have to do everything without help. As you need assistance +getting up and running with Rails, feel free to consult these support +resources: * The [Ruby on Rails Guides](index.html) * The [Ruby on Rails Tutorial](http://railstutorial.org/book) * The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk) * The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net -Rails also comes with built-in help that you can generate using the rake -command-line utility: - -* Running `rake doc:guides` will put a full copy of the Rails Guides in the - `doc/guides` folder of your application. Open `doc/guides/index.html` in your - web browser to explore the Guides. -* Running `rake doc:rails` will put a full copy of the API documentation for - Rails in the `doc/api` folder of your application. Open `doc/api/index.html` - in your web browser to explore the API documentation. - -TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake -task you need to install the RedCloth gem. Add it to your `Gemfile` and run -`bundle install` and you're ready to go. Configuration Gotchas --------------------- diff --git a/source/i18n.md b/source/i18n.md index 1023598..6c8706b 100644 --- a/source/i18n.md +++ b/source/i18n.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails Internationalization (I18n) API ===================================== @@ -23,12 +25,12 @@ After reading this guide, you will know: * How I18n works in Ruby on Rails * How to correctly use I18n into a RESTful application in various ways -* How to use I18n to translate ActiveRecord errors or ActionMailer E-mail subjects +* How to use I18n to translate Active Record errors or Action Mailer E-mail subjects * Some other tools to go further with the translation process of your application -------------------------------------------------------------------------------- -NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See the Ruby [I18n Wiki](http://ruby-i18n.org/wiki) for more information. +NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, also use various gems available to add additional functionality or features. See the [rails-i18n gem](https://github.com/svenfuchs/rails-i18n) for more information. How I18n in Ruby on Rails Works ------------------------------- @@ -38,7 +40,7 @@ Internationalization is a complex problem. Natural languages differ in so many w * providing support for English and similar languages out of the box * making it easy to customize and extend everything for other languages -As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**, so _localization_ of a Rails application means "over-riding" these defaults. +As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**. _Localization_ of a Rails application means defining translated values for these strings in desired languages. ### The Overall Architecture of the Library @@ -49,7 +51,7 @@ Thus, the Ruby I18n gem is split into two parts: As a user you should always only access the public methods on the I18n module, but it is useful to know about the capabilities of the backend. -NOTE: It is possible (or even desirable) to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section [Using different backends](#using-different-backends) below. +NOTE: It is possible to swap the shipped Simple backend with a more powerful one, which would store translation data in a relational database, GetText dictionary, or similar. See section [Using different backends](#using-different-backends) below. ### The Public I18n API @@ -70,11 +72,13 @@ I18n.l Time.now There are also attribute readers and writers for the following attributes: ```ruby -load_path # Announce your custom translation files -locale # Get and set the current locale -default_locale # Get and set the default locale -exception_handler # Use a different exception_handler -backend # Use a different backend +load_path # Announce your custom translation files +locale # Get and set the current locale +default_locale # Get and set the default locale +available_locales # Whitelist locales available for the application +enforce_available_locales # Enforce locale whitelisting (true or false) +exception_handler # Use a different exception_handler +backend # Use a different backend ``` So, let's internationalize a simple Rails application from the ground up in the next chapters! @@ -82,13 +86,13 @@ So, let's internationalize a simple Rails application from the ground up in the Setup the Rails Application for Internationalization ---------------------------------------------------- -There are just a few simple steps to get up and running with I18n support for your application. +There are a few steps to get up and running with I18n support for a Rails application. ### Configure the I18n Module -Following the _convention over configuration_ philosophy, Rails will set up your application with reasonable defaults. If you need different settings, you can overwrite them easily. +Following the _convention over configuration_ philosophy, Rails I18n provides reasonable default translation strings. When different translation strings are needed, they can be overridden. -Rails adds all `.rb` and `.yml` files from the `config/locales` directory to your **translations load path**, automatically. +Rails adds all `.rb` and `.yml` files from the `config/locales` directory to the **translations load path**, automatically. The default `en.yml` locale in this directory contains a sample pair of translation strings: @@ -99,47 +103,43 @@ en: This means, that in the `:en` locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Model validation messages in the [`activemodel/lib/active_model/locale/en.yml`](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml) file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend. -The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations. +The I18n library will use **English** as a **default locale**, i.e. if a different locale is not set, `:en` will be used for looking up translations. -NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. +NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. -The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. +The **translations load path** (`I18n.load_path`) is an array of paths to files that will be loaded automatically. Configuring this path allows for customization of translations directory structure and file naming scheme. -NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced. +NOTE: The backend lazy-loads these translations when a translation is looked up for the first time. This backend can be swapped with something else even after translations have already been announced. -The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines. +You can change the default locale as well as configure the translations load paths in `config/application.rb` as follows: ```ruby -# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. -# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] -# config.i18n.default_locale = :de + config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + config.i18n.default_locale = :de ``` -### Optional: Custom I18n Configuration Setup - -For the sake of completeness, let's mention that if you do not want to use the `application.rb` file for some reason, you can always wire up things manually, too. - -To tell the I18n library where it can find your custom translation files you can specify the load path anywhere in your application - just make sure it gets run before any translations are actually looked up. You might also want to change the default locale. The simplest thing possible is to put the following into an initializer: +The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `config/application.rb`: ```ruby -# in config/initializers/locale.rb +# config/initializers/locale.rb -# tell the I18n library where to find your translations +# Where the I18n library should search for translation files I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] -# set default locale to something other than :en +# Whitelist locales available for the application +I18n.available_locales = [:en, :pt] + +# Set default locale to something other than :en I18n.default_locale = :pt ``` -### Setting and Passing the Locale +### Managing the Locale across Requests -If you want to translate your Rails application to a **single language other than English** (the default locale), you can set I18n.default_locale to your locale in `application.rb` or an initializer as shown above, and it will persist through the requests. +The default locale is used for all translations unless `I18n.locale` is explicitly set. -However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests. +A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request. -WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. - -The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this: +The locale can be set in a `before_action` in the `ApplicationController`: ```ruby before_action :set_locale @@ -149,11 +149,11 @@ def set_locale end ``` -This requires you to pass the locale as a URL query parameter as in `http://example.com/books?locale=pt`. (This is, for example, Google's approach.) So `http://localhost:3000?locale=pt` will load the Portuguese localization, whereas `http://localhost:3000?locale=de` would load the German localization, and so on. You may skip the next section and head over to the **Internationalize your application** section, if you want to try things out by manually placing the locale in the URL and reloading the page. +This example illustrates this using a URL query parameter to set the locale (e.g. `http://example.com/books?locale=pt`). With this approach, `http://localhost:3000?locale=pt` renders the Portuguese localization, while `http://localhost:3000?locale=de` loads a German localization. -Of course, you probably don't want to manually include the locale in every URL all over your application, or want the URLs look differently, e.g. the usual `http://example.com/pt/books` versus `http://example.com/en/books`. Let's discuss the different options you have. +The locale can be set using one of many different approaches. -### Setting the Locale from the Domain Name +#### Setting the Locale from the Domain Name One option you have is to set the locale from the domain name where your application runs. For example, we want `www.example.com` to load the English (or default) locale, and `www.example.es` to load the Spanish locale. Thus the _top-level domain name_ is used for locale setting. This has several advantages: @@ -171,7 +171,7 @@ def set_locale I18n.locale = extract_locale_from_tld || I18n.default_locale end -# Get locale from top-level domain or return nil if such locale is not available +# Get locale from top-level domain or return +nil+ if such locale is not available # You have to put something like: # 127.0.0.1 application.com # 127.0.0.1 application.it @@ -199,14 +199,14 @@ end If your application includes a locale switching menu, you would then have something like this in it: ```ruby -link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}") +link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}") ``` assuming you would set `APP_CONFIG[:deutsch_website_url]` to some value like `http://www.application.de`. This solution has aforementioned advantages, however, you may not be able or may not want to provide different localizations ("language versions") on different domains. The most obvious solution would be to include locale code in the URL params (or request path). -### Setting the Locale from the URL Params +#### Setting the Locale from URL Params The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case. @@ -214,14 +214,14 @@ This approach has almost the same set of advantages as setting the locale from t Getting the locale from `params` and setting it accordingly is not hard; including it in every URL and thus **passing it through the requests** is. To include an explicit option in every URL, e.g. `link_to(books_url(/service/locale: I18n.locale))`, would be tedious and probably impossible, of course. -Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding this method). +Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding `default_url_options`). We can include something like this in our `ApplicationController` then: ```ruby # app/controllers/application_controller.rb -def default_url_options(options = {}) - { locale: I18n.locale }.merge options +def default_url_options + { locale: I18n.locale } end ``` @@ -229,7 +229,7 @@ Every helper method dependent on `url_for` (e.g. helpers for named routes like ` You may be satisfied with this. It does impact the readability of URLs, though, when the locale "hangs" at the end of every URL in your application. Moreover, from the architectural standpoint, locale is usually hierarchically above the other parts of the application domain: and URLs should reflect this. -You probably want URLs to look like this: `www.example.com/en/books` (which loads the English locale) and `www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scoping`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html) option in this way: +You probably want URLs to look like this: `http://www.example.com/en/books` (which loads the English locale) and `http://www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scope`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html): ```ruby # config/routes.rb @@ -238,7 +238,9 @@ scope "/:locale" do end ``` -Now, when you call the `books_path` method you should get `"/en/books"` (for the default locale). An URL like `http://localhost:3001/nl/books` should load the Dutch locale, then, and following calls to `books_path` should return `"/nl/books"` (because the locale changed). +Now, when you call the `books_path` method you should get `"/en/books"` (for the default locale). A URL like `http://localhost:3001/nl/books` should load the Dutch locale, then, and following calls to `books_path` should return `"/nl/books"` (because the locale changed). + +WARNING. Since the return value of `default_url_options` is cached per request, the URLs in a locale selector cannot be generated invoking helpers in a loop that sets the corresponding `I18n.locale` in each iteration. Instead, leave `I18n.locale` untouched, and pass an explicit `:locale` option to the helper, or edit `request.original_fullpath`. If you don't want to force the use of a locale in your routes you can use an optional path scope (denoted by the parentheses) like so: @@ -251,7 +253,7 @@ end With this approach you will not get a `Routing Error` when accessing your resources such as `http://localhost:3001/books` without a locale. This is useful for when you want to use the default locale when one is not specified. -Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. An URL like `http://localhost:3001/nl` will not work automatically, because the `root to: "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.) +Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. A URL like `http://localhost:3001/nl` will not work automatically, because the `root to: "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.) You would probably need to map URLs like these: @@ -262,16 +264,25 @@ get '/:locale' => 'dashboard#index' Do take special care about the **order of your routes**, so this route declaration does not "eat" other ones. (You may want to add it directly before the `root :to` declaration.) -NOTE: Have a look at two plugins which simplify working with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master). +NOTE: Have a look at various gems which simplify working with routes: [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master), [rails-translate-routes](https://github.com/francesc/rails-translate-routes), [route_translator](https://github.com/enriclluelles/route_translator). + +#### Setting the Locale from User Preferences + +An application with authenticated users may allow users to set a locale preference through the application's interface. With this approach, a user's selected locale preference is persisted in the database and used to set the locale for authenticated requests by that user. -### Setting the Locale from the Client Supplied Information +```ruby +def set_locale + I18n.locale = current_user.try(:locale) || I18n.default_locale +end +``` -In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites - see the box about _sessions_, _cookies_ and RESTful architecture above. +#### Choosing an Implied Locale +When an explicit locale has not been set for a request (e.g. via one of the above methods), an application should attempt to infer the desired locale. -#### Using `Accept-Language` +##### Inferring Locale from the Language Header -One source of client supplied information would be an `Accept-Language` HTTP header. People may [set this in their browser](http://www.w3.org/International/questions/qa-lang-priorities) or other clients (such as _curl_). +The `Accept-Language` HTTP header indicates the preferred language for request's response. Browsers [set this header value based on the user's language preference settings](http://www.w3.org/International/questions/qa-lang-priorities), making it a good first choice when inferring a locale. A trivial implementation of using an `Accept-Language` header would be: @@ -288,24 +299,27 @@ private end ``` -Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb). -#### Using GeoIP (or Similar) Database +In practice, more robust code is necessary to do this reliably. Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) library or Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb) Rack middleware provide solutions to this problem. + +##### Inferring the Locale from IP Geolocation -Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above - you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned. +The IP address of the client making the request can be used to infer the client's region and thus their locale. Services such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry) or gems like [geocoder](https://github.com/alexreisner/geocoder) can be used to implement this approach. -#### User Profile +In general, this approach is far less reliable than using the language header and is not recommended for most web applications. -You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above - you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value. +#### Storing the Locale from the Session or Cookies -Internationalizing your Application +WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. + +Internationalization and Localization ----------------------------------- -OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. With that in place, you're now ready for the really interesting stuff. +OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. -Let's _internationalize_ our application, i.e. abstract every locale-specific parts, and then _localize_ it, i.e. provide necessary translations for these abstracts. +Next we need to _internationalize_ our application by abstracting every locale-specific element. Finally, we need to _localize_ it by providing necessary translations for these abstracts. -You most probably have something like this in one of your applications: +Given the following example: ```ruby # config/routes.rb @@ -342,9 +356,9 @@ end ![rails i18n demo untranslated](images/i18n/demo_untranslated.png) -### Adding Translations +### Abstracting Localized Code -Obviously there are **two strings that are localized to English**. In order to internationalize this code, **replace these strings** with calls to Rails' `#t` helper with a key that makes sense for the translation: +There are two strings in our code that are in English and that users will be rendered in our response ("Hello Flash" and "Hello World"). In order to internationalize this code, these strings need to be replaced by calls to Rails' `#t` helper with an appropriate key for each string: ```ruby # app/controllers/home_controller.rb @@ -361,13 +375,15 @@ end

<%= flash[:notice] %>

``` -When you now render this view, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing. +Now, when this view is rendered, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing. ![rails i18n demo translation missing](images/i18n/demo_translation_missing.png) NOTE: Rails adds a `t` (`translate`) helper method to your views so that you do not need to spell out `I18n.t` all the time. Additionally this helper will catch missing translations and wrap the resulting error message into a ``. -So let's add the missing translations into the dictionary files (i.e. do the "localization" part): +### Providing Translations for Internationalized Strings + +Add the missing translations into the translation dictionary files: ```yaml # config/locales/en.yml @@ -381,11 +397,11 @@ pirate: hello_flash: Ahoy Flash ``` -There you go. Because you haven't changed the default_locale, I18n will use English. Your application now shows: +Because the `default_locale` hasn't changed, translations use the `:en` locale and the response renders the english strings: ![rails i18n demo translated to English](images/i18n/demo_translated_en.png) -And when you change the URL to pass the pirate locale (`http://localhost:3000?locale=pirate`), you'll get: +If the locale is set via the URL to the pirate locale (`http://localhost:3000?locale=pirate`), the response renders the pirate strings: ![rails i18n demo translated to pirate](images/i18n/demo_translated_pirate.png) @@ -393,21 +409,93 @@ NOTE: You need to restart the server when you add new locale files. You may use YAML (`.yml`) or plain Ruby (`.rb`) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.) -### Passing variables to translations +If your translations are stored in YAML files, certain keys must be escaped. They are: + +* true, on, yes +* false, off, no -You can use variables in the translation messages and pass their values from the view. +Examples: ```erb -# app/views/home/index.html.erb -<%=t 'greet_username', user: "Bill", message: "Goodbye" %> +# config/locales/en.yml +en: + success: + 'true': 'True!' + 'on': 'On!' + 'false': 'False!' + failure: + true: 'True!' + off: 'Off!' + false: 'False!' +``` + +```ruby +I18n.t 'success.true' # => 'True!' +I18n.t 'success.on' # => 'On!' +I18n.t 'success.false' # => 'False!' +I18n.t 'failure.false' # => Translation Missing +I18n.t 'failure.off' # => Translation Missing +I18n.t 'failure.true' # => Translation Missing +``` + +### Passing Variables to Translations + +One key consideration for successfully internationalizing an application is to +avoid making incorrect assumptions about grammar rules when abstracting localized +code. Grammar rules that seem fundamental in one locale may not hold true in +another one. + +Improper abstraction is shown in the following example, where assumptions are +made about the ordering of the different parts of the translation. Note that Rails +provides a `number_to_currency` helper to handle the following case. + +```erb +# app/views/products/show.html.erb +<%= "#{t('currency')}#{@product.price}" %> +``` + +```yaml +# config/locales/en.yml +en: + currency: "$" + +# config/locales/es.yml +es: + currency: "€" +``` + +If the product's price is 10 then the proper translation for Spanish is "10 €" +instead of "€10" but the abstraction cannot give it. + +To create proper abstraction, the I18n gem ships with a feature called variable +interpolation that allows you to use variables in translation definitions and +pass the values for these variables to the translation method. + +Proper abstraction is shown in the following example: + +```erb +# app/views/products/show.html.erb +<%= t('product_price', price: @product.price) %> ``` ```yaml # config/locales/en.yml en: - greet_username: "%{message}, %{user}!" + product_price: "$%{price}" + +# config/locales/es.yml +es: + product_price: "%{price} €" ``` +All grammatical and punctuation decisions are made in the definition itself, so +the abstraction can give a proper translation. + +NOTE: The `default` and `scope` keywords are reserved and can't be used as +variable names. If used, an `I18n::ReservedInterpolationKey` exception is raised. +If a translation expects an interpolation variable, but this has not been passed +to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. + ### Adding Date/Time Formats OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option - by default the `:default` format is used. @@ -415,7 +503,7 @@ OK! Now let's add a timestamp to the view, so we can demo the **date/time locali ```erb # app/views/home/index.html.erb

<%=t :hello_world %>

-

<%= flash[:notice] %>

<%= flash[:notice] %>

<%= l Time.now, format: :short %>

``` @@ -447,7 +535,10 @@ You can make use of this feature, e.g. when working with a large amount of stati ### Organization of Locale Files -When you are using the default SimpleStore shipped with the i18n library, dictionaries are stored in plain-text files on the disc. Putting translations for all parts of your application in one file per locale could be hard to manage. You can store these files in a hierarchy which makes sense to you. +When you are using the default SimpleStore shipped with the i18n library, +dictionaries are stored in plain-text files on the disk. Putting translations +for all parts of your application in one file per locale could be hard to +manage. You can store these files in a hierarchy which makes sense to you. For example, your `config/locales` directory could look like this: @@ -484,12 +575,12 @@ NOTE: The default locale loading mechanism in Rails does not load locale files i ``` -Do check the [Rails i18n Wiki](http://rails-i18n.org/wiki) for list of tools available for managing translations. - Overview of the I18n API Features --------------------------------- -You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth. +You should have a good understanding of using the i18n library now and know how +to internationalize a basic Rails application. In the following chapters, we'll +cover its features in more depth. These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method). @@ -530,7 +621,7 @@ Thus the following calls are equivalent: ```ruby I18n.t 'activerecord.errors.messages.record_invalid' -I18n.t 'errors.messages.record_invalid', scope: :active_record +I18n.t 'errors.messages.record_invalid', scope: :activerecord I18n.t :record_invalid, scope: 'activerecord.errors.messages' I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] ``` @@ -588,28 +679,35 @@ you can look up the `books.index.title` value **inside** `app/views/books/index. NOTE: Automatic translation scoping by partial is only available from the `translate` view helper method. -### Interpolation +"Lazy" lookup can also be used in controllers: -In many cases you want to abstract your translations so that **variables can be interpolated into the translation**. For this reason the I18n API provides an interpolation feature. +```yaml +en: + books: + create: + success: Book created! +``` -All options besides `:default` and `:scope` that are passed to `#translate` will be interpolated to the translation: +This is useful for setting flash messages for instance: ```ruby -I18n.backend.store_translations :en, thanks: 'Thanks %{name}!' -I18n.translate :thanks, name: 'Jeremy' -# => 'Thanks Jeremy!' +class BooksController < ApplicationController + def create + # ... + redirect_to books_url, notice: t('.success') + end +end ``` -If a translation uses `:default` or `:scope` as an interpolation variable, an `I18n::ReservedInterpolationKey` exception is raised. If a translation expects an interpolation variable, but this has not been passed to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. - ### Pluralization -In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ar), [Japanese](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ja), [Russian](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Thus, the I18n API provides a flexible pluralization feature. +In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ar), [Japanese](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ja), [Russian](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://cldr.unicode.org/index/cldr-spec/plural-rules). Thus, the I18n API provides a flexible pluralization feature. The `:count` interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by CLDR: ```ruby I18n.backend.store_translations :en, inbox: { + zero: 'no messages', # optional one: 'one message', other: '%{count} messages' } @@ -618,17 +716,22 @@ I18n.translate :inbox, count: 2 I18n.translate :inbox, count: 1 # => 'one message' + +I18n.translate :inbox, count: 0 +# => 'no messages' ``` The algorithm for pluralizations in `:en` is as simple as: ```ruby -entry[count == 1 ? 0 : 1] +lookup_key = :zero if count == 0 && entry.has_key?(:zero) +lookup_key ||= count == 1 ? :one : :other +entry[lookup_key] ``` -I.e. the translation denoted as `:one` is regarded as singular, the other is used as plural (including the count being zero). +The translation denoted as `:one` is regarded as singular, and the `:other` is used as plural. If the count is zero, and a `:zero` entry is present, then it will be used instead of `:other`. -If the lookup for the key does not return a Hash suitable for pluralization, an `18n::InvalidPluralizationData` exception is raised. +If the lookup for the key does not return a Hash suitable for pluralization, an `I18n::InvalidPluralizationData` exception is raised. ### Setting and Passing a Locale @@ -676,6 +779,22 @@ en:
<%= t('title.html') %>
``` +Interpolation escapes as needed though. For example, given: + +```yaml +en: + welcome_html: "Welcome %{username}!" +``` + +you can safely pass the username as set by the user: + +```erb +<%# This is safe, it is going to be escaped if needed. %> +<%= t('welcome_html', username: @current_user.username) %> +``` + +Safe strings on the other hand are interpolated verbatim. + NOTE: Automatic conversion to HTML safe translate text is only available from the `translate` view helper method. ![i18n demo html safe](images/i18n/demo_html_safe.png) @@ -725,6 +844,8 @@ en: Then `User.human_attribute_name("gender.female")` will return "Female". +NOTE: If you are using a class which includes `ActiveModel` and does not inherit from `ActiveRecord::Base`, replace `activerecord` with `activemodel` in the above key paths. + #### Error Message Scopes Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account. @@ -734,7 +855,7 @@ This gives you quite powerful means to flexibly adjust your messages to your app Consider a User model with a validation for the name attribute like this: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :name, presence: true end ``` @@ -785,7 +906,7 @@ This way you can provide special translations for various error messages at diff #### Error Message Interpolation -The translated model name, translated attribute name, and value are always available for interpolation. +The translated model name, translated attribute name, and value are always available for interpolation as `model`, `attribute` and `value` respectively. So, for example, instead of the default error message `"cannot be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`. @@ -793,7 +914,7 @@ So, for example, instead of the default error message `"cannot be blank"` you co | validation | with option | message | interpolation | | ------------ | ------------------------- | ------------------------- | ------------- | -| confirmation | - | :confirmation | - | +| confirmation | - | :confirmation | attribute | | acceptance | - | :accepted | - | | presence | - | :blank | - | | absence | - | :present | - | @@ -807,12 +928,14 @@ So, for example, instead of the default error message `"cannot be blank"` you co | inclusion | - | :inclusion | - | | exclusion | - | :exclusion | - | | associated | - | :invalid | - | +| non-optional association | - | :required | - | | numericality | - | :not_a_number | - | | numericality | :greater_than | :greater_than | count | | numericality | :greater_than_or_equal_to | :greater_than_or_equal_to | count | | numericality | :equal_to | :equal_to | count | | numericality | :less_than | :less_than | count | | numericality | :less_than_or_equal_to | :less_than_or_equal_to | count | +| numericality | :other_than | :other_than | count | | numericality | :only_integer | :not_an_integer | - | | numericality | :odd | :odd | - | | numericality | :even | :even | - | @@ -993,7 +1116,7 @@ In other contexts you might want to change this behavior, though. E.g. the defau module I18n class JustRaiseExceptionHandler < ExceptionHandler def call(exception, locale, key, options) - if exception.is_a?(MissingTranslation) + if exception.is_a?(MissingTranslationData) raise exception.to_exception else super @@ -1010,7 +1133,7 @@ This would re-raise only the `MissingTranslationData` exception, passing all oth However, if you are using `I18n::Backend::Pluralization` this handler will also raise `I18n::MissingTranslationData: translation missing: en.i18n.plural.rule` exception that should normally be ignored to fall back to the default pluralization rule for English locale. To avoid this you may use additional check for translation key: ```ruby -if exception.is_a?(MissingTranslation) && key.to_s != 'i18n.plural.rule' +if exception.is_a?(MissingTranslationData) && key.to_s != 'i18n.plural.rule' raise exception.to_exception else super @@ -1030,38 +1153,32 @@ Conclusion At this point you should have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project. -If you find anything missing or wrong in this guide, please file a ticket on our [issue tracker](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview). If you want to discuss certain portions or have questions, please sign up to our [mailing list](http://groups.google.com/group/rails-i18n). +If you want to discuss certain portions or have questions, please sign up to the [rails-i18n mailing list](http://groups.google.com/group/rails-i18n). Contributing to Rails I18n -------------------------- -I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in plugins and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core. +I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in gems and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core. -Thus we encourage everybody to experiment with new ideas and features in plugins or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n!)) +Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n)!) -If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data and send a [pull request](https://github.com/guides/pull-requests). +If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data and send a [pull request](https://help.github.com/articles/about-pull-requests/). Resources --------- -* [rails-i18n.org](http://rails-i18n.org) - Homepage of the rails-i18n project. You can find lots of useful resources on the [wiki](http://rails-i18n.org/wiki). * [Google group: rails-i18n](http://groups.google.com/group/rails-i18n) - The project's mailing list. -* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n/tree/master) - Code repository for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases. -* [GitHub: i18n](https://github.com/svenfuchs/i18n/tree/master) - Code repository for the i18n gem. -* [Lighthouse: rails-i18n](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview) - Issue tracker for the rails-i18n project. -* [Lighthouse: i18n](http://i18n.lighthouseapp.com/projects/14947-ruby-i18n/overview) - Issue tracker for the i18n gem. +* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n) - Code repository and issue tracker for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases. +* [GitHub: i18n](https://github.com/svenfuchs/i18n) - Code repository and issue tracker for the i18n gem. Authors ------- -* [Sven Fuchs](http://www.workingwithrails.com/person/9963-sven-fuchs) (initial author) -* [Karel Minařík](http://www.workingwithrails.com/person/7476-karel-mina-k) - -If you found this guide useful, please consider recommending its authors on [workingwithrails](http://www.workingwithrails.com). - +* [Sven Fuchs](http://svenfuchs.com) (initial author) +* [Karel Minařík](http://www.karmi.cz) Footnotes --------- diff --git a/source/initialization.md b/source/initialization.md index b81b048..3ea156c 100644 --- a/source/initialization.md +++ b/source/initialization.md @@ -1,8 +1,10 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + The Rails Initialization Process ================================ -This guide explains the internals of the initialization process in Rails -as of Rails 4. It is an extremely in-depth guide and recommended for advanced Rails developers. +This guide explains the internals of the initialization process in Rails. +It is an extremely in-depth guide and recommended for advanced Rails developers. After reading this guide, you will know: @@ -14,7 +16,7 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- This guide goes through every method call that is -required to boot up the Ruby on Rails stack for a default Rails 4 +required to boot up the Ruby on Rails stack for a default Rails application, explaining each part in detail along the way. For this guide, we will be focusing on what happens when you execute `rails server` to boot your app. @@ -32,7 +34,7 @@ Launch! Let's start to boot and initialize the app. A Rails application is usually started by running `rails console` or `rails server`. -### `railties/bin/rails` +### `railties/exe/rails` The `rails` in the command `rails server` is a ruby executable in your load path. This executable contains the following lines: @@ -43,7 +45,7 @@ load Gem.bin_path('railties', 'rails', version) ``` If you try out this command in a Rails console, you would see that this loads -`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the +`railties/exe/rails`. A part of the file `railties/exe/rails.rb` has the following code: ```ruby @@ -51,11 +53,11 @@ require "rails/cli" ``` The file `railties/lib/rails/cli` in turn calls -`Rails::AppRailsLoader.exec_app_rails`. +`Rails::AppLoader.exec_app`. -### `railties/lib/rails/app_rails_loader.rb` +### `railties/lib/rails/app_loader.rb` -The primary goal of the function `exec_app_rails` is to execute your app's +The primary goal of the function `exec_app` is to execute your app's `bin/rails`. If the current directory does not have a `bin/rails`, it will navigate upwards until it finds a `bin/rails` executable. Thus one can invoke a `rails` command from anywhere inside a rails application. @@ -72,7 +74,7 @@ This file is as follows: ```ruby #!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' ``` @@ -84,10 +86,9 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot `config/boot.rb` contains: ```ruby -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' # Set up gems listed in the Gemfile. ``` In a standard Rails application, there's a `Gemfile` which declares all @@ -104,6 +105,7 @@ A standard Rails application depends on several gems, specifically: * activemodel * activerecord * activesupport +* activejob * arel * builder * bundler @@ -111,7 +113,6 @@ A standard Rails application depends on several gems, specifically: * i18n * mail * mime-types -* polyglot * rack * rack-cache * rack-mount @@ -121,7 +122,6 @@ A standard Rails application depends on several gems, specifically: * rake * sqlite3 * thor -* treetop * tzinfo ### `rails/commands.rb` @@ -131,7 +131,7 @@ Once `config/boot.rb` has finished, the next file that is required is `ARGV` array simply contains `server` which will be passed over: ```ruby -ARGV << '--help' if ARGV.empty? +require "rails/command" aliases = { "g" => "generate", @@ -139,38 +139,44 @@ aliases = { "c" => "console", "s" => "server", "db" => "dbconsole", - "r" => "runner" + "r" => "runner", + "t" => "test" } command = ARGV.shift command = aliases[command] || command -require 'rails/commands/commands_tasks' - -Rails::CommandsTasks.new(ARGV).run_command!(command) +Rails::Command.invoke command, ARGV ``` -TIP: As you can see, an empty ARGV list will make Rails show the help -snippet. - If we had used `s` rather than `server`, Rails would have used the `aliases` defined here to find the matching command. -### `rails/commands/command_tasks.rb` +### `rails/command.rb` -When one types an incorrect rails command, the `run_command` is responsible for -throwing an error message. If the command is valid, a method of the same name -is called. +When one types a Rails command, `invoke` tries to lookup a command for the given +namespace and executing the command if found. -```ruby -COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) +If Rails doesn't recognize the command, it hands the reins over to Rake +to run a task of the same name. -def run_command!(command) - command = parse_command(command) - if COMMAND_WHITELIST.include?(command) - send(command) - else - write_error_message(command) +As shown, `Rails::Command` displays the help output automatically if the `args` +are empty. + +```ruby +module Rails::Command + class << self + def invoke(namespace, args = [], **config) + namespace = namespace.to_s + namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) + namespace = "version" if %w( -v --version ).include? namespace + + if command = find_by_namespace(namespace) + command.perform(namespace, args, config) + else + find_by_namespace("rake").perform(namespace, args, config) + end + end end end ``` @@ -178,53 +184,39 @@ end With the `server` command, Rails will further run the following code: ```ruby -def set_application_directory! - Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru")) -end - -def server - set_application_directory! - require_command!("server") - - Rails::Server.new.tap do |server| - # We need to require application after the server sets environment, - # otherwise the --environment option given to the server won't propagate. - require APP_PATH - Dir.chdir(Rails.application.root) - server.start +module Rails + module Command + class ServerCommand < Base # :nodoc: + def perform + set_application_directory! + + Rails::Server.new.tap do |server| + # Require application after server sets environment to propagate + # the --environment option. + require APP_PATH + Dir.chdir(Rails.application.root) + server.start + end + end + end end end - -def require_command!(command) - require "rails/commands/#{command}" -end ``` This file will change into the Rails root directory (a path two directories up from `APP_PATH` which points at `config/application.rb`), but only if the -`config.ru` file isn't found. This then requires `rails/commands/server` which -sets up the `Rails::Server` class. - -```ruby -require 'fileutils' -require 'optparse' -require 'action_dispatch' -require 'rails' - -module Rails - class Server < ::Rack::Server -``` - -`fileutils` and `optparse` are standard Ruby libraries which provide helper functions for working with files and parsing options. +`config.ru` file isn't found. This then starts up the `Rails::Server` class. ### `actionpack/lib/action_dispatch.rb` Action Dispatch is the routing component of the Rails framework. It adds functionality like routing, session, and common middlewares. -### `rails/commands/server.rb` +### `rails/commands/server/server_command.rb` -The `Rails::Server` class is defined in this file by inheriting from `Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` method in `rails/commands/server.rb`: +The `Rails::Server` class is defined in this file by inheriting from +`Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` +method in `rails/commands/server/server_command.rb`: ```ruby def initialize(*) @@ -250,7 +242,10 @@ end In this case, `options` will be `nil` so nothing happens in this method. -After `super` has finished in `Rack::Server`, we jump back to `rails/commands/server.rb`. At this point, `set_environment` is called within the context of the `Rails::Server` object and this method doesn't appear to do much at first glance: +After `super` has finished in `Rack::Server`, we jump back to +`rails/commands/server/server_command.rb`. At this point, `set_environment` +is called within the context of the `Rails::Server` object and this method +doesn't appear to do much at first glance: ```ruby def set_environment @@ -287,17 +282,15 @@ With the `default_options` set to this: ```ruby def default_options - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - { - :environment => environment, - :pid => nil, - :Port => 9292, - :Host => default_host, - :AccessLog => [], - :config => "config.ru" - } + super.merge( + Port: ENV.fetch("https://github.com/PORT", 3000).to_i, + Host: ENV.fetch("https://github.com/HOST", "localhost").dup, + DoNotReverseLookup: true, + environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup, + daemonize: false, + caching: nil, + pid: Options::DEFAULT_PID_PATH, + restart_cmd: restart_command) end ``` @@ -309,22 +302,25 @@ def opt_parser end ``` -The class **is** defined in `Rack::Server`, but is overwritten in `Rails::Server` to take different arguments. Its `parse!` method begins like this: +The class **is** defined in `Rack::Server`, but is overwritten in +`Rails::Server` to take different arguments. Its `parse!` method looks +like this: ```ruby def parse!(args) args, options = args.dup, {} - opt_parser = OptionParser.new do |opts| - opts.banner = "Usage: rails server [mongrel, thin, etc] [options]" - opts.on("-p", "--port=port", Integer, - "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } - ... + option_parser(options).parse! args + + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" + options[:server] = args.shift + options +end ``` This method will set up keys for the `options` which Rails will then be able to use to determine how its server should run. After `initialize` -has finished, we jump back into `rails/server` where `APP_PATH` (which was +has finished, we jump back into the server command where `APP_PATH` (which was set earlier) is required. ### `config/application` @@ -343,6 +339,7 @@ def start print_boot_information trap(:INT) { exit } create_tmp_directories + setup_dev_caching log_to_stdout if options[:log_stdout] super @@ -350,38 +347,43 @@ def start end private - def print_boot_information ... puts "=> Run `rails server -h` for more startup options" - ... - puts "=> Ctrl-C to shutdown server" unless options[:daemonize] end def create_tmp_directories - %w(cache pids sessions sockets).each do |dir_to_make| + %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) end end + def setup_dev_caching + if options[:environment] == "development" + Rails::DevCaching.enable_by_argument(options[:caching]) + end + end + def log_to_stdout wrapped_app # touch the app so the logger is set up - - console = ActiveSupport::Logger.new($stdout) + + console = ActiveSupport::Logger.new(STDOUT) console.formatter = Rails.logger.formatter console.level = Rails.logger.level - - Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end end ``` -This is where the first output of the Rails initialization happens. This -method creates a trap for `INT` signals, so if you `CTRL-C` the server, -it will exit the process. As we can see from the code here, it will -create the `tmp/cache`, `tmp/pids`, `tmp/sessions` and `tmp/sockets` -directories. It then calls `wrapped_app` which is responsible for -creating the Rack app, before creating and assigning an -instance of `ActiveSupport::Logger`. +This is where the first output of the Rails initialization happens. This method +creates a trap for `INT` signals, so if you `CTRL-C` the server, it will exit the +process. As we can see from the code here, it will create the `tmp/cache`, +`tmp/pids`, and `tmp/sockets` directories. It then enables caching in development +if `rails server` is called with `--dev-caching`. Finally, it calls `wrapped_app` which is +responsible for creating the Rack app, before creating and assigning an instance +of `ActiveSupport::Logger`. The `super` method will call `Rack::Server.start` which begins its definition like this: @@ -465,7 +467,7 @@ The `options[:config]` value defaults to `config.ru` which contains this: ```ruby # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require_relative 'config/environment' run <%= app_const %> ``` @@ -486,7 +488,7 @@ end The `initialize` method of `Rack::Builder` will take the block here and execute it within an instance of `Rack::Builder`. This is where the majority of the initialization process of Rails happens. The `require` line for `config/environment.rb` in `config.ru` is the first to run: ```ruby -require ::File.expand_path('../config/environment', __FILE__) +require_relative 'config/environment' ``` ### `config/environment.rb` @@ -496,7 +498,7 @@ This file is the common file required by `config.ru` (`rails server`) and Passen This file begins with requiring `config/application.rb`: ```ruby -require File.expand_path('../application', __FILE__) +require_relative 'application' ``` ### `config/application.rb` @@ -504,7 +506,7 @@ require File.expand_path('../application', __FILE__) This file requires `config/boot.rb`: ```ruby -require File.expand_path('../boot', __FILE__) +require_relative 'boot' ``` But only if it hasn't been required before, which would be the case in `rails server` @@ -529,15 +531,17 @@ This file is responsible for requiring all the individual frameworks of Rails: require "rails" %w( - active_record - action_controller - action_view - action_mailer - rails/test_unit - sprockets -).each do |framework| + active_record/railtie + action_controller/railtie + action_view/railtie + action_mailer/railtie + active_job/railtie + action_cable/engine + rails/test_unit/railtie + sprockets/railtie +).each do |railtie| begin - require "#{framework}/railtie" + require railtie rescue LoadError end end @@ -556,9 +560,8 @@ I18n and Rails configuration are all being defined here. The rest of `config/application.rb` defines the configuration for the `Rails::Application` which will be used once the application is fully initialized. When `config/application.rb` has finished loading Rails and defined -the application namespace, we go back to `config/environment.rb`, -where the application is initialized. For example, if the application was called -`Blog`, here we would find `Rails.application.initialize!`, which is +the application namespace, we go back to `config/environment.rb`. Here, the +application is initialized with `Rails.application.initialize!`, which is defined in `rails/application.rb`. ### `railties/lib/rails/application.rb` @@ -663,7 +666,7 @@ DEFAULT_OPTIONS = { } def self.run(app, options = {}) - options = DEFAULT_OPTIONS.merge(options) + options = DEFAULT_OPTIONS.merge(options) if options[:Verbose] app = Rack::CommonLogger.new(app, STDOUT) diff --git a/source/kindle/layout.html.erb b/source/kindle/layout.html.erb index f0a2862..fd87467 100644 --- a/source/kindle/layout.html.erb +++ b/source/kindle/layout.html.erb @@ -14,12 +14,12 @@ <% if content_for? :header_section %> <%= yield :header_section %> -
+
<% end %> <% if content_for? :index_section %> <%= yield :index_section %> -
+
<% end %> <%= yield.html_safe %> diff --git a/source/kindle/rails_guides.opf.erb b/source/kindle/rails_guides.opf.erb index 547abcb..63eeb00 100644 --- a/source/kindle/rails_guides.opf.erb +++ b/source/kindle/rails_guides.opf.erb @@ -5,7 +5,7 @@ - Ruby on Rails Guides (<%= @version %>) + Ruby on Rails Guides (<%= @version || "master@#{@edge[0, 7]}" %>) en-us Ruby on Rails diff --git a/source/kindle/toc.ncx.erb b/source/kindle/toc.ncx.erb index 2c6d8e3..5094fea 100644 --- a/source/kindle/toc.ncx.erb +++ b/source/kindle/toc.ncx.erb @@ -32,12 +32,12 @@ Credits - + Copyright & License - - + + <% play_order = 4 %> @@ -47,7 +47,7 @@ <%= section['name'] %> - + <% section['documents'].each_with_index do |document, document_no| %> diff --git a/source/kindle/welcome.html.erb b/source/kindle/welcome.html.erb index 610a715..ef3397f 100644 --- a/source/kindle/welcome.html.erb +++ b/source/kindle/welcome.html.erb @@ -2,4 +2,6 @@

Kindle Edition

-The Kindle Edition of the Rails Guides should be considered a work in progress. Feedback is really welcome. Please see the "Feedback" section at the end of each guide for instructions. +
+ The Kindle Edition of the Rails Guides should be considered a work in progress. Feedback is really welcome. Please see the "Feedback" section at the end of each guide for instructions. +
diff --git a/source/layout.html.erb b/source/layout.html.erb index 1005057..bb50761 100644 --- a/source/layout.html.erb +++ b/source/layout.html.erb @@ -29,14 +29,11 @@ More Ruby on Rails
@@ -91,7 +88,7 @@
- <%= yield.html_safe %> + <%= yield %>

Feedback

@@ -114,7 +111,7 @@ <%= link_to 'open an issue', 'https://github.com/rails/rails/issues' %>.

And last but not least, any kind of discussion regarding Ruby on Rails - documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'http://groups.google.com/group/rubyonrails-docs' %>. + documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'https://groups.google.com/forum/#!forum/rubyonrails-docs' %>.

@@ -130,13 +127,11 @@ - - - - - + diff --git a/source/layouts_and_rendering.md b/source/layouts_and_rendering.md index f00f7bc..48bb314 100644 --- a/source/layouts_and_rendering.md +++ b/source/layouts_and_rendering.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Layouts and Rendering in Rails ============================== @@ -101,34 +103,6 @@ In most cases, the `ActionController::Base#render` method does the heavy lifting TIP: If you want to see the exact results of a call to `render` without needing to inspect it in a browser, you can call `render_to_string`. This method takes exactly the same options as `render`, but it returns a string instead of sending a response back to the browser. -#### Rendering Nothing - -Perhaps the simplest thing you can do with `render` is to render nothing at all: - -```ruby -render nothing: true -``` - -If you look at the response for this using cURL, you will see the following: - -```bash -$ curl -i 127.0.0.1:3000/books -HTTP/1.1 200 OK -Connection: close -Date: Sun, 24 Jan 2010 09:25:18 GMT -Transfer-Encoding: chunked -Content-Type: */*; charset=utf-8 -X-Runtime: 0.014297 -Set-Cookie: _blog_session=...snip...; path=/; HttpOnly -Cache-Control: no-cache - -$ -``` - -We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for Ajax requests where all you want to send back to the browser is an acknowledgment that the request was completed. - -TIP: You should probably be using the `head` method, discussed later in this guide, instead of `render :nothing`. This provides additional flexibility and makes it explicit that you're only generating HTTP headers. - #### Rendering an Action's View If you want to render the view that corresponds to a different template within the same controller, you can use `render` with the name of the view: @@ -175,23 +149,22 @@ render template: "products/show" #### Rendering an Arbitrary File -The `render` method can also use a view that's entirely outside of your application (perhaps you're sharing views between two Rails applications): - -```ruby -render "/u/apps/warehouse_app/current/app/views/products/show" -``` - -Rails determines that this is a file render because of the leading slash character. To be explicit, you can use the `:file` option (which was required on Rails 2.2 and earlier): +The `render` method can also use a view that's entirely outside of your application: ```ruby render file: "/u/apps/warehouse_app/current/app/views/products/show" ``` -The `:file` option takes an absolute file-system path. Of course, you need to have rights to the view that you're using to render the content. +The `:file` option takes an absolute file-system path. Of course, you need to have rights +to the view that you're using to render the content. + +NOTE: Using the `:file` option in combination with users input can lead to security problems +since an attacker could use this action to access security sensitive files in your file system. -NOTE: By default, the file is rendered without using the current layout. If you want Rails to put the file into the current layout, you need to add the `layout: true` option. +NOTE: By default, the file is rendered using the current layout. -TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to render a file, because Windows filenames do not have the same format as Unix filenames. +TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to +render a file, because Windows filenames do not have the same format as Unix filenames. #### Wrapping it up @@ -248,11 +221,12 @@ service requests that are expecting something other than proper HTML. NOTE: By default, if you use the `:plain` option, the text is rendered without using the current layout. If you want Rails to put the text into the current -layout, you need to add the `layout: true` option. +layout, you need to add the `layout: true` option and use the `.txt.erb` +extension for the layout file. #### Rendering HTML -You can send a HTML string back to the browser by using the `:html` option to +You can send an HTML string back to the browser by using the `:html` option to `render`: ```ruby @@ -263,7 +237,7 @@ TIP: This is useful when you're rendering a small snippet of HTML code. However, you might want to consider moving it to a template file if the markup is complex. -NOTE: This option will escape HTML entities if the string is not html safe. +NOTE: When using `html:` option, HTML entities will be escaped if the string is not marked as HTML safe by using `html_safe` method. #### Rendering JSON @@ -305,7 +279,7 @@ render body: "raw" ``` TIP: This option should be used only if you don't care about the content type of -the response. Using `:plain` or `:html` might be more appropriate in most of the +the response. Using `:plain` or `:html` might be more appropriate most of the time. NOTE: Unless overridden, your response returned from this render option will be @@ -313,12 +287,13 @@ NOTE: Unless overridden, your response returned from this render option will be #### Options for `render` -Calls to the `render` method generally accept four options: +Calls to the `render` method generally accept five options: * `:content_type` * `:layout` * `:location` * `:status` +* `:formats` ##### The `:content_type` Option @@ -384,7 +359,6 @@ Rails understands both numeric status codes and the corresponding symbols shown | | 303 | :see_other | | | 304 | :not_modified | | | 305 | :use_proxy | -| | 306 | :reserved | | | 307 | :temporary_redirect | | | 308 | :permanent_redirect | | **Client Error** | 400 | :bad_request | @@ -400,10 +374,10 @@ Rails understands both numeric status codes and the corresponding symbols shown | | 410 | :gone | | | 411 | :length_required | | | 412 | :precondition_failed | -| | 413 | :request_entity_too_large | -| | 414 | :request_uri_too_long | +| | 413 | :payload_too_large | +| | 414 | :uri_too_long | | | 415 | :unsupported_media_type | -| | 416 | :requested_range_not_satisfiable | +| | 416 | :range_not_satisfiable | | | 417 | :expectation_failed | | | 422 | :unprocessable_entity | | | 423 | :locked | @@ -424,6 +398,21 @@ Rails understands both numeric status codes and the corresponding symbols shown | | 510 | :not_extended | | | 511 | :network_authentication_required | +NOTE: If you try to render content along with a non-content status code +(100-199, 204, 205 or 304), it will be dropped from the response. + +##### The `:formats` Option + +Rails uses the format specified in the request (or `:html` by default). You can +change this passing the `:formats` option with a symbol or an array: + +```ruby +render formats: :xml +render formats: [:json, :xml] +``` + +If a template with the specified format does not exist an `ActionView::MissingTemplate` error is raised. + #### Finding Layouts To find the current layout, Rails first looks for a file in `app/views/layouts` with the same base name as the controller. For example, rendering actions from the `PhotosController` class will use `app/views/layouts/photos.html.erb` (or `app/views/layouts/photos.builder`). If there is no such controller-specific layout, Rails will use `app/views/layouts/application.html.erb` or `app/views/layouts/application.builder`. If there is no `.erb` layout, Rails will use a `.builder` layout if one exists. Rails also provides several ways to more precisely assign specific layouts to individual controllers and actions. @@ -547,6 +536,42 @@ In this application: * `OldArticlesController#show` will use no layout at all * `OldArticlesController#index` will use the `old` layout +##### Template Inheritance + +Similar to the Layout Inheritance logic, if a template or partial is not found in the conventional path, the controller will look for a template or partial to render in its inheritance chain. For example: + +```ruby +# in app/controllers/application_controller +class ApplicationController < ActionController::Base +end + +# in app/controllers/admin_controller +class AdminController < ApplicationController +end + +# in app/controllers/admin/products_controller +class Admin::ProductsController < AdminController + def index + end +end +``` + +The lookup order for an `admin/products#index` action will be: + +* `app/views/admin/products/` +* `app/views/admin/` +* `app/views/application/` + +This makes `app/views/application/` a great place for your shared partials, which can then be rendered in your ERB as such: + +```erb +<%# app/views/admin/products/index.html.erb %> +<%= render @products || "empty_list" %> + +<%# app/views/application/_empty_list.html.erb %> +There are no items in this list yet. +``` + #### Avoiding Double Render Errors Sooner or later, most Rails developers will see the error message "Can only render or redirect once per action". While this is annoying, it's relatively easy to fix. Usually it happens because of a fundamental misunderstanding of the way that `render` works. @@ -598,12 +623,17 @@ Another way to handle returning responses to an HTTP request is with `redirect_t redirect_to photos_url ``` -You can use `redirect_to` with any arguments that you could use with `link_to` or `url_for`. There's also a special redirect that sends the user back to the page they just came from: +You can use `redirect_back` to return the user to the page they just came from. +This location is pulled from the `HTTP_REFERER` header which is not guaranteed +to be set by the browser, so you must provide the `fallback_location` +to use in this case. ```ruby -redirect_to :back +redirect_back(fallback_location: root_path) ``` +NOTE: `redirect_to` and `redirect_back` do not halt and return immediately from method execution, but simply set HTTP responses. Statements occurring after them in a method will be executed. You can halt by an explicit `return` or some other halting mechanism, if needed. + #### Getting a Different Redirect Status Code Rails uses HTTP status code 302, a temporary redirect, when you call `redirect_to`. If you'd like to use a different status code, perhaps 301, a permanent redirect, you can use the `:status` option: @@ -673,7 +703,7 @@ This would detect that there are no books with the specified ID, populate the `@ ### Using `head` To Build Header-Only Responses -The `head` method can be used to send responses with only headers to the browser. It provides a more obvious alternative to calling `render :nothing`. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing a HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header: +The `head` method can be used to send responses with only headers to the browser. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing an HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header: ```ruby head :bad_request @@ -723,7 +753,7 @@ When Rails renders a view as a response, it does so by combining the view with t ### Asset Tag Helpers -Asset tag helpers provide methods for generating HTML that link views to feeds, JavaScript, stylesheets, images, videos and audios. There are six asset tag helpers available in Rails: +Asset tag helpers provide methods for generating HTML that link views to feeds, JavaScript, stylesheets, images, videos, and audios. There are six asset tag helpers available in Rails: * `auto_discovery_link_tag` * `javascript_include_tag` @@ -757,7 +787,7 @@ The `javascript_include_tag` helper returns an HTML `script` tag for each source If you are using Rails with the [Asset Pipeline](asset_pipeline.html) enabled, this helper will generate a link to `/assets/javascripts/` rather than `public/javascripts` which was used in earlier versions of Rails. This link is then served by the asset pipeline. -A JavaScript file within a Rails application or Rails engine goes in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`. These locations are explained in detail in the [Asset Organization section in the Asset Pipeline Guide](asset_pipeline.html#asset-organization) +A JavaScript file within a Rails application or Rails engine goes in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`. These locations are explained in detail in the [Asset Organization section in the Asset Pipeline Guide](asset_pipeline.html#asset-organization). You can specify a full path relative to the document root, or a URL, if you prefer. For example, to link to a JavaScript file that is inside a directory called `javascripts` inside of one of `app/assets`, `lib/assets` or `vendor/assets`, you would do this: @@ -903,7 +933,10 @@ You can also specify multiple videos to play by passing an array of videos to th This will produce: ```erb - + ``` #### Linking to Audio Files with the `audio_tag` @@ -1019,7 +1052,48 @@ One way to use partials is to treat them as the equivalent of subroutines: as a <%= render "shared/footer" %> ``` -Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain content that is shared among many pages in your application. You don't need to see the details of these sections when you're concentrating on a particular page. +Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain +content that is shared by many pages in your application. You don't need to see +the details of these sections when you're concentrating on a particular page. + +As seen in the previous sections of this guide, `yield` is a very powerful tool +for cleaning up your layouts. Keep in mind that it's pure Ruby, so you can use +it almost everywhere. For example, we can use it to DRY up form layout +definitions for several similar resources: + +* `users/index.html.erb` + + ```html+erb + <%= render "shared/search_filters", search: @q do |f| %> +

+ Name contains: <%= f.text_field :name_contains %> +

+ <% end %> + ``` + +* `roles/index.html.erb` + + ```html+erb + <%= render "shared/search_filters", search: @q do |f| %> +

+ Title contains: <%= f.text_field :title_contains %> +

+ <% end %> + ``` + +* `shared/_search_filters.html.erb` + + ```html+erb + <%= form_for(search) do |f| %> +

Search form:

+
+ <%= yield f %> +
+

+ <%= f.submit "Search" %> +

+ <% end %> + ``` TIP: For content that is shared among all pages in your application, you can use partials directly from layouts. @@ -1069,6 +1143,34 @@ You can also pass local variables into partials, making them even more powerful Although the same partial will be rendered into both views, Action View's submit helper will return "Create Zone" for the new action and "Update Zone" for the edit action. +To pass a local variable to a partial in only specific cases use the `local_assigns`. + +* `index.html.erb` + + ```erb + <%= render user.articles %> + ``` + +* `show.html.erb` + + ```erb + <%= render article, full: true %> + ``` + +* `_article.html.erb` + + ```erb +

<%= article.title %>

+ + <% if local_assigns[:full] %> + <%= simple_format article.body %> + <% else %> + <%= truncate article.body %> + <% end %> + ``` + +This way it is possible to use the partial without the need to declare all local variables. + Every partial also has a local variable with the same name as the partial (minus the underscore). You can pass an object in to this local variable via the `:object` option: ```erb @@ -1180,7 +1282,7 @@ When rendering collections it is also possible to use the `:layout` option: <%= render partial: "product", collection: @products, layout: "special_layout" %> ``` -The layout will be rendered together with the partial for each item in the collection. The current object and object_counter variables will be available in the layout as well, the same way they do within the partial. +The layout will be rendered together with the partial for each item in the collection. The current object and object_counter variables will be available in the layout as well, the same way they are within the partial. ### Using Nested Layouts diff --git a/source/maintenance_policy.md b/source/maintenance_policy.md index 6f8584b..1d6a4ed 100644 --- a/source/maintenance_policy.md +++ b/source/maintenance_policy.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Maintenance Policy for Ruby on Rails ==================================== @@ -39,7 +41,10 @@ Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from. -**Currently included series:** `4.1.Z`, `4.0.Z`. +In special situations, where someone from the Core Team agrees to support more series, +they are included in the list of supported series. + +**Currently included series:** `5.1.Z`. Security Issues --------------- @@ -54,7 +59,7 @@ be built from 1.2.2, and then added to the end of 1-2-stable. This means that security releases are easy to upgrade to if you're running the latest version of Rails. -**Currently included series:** `4.1.Z`, `4.0.Z`. +**Currently included series:** `5.1.Z`, `5.0.Z`. Severe Security Issues ---------------------- @@ -63,7 +68,7 @@ For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. -**Currently included series:** `4.1.Z`, `4.0.Z`, `3.2.Z`. +**Currently included series:** `5.1.Z`, `5.0.Z`, `4.2.Z`. Unsupported Release Series -------------------------- diff --git a/source/nested_model_forms.md b/source/nested_model_forms.md index 4f0634d..71efa4b 100644 --- a/source/nested_model_forms.md +++ b/source/nested_model_forms.md @@ -1,4 +1,6 @@ -Rails nested model forms +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +Rails Nested Model Forms ======================== Creating a form for a model _and_ its associations can become quite tedious. Therefore Rails provides helpers to assist in dealing with the complexities of generating these forms _and_ the required CRUD operations to create, update, and destroy associations. @@ -30,7 +32,7 @@ For an ActiveRecord::Base model and association this writer method is commonly d #### has_one ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_one :address accepts_nested_attributes_for :address end @@ -39,7 +41,7 @@ end #### belongs_to ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord belongs_to :firm accepts_nested_attributes_for :firm end @@ -48,12 +50,15 @@ end #### has_many / has_and_belongs_to_many ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :projects accepts_nested_attributes_for :projects end ``` +NOTE: For greater detail on associations see [Active Record Associations](association_basics.html). +For a complete reference on associations please visit the API documentation for [ActiveRecord::Associations::ClassMethods](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html). + ### Custom model As you might have inflected from this explanation, you _don't_ necessarily need an ActiveRecord::Base model to use this functionality. The following examples are sufficient to enable the nested model form behavior: @@ -101,7 +106,7 @@ Consider the following typical RESTful controller which will prepare a new Perso class PeopleController < ApplicationController def new @person = Person.new - @person.built_address + @person.build_address 2.times { @person.projects.build } end diff --git a/source/plugins.md b/source/plugins.md index f10699f..760ff43 100644 --- a/source/plugins.md +++ b/source/plugins.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + The Basics of Creating Rails Plugins ==================================== @@ -15,7 +17,7 @@ After reading this guide, you will know: This guide describes how to build a test-driven plugin that will: * Extend core Ruby classes like Hash and String. -* Add methods to `ActiveRecord::Base` in the tradition of the `acts_as` plugins. +* Add methods to `ApplicationRecord` in the tradition of the `acts_as` plugins. * Give you information about where to put generators in your plugin. For the purpose of this guide pretend for a moment that you are an avid bird watcher. @@ -28,36 +30,36 @@ Setup ----- Currently, Rails plugins are built as gems, _gemified plugins_. They can be shared across -different rails applications using RubyGems and Bundler if desired. +different Rails applications using RubyGems and Bundler if desired. ### Generate a gemified plugin. Rails ships with a `rails plugin new` command which creates a skeleton for developing any kind of Rails extension with the ability -to run integration tests using a dummy Rails application. Create your +to run integration tests using a dummy Rails application. Create your plugin with the command: ```bash -$ bin/rails plugin new yaffle +$ rails plugin new yaffle ``` See usage and options by asking for help: ```bash -$ bin/rails plugin new --help +$ rails plugin new --help ``` Testing Your Newly Generated Plugin ----------------------------------- You can navigate to the directory that contains the plugin, run the `bundle install` command - and run the one generated test using the `rake` command. + and run the one generated test using the `bin/test` command. You should see: ```bash - 2 tests, 2 assertions, 0 failures, 0 errors, 0 skips + 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips ``` This will tell you that everything got generated properly and you are ready to start adding functionality. @@ -65,7 +67,7 @@ This will tell you that everything got generated properly and you are ready to s Extending Core Classes ---------------------- -This section will explain how to add a method to String that will be available anywhere in your rails application. +This section will explain how to add a method to String that will be available anywhere in your Rails application. In this example you will add a method to String named `to_squawk`. To begin, create a new test file with a few assertions: @@ -81,13 +83,23 @@ class CoreExtTest < ActiveSupport::TestCase end ``` -Run `rake` to run the test. This test should fail because we haven't implemented the `to_squawk` method: +Run `bin/test` to run the test. This test should fail because we haven't implemented the `to_squawk` method: ```bash - 1) Error: - test_to_squawk_prepends_the_word_squawk(CoreExtTest): - NoMethodError: undefined method `to_squawk' for [Hello World](String) - test/core_ext_test.rb:5:in `test_to_squawk_prepends_the_word_squawk' +E + +Error: +CoreExtTest#test_to_squawk_prepends_the_word_squawk: +NoMethodError: undefined method `to_squawk' for "Hello World":String + + +bin/test /path/to/yaffle/test/core_ext_test.rb:4 + +. + +Finished in 0.003358s, 595.6483 runs/s, 297.8242 assertions/s. + +2 runs, 1 assertions, 0 failures, 1 errors, 0 skips ``` Great - now you are ready to start development. @@ -115,10 +127,10 @@ String.class_eval do end ``` -To test that your method does what it says it does, run the unit tests with `rake` from your plugin directory. +To test that your method does what it says it does, run the unit tests with `bin/test` from your plugin directory. ```bash - 3 tests, 3 assertions, 0 failures, 0 errors, 0 skips + 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips ``` To see this in action, change to the test/dummy directory, fire up a console and start squawking: @@ -180,7 +192,6 @@ To start out, write a failing test that shows the behavior you'd like: require 'test_helper' class ActsAsYaffleTest < ActiveSupport::TestCase - def test_a_hickwalls_yaffle_text_field_should_be_last_squawk assert_equal "last_squawk", Hickwall.yaffle_text_field end @@ -188,24 +199,37 @@ class ActsAsYaffleTest < ActiveSupport::TestCase def test_a_wickwalls_yaffle_text_field_should_be_last_tweet assert_equal "last_tweet", Wickwall.yaffle_text_field end - end ``` -When you run `rake`, you should see the following: +When you run `bin/test`, you should see the following: ``` - 1) Error: - test_a_hickwalls_yaffle_text_field_should_be_last_squawk(ActsAsYaffleTest): - NameError: uninitialized constant ActsAsYaffleTest::Hickwall - test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk' +# Running: + +..E - 2) Error: - test_a_wickwalls_yaffle_text_field_should_be_last_tweet(ActsAsYaffleTest): - NameError: uninitialized constant ActsAsYaffleTest::Wickwall - test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet' +Error: +ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet: +NameError: uninitialized constant ActsAsYaffleTest::Wickwall - 5 tests, 3 assertions, 0 failures, 2 errors, 0 skips + +bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8 + +E + +Error: +ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk: +NameError: uninitialized constant ActsAsYaffleTest::Hickwall + + +bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4 + + + +Finished in 0.004812s, 831.2949 runs/s, 415.6475 assertions/s. + +4 runs, 2 assertions, 0 failures, 2 errors, 0 skips ``` This tells us that we don't have the necessary models (Hickwall and Wickwall) that we are trying to test. @@ -223,7 +247,7 @@ and migrating the database. First, run: ```bash $ cd test/dummy -$ bin/rake db:migrate +$ bin/rails db:migrate ``` While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act @@ -232,22 +256,22 @@ like yaffles. ```ruby # test/dummy/app/models/hickwall.rb -class Hickwall < ActiveRecord::Base +class Hickwall < ApplicationRecord acts_as_yaffle end # test/dummy/app/models/wickwall.rb -class Wickwall < ActiveRecord::Base +class Wickwall < ApplicationRecord acts_as_yaffle yaffle_text_field: :last_tweet end - ``` We will also add code to define the `acts_as_yaffle` method. ```ruby # yaffle/lib/yaffle/acts_as_yaffle.rb + module Yaffle module ActsAsYaffle extend ActiveSupport::Concern @@ -263,26 +287,43 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +# test/dummy/app/models/application_record.rb + +class ApplicationRecord < ActiveRecord::Base + include Yaffle::ActsAsYaffle + + self.abstract_class = true +end ``` -You can then return to the root directory (`cd ../..`) of your plugin and rerun the tests using `rake`. +You can then return to the root directory (`cd ../..`) of your plugin and rerun the tests using `bin/test`. ``` - 1) Error: - test_a_hickwalls_yaffle_text_field_should_be_last_squawk(ActsAsYaffleTest): - NoMethodError: undefined method `yaffle_text_field' for # - /Users/xxx/.rvm/gems/ruby-1.9.2-p136@xxx/gems/activerecord-3.0.3/lib/active_record/base.rb:1008:in `method_missing' - test/acts_as_yaffle_test.rb:5:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk' +# Running: + +.E + +Error: +ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk: +NoMethodError: undefined method `yaffle_text_field' for # + + +bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4 + +E + +Error: +ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet: +NoMethodError: undefined method `yaffle_text_field' for # - 2) Error: - test_a_wickwalls_yaffle_text_field_should_be_last_tweet(ActsAsYaffleTest): - NoMethodError: undefined method `yaffle_text_field' for # - Users/xxx/.rvm/gems/ruby-1.9.2-p136@xxx/gems/activerecord-3.0.3/lib/active_record/base.rb:1008:in `method_missing' - test/acts_as_yaffle_test.rb:9:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet' - 5 tests, 3 assertions, 0 failures, 2 errors, 0 skips +bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8 +. + +Finished in 0.008263s, 484.0999 runs/s, 242.0500 assertions/s. + +4 runs, 2 assertions, 0 failures, 2 errors, 0 skips ``` Getting closer... Now we will implement the code of the `acts_as_yaffle` method to make the tests pass. @@ -292,7 +333,7 @@ Getting closer... Now we will implement the code of the `acts_as_yaffle` method module Yaffle module ActsAsYaffle - extend ActiveSupport::Concern + extend ActiveSupport::Concern included do end @@ -306,13 +347,19 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +# test/dummy/app/models/application_record.rb + +class ApplicationRecord < ActiveRecord::Base + include Yaffle::ActsAsYaffle + + self.abstract_class = true +end ``` -When you run `rake`, you should see the tests all pass: +When you run `bin/test`, you should see the tests all pass: ```bash - 5 tests, 5 assertions, 0 failures, 0 errors, 0 skips + 4 runs, 4 assertions, 0 failures, 0 errors, 0 skips ``` ### Add an Instance Method @@ -327,7 +374,6 @@ To start out, write a failing test that shows the behavior you'd like: require 'test_helper' class ActsAsYaffleTest < ActiveSupport::TestCase - def test_a_hickwalls_yaffle_text_field_should_be_last_squawk assert_equal "last_squawk", Hickwall.yaffle_text_field end @@ -380,13 +426,19 @@ module Yaffle end end -ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle +# test/dummy/app/models/application_record.rb + +class ApplicationRecord < ActiveRecord::Base + include Yaffle::ActsAsYaffle + + self.abstract_class = true +end ``` -Run `rake` one final time and you should see: +Run `bin/test` one final time and you should see: ``` - 7 tests, 7 assertions, 0 failures, 0 errors, 0 skips + 6 runs, 6 assertions, 0 failures, 0 errors, 0 skips ``` NOTE: The use of `write_attribute` to write to the field in model is just one example of how a plugin can interact with the model, and will not always be the right method to use. For example, you could also use: @@ -433,7 +485,7 @@ Once your README is solid, go through and add rdoc comments to all of the method Once your comments are good to go, navigate to your plugin directory and run: ```bash -$ bin/rake rdoc +$ bundle exec rake rdoc ``` ### References @@ -441,4 +493,3 @@ $ bin/rake rdoc * [Developing a RubyGem using Bundler](https://github.com/radar/guides/blob/master/gem-development.md) * [Using .gemspecs as Intended](http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/) * [Gemspec Reference](http://guides.rubygems.org/specification-reference/) -* [GemPlugins: A Brief Introduction to the Future of Rails Plugins](http://www.intridea.com/blog/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins) diff --git a/source/profiling.md b/source/profiling.md new file mode 100644 index 0000000..ce093f7 --- /dev/null +++ b/source/profiling.md @@ -0,0 +1,16 @@ +*DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + +A Guide to Profiling Rails Applications +======================================= + +This guide covers built-in mechanisms in Rails for profiling your application. + +After reading this guide, you will know: + +* Rails profiling terminology. +* How to write benchmark tests for your application. +* Other benchmarking approaches and plugins. + +-------------------------------------------------------------------------------- + + diff --git a/source/rails_application_templates.md b/source/rails_application_templates.md index 6512b14..3e99ee7 100644 --- a/source/rails_application_templates.md +++ b/source/rails_application_templates.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails Application Templates =========================== @@ -13,18 +15,18 @@ After reading this guide, you will know: Usage ----- -To apply a template, you need to provide the Rails generator with the location of the template you wish to apply using the -m option. This can either be a path to a file or a URL. +To apply a template, you need to provide the Rails generator with the location of the template you wish to apply using the `-m` option. This can either be a path to a file or a URL. ```bash $ rails new blog -m ~/template.rb $ rails new blog -m http://example.com/template.rb ``` -You can use the rake task `rails:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL. +You can use the `app:template` Rake task to apply templates to an existing Rails application. The location of the template needs to be passed in via the LOCATION environment variable. Again, this can either be path to a file or a URL. ```bash -$ bin/rake rails:template LOCATION=~/template.rb -$ bin/rake rails:template LOCATION=http://example.com/template.rb +$ bin/rails app:template LOCATION=~/template.rb +$ bin/rails app:template LOCATION=http://example.com/template.rb ``` Template API @@ -36,7 +38,7 @@ The Rails templates API is easy to understand. Here's an example of a typical Ra # template.rb generate(:scaffold, "person name:string") route "root to: 'people#index'" -rake("db:migrate") +rails_command("db:migrate") after_bundle do git :init @@ -76,7 +78,7 @@ gem_group :development, :test do end ``` -### add_source(source, options = {}) +### add_source(source, options={}, &block) Adds the given source to the generated application's `Gemfile`. @@ -86,6 +88,14 @@ For example, if you need to source a gem from `"http://code.whytheluckystiff.net add_source "http://code.whytheluckystiff.net" ``` +If block is given, gem entries in block are wrapped into the source group. + +```ruby +add_source "http://gems.github.com/" do + gem "rspec-rails" +end +``` + ### environment/application(data=nil, options={}, &block) Adds a line inside the `Application` class for `config/application.rb`. @@ -165,18 +175,24 @@ Executes an arbitrary command. Just like the backticks. Let's say you want to re run "rm README.rdoc" ``` -### rake(command, options = {}) +### rails_command(command, options = {}) + +Runs the supplied task in the Rails application. Let's say you want to migrate the database: + +```ruby +rails_command "db:migrate" +``` -Runs the supplied rake tasks in the Rails application. Let's say you want to migrate the database: +You can also run tasks with a different Rails environment: ```ruby -rake "db:migrate" +rails_command "db:migrate", env: 'production' ``` -You can also run rake tasks with a different Rails environment: +You can also run tasks as a super-user: ```ruby -rake "db:migrate", env: 'production' +rails_command "log:clear", sudo: true ``` ### route(routing_code) @@ -213,10 +229,10 @@ CODE ### yes?(question) or no?(question) -These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to freeze rails only if the user wants to: +These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to Freeze Rails only if the user wants to: ```ruby -rake("rails:freeze:gems") if yes?("Freeze rails gems?") +rails_command("rails:freeze:gems") if yes?("Freeze rails gems?") # no?(question) acts just the opposite. ``` diff --git a/source/rails_on_rack.md b/source/rails_on_rack.md index 01941fa..340933c 100644 --- a/source/rails_on_rack.md +++ b/source/rails_on_rack.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails on Rack ============= @@ -56,34 +58,13 @@ class Server < ::Rack::Server end ``` -Here's how it loads the middlewares: - -```ruby -def middleware - middlewares = [] - middlewares << [Rails::Rack::Debugger] if options[:debugger] - middlewares << [::Rack::ContentLength] - Hash.new(middlewares) -end -``` - -`Rails::Rack::Debugger` is primarily useful only in the development environment. The following table explains the usage of the loaded middlewares: - -| Middleware | Purpose | -| ----------------------- | --------------------------------------------------------------------------------- | -| `Rails::Rack::Debugger` | Starts Debugger | -| `Rack::ContentLength` | Counts the number of bytes in the response and set the HTTP Content-Length header | - ### `rackup` To use `rackup` instead of Rails' `rails server`, you can put the following inside `config.ru` of your Rails application's root directory: ```ruby # Rails.root/config.ru -require ::File.expand_path('../config/environment', __FILE__) - -use Rails::Rack::Debugger -use Rack::ContentLength +require_relative 'config/environment' run Rails.application ``` @@ -99,6 +80,10 @@ To find out more about different `rackup` options: $ rackup --help ``` +### Development and auto-reloading + +Middlewares are loaded once and are not monitored for changes. You will have to restart the server for changes to be reflected in the running application. + Action Dispatcher Middleware Stack ---------------------------------- @@ -108,10 +93,10 @@ NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`, ### Inspecting Middleware Stack -Rails has a handy rake task for inspecting the middleware stack in use: +Rails has a handy task for inspecting the middleware stack in use: ```bash -$ bin/rake middleware +$ bin/rails middleware ``` For a freshly generated Rails application, this might produce something like: @@ -119,24 +104,22 @@ For a freshly generated Rails application, this might produce something like: ```ruby use Rack::Sendfile use ActionDispatch::Static -use Rack::Lock -use # +use ActionDispatch::Executor +use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use Rails::Rack::Logger use ActionDispatch::ShowExceptions +use WebConsole::Middleware use ActionDispatch::DebugExceptions use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending -use ActiveRecord::ConnectionAdapters::ConnectionManagement -use ActiveRecord::QueryCache use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash -use ActionDispatch::ParamsParser use Rack::Head use Rack::ConditionalGet use Rack::ETag @@ -165,9 +148,9 @@ You can add a new middleware to the middleware stack using any of the following # Push Rack::BounceFavicon at the bottom config.middleware.use Rack::BounceFavicon -# Add Lifo::Cache after ActiveRecord::QueryCache. +# Add Lifo::Cache after ActionDispatch::Executor. # Pass { page_cache: false } argument to Lifo::Cache. -config.middleware.insert_after ActiveRecord::QueryCache, Lifo::Cache, page_cache: false +config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false ``` #### Swapping a Middleware @@ -187,18 +170,17 @@ Add the following lines to your application configuration: ```ruby # config/application.rb -config.middleware.delete "Rack::Lock" +config.middleware.delete Rack::Runtime ``` -And now if you inspect the middleware stack, you'll find that `Rack::Lock` is +And now if you inspect the middleware stack, you'll find that `Rack::Runtime` is not a part of it. ```bash -$ bin/rake middleware +$ bin/rails middleware (in /Users/lifo/Rails/blog) use ActionDispatch::Static use # -use Rack::Runtime ... run Rails.application.routes ``` @@ -207,16 +189,16 @@ If you want to remove session related middleware, do the following: ```ruby # config/application.rb -config.middleware.delete "ActionDispatch::Cookies" -config.middleware.delete "ActionDispatch::Session::CookieStore" -config.middleware.delete "ActionDispatch::Flash" +config.middleware.delete ActionDispatch::Cookies +config.middleware.delete ActionDispatch::Session::CookieStore +config.middleware.delete ActionDispatch::Flash ``` And to remove browser related middleware, ```ruby # config/application.rb -config.middleware.delete "Rack::MethodOverride" +config.middleware.delete Rack::MethodOverride ``` ### Internal Middleware Stack @@ -229,12 +211,16 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol **`ActionDispatch::Static`** -* Used to serve static assets. Disabled if `config.serve_static_assets` is `false`. +* Used to serve static files from the public directory. Disabled if `config.public_file_server.enabled` is `false`. **`Rack::Lock`** * Sets `env["rack.multithread"]` flag to `false` and wraps the application within a Mutex. +**`ActionDispatch::Executor`** + +* Used for thread safe code reloading during development. + **`ActiveSupport::Cache::Strategy::LocalCache::Middleware`** * Used for memory caching. This cache is not thread safe. @@ -249,7 +235,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol **`ActionDispatch::RequestId`** -* Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#uuid` method. +* Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#request_id` method. **`Rails::Rack::Logger`** @@ -273,20 +259,12 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol **`ActionDispatch::Callbacks`** -* Runs the prepare callbacks before serving the request. +* Provides callbacks to be executed before and after dispatching the request. **`ActiveRecord::Migration::CheckPending`** * Checks pending migrations and raises `ActiveRecord::PendingMigrationError` if any migrations are pending. -**`ActiveRecord::ConnectionAdapters::ConnectionManagement`** - -* Cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`. - -**`ActiveRecord::QueryCache`** - -* Enables the Active Record query cache. - **`ActionDispatch::Cookies`** * Sets cookies for the request. @@ -299,11 +277,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value. -**`ActionDispatch::ParamsParser`** - -* Parses out parameters from the request into `params`. - -**`ActionDispatch::Head`** +**`Rack::Head`** * Converts HEAD requests to `GET` requests and serves them as so. @@ -324,8 +298,6 @@ Resources * [Official Rack Website](http://rack.github.io) * [Introducing Rack](http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html) -* [Ruby on Rack #1 - Hello Rack!](http://m.onkey.org/ruby-on-rack-1-hello-rack) -* [Ruby on Rack #2 - The Builder](http://m.onkey.org/ruby-on-rack-2-the-builder) ### Understanding Middlewares diff --git a/source/routing.md b/source/routing.md index af8c1bb..86492a9 100644 --- a/source/routing.md +++ b/source/routing.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Rails Routing from the Outside In ================================= @@ -5,18 +7,18 @@ This guide covers the user-facing features of Rails routing. After reading this guide, you will know: -* How to interpret the code in `routes.rb`. +* How to interpret the code in `config/routes.rb`. * How to construct your own routes, using either the preferred resourceful style or the `match` method. -* What parameters to expect an action to receive. +* How to declare route parameters, which are passed onto controller actions. * How to automatically create paths and URLs using route helpers. -* Advanced techniques such as constraints and Rack endpoints. +* Advanced techniques such as creating constraints and mounting Rack endpoints. -------------------------------------------------------------------------------- The Purpose of the Rails Router ------------------------------- -The Rails router recognizes URLs and dispatches them to a controller's action. It can also generate paths and URLs, avoiding the need to hardcode strings in your views. +The Rails router recognizes URLs and dispatches them to a controller's action, or to a Rack application. It can also generate paths and URLs, avoiding the need to hardcode strings in your views. ### Connecting URLs to Code @@ -77,11 +79,13 @@ it asks the router to map it to a controller action. If the first matching route resources :photos ``` -Rails would dispatch that request to the `destroy` method on the `photos` controller with `{ id: '17' }` in `params`. +Rails would dispatch that request to the `destroy` action on the `photos` controller with `{ id: '17' }` in `params`. ### CRUD, Verbs, and Actions -In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to particular CRUD operations in a database. A single entry in the routing file, such as: +In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to +controller actions. By convention, each action also maps to a specific CRUD +operation in a database. A single entry in the routing file, such as: ```ruby resources :photos @@ -138,10 +142,10 @@ Sometimes, you have a resource that clients always look up without referencing a get 'profile', to: 'users#show' ``` -Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action: +Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action but you must also specify the `controller:` to use: ```ruby -get 'profile', to: :show +get 'profile', to: :show, controller: 'users' ``` This resourceful route: @@ -175,6 +179,8 @@ WARNING: A [long-standing bug](https://github.com/rails/rails/issues/1769) preve ```ruby form_for @geocoder, url: geocoder_path do |f| + +# snippet for brevity ``` ### Controller Namespaces and Routing @@ -227,7 +233,7 @@ or, for a single case: resources :articles, path: '/admin/articles' ``` -In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `PostsController`: +In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `ArticlesController`: | HTTP Verb | Path | Controller#Action | Named Helper | | --------- | ------------------------ | -------------------- | ---------------------- | @@ -246,11 +252,11 @@ TIP: _If you need to use a different controller namespace inside a `namespace` b It's common to have resources that are logically children of other resources. For example, suppose your application includes these models: ```ruby -class Magazine < ActiveRecord::Base +class Magazine < ApplicationRecord has_many :ads end -class Ad < ActiveRecord::Base +class Ad < ApplicationRecord belongs_to :magazine end ``` @@ -386,7 +392,7 @@ The comments resource here will have the following routes generated for it: ### Routing concerns -Routing Concerns allows you to declare common routes that can be reused inside other resources and routes. To define a concern: +Routing concerns allow you to declare common routes that can be reused inside other resources and routes. To define a concern: ```ruby concern :commentable do @@ -547,29 +553,23 @@ In particular, simple routing makes it very easy to map legacy URLs to new Rails ### Bound Parameters -When you set up a regular route, you supply a series of symbols that Rails maps to parts of an incoming HTTP request. Two of these symbols are special: `:controller` maps to the name of a controller in your application, and `:action` maps to the name of an action within that controller. For example, consider this route: +When you set up a regular route, you supply a series of symbols that Rails maps to parts of an incoming HTTP request. For example, consider this route: ```ruby -get ':controller(/:action(/:id))' +get 'photos(/:id)', to: :display ``` -If an incoming request of `/photos/show/1` is processed by this route (because it hasn't matched any previous route in the file), then the result will be to invoke the `show` action of the `PhotosController`, and to make the final parameter `"1"` available as `params[:id]`. This route will also route the incoming request of `/photos` to `PhotosController#index`, since `:action` and `:id` are optional parameters, denoted by parentheses. +If an incoming request of `/photos/1` is processed by this route (because it hasn't matched any previous route in the file), then the result will be to invoke the `display` action of the `PhotosController`, and to make the final parameter `"1"` available as `params[:id]`. This route will also route the incoming request of `/photos` to `PhotosController#display`, since `:id` is an optional parameter, denoted by parentheses. ### Dynamic Segments -You can set up as many dynamic segments within a regular route as you like. Anything other than `:controller` or `:action` will be available to the action as part of `params`. If you set up this route: +You can set up as many dynamic segments within a regular route as you like. Any segment will be available to the action as part of `params`. If you set up this route: ```ruby -get ':controller/:action/:id/:user_id' +get 'photos/:id/:user_id', to: 'photos#show' ``` -An incoming path of `/photos/show/1/2` will be dispatched to the `show` action of the `PhotosController`. `params[:id]` will be `"1"`, and `params[:user_id]` will be `"2"`. - -NOTE: You can't use `:namespace` or `:module` with a `:controller` path segment. If you need to do this then use a constraint on :controller that matches the namespace you require. e.g: - -```ruby -get ':controller(/:action(/:id))', controller: /admin\/[^\/]+/ -``` +An incoming path of `/photos/1/2` will be dispatched to the `show` action of the `PhotosController`. `params[:id]` will be `"1"`, and `params[:user_id]` will be `"2"`. TIP: By default, dynamic segments don't accept dots - this is because the dot is used as a separator for formatted routes. If you need to use a dot within a dynamic segment, add a constraint that overrides this – for example, `id: /[^\/]+/` allows anything except a slash. @@ -578,38 +578,40 @@ TIP: By default, dynamic segments don't accept dots - this is because the dot is You can specify static segments when creating a route by not prepending a colon to a fragment: ```ruby -get ':controller/:action/:id/with_user/:user_id' +get 'photos/:id/with_user/:user_id', to: 'photos#show' ``` -This route would respond to paths such as `/photos/show/1/with_user/2`. In this case, `params` would be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`. +This route would respond to paths such as `/photos/1/with_user/2`. In this case, `params` would be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`. ### The Query String The `params` will also include any parameters from the query string. For example, with this route: ```ruby -get ':controller/:action/:id' +get 'photos/:id', to: 'photos#show' ``` -An incoming path of `/photos/show/1?user_id=2` will be dispatched to the `show` action of the `Photos` controller. `params` will be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`. +An incoming path of `/photos/1?user_id=2` will be dispatched to the `show` action of the `Photos` controller. `params` will be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`. ### Defining Defaults -You do not need to explicitly use the `:controller` and `:action` symbols within a route. You can supply them as defaults: +You can define defaults in a route by supplying a hash for the `:defaults` option. This even applies to parameters that you do not specify as dynamic segments. For example: ```ruby -get 'photos/:id', to: 'photos#show' +get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' } ``` -With this route, Rails will match an incoming path of `/photos/12` to the `show` action of `PhotosController`. +Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`. -You can also define other defaults in a route by supplying a hash for the `:defaults` option. This even applies to parameters that you do not specify as dynamic segments. For example: +You can also use `defaults` in a block format to define the defaults for multiple items: ```ruby -get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' } +defaults format: :json do + resources :photos +end ``` -Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`. +NOTE: You cannot override defaults via query parameters - this is for security reasons. The only defaults that can be overridden are dynamic segments via substitution in the URL path. ### Naming Routes @@ -698,6 +700,8 @@ end NOTE: Request constraints work by calling a method on the [Request object](action_controller_overview.html#the-request-object) with the same name as the hash key and then compare the return value with the hash value. Therefore, constraint values should match the corresponding Request object method return type. For example: `constraints: { subdomain: 'api' }` will match an `api` subdomain as expected, however using a symbol `constraints: { subdomain: :api }` will not, because `request.subdomain` returns `'api'` as a String. +NOTE: There is an exception for the `format` constraint: while it's a method on the Request object, it's also an implicit optional parameter on every path. Segment constraints take precedence and the `format` constraint is only applied as such when enforced through a hash. For example, `get 'foo', constraints: { format: 'json' }` will match `GET /foo` because the format is optional by default. However, you can [use a lambda](#advanced-constraints) like in `get 'foo', constraints: lambda { |req| req.format == :json }` and the route will only match explicit JSON requests. + ### Advanced Constraints If you have a more advanced constraint, you can provide an object that responds to `matches?` that Rails should use. Let's say you wanted to route all users on a blacklist to the `BlacklistController`. You could do: @@ -756,7 +760,7 @@ get '*a/foo/*b', to: 'test#index' would match `zoo/woo/foo/bar/baz` with `params[:a]` equals `'zoo/woo'`, and `params[:b]` equals `'bar/baz'`. -NOTE: By requesting `'/foo/bar.json'`, your `params[:pages]` will be equals to `'foo/bar'` with the request format of JSON. If you want the old 3.0.x behavior back, you could supply `format: false` like this: +NOTE: By requesting `'/foo/bar.json'`, your `params[:pages]` will be equal to `'foo/bar'` with the request format of JSON. If you want the old 3.0.x behavior back, you could supply `format: false` like this: ```ruby get '*pages', to: 'pages#show', format: false @@ -789,7 +793,11 @@ get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" } ``` -Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. +Please note that default redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible. You can use the `:status` option to change the response status: + +```ruby +get '/stories/:name', to: redirect('/articles/%{name}', status: 302) +``` In all of these cases, if you don't provide the leading host (`http://www.example.com`), Rails will take those details from the current request. @@ -798,13 +806,28 @@ In all of these cases, if you don't provide the leading host (`http://www.exampl Instead of a String like `'articles#index'`, which corresponds to the `index` action in the `ArticlesController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher: ```ruby -match '/application.js', to: Sprockets, via: :all +match '/application.js', to: MyRackApp, via: :all ``` -As long as `Sprockets` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate. +As long as `MyRackApp` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate. NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application. +If you specify a Rack application as the endpoint for a matcher, remember that +the route will be unchanged in the receiving application. With the following +route your Rack application should expect the route to be '/admin': + +```ruby +match '/admin', to: AdminApp, via: :all +``` + +If you would prefer to have your Rack application receive requests at the root +path instead, use mount: + +```ruby +mount AdminApp, at: '/admin' +``` + ### Using `root` You can specify what Rails should route `'/'` to with the `root` method: @@ -907,7 +930,7 @@ The `:as` option lets you override the normal naming for the named route helpers resources :photos, as: 'images' ``` -will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the :as option to name the helpers. +will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the `:as` option to name the helpers. | HTTP Verb | Path | Controller#Action | Named Helper | | --------- | ---------------- | ----------------- | -------------------- | @@ -1004,7 +1027,7 @@ TIP: If your application has many RESTful routes, using `:only` and `:except` to ### Translated Paths -Using `scope`, we can alter path names generated by resources: +Using `scope`, we can alter path names generated by `resources`: ```ruby scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do @@ -1068,6 +1091,20 @@ edit_videos GET /videos/:identifier/edit(.:format) videos#edit Video.find_by(identifier: params[:identifier]) ``` +You can override `ActiveRecord::Base#to_param` of a related model to construct +a URL: + +```ruby +class Video < ApplicationRecord + def to_param + identifier + end +end + +video = Video.find_by(identifier: "Roman-Holiday") +edit_videos_path(video) # => "/videos/Roman-Holiday" +``` + Inspecting and Testing Routes ----------------------------- @@ -1075,16 +1112,16 @@ Rails offers facilities for inspecting and testing your routes. ### Listing Existing Routes -To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rake routes` command in your terminal to produce the same output. +To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rails routes` command in your terminal to produce the same output. -Both methods will list all of your routes, in the same order that they appear in `routes.rb`. For each route, you'll see: +Both methods will list all of your routes, in the same order that they appear in `config/routes.rb`. For each route, you'll see: * The route name (if any) * The HTTP verb used (if the route doesn't respond to all verbs) * The URL pattern to match * The routing parameters for the route -For example, here's a small section of the `rake routes` output for a RESTful route: +For example, here's a small section of the `rails routes` output for a RESTful route: ``` users GET /users(.:format) users#index @@ -1093,13 +1130,24 @@ For example, here's a small section of the `rake routes` output for a RESTful ro edit_user GET /users/:id/edit(.:format) users#edit ``` -You may restrict the listing to the routes that map to a particular controller setting the `CONTROLLER` environment variable: +You can search through your routes with the grep option: -g. This outputs any routes that partially match the URL helper method name, the HTTP verb, or the URL path. -```bash -$ CONTROLLER=users bin/rake routes +``` +$ bin/rails routes -g new_comment +$ bin/rails routes -g POST +$ bin/rails routes -g admin +``` + +If you only want to see the routes that map to a specific controller, there's the -c option. + +``` +$ bin/rails routes -c users +$ bin/rails routes -c admin/users +$ bin/rails routes -c Comments +$ bin/rails routes -c Articles::CommentsController ``` -TIP: You'll find that the output from `rake routes` is much more readable if you widen your terminal window until the output lines don't wrap. +TIP: You'll find that the output from `rails routes` is much more readable if you widen your terminal window until the output lines don't wrap. ### Testing Routes diff --git a/source/ruby_on_rails_guides_guidelines.md b/source/ruby_on_rails_guides_guidelines.md index 6206b3c..de63e19 100644 --- a/source/ruby_on_rails_guides_guidelines.md +++ b/source/ruby_on_rails_guides_guidelines.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails Guides Guidelines =============================== @@ -48,12 +50,55 @@ Use the same inline formatting as regular text: ##### The `:content_type` Option ``` +Linking to the API +------------------ + +Links to the API (`api.rubyonrails.org`) are processed by the guides generator in the following manner: + +Links that include a release tag are left untouched. For example + +``` +http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html +``` + +is not modified. + +Please use these in release notes, since they should point to the corresponding version no matter the target being generated. + +If the link does not include a release tag and edge guides are being generated, the domain is replaced by `edgeapi.rubyonrails.org`. For example, + +``` +http://api.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +becomes + +``` +http://edgeapi.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +If the link does not include a release tag and release guides are being generated, the Rails version is injected. For example, if we are generating the guides for v5.1.0 the link + +``` +http://api.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +becomes + +``` +http://api.rubyonrails.org/v5.1.0/classes/ActionDispatch/Response.html +``` + +Please don't link to `edgeapi.rubyonrails.org` manually. + + API Documentation Guidelines ---------------------------- The guides and the API should be coherent and consistent where appropriate. In particular, these sections of the [API Documentation Guidelines](api_documentation_guidelines.html) also apply to the guides: * [Wording](api_documentation_guidelines.html#wording) +* [English](api_documentation_guidelines.html#english) * [Example Code](api_documentation_guidelines.html#example-code) * [Filenames](api_documentation_guidelines.html#file-names) * [Fonts](api_documentation_guidelines.html#fonts) @@ -61,7 +106,9 @@ The guides and the API should be coherent and consistent where appropriate. In p HTML Guides ----------- -Before generating the guides, make sure that you have the latest version of Bundler installed on your system. As of this writing, you must install Bundler 1.3.5 on your device. +Before generating the guides, make sure that you have the latest version of +Bundler installed on your system. As of this writing, you must install Bundler +1.3.5 or later on your device. To install the latest version of Bundler, run `gem install bundler`. @@ -79,6 +126,8 @@ or bundle exec rake guides:generate:html ``` +Resulting HTML files can be found in the `./output` directory. + To process `my_guide.md` and nothing else use the `ONLY` environment variable: ``` @@ -90,8 +139,6 @@ By default, guides that have not been modified are not processed, so `ONLY` is r To force processing all the guides, pass `ALL=1`. -It is also recommended that you work with `WARNINGS=1`. This detects duplicate IDs and warns about broken internal links. - If you want to generate guides in a language other than English, you can keep them in a separate directory under `source` (eg. `source/es`) and use the `GUIDES_LANGUAGE` environment variable: ``` diff --git a/source/security.md b/source/security.md index ebfcc5b..a14134f 100644 --- a/source/security.md +++ b/source/security.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Ruby on Rails Security Guide ============================ @@ -21,7 +23,7 @@ Web application frameworks are made to help developers build web applications. S In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server and the web application itself (and possibly other layers or applications). -The Gartner Group however estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person. +The Gartner Group, however, estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person. The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at. @@ -39,28 +41,28 @@ NOTE: _HTTP is a stateless protocol. Sessions make it stateful._ Most applications need to keep track of certain state of a particular user. This could be the contents of a shopping basket or the user id of the currently logged in user. Without the idea of sessions, the user would have to identify, and probably authenticate, on every request. Rails will create a new session automatically if a new user accesses the application. It will load an existing session if the user has already used the application. -A session usually consists of a hash of values and a session id, usually a 32-character string, to identify the hash. Every cookie sent to the client's browser includes the session id. And the other way round: the browser will send it to the server on every request from the client. In Rails you can save and retrieve values using the session method: +A session usually consists of a hash of values and a session ID, usually a 32-character string, to identify the hash. Every cookie sent to the client's browser includes the session ID. And the other way round: the browser will send it to the server on every request from the client. In Rails you can save and retrieve values using the session method: ```ruby session[:user_id] = @current_user.id User.find(session[:user_id]) ``` -### Session id +### Session ID -NOTE: _The session id is a 32 byte long MD5 hash value._ +NOTE: _The session ID is a 32-character random hex string._ -A session id consists of the hash value of a random string. The random string is the current time, a random number between 0 and 1, the process id number of the Ruby interpreter (also basically a random number) and a constant string. Currently it is not feasible to brute-force Rails' session ids. To date MD5 is uncompromised, but there have been collisions, so it is theoretically possible to create another input text with the same hash value. But this has had no security impact to date. +The session ID is generated using `SecureRandom.hex` which generates a random hex string using platform specific methods (such as OpenSSL, /dev/urandom or Win32) for generating cryptographically secure random numbers. Currently it is not feasible to brute-force Rails' session IDs. ### Session Hijacking -WARNING: _Stealing a user's session id lets an attacker use the web application in the victim's name._ +WARNING: _Stealing a user's session ID lets an attacker use the web application in the victim's name._ -Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session id in the cookie identifies the session. +Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session ID in the cookie identifies the session. Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user - with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures: -* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: +* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN, it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: ```ruby config.force_ssl = true @@ -87,13 +89,20 @@ This will also be a good idea, if you modify the structure of an object and old NOTE: _Rails provides several storage mechanisms for the session hashes. The most important is `ActionDispatch::Session::CookieStore`._ -Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session id. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it: +Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session ID. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it: * Cookies imply a strict size limit of 4kB. This is fine as you should not store large amounts of data in a session anyway, as described before. _Storing the current user's database id in a session is usually ok_. -* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret and inserted into the end of the cookie. +* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie. + +However, since Rails 4, the default store is EncryptedCookieStore. With +EncryptedCookieStore the session is encrypted before being stored in a cookie. +This prevents the user from accessing and tampering the content of the cookie. +Thus the session becomes a more secure place to store data. The encryption is +done using a server-side secret key `secrets.secret_key_base` stored in +`config/secrets.yml`. -That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_. +That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters, use `rails secret` instead_. `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: @@ -118,26 +127,26 @@ It works like this: * A user receives credits, the amount is stored in a session (which is a bad idea anyway, but we'll do this for demonstration purposes). * The user buys something. -* Their new, lower credit will be stored in the session. -* The dark side of the user forces them to take the cookie from the first step (which they copied) and replace the current cookie in the browser. -* The user has their credit back. +* The new adjusted credit value is stored in the session. +* The user takes the cookie from the first step (which they previously copied) and replaces the current cookie in the browser. +* The user has their original credit back. -Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers (mongrels). Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database). +Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers. Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database). The best _solution against it is not to store this kind of data in a session, but in the database_. In this case store the credit in the database and the logged_in_user_id in the session. ### Session Fixation -NOTE: _Apart from stealing a user's session id, the attacker may fix a session id known to them. This is called session fixation._ +NOTE: _Apart from stealing a user's session ID, the attacker may fix a session ID known to them. This is called session fixation._ ![Session fixation](images/session_fixation.png) -This attack focuses on fixing a user's session id known to the attacker, and forcing the user's browser into using this id. It is therefore not necessary for the attacker to steal the session id afterwards. Here is how this attack works: +This attack focuses on fixing a user's session ID known to the attacker, and forcing the user's browser into using this ID. It is therefore not necessary for the attacker to steal the session ID afterwards. Here is how this attack works: -* The attacker creates a valid session id: They load the login page of the web application where they want to fix the session, and take the session id in the cookie from the response (see number 1 and 2 in the image). +* The attacker creates a valid session ID: They load the login page of the web application where they want to fix the session, and take the session ID in the cookie from the response (see number 1 and 2 in the image). * They maintain the session by accessing the web application periodically in order to keep an expiring session alive. -* The attacker forces the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: ``. Read more about XSS and injection later on. -* The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session id to the trap session id. +* The attacker forces the user's browser into using this session ID (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: ``. Read more about XSS and injection later on. +* The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session ID to the trap session ID. * As the new trap session is unused, the web application will require the user to authenticate. * From now on, the victim and the attacker will co-use the web application with the same session: The session became valid and the victim didn't notice the attack. @@ -151,7 +160,7 @@ The most effective countermeasure is to _issue a new session identifier_ and dec reset_session ``` -If you use the popular RestfulAuthentication plugin for user management, add reset_session to the SessionsController#create action. Note that this removes any value from the session, _you have to transfer them to the new session_. +If you use the popular [Devise](https://rubygems.org/gems/devise) gem for user management, it will automatically expire sessions on sign in and sign out for you. If you roll your own, remember to expire the session after your sign in action (when the session is created). This will remove values from the session, therefore _you will have to transfer them to the new session_. Another countermeasure is to _save user-specific properties in the session_, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. _These might change over the course of a session_, so these users will not be able to use your application, or only in a limited way. @@ -159,10 +168,10 @@ Another countermeasure is to _save user-specific properties in the session_, ver NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation._ -One possibility is to set the expiry time-stamp of the cookie with the session id. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago. +One possibility is to set the expiry time-stamp of the cookie with the session ID. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago. ```ruby -class Session < ActiveRecord::Base +class Session < ApplicationRecord def self.sweep(time = 1.hour) if time.is_a?(String) time = time.split.inject { |count, unit| count.to_i.send(unit) } @@ -187,13 +196,12 @@ This attack method works by including malicious code or a link in a page that ac ![](images/csrf.png) -In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example: +In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session ID in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is that if the request comes from a site of a different domain, it will also send the cookie. Let's start with an example: -* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file. -* `` -* Bob's session at www.webapp.com is still alive, because he didn't log out a few minutes ago. -* By viewing the post, the browser finds an image tag. It tries to load the suspected image from www.webapp.com. As explained before, it will also send along the cookie with the valid session id. -* The web application at www.webapp.com verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image. +* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file: `` +* Bob's session at `www.webapp.com` is still alive, because he didn't log out a few minutes ago. +* By viewing the post, the browser finds an image tag. It tries to load the suspected image from `www.webapp.com`. As explained before, it will also send along the cookie with the valid session ID. +* The web application at `www.webapp.com` verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image. * Bob doesn't notice the attack - but a few days later he finds out that project number one is gone. It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post or email. @@ -216,9 +224,9 @@ The HTTP protocol basically provides two main types of requests - GET and POST ( * The interaction _changes the state_ of the resource in a way that the user would perceive (e.g., a subscription to a service), or * The user is _held accountable for the results_ of the interaction. -If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Most of today's web browsers, however do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier. +If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Most of today's web browsers, however, do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier. -_POST requests can be sent automatically, too_. Here is an example for a link which displays www.harmless.com as destination in the browser's status bar. In fact it dynamically creates a new form that sends a POST request. +_POST requests can be sent automatically, too_. In this example, the link www.harmless.com is shown as the destination in the browser's status bar. But it has actually dynamically created a new form that sends a POST request. ```html ``` -There are many other possibilities, like using a ` - - -``` - -#### `register_stylesheet_expansion` - -这个方法注册一到多个样式表文件,把 Symbol 传给 `stylesheet_link_tag` 方法时,会引入相应的文件。这个方法经常用在插件的初始化代码中,注册保存在 `vendor/assets/stylesheets` 文件夹中的样式表文件。 +`auto_discovery_link_tag` 方法用于返回链接标签,使浏览器和订阅阅读器可以自动检测 RSS 或 Atom 订阅源。 ```ruby -ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion monkey: ["head", "body", "tail"] - -stylesheet_link_tag :monkey # => - - - +auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) +# => ``` -#### `auto_discovery_link_tag` + -返回一个 `link` 标签,浏览器和 Feed 阅读器用来自动检测 RSS 或 Atom Feed。 +#### `image_path` 方法 -```ruby -auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # => - -``` - -#### `image_path` - -生成 `app/assets/images` 文件夹中所存图片的地址。得到的地址是从根目录到图片的完整路径。用于 `image_tag` 方法,获取图片的路径。 +`image_path` 方法用于计算 `app/assets/images` 文件夹中图像资源的路径,得到的路径是从根目录开始的完整路径(也就是绝对路径)。`image_tag` 方法在内部使用 `image_path` 方法生成图像路径。 ```ruby image_path("edit.png") # => /assets/edit.png ``` -如果 `config.assets.digest` 选项为 `true`,图片文件名后会加上指纹码。 +当 `config.assets.digest` 选项设置为 `true` 时,Rails 会为图像资源的文件名添加指纹。 ```ruby image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png ``` -#### `image_url` + + +#### `image_url` 方法 -生成 `app/assets/images` 文件夹中所存图片的 URL 地址。`image_url` 会调用 `image_path`,然后加上程序的主机地址或静态文件的主机地址。 +`image_url` 方法用于计算 `app/assets/images` 文件夹中图像资源的 URL 地址。`image_url` 方法在内部调用了 `image_path` 方法,并把得到的图像资源路径和当前主机或静态资源文件服务器的 URL 地址合并。 ```ruby image_url("https://github.com/edit.png") # => http://www.example.com/assets/edit.png ``` -#### `image_tag` + + +#### `image_tag` 方法 -生成图片的 HTML `image` 标签。图片的地址可以是完整的 URL,或者 `app/assets/images` 文件夹中的图片。 +`image_tag` 方法用于返回 HTML 图像标签。此方法接受图像的完整路径或 `app/assets/images` 文件夹中图像的文件名作为参数。 ```ruby image_tag("icon.png") # => Icon ``` -#### `javascript_include_tag` + -为指定的每个资源生成 HTML `script` 标签。可以传入 `app/assets/javascripts` 文件夹中所存 JavaScript 文件的文件名(扩展名 `.js` 可加可不加),或者可以使用相对文件根目录的完整路径。 +#### `javascript_include_tag` 方法 + +`javascript_include_tag` 方法用于返回 HTML 脚本标签。此方法接受 `app/assets/javascripts` 文件夹中 JavaScript 文件的文件名(`.js` 后缀可以省略)或 JavaScript 文件的完整路径(绝对路径)作为参数。 ```ruby javascript_include_tag "common" # => ``` -如果程序不使用 Asset Pipeline,要想引入 jQuery,可以传入 `:default`。使用 `:default` 时,如果 `app/assets/javascripts` 文件夹中存在 `application.js` 文件,也会将其引入。 +如果 Rails 应用不使用 Asset Pipeline,就需要向 `javascript_include_tag` 方法传递 `:defaults` 参数来包含 jQuery JavaScript 库。此时,如果 `app/assets/javascripts` 文件夹中存在 `application.js` 文件,那么这个文件也会包含到页面中。 ```ruby javascript_include_tag :defaults ``` -还可以使用 `:all` 引入 `app/assets/javascripts` 文件夹中所有的 JavaScript 文件。 +通过向 `javascript_include_tag` 方法传递 `:all` 参数,可以把 `app/assets/javascripts` 文件夹下的所有 JavaScript 文件包含到页面中。 ```ruby javascript_include_tag :all ``` -多个 JavaScript 文件还可合并成一个文件,减少 HTTP 连接数,还可以使用 gzip 压缩(提升传输速度)。只有 `ActionController::Base.perform_caching` 为 `true`(生产环境的默认值,开发环境为 `false`)时才会合并文件。 +我们还可以把多个 JavaScript 文件缓存为一个文件,这样可以减少下载时的 HTTP 连接数,同时还可以启用 gzip 压缩来提高传输速度。当 `ActionController::Base.perform_caching` 选项设置为 `true` 时才会启用缓存,此选项在生产环境下默认为 `true`,在开发环境下默认为 `false`。 ```ruby -javascript_include_tag :all, cache: true # => - +javascript_include_tag :all, cache: true +# => ``` -#### `javascript_path` + + +#### `javascript_path` 方法 -生成 `app/assets/javascripts` 文件夹中 JavaScript 文件的地址。如果没指定文件的扩展名,会自动加上 `.js`。参数也可以使用相对文档根路径的完整地址。这个方法在 `javascript_include_tag` 中调用,用来生成脚本的地址。 +`javascript_path` 方法用于计算 `app/assets/javascripts` 文件夹中 JavaScript 资源的路径。如果没有指定文件的扩展名,Rails 会自动添加 `.js`。`javascript_path` 方法返回 JavaScript 资源的完整路径(绝对路径)。`javascript_include_tag` 方法在内部使用 `javascript_path` 方法生成脚本路径。 ```ruby javascript_path "common" # => /assets/common.js ``` -#### `javascript_url` + -生成 `app/assets/javascripts` 文件夹中 JavaScript 文件的 URL 地址。这个方法调用 `javascript_path`,然后再加上当前程序的主机地址或静态资源文件的主机地址。 +#### `javascript_url` 方法 + +`javascript_url` 方法用于计算 `app/assets/javascripts` 文件夹中 JavaScript 资源的 URL 地址。`javascript_url` 方法在内部调用了 `javascript_path` 方法,并把得到的 JavaScript 资源的路径和当前主机或静态资源文件服务器的 URL 地址合并。 ```ruby javascript_url "common" # => http://www.example.com/assets/common.js ``` -#### `stylesheet_link_tag` + + +#### `stylesheet_link_tag` 方法 -返回指定资源的样式表 `link` 标签。如果没提供扩展名,会自动加上 `.css`。 +`stylesheet_link_tag` 方法用于返回样式表链接标签。如果没有指定文件的扩展名,Rails 会自动添加 `.css`。 ```ruby -stylesheet_link_tag "application" # => +stylesheet_link_tag "application" +# => ``` -还可以使用 `:all`,引入 `app/assets/stylesheets` 文件夹中的所有样式表。 +通过向 `stylesheet_link_tag` 方法传递 `:all` 参数,可以把样式表文件夹中的所有样式表包含到页面中。 ```ruby stylesheet_link_tag :all ``` -多个样式表还可合并成一个文件,减少 HTTP 连接数,还可以使用 gzip 压缩(提升传输速度)。只有 `ActionController::Base.perform_caching` 为 `true`(生产环境的默认值,开发环境为 `false`)时才会合并文件。 +我们还可以把多个样式表缓存为一个文件,这样可以减少下载时的 HTTP 连接数,同时还可以启用 gzip 压缩来提高传输速度。当 `ActionController::Base.perform_caching` 选项设置为 `true` 时才会启用缓存,此选项在生产环境下默认为 `true`,在开发环境下默认为 `false`。 ```ruby stylesheet_link_tag :all, cache: true # => ``` -#### `stylesheet_path` + -生成 `app/assets/stylesheets` 文件夹中样式表的地址。如果没指定文件的扩展名,会自动加上 `.css`。参数也可以使用相对文档根路径的完整地址。这个方法在 `stylesheet_link_tag` 中调用,用来生成样式表的地址。 +#### `stylesheet_path` 方法 + +`stylesheet_path` 方法用于计算 `app/assets/stylesheets` 文件夹中样式表资源的路径。如果没有指定文件的扩展名,Rails 会自动添加 `.css`。`stylesheet_path` 方法返回样式表资源的完整路径(绝对路径)。`stylesheet_link_tag` 方法在内部使用 `stylesheet_path` 方法生成样式表路径。 ```ruby stylesheet_path "application" # => /assets/application.css ``` -#### `stylesheet_url` + + +#### `stylesheet_url` 方法 -生成 `app/assets/stylesheets` 文件夹中样式表的 URL 地址。这个方法调用 `stylesheet_path`,然后再加上当前程序的主机地址或静态资源文件的主机地址。 +`stylesheet_url` 方法用于计算 `app/assets/stylesheets` 文件夹中样式表资源的 URL 地址。`stylesheet_url` 方法在内部调用了 `stylesheet_path` 方法,并把得到的样式表资源路径和当前主机或静态资源文件服务器的 URL 地址合并。 ```ruby stylesheet_url "application" # => http://www.example.com/assets/application.css ``` -### `AtomFeedHelper` + + +### `AtomFeedHelper` 模块 -#### `atom_feed` + -这个帮助方法可以简化生成 Atom Feed 的过程。下面是个完整的示例: +#### `atom_feed` 方法 + +通过 `atom_feed` 辅助方法我们可以轻松创建 Atom 订阅源。下面是一个完整的示例: + +`config/routes.rb` ```ruby -resources :posts +resources :articles ``` +`app/controllers/articles_controller.rb` + ```ruby def index - @posts = Post.all + @articles = Article.all respond_to do |format| format.html @@ -592,29 +604,35 @@ def index end ``` +`app/views/articles/index.atom.builder` + ```ruby atom_feed do |feed| - feed.title("Posts Index") - feed.updated((@posts.first.created_at)) + feed.title("Articles Index") + feed.updated(@articles.first.created_at) - @posts.each do |post| - feed.entry(post) do |entry| - entry.title(post.title) - entry.content(post.body, type: 'html') + @articles.each do |article| + feed.entry(article) do |entry| + entry.title(article.title) + entry.content(article.body, type: 'html') entry.author do |author| - author.name(post.author_name) + author.name(article.author_name) end end end end ``` -### `BenchmarkHelper` + + +### `BenchmarkHelper` 模块 + + -#### `benchmark` +#### `benchmark` 方法 -这个方法可以计算模板中某个代码块的执行时间,然后把结果写入日志。可以把耗时的操作或瓶颈操作放入 `benchmark` 代码块中,查看此项操作使用的时间。 +`benchmark` 方法用于测量模板中某个块的执行时间,并把测量结果写入日志。`benchmark` 方法常用于测量耗时操作或可能的性能瓶颈的执行时间。 ```erb <% benchmark "Process data files" do %> @@ -622,13 +640,17 @@ end <% end %> ``` -上述代码会在日志中写入类似“Process data files (0.34523)”的文本,可用来对比优化前后的时间。 +上面的代码会在日志中写入类似 `Process data files (0.34523)` 的测量结果,我们可以通过比较执行时间来优化代码。 -### `CacheHelper` + -#### `cache` +### `CacheHelper` 模块 -这个方法缓存视图片段,而不是整个动作或页面。常用来缓存目录,新话题列表,静态 HTML 片段等。此方法接受一个代码块,即要缓存的内容。详情参见 `ActionController::Caching::Fragments` 模块的文档。 + + +#### `cache` 方法 + +`cache` 方法用于缓存视图片断而不是整个动作或页面。此方法常用于缓存页面中诸如菜单、新闻主题列表、静态 HTML 片断等内容。`cache` 方法接受块作为参数,块中包含要缓存的内容。关于 `cache` 方法的更多介绍,请参阅 `AbstractController::Caching::Fragments` 模块的文档。 ```erb <% cache do %> @@ -636,11 +658,15 @@ end <% end %> ``` -### `CaptureHelper` + + +### `CaptureHelper` 模块 + + -#### `capture` +#### `capture` 方法 -`capture` 方法可以把视图中的一段代码赋值给一个变量,这个变量可以在任何模板或视图中使用。 +`capture` 方法用于取出模板的一部分并储存在变量中,然后我们可以在模板或布局中的任何地方使用这个变量。 ```erb <% @greeting = capture do %> @@ -648,7 +674,7 @@ end <% end %> ``` -`@greeting` 变量可以在任何地方使用。 +可以在模板或布局中的任何地方使用 `@greeting` 变量。 ```erb @@ -661,11 +687,15 @@ end ``` -#### `content_for` + -`content_for` 方法用一个标记符表示一段代码,在其他模板或布局中,可以把这个标记符传给 `yield` 方法,调用这段代码。 +#### `content_for` 方法 -例如,程序有个通用的布局,但还有一个特殊页面,用到了其他页面不需要的 JavaScript 文件,此时就可以在这个特殊的页面中使用 `content_for` 方法,在不影响其他页面的情况下,引入所需的 JavaScript。 +`content_for` 方法以块的方式把模板内容保存在标识符中,然后我们可以在模板或布局中把这个标识符传递给 `yield` 方法作为参数来调用所保存的内容。 + +假如应用拥有标准布局,同时拥有一个特殊页面,这个特殊页面需要包含其他页面都不需要的 JavaScript 脚本。为此我们可以在这个特殊页面中使用 `content_for` 方法来包含所需的 JavaScript 脚本,而不必增加其他页面的体积。 + +`app/views/layouts/application.html.erb` ```erb @@ -679,6 +709,8 @@ end ``` +`app/views/articles/special.html.erb` + ```erb

This is a special page.

@@ -687,149 +719,181 @@ end <% end %> ``` -### `DateHelper` + -#### `date_select` +### `DateHelper` 模块 -这个方法会生成一组选择列表,分别对应年月日,用来设置日期相关的属性。 + + +#### `date_select` 方法 + +`date_select` 方法返回年、月、日的选择列表标签,用于设置 `date` 类型的属性的值。 ```ruby -date_select("post", "published_on") +date_select("article", "published_on") ``` -#### `datetime_select` + + +#### `datetime_select` 方法 -这个方法会生成一组选择列表,分别对应年月日时分,用来设置日期和时间相关的属性。 +`datetime_select` 方法返回年、月、日、时、分的选择列表标签,用于设置 `datetime` 类型的属性的值。 ```ruby -datetime_select("post", "published_on") +datetime_select("article", "published_on") ``` -#### `distance_of_time_in_words` + -这个方法会计算两个时间、两个日期或两个秒数之间的近似间隔。如果想得到更精准的间隔,可以把 `include_seconds` 选项设为 `true`。 +#### `distance_of_time_in_words` 方法 + +`distance_of_time_in_words` 方法用于计算两个 `Time` 对象、`Date` 对象或秒数的大致时间间隔。把 `include_seconds` 选项设置为 `true` 可以得到更精确的时间间隔。 ```ruby distance_of_time_in_words(Time.now, Time.now + 15.seconds) # => less than a minute distance_of_time_in_words(Time.now, Time.now + 15.seconds, include_seconds: true) # => less than 20 seconds ``` -#### `select_date` + + +#### `select_date` 方法 -返回一组 HTML 选择列表标签,分别对应年月日,并且选中指定的日期。 +`select_date` 方法返回年、月、日的选择列表标签,并通过 `Date` 对象来设置默认值。 ```ruby -# Generates a date select that defaults to the date provided (six days after today) +# 生成一个日期选择列表,默认选中指定的日期(六天以后) select_date(Time.today + 6.days) -# Generates a date select that defaults to today (no specified date) +# 生成一个日期选择列表,默认选中今天(未指定日期) select_date() ``` -#### `select_datetime` + + +#### `select_datetime` 方法 -返回一组 HTML 选择列表标签,分别对应年月日时分,并且选中指定的日期和时间。 +`select_datetime` 方法返回年、月、日、时、分的选择列表标签,并通过 `Datetime` 对象来设置默认值。 ```ruby -# Generates a datetime select that defaults to the datetime provided (four days after today) +# 生成一个日期时间选择列表,默认选中指定的日期时间(四天以后) select_datetime(Time.now + 4.days) -# Generates a datetime select that defaults to today (no specified datetime) +# 生成一个日期时间选择列表,默认选中今天(未指定日期时间) select_datetime() ``` -#### `select_day` + -返回一个选择列表标签,其选项是当前月份的每一天,并且选中当日。 +#### `select_day` 方法 + +`select_day` 方法返回当月全部日子的选择列表标签,如 1 到 31,并把当日设置为默认值。 ```ruby -# Generates a select field for days that defaults to the day for the date provided +# 生成一个日子选择列表,默认选中指定的日子 select_day(Time.today + 2.days) -# Generates a select field for days that defaults to the number given +# 生成一个日子选择列表,默认选中指定数字对应的日子 select_day(5) ``` -#### `select_hour` + + +#### `select_hour` 方法 -返回一个选择列表标签,其选项是一天中的每一个小时(0-23),并且选中当前的小时数。 +`select_hour` 方法返回一天中 24 小时的选择列表标签,即 0 到 23,并把当前小时设置为默认值。 ```ruby -# Generates a select field for hours that defaults to the hours for the time provided +# 生成一个小时选择列表,默认选中指定的小时 select_hour(Time.now + 6.hours) ``` -#### `select_minute` + -返回一个选择列表标签,其选项是一小时中的每一分钟(0-59),并且选中当前的分钟数。 +#### `select_minute` 方法 + +`select_minute` 方法返回一小时中 60 分钟的选择列表标签,即 0 到 59,并把当前分钟设置为默认值。 ```ruby -# Generates a select field for minutes that defaults to the minutes for the time provided. -select_minute(Time.now + 6.hours) +# 生成一个分钟选择列表,默认选中指定的分钟 +select_minute(Time.now + 10.minutes) ``` -#### `select_month` + + +#### `select_month` 方法 -返回一个选择列表标签,其选项是一年之中的所有月份(“January”-“December”),并且选中当前月份。 +`select_month` 方法返回一年中 12 个月的选择列表标签,并把当月设置为默认值。 ```ruby -# Generates a select field for months that defaults to the current month +# 生成一个月份选择列表,默认选中当前月份 select_month(Date.today) ``` -#### `select_second` + -返回一个选择列表标签,其选项是一分钟内的各秒数(0-59),并且选中当前时间的秒数。 +#### `select_second` 方法 + +`select_second` 方法返回一分钟中 60 秒的选择列表标签,即 0 到 59,并把当前秒设置为默认值。 ```ruby -# Generates a select field for seconds that defaults to the seconds for the time provided -select_second(Time.now + 16.minutes) +# 生成一个秒数选择列表,默认选中指定的秒数 +select_second(Time.now + 16.seconds) ``` -#### `select_time` + + +#### `select_time` 方法 -返回一组 HTML 选择列表标签,分别对应小时和分钟。 +`select_time` 方法返回时、分的选择列表标签,并通过 `Time` 对象来设置默认值。 ```ruby -# Generates a time select that defaults to the time provided +# 生成一个时间选择列表,默认选中指定的时间 select_time(Time.now) ``` -#### `select_year` + + +#### `select_year` 方法 -返回一个选择列表标签,其选项是今年前后各五年,并且选择今年。年份的前后范围可使用 `:start_year` 和 `:end_year` 选项指定。 +`select_year` 方法返回当年和前后各五年的选择列表标签,并把当年设置为默认值。可以通过 `:start_year` 和 `:end_year` 选项自定义年份范围。 ```ruby -# Generates a select field for five years on either side of Date.today that defaults to the current year +# 选择今天所在年份前后五年的年份选择列表,默认选中当年 select_year(Date.today) -# Generates a select field from 1900 to 2009 that defaults to the current year +# 选择一个从 1900 年到 20009 年的年份选择列表,默认选中当年 select_year(Date.today, start_year: 1900, end_year: 2009) ``` -#### `time_ago_in_words` + -和 `distance_of_time_in_words` 方法作用类似,但是后一个时间点固定为当前时间(`Time.now`)。 +#### `time_ago_in_words` 方法 + +`time_ago_in_words` 方法和 `distance_of_time_in_words` 方法类似,区别在于 `time_ago_in_words` 方法计算的是指定时间到 `Time.now` 对应的当前时间的时间间隔。 ```ruby time_ago_in_words(3.minutes.from_now) # => 3 minutes ``` -#### `time_select` + + +#### `time_select` 方法 -返回一组选择列表标签,分别对应小时和分钟,秒数是可选的,用来设置基于时间的属性。选中的值会作为多个参数赋值给 Active Record 对象。 +`time_select` 方返回时、分、秒的选择列表标签(其中秒可选),用于设置 `time` 类型的属性的值。选择的结果作为多个参数赋值给 Active Record 对象。 ```ruby -# Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute +# 生成一个时间选择标签,通过 POST 发送后存储在提交的属性中的 order 变量中 time_select("order", "submitted") ``` -### `DebugHelper` + + +### `DebugHelper` 模块 -返回一个 `pre` 标签,以 YAML 格式显示对象。用这种方法审查对象,可读性极高。 +`debug` 方法返回放在 `pre` 标签里的 YAML 格式的对象内容。这种审查对象的方式可读性很好。 ```ruby -my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]} +my_hash = { 'first' => 1, 'second' => 'two', 'third' => [1,2,3] } debug(my_hash) ``` @@ -844,24 +908,26 @@ third: ``` -### `FormHelper` + -表单帮助方法的目的是替代标准的 HTML 元素,简化处理模型的过程。`FormHelper` 模块提供了很多方法,基于模型创建表单,不单可以生成表单的 HTML 标签,还能生成各种输入框标签,例如文本输入框,密码输入框,选择列表等。提交表单后(用户点击提交按钮,或者在 JavaScript 中调用 `form.submit`),其输入框中的值会存入 `params` 对象,传给控制器。 +### `FormHelper` 模块 -表单帮助方法分为两类,一种专门处理模型,另一种则不是。前者处理模型的属性;后者不处理模型属性,详情参见 `ActionView::Helpers::FormTagHelper` 模块的文档。 +和仅使用标准 HTML 元素相比,表单辅助方法提供了一组基于模型创建表单的方法,可以大大简化模型的处理过程。表单辅助方法生成表单的 HTML 代码,并提供了用于生成各种输入组件(如文本框、密码框、选择列表等)的 HTML 代码的辅助方法。在提交表单时(用户点击提交按钮或通过 JavaScript 调用 `form.submit`),表单输入会绑定到 `params` 对象上并回传给控制器。 -`FormHelper` 模块的核心是 `form_for` 方法,生成处理模型实例的表单。例如,有个名为 `Person` 的模型,要创建一个新实例,可使用下面的代码实现: +表单辅助方法分为两类:一类专门用于处理模型属性,另一类不处理模型属性。本节中介绍的辅助方法都属于前者,后者的例子可参阅 `ActionView::Helpers::FormTagHelper` 模块的文档。 + +`form_for` 辅助方法是 `FormHelper` 模块中最核心的方法,用于创建处理模型实例的表单。例如,假设我们想为 `Person` 模型创建实例: ```erb -# Note: a @person variable will have been created in the controller (e.g. @person = Person.new) -<%= form_for @person, url: {action: "create"} do |f| %> +# 注意:要在控制器中创建 @person 变量(例如 @person = Person.new) +<%= form_for @person, url: { action: "create" } do |f| %> <%= f.text_field :first_name %> <%= f.text_field :last_name %> <%= submit_tag 'Create' %> <% end %> ``` -生成的 HTML 如下: +上面的代码会生成下面的 HTML: ```html
@@ -871,31 +937,35 @@ third:
``` -表单提交后创建的 `params` 对象如下: +提交表单时创建的 `params` 对象会像下面这样: ```ruby -{"action" => "create", "controller" => "people", "person" => {"first_name" => "William", "last_name" => "Smith"}} +{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } } ``` -`params` 中有个嵌套 Hash `person`,在控制器中使用 `params[:person]` 获取。 +`params` 散列包含了嵌套的 `person` 值,这个值可以在控制器中通过 `params[:person]` 访问。 + + -#### `check_box` +#### `check_box` 方法 -返回一个复选框标签,处理指定的属性。 +`check_box` 方法返回用于处理指定模型属性的复选框标签。 ```ruby -# Let's say that @post.validated? is 1: -check_box("post", "validated") -# => -# +# 假设 @article.validated? 的值是 1 +check_box("article", "validated") +# => +# ``` -#### `fields_for` + + +#### `fields_for` 方法 -类似 `form_for`,为指定的模型创建一个作用域,但不会生成 `form` 标签。特别适合在同一个表单中处理多个模型。 +和 `form_for` 方法类似,`fields_for` 方法创建用于处理指定模型对象的作用域,区别在于 `fields_for` 方法不会创建 `form` 标签。`fields_for` 方法适用于在同一个表单中指明附加的模型对象。 ```erb -<%= form_for @person, url: {action: "update"} do |person_form| %> +<%= form_for @person, url: { action: "update" } do |person_form| %> First name: <%= person_form.text_field :first_name %> Last name : <%= person_form.text_field :last_name %> @@ -905,21 +975,25 @@ check_box("post", "validated") <% end %> ``` -#### `file_field` + -返回一个文件上传输入框,处理指定的属性。 +#### `file_field` 方法 + +`file_field` 方法返回用于处理指定模型属性的文件上传组件标签。 ```ruby file_field(:user, :avatar) # => ``` -#### `form_for` + + +#### `form_for` 方法 -为指定的模型创建一个表单和作用域,表单中各字段的值都通过这个模型获取。 +`form_for` 方法创建用于处理指定模型对象的表单和作用域,表单的各个组件用于处理模型对象的对应属性。 ```erb -<%= form_for @post do |f| %> +<%= form_for @article do |f| %> <%= f.label :title, 'Title' %>: <%= f.text_field :title %>
<%= f.label :body, 'Body' %>: @@ -927,48 +1001,58 @@ file_field(:user, :avatar) <% end %> ``` -#### `hidden_field` + -返回一个隐藏 `input` 标签,处理指定的属性。 +#### `hidden_​​field` 方法 + +`hidden_​​field` 方法返回用于处理指定模型属性的隐藏输入字段标签。 ```ruby hidden_field(:user, :token) # => ``` -#### `label` + + +#### `label` 方法 -返回一个 `label` 标签,为指定属性的输入框加上标签。 +`label` 方法返回用于处理指定模型属性的文本框的 label 标签。 ```ruby -label(:post, :title) -# => +label(:article, :title) +# => ``` -#### `password_field` + -返回一个密码输入框,处理指定的属性。 +#### `password_field` 方法 + +`password_field` 方法返回用于处理指定模型属性的密码框标签。 ```ruby password_field(:login, :pass) # => ``` -#### `radio_button` + + +#### `radio_button` 方法 -返回一个单选框,处理指定的属性。 +`radio_button` 方法返回用于处理指定模型属性的单选按钮标签。 ```ruby -# Let's say that @post.category returns "rails": -radio_button("post", "category", "rails") -radio_button("post", "category", "java") -# => -# +# 假设 @article.category 的值是“rails” +radio_button("article", "category", "rails") +radio_button("article", "category", "java") +# => +# ``` -#### `text_area` + + +#### `text_area` 方法 -返回一个多行文本输入框,处理指定的属性。 +`text_area` 方法返回用于处理指定模型属性的文本区域标签。 ```ruby text_area(:comment, :text, size: "20x30") @@ -977,66 +1061,76 @@ text_area(:comment, :text, size: "20x30") # ``` -#### `text_field` + -返回一个文本输入框,处理指定的属性。 +#### `text_field` 方法 + +`text_field` 方法返回用于处理指定模型属性的文本框标签。 ```ruby -text_field(:post, :title) -# => +text_field(:article, :title) +# => ``` -#### `email_field` + + +#### `email_field` 方法 -返回一个 Email 输入框,处理指定的属性。 +`email_field` 方法返回用于处理指定模型属性的电子邮件地址输入框标签。 ```ruby email_field(:user, :email) # => ``` -#### `url_field` + + +#### `url_field` 方法 -返回一个 URL 输入框,处理指定的属性。 +`url_field` 方法返回用于处理指定模型属性的 URL 地址输入框标签。 ```ruby url_field(:user, :url) # => ``` -### `FormOptionsHelper` + -这个模块提供很多方法用来把不同类型的集合转换成一组 `option` 标签。 +### `FormOptionsHelper` 模块 -#### `collection_select` +`FormOptionsHelper` 模块提供了许多方法,用于把不同类型的容器转换为一组选项标签。 -为 `object` 类的 `method` 方法返回的集合创建 `select` 和 `option` 标签。 + -使用此方法的模型示例: +#### `collection_select` 方法 + +`collection_select` 方法返回一个集合的选择列表标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。 + +在下面的示例代码中,我们定义了两个模型: ```ruby -class Post < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :author end -class Author < ActiveRecord::Base - has_many :posts +class Author < ApplicationRecord + has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -使用举例,为文章实例(`@post`)选择作者(`Author`): +在下面的示例代码中,`collection_select` 方法用于生成 `Article` 模型的实例 `@article` 的相关作者的选择列表: ```ruby -collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {prompt: true}) +collection_select(:article, :author_id, Author.all, :id, :name_with_initial, { prompt: true }) ``` -如果 `@post.author_id` 的值是 1,上述代码生成的 HTML 如下: +如果 `@article.author_id` 的值为 1,上面的代码会生成下面的 HTML: ```html - @@ -1044,112 +1138,110 @@ collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {promp ``` -#### `collection_radio_buttons` + + +#### `collection_radio_buttons` 方法 -为 `object` 类的 `method` 方法返回的集合创建 `radio_button` 标签。 +`collection_radio_buttons` 方法返回一个集合的单选按钮标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。 -使用此方法的模型示例: +在下面的示例代码中,我们定义了两个模型: ```ruby -class Post < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :author end -class Author < ActiveRecord::Base - has_many :posts +class Author < ApplicationRecord + has_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -使用举例,为文章实例(`@post`)选择作者(`Author`): +在下面的示例代码中,`collection_radio_buttons` 方法用于生成 `Article` 模型的实例 `@article` 的相关作者的单选按钮: ```ruby -collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) +collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial) ``` -如果 `@post.author_id` 的值是 1,上述代码生成的 HTML 如下: +如果 `@article.author_id` 的值为 1,上面的代码会生成下面的 HTML: ```html - - - - - - + + + + + + ``` -#### `collection_check_boxes` + -为 `object` 类的 `method` 方法返回的集合创建复选框标签。 +#### `collection_check_boxes` 方法 -使用此方法的模型示例: +`collection_check_boxes` 方法返回一个集合的复选框标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。 + +在下面的示例代码中,我们定义了两个模型: ```ruby -class Post < ActiveRecord::Base +class Article < ApplicationRecord has_and_belongs_to_many :authors end -class Author < ActiveRecord::Base - has_and_belongs_to_many :posts +class Author < ApplicationRecord + has_and_belongs_to_many :articles def name_with_initial "#{first_name.first}. #{last_name}" end end ``` -使用举例,为文章实例(`@post`)选择作者(`Author`): +在下面的示例代码中,`collection_check_boxes` 方法用于生成 `Article` 模型的实例 `@article` 的相关作者的复选框: ```ruby -collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) +collection_check_boxes(:article, :author_ids, Author.all, :id, :name_with_initial) ``` -如果 `@post.author_ids` 的值是 `[1]`,上述代码生成的 HTML 如下: +如果 `@article.author_ids` 的值为 `[1]`,上面的代码会生成下面的 HTML: ```html - - - - - - - + + + + + + + ``` -#### `country_options_for_select` - -返回一组 `option` 标签,几乎包含世界上所有国家。 - -#### `country_select` - -返回指定对象和方法的 `select` 和 `option` 标签。使用 `country_options_for_select` 方法生成各个 `option` 标签。 + -#### `option_groups_from_collection_for_select` +#### `option_groups_from_collection_for_select` 方法 -返回一个字符串,由多个 `option` 标签组成。和 `options_from_collection_for_select` 方法类似,但会根据对象之间的关系使用 `optgroup` 标签分组。 +和 `options_from_collection_for_select` 方法类似,`option_groups_from_collection_for_select` 方法返回一组选项标签,区别在于使用 `option_groups_from_collection_for_select` 方法时这些选项会根据模型的关联关系用 `optgroup` 标签分组。 -使用此方法的模型示例: +在下面的示例代码中,我们定义了两个模型: ```ruby -class Continent < ActiveRecord::Base +class Continent < ApplicationRecord has_many :countries # attribs: id, name end -class Country < ActiveRecord::Base +class Country < ApplicationRecord belongs_to :continent # attribs: id, name, continent_id end ``` -使用举例: +示例用法: ```ruby option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3) ``` -可能得到的输出如下: +可能的输出结果: ```html @@ -1165,93 +1257,111 @@ option_groups_from_collection_for_select(@continents, :countries, :name, :id, :n ``` -注意,这个方法只会返回 `optgroup` 和 `option` 标签,所以你要把输出放入 `select` 标签中。 +注意:`option_groups_from_collection_for_select` 方法只返回 `optgroup` 和 `option` 标签,我们要把这些 `optgroup` 和 `option` 标签放在 `select` 标签里。 -#### `options_for_select` + -接受一个集合(Hash,数组,可枚举的对象等),返回一个由 `option` 标签组成的字符串。 +#### `options_for_select` 方法 + +`options_for_select` 方法接受容器(如散列、数组、可枚举对象、自定义类型)作为参数,返回一组选项标签。 ```ruby options_for_select([ "VISA", "MasterCard" ]) # => ``` -注意,这个方法只返回 `option` 标签,所以你要把输出放入 `select` 标签中。 +注意:`options_for_select` 方法只返回 `option` 标签,我们要把这些 `option` 标签放在 `select` 标签里。 + + -#### `options_from_collection_for_select` +#### `options_from_collection_for_select` 方法 -遍历 `collection`,返回一组 `option` 标签。每个 `option` 标签的值是在 `collection` 元素上调用 `value_method` 方法得到的结果,`option` 标签的显示文本是在 `collection` 元素上调用 `text_method` 方法得到的结果 +`options_from_collection_for_select` 方法通过遍历集合返回一组选项标签,其中每个集合元素的 `value_method` 和 `text_method` 方法的返回值分别是每个选项的值和文本。 ```ruby # options_from_collection_for_select(collection, value_method, text_method, selected = nil) ``` -例如,下面的代码遍历 `@project.people`,生成一组 `option` 标签: +在下面的示例代码中,我们遍历 `@project.people` 集合得到 `person` 元素,`person.id` 和 `person.name` 方法分别是前面提到的 `value_method` 和 `text_method` 方法,这两个方法分别返回选项的值和文本: ```ruby options_from_collection_for_select(@project.people, "id", "name") # => ``` -注意:`options_from_collection_for_select` 方法只返回 `option` 标签,你应该将其放在 `select` 标签中。 +注意:`options_from_collection_for_select` 方法只返回 `option` 标签,我们要把这些 `option` 标签放在 `select` 标签里。 + + -#### `select` +#### `select` 方法 -创建一个 `select` 元素以及根据指定对象和方法得到的一系列 `option` 标签。 +`select` 方法使用指定对象和方法创建选择列表标签。 -例如: +示例用法: ```ruby -select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true}) +select("article", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) ``` -如果 `@post.person_id` 的值为 1,返回的结果是: +如果 `@article.persion_id` 的值为 1,上面的代码会生成下面的 HTML: ```html - - - + + ``` -#### `time_zone_options_for_select` + -返回一组 `option` 标签,包含几乎世界上所有的时区。 +#### `time_zone_options_for_select` 方法 -#### `time_zone_select` +`time_zone_options_for_select` 方法返回一组选项标签,其中每个选项对应一个时区,这些时区几乎包含了世界上所有的时区。 -为指定的对象和方法返回 `select` 标签和 `option` 标签,`option` 标签使用 `time_zone_options_for_select` 方法生成。 + + +#### `time_zone_select` 方法 + +`time_zone_select` 方法返回时区的选择列表标签,其中选项标签是通过 `time_zone_options_for_select` 方法生成的。 ```ruby time_zone_select( "user", "time_zone") ``` -#### `date_field` + + +#### `date_field` 方法 -返回一个 `date` 类型的 `input` 标签,用于访问指定的属性。 +`date_field` 方法返回用于处理指定模型属性的日期输入框标签。 ```ruby date_field("user", "dob") ``` -### `FormTagHelper` + -这个模块提供一系列方法用于创建表单标签。`FormHelper` 依赖于传入模板的 Active Record 对象,但 `FormTagHelper` 需要手动指定标签的 `name` 属性和 `value` 属性。 +### `FormTagHelper` 模块 -#### `check_box_tag` +`FormTagHelper` 模块提供了许多用于创建表单标签的方法。和 `FormHelper` 模块不同,`FormTagHelper` 模块提供的方法不依赖于传递给模板的 Active Record 对象。作为替代,我们可以手动为表单的各个组件的标签提供 `name` 和 `value` 属性。 -为表单创建一个复选框标签。 + + +#### `check_box_tag` 方法 + +`check_box_tag` 方法用于创建复选框标签。 ```ruby check_box_tag 'accept' # => ``` -#### `field_set_tag` + + +#### `field_set_tag` 方法 -创建 `fieldset` 标签,用于分组 HTML 表单元素。 +`field_set_tag` 方法用于创建 `fieldset` 标签。 ```erb <%= field_set_tag do %> @@ -1260,170 +1370,188 @@ check_box_tag 'accept' # =>

``` -#### `file_field_tag` + -创建一个文件上传输入框。 +#### `file_field_tag` 方法 + +`file_field_tag` 方法用于创建文件上传组件标签。 ```erb -<%= form_tag({action:"post"}, multipart: true) do %> +<%= form_tag({ action: "post" }, multipart: true) do %> <%= file_field_tag "file" %> <%= submit_tag %> <% end %> ``` -结果示例: +示例输出: ```ruby file_field_tag 'attachment' # => ``` -#### `form_tag` + + +#### `form_tag` 方法 -创建 `form` 标签,指向的地址由 `url_for_options` 选项指定,和 `ActionController::Base#url_for` 方法类似。 +`form_tag` 方法用于创建表单标签。和 `ActionController::Base#url_for` 方法类似,`form_tag` 方法的第一个参数是 `url_for_options` 选项,用于说明提交表单的 URL。 ```erb -<%= form_tag '/posts' do %> +<%= form_tag '/articles' do %>
<%= submit_tag 'Save' %>
<% end %> -# =>
+# =>
``` -#### `hidden_field_tag` + -为表单创建一个隐藏的 `input` 标签,用于传递由于 HTTP 无状态的特性而丢失的数据,或者隐藏不想让用户看到的数据。 +#### `hidden_​​field_tag` 方法 + +`hidden_​​field_tag` 方法用于创建隐藏输入字段标签。隐藏输入字段用于传递因 HTTP 无状态特性而丢失的数据,或不想让用户看到的数据。 ```ruby hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@' # => ``` -#### `image_submit_tag` + + +#### `image_submit_tag` 方法 -显示一个图片,点击后提交表单。 +`image_submit_tag` 方法会显示一张图像,点击这张图像会提交表单。 ```ruby image_submit_tag("login.png") # => ``` -#### `label_tag` + -创建一个 `label` 标签。 +#### `label_tag` 方法 + +`label_tag` 方法用于创建 `label` 标签。 ```ruby label_tag 'name' # => ``` -#### `password_field_tag` + + +#### `password_field_tag` 方法 -创建一个密码输入框,用户输入的值会被遮盖。 +`password_field_tag` 方法用于创建密码框标签。用户在密码框中输入的密码会被隐藏起来。 ```ruby password_field_tag 'pass' # => ``` -#### `radio_button_tag` + + +#### `radio_button_tag` 方法 -创建一个单选框。如果希望用户从一组选项中选择,可以使用多个单选框,`name` 属性的值都设为一样的。 +`radio_button_tag` 方法用于创建单选按钮标签。为一组单选按钮设置相同的 `name` 属性即可实现对一组选项进行单选。 ```ruby radio_button_tag 'gender', 'male' # => ``` -#### `select_tag` + -创建一个下拉选择框。 +#### `select_tag` 方法 + +`select_tag` 方法用于创建选择列表标签。 ```ruby select_tag "people", "" # => ``` -#### `submit_tag` + + +#### `submit_tag` 方法 -创建一个提交按钮,按钮上显示指定的文本。 +`submit_tag` 方法用于创建提交按钮标签,并在按钮上显示指定的文本。 ```ruby -submit_tag "Publish this post" -# => +submit_tag "Publish this article" +# => ``` -#### `text_area_tag` + + +#### `text_area_tag` 方法 -创建一个多行文本输入框,用于输入大段文本,例如博客和描述信息。 +`text_area_tag` 方法用于创建文本区域标签。文本区域用于输入较长的文本,如博客帖子或页面描述。 ```ruby -text_area_tag 'post' -# => +text_area_tag 'article' +# => ``` -#### `text_field_tag` + -创建一个标准文本输入框,用于输入小段文本,例如用户名和搜索关键字。 +#### `text_field_tag` 方法 + +`text_field_tag` 方法用于创建文本框标签。文本框用于输入较短的文本,如用户名或搜索关键词。 ```ruby text_field_tag 'name' # => ``` -#### `email_field_tag` + + +#### `email_field_tag` 方法 -创建一个标准文本输入框,用于输入 Email 地址。 +`email_field_tag` 方法用于创建电子邮件地址输入框标签。 ```ruby email_field_tag 'email' # => ``` -#### `url_field_tag` + + +#### `url_field_tag` 方法 -创建一个标准文本输入框,用于输入 URL 地址。 +`url_field_tag` 方法用于创建 URL 地址输入框标签。 ```ruby url_field_tag 'url' # => ``` -#### `date_field_tag` + -创建一个标准文本输入框,用于输入日期。 +#### `date_field_tag` 方法 + +`date_field_tag` 方法用于创建日期输入框标签。 ```ruby date_field_tag "dob" # => ``` -### `JavaScriptHelper` + -这个模块提供在视图中使用 JavaScript 的相关方法。 +### `JavaScriptHelper` 模块 -#### `button_to_function` +`JavaScriptHelper` 模块提供在视图中使用 JavaScript 的相关方法。 -返回一个按钮,点击后触发一个 JavaScript 函数。例如: + -```ruby -button_to_function "Greeting", "alert('Hello world!')" -button_to_function "Delete", "if (confirm('Really?')) do_delete()" -button_to_function "Details" do |page| - page[:details].visual_effect :toggle_slide -end -``` +#### `escape_javascript` 方法 -#### `define_javascript_functions` +`escape_javascript` 方法转义 JavaScript 代码中的回车符、单引号和双引号。 -在一个 `script` 标签中引入 Action Pack JavaScript 代码库。 + -#### `escape_javascript` +#### `javascript_tag` 方法 -转义 JavaScript 中的回车符、单引号和双引号。 - -#### `javascript_tag` - -返回一个 `script` 标签,把指定的代码放入其中。 +`javascript_tag` 方法返回放在 `script` 标签里的 JavaScript 代码。 ```ruby javascript_tag "alert('All is good')" @@ -1437,88 +1565,97 @@ alert('All is good') ``` -#### `link_to_function` + -返回一个链接,点击后触发指定的 JavaScript 函数并返回 `false`。 +### `NumberHelper` 模块 -```ruby -link_to_function "Greeting", "alert('Hello world!')" -# => Greeting -``` - -### `NumberHelper` +`NumberHelper` 模块提供把数字转换为格式化字符串的方法,包括把数字转换为电话号码、货币、百分数、具有指定精度的数字、带有千位分隔符的数字和文件大小的方法。 -这个模块提供用于把数字转换成格式化字符串所需的方法。包括用于格式化电话号码、货币、百分比、精度、进位制和文件大小的方法。 + -#### `number_to_currency` +#### `number_to_currency` 方法 -把数字格式化成货币字符串,例如 $13.65。 +`number_to_currency` 方法用于把数字转换为货币字符串(例如 $13.65)。 ```ruby number_to_currency(1234567890.50) # => $1,234,567,890.50 ``` -#### `number_to_human_size` + + +#### `number_to_human_size` 方法 -把字节数格式化成更易理解的形式,显示文件大小时特别有用。 +`number_to_human_size` 方法用于把数字转换为容易阅读的形式,常用于显示文件大小。 ```ruby number_to_human_size(1234) # => 1.2 KB number_to_human_size(1234567) # => 1.2 MB ``` -#### `number_to_percentage` + -把数字格式化成百分数形式。 +#### `number_to_percentage` 方法 + +`number_to_percentage` 方法用于把数字转换为百分数字符串。 ```ruby number_to_percentage(100, precision: 0) # => 100% ``` -#### `number_to_phone` + + +#### `number_to_phone` 方法 -把数字格式化成美国使用的电话号码形式。 +`number_to_phone` 方法用于把数字转换为电话号码(默认为美国)。 ```ruby number_to_phone(1235551234) # => 123-555-1234 ``` -#### `number_with_delimiter` + + +#### `number_with_delimiter` 方法 -格式化数字,使用分隔符隔开每三位数字。 +`number_with_delimiter` 方法用于把数字转换为带有千位分隔符的数字。 ```ruby number_with_delimiter(12345678) # => 12,345,678 ``` -#### `number_with_precision` + -使用指定的精度格式化数字,精度默认值为 3。 +#### `number_with_precision` 方法 + +`number_with_precision` 方法用于把数字转换为具有指定精度的数字,默认精度为 3。 ```ruby number_with_precision(111.2345) # => 111.235 -number_with_precision(111.2345, 2) # => 111.23 +number_with_precision(111.2345, precision: 2) # => 111.23 ``` -### `SanitizeHelper` + + +### `SanitizeHelper` 模块 -`SanitizeHelper` 模块提供一系列方法,用于剔除不想要的 HTML 元素。 +`SanitizeHelper` 模块提供从文本中清除不需要的 HTML 元素的方法。 -#### `sanitize` + -`sanitize` 方法会编码所有标签,并删除所有不允许使用的属性。 +#### `sanitize` 方法 + +`sanitize` 方法会对所有标签进行 HTML 编码,并清除所有未明确允许的属性。 ```ruby sanitize @article.body ``` -如果指定了 `:attributes` 或 `:tags` 选项,只允许使用指定的标签和属性。 +如果指定了 `:attributes` 或 `:tags` 选项,那么只有指定的属性或标签才不会被清除。 ```ruby sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) ``` -要想修改默认值,例如允许使用 `table` 标签,可以这么设置: +要想修改 `sanitize` 方法的默认选项,例如把表格标签设置为允许的属性,可以按下面的方式设置: ```ruby class Application < Rails::Application @@ -1526,21 +1663,25 @@ class Application < Rails::Application end ``` -#### `sanitize_css(style)` + + +#### `sanitize_css(style)` 方法 -过滤一段 CSS 代码。 +`sanitize_css(style)` 方法用于净化 CSS 代码。 -#### `strip_links(html)` + -删除文本中的所有链接标签,但保留链接文本。 +#### `strip_links(html)` 方法 + +`strip_links(html)` 方法用于清除文本中所有的链接标签,只保留链接文本。 ```ruby -strip_links("Ruby on Rails") +strip_links('Ruby on Rails') # => Ruby on Rails ``` ```ruby -strip_links("emails to me@email.com.") +strip_links('emails to me@email.com.') # => emails to me@email.com. ``` @@ -1549,11 +1690,11 @@ strip_links('Blog: Visit.') # => Blog: Visit. ``` -#### `strip_tags(html)` + -过滤 `html` 中的所有 HTML 标签,以及注释。 +#### `strip_tags(html)` 方法 -这个方法使用 `html-scanner` 解析 HTML,所以解析能力受 `html-scanner` 的限制。 +`strip_tags(html)` 方法用于清除包括注释在内的所有 HTML 标签。这个方法的功能由 rails-html-sanitizer gem 提供。 ```ruby strip_tags("Strip these tags!") @@ -1565,18 +1706,31 @@ strip_tags("Bold no more! See more") # => Bold no more! See more ``` -注意,得到的结果中可能仍然有字符 `<`、`>` 和 `&`,会导致浏览器显示异常。 +注意:使用 `strip_tags(html)` 方法清除后的文本仍然可能包含 <、> 和 & 字符,从而导致浏览器显示异常。 + + + +### `CsrfHelper` 模块 + +`csrf_meta_tags` 方法用于生成 `csrf-param` 和 `csrf-token` 这两个元标签,它们分别是跨站请求伪造保护的参数和令牌。 + +```erb +<%= csrf_meta_tags %> +``` + +NOTE: 普通表单生成隐藏字段,因此不使用这些标签。关于这个问题的更多介绍,请参阅 [跨站请求伪造(CSRF)](security.html#cross-site-request-forgery-csrf)。 + + -视图本地化 ---------- +## 本地化视图 Action View 可以根据当前的本地化设置渲染不同的模板。 -例如,假设有个 `PostsController`,在其中定义了 `show` 动作。默认情况下,执行这个动作时渲染的是 `app/views/posts/show.html.erb`。如果设置了 `I18n.locale = :de`,渲染的则是 `app/views/posts/show.de.html.erb`。如果本地化对应的模板不存在就使用默认模板。也就是说,没必要为所有动作编写本地化视图,但如果有本地化对应的模板就会使用。 +假如 `ArticlesController` 控制器中有 `show` 动作。默认情况下,调用 `show` 动作会渲染 `app/views/articles/show.html.erb` 模板。如果我们设置了 `I18n.locale = :de`,那么调用 `show` 动作会渲染 `app/views/articles/show.de.html.erb` 模板。如果对应的本地化模板不存在,就会使用对应的默认模板。这意味着我们不需要为所有情况提供本地化视图,但如果本地化视图可用就会优先使用。 -相同的技术还可用在 `public` 文件夹中的错误文件上。例如,设置了 `I18n.locale = :de`,并创建了 `public/500.de.html` 和 `public/404.de.html`,就能显示本地化的错误页面。 +我们可以使用相同的技术来本地化公共目录中的错误文件。例如,通过设置 `I18n.locale = :de` 并创建 `public/500.de.html` 和 `public/404.de.html` 文件,我们就拥有了本地化的错误文件。 -Rails 并不限制 `I18n.locale` 选项的值,因此可以根据任意需求显示不同的内容。假设想让专业用户看到不同于普通用户的页面,可以在 `app/controllers/application_controller.rb` 中这么设置: +由于 Rails 不会限制用于设置 `I18n.locale` 的符号,我们可以利用本地化视图根据我们喜欢的任何东西来显示不同的内容。例如,假设专家用户应该看到和普通用户不同的页面,我们可以在 `app/controllers/application.rb` 配置文件中进行如下设置: ```ruby before_action :set_expert_locale @@ -1586,6 +1740,6 @@ def set_expert_locale end ``` -然后创建只显示给专业用户的 `app/views/posts/show.expert.html.erb` 视图。 +然后创建 `app/views/articles/show.expert.html.erb` 这样的显示给专家用户看的特殊视图。 -详情参阅“[Rails 国际化 API](i18n.html)”一文。 +关于 Rails 国际化的更多介绍,请参阅[Rails 国际化 API](i18n.html)。 diff --git a/source/zh-CN/active_job_basics.md b/source/zh-CN/active_job_basics.md new file mode 100644 index 0000000..7628a82 --- /dev/null +++ b/source/zh-CN/active_job_basics.md @@ -0,0 +1,361 @@ +# Active Job 基础 + +本文全面说明创建、入队和执行后台作业的基础知识。 + +读完本文后,您将学到: + +* 如何创建作业; +* 如何入队作业; +* 如何在后台运行作业; +* 如何在应用中异步发送电子邮件。 + +----------------------------------------------------------------------------- + + + +## 简介 + +Active Job 框架负责声明作业,在各种队列后端中运行。作业各种各样,可以是定期清理、账单支付和寄信。其实,任何可以分解且并行运行的工作都可以。 + + + +## Active Job 的作用 + +主要作用是确保所有 Rails 应用都有作业基础设施。这样便可以在此基础上构建各种功能和其他 gem,而无需担心不同作业运行程序(如 Delayed Job 和 Resque)的 API 之间的差异。此外,选用哪个队列后端只是战术问题。而且,切换队列后端也不用重写作业。 + +NOTE: Rails 自带了一个在进程内线程池中执行作业的异步队列。这些作业虽然是异步执行的,但是重启后队列中的作业就会丢失。 + + + +## 创建作业 + +本节逐步说明创建和入队作业的过程。 + + + +### 创建作业 + +Active Job 提供了一个 Rails 生成器,用于创建作业。下述命令在 `app/jobs` 目录中创建一个作业(还在 `test/jobs` 目录中创建相关的测试用例): + +```sh +$ bin/rails generate job guests_cleanup +invoke test_unit +create test/jobs/guests_cleanup_job_test.rb +create app/jobs/guests_cleanup_job.rb +``` + +还可以创建在指定队列中运行的作业: + +```sh +$ bin/rails generate job guests_cleanup --queue urgent +``` + +如果不想使用生成器,可以自己动手在 `app/jobs` 目录中新建文件,不过要确保继承自 `ApplicationJob`。 + +看一下作业: + +```ruby +class GuestsCleanupJob < ApplicationJob + queue_as :default + + def perform(*guests) + # 稍后做些事情 + end +end +``` + +注意,`perform` 方法的参数是任意个。 + + + +### 入队作业 + +像下面这样入队作业: + +```ruby +# 入队作业,作业在队列系统空闲时立即执行 +GuestsCleanupJob.perform_later guest +``` + +```ruby +# 入队作业,在明天中午执行 +GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest) +``` + +```ruby +# 入队作业,在一周以后执行 +GuestsCleanupJob.set(wait: 1.week).perform_later(guest) +``` + +```ruby +# `perform_now` 和 `perform_later` 会在幕后调用 `perform` +# 因此可以传入任意个参数 +GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter') +``` + +就这么简单! + + + +## 执行作业 + +在生产环境中入队和执行作业需要使用队列后端,即要为 Rails 提供一个第三方队列库。Rails 本身只提供了一个进程内队列系统,把作业存储在 RAM 中。如果进程崩溃,或者设备重启了,默认的异步后端会丢失所有作业。这对小型应用或不重要的作业来说没什么,但是生产环境中的多数应用应该挑选一个持久后端。 + + + +### 后端 + +Active Job 为多种队列后端(Sidekiq、Resque、Delayed Job,等等)内置了适配器。最新的适配器列表参见 [`ActiveJob::QueueAdapters` 的 API 文档](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html)。 + + + +### 设置后端 + +队列后端易于设置: + +```ruby +# config/application.rb +module YourApp + class Application < Rails::Application + # 要把适配器的 gem 写入 Gemfile + # 请参照适配器的具体安装和部署说明 + config.active_job.queue_adapter = :sidekiq + end +end +``` + +也可以在各个作业中配置后端: + +```ruby +class GuestsCleanupJob < ApplicationJob + self.queue_adapter = :resque + #.... +end + +# 现在,这个作业使用 `resque` 作为后端队列适配器 +# 把 `config.active_job.queue_adapter` 配置覆盖了 +``` + + + +### 启动后端 + +Rails 应用中的作业并行运行,因此多数队列库要求为自己启动专用的队列服务(与启动 Rails 应用的服务不同)。启动队列后端的说明参见各个库的文档。 + +下面列出部分文档: + +* [Sidekiq](https://github.com/mperham/sidekiq/wiki/Active-Job) +* [Resque](https://github.com/resque/resque/wiki/ActiveJob) +* [Sucker Punch](https://github.com/brandonhilkert/sucker_punch#active-job) +* [Queue Classic](https://github.com/QueueClassic/queue_classic#active-job) + + + +## 队列 + +多数适配器支持多个队列。Active Job 允许把作业调度到具体的队列中: + +```ruby +class GuestsCleanupJob < ApplicationJob + queue_as :low_priority + #.... +end +``` + +队列名称可以使用 `application.rb` 文件中的 `config.active_job.queue_name_prefix` 选项配置前缀: + +```ruby +# config/application.rb +module YourApp + class Application < Rails::Application + config.active_job.queue_name_prefix = Rails.env + end +end + +# app/jobs/guests_cleanup_job.rb +class GuestsCleanupJob < ApplicationJob + queue_as :low_priority + #.... +end + +# 在生产环境中,作业在 production_low_priority 队列中运行 +# 在交付准备环境中,作业在 staging_low_priority 队列中运行 +``` + +默认的队列名称前缀分隔符是 `'_'`。这个值可以使用 `application.rb` 文件中的 `config.active_job.queue_name_delimiter` 选项修改: + +```ruby +# config/application.rb +module YourApp + class Application < Rails::Application + config.active_job.queue_name_prefix = Rails.env + config.active_job.queue_name_delimiter = '.' + end +end + +# app/jobs/guests_cleanup_job.rb +class GuestsCleanupJob < ApplicationJob + queue_as :low_priority + #.... +end + +# 在生产环境中,作业在 production.low_priority 队列中运行 +# 在交付准备环境中,作业在 staging.low_priority 队列中运行 +``` + +如果想更进一步控制作业在哪个队列中运行,可以把 `:queue` 选项传给 `#set` 方法: + +```ruby +MyJob.set(queue: :another_queue).perform_later(record) +``` + +如果想在作业层控制队列,可以把一个块传给 `#queue_as` 方法。那个块在作业的上下文中执行(因此可以访问 `self.arguments`),必须返回队列的名称: + +```ruby +class ProcessVideoJob < ApplicationJob + queue_as do + video = self.arguments.first + if video.owner.premium? + :premium_videojobs + else + :videojobs + end + end + + def perform(video) + # 处理视频 + end +end + +ProcessVideoJob.perform_later(Video.last) +``` + +NOTE: 确保队列后端“监听”着队列名称。某些后端要求指定要监听的队列。 + + + +## 回调 + +Active Job 在作业的生命周期内提供了多个钩子。回调用于在作业的生命周期内触发逻辑。 + + + +### 可用的回调 + +* `before_enqueue` +* `around_enqueue` +* `after_enqueue` +* `before_perform` +* `around_perform` +* `after_perform` + + + +### 用法 + +```ruby +class GuestsCleanupJob < ApplicationJob + queue_as :default + + before_enqueue do |job| + # 对作业实例做些事情 + end + + around_perform do |job, block| + # 在执行之前做些事情 + block.call + # 在执行之后做些事情 + end + + def perform + # 稍后做些事情 + end +end +``` + + + +## Action Mailer + +对现代的 Web 应用来说,最常见的作业是在请求-响应循环之外发送电子邮件,这样用户无需等待。Active Job 与 Action Mailer 是集成的,因此可以轻易异步发送电子邮件: + +```ruby +# 如需想现在发送电子邮件,使用 #deliver_now +UserMailer.welcome(@user).deliver_now + +# 如果想通过 Active Job 发送电子邮件,使用 #deliver_later +UserMailer.welcome(@user).deliver_later +``` + + + +## 国际化 + +创建作业时,使用 `I18n.locale` 设置。如果异步发送电子邮件,可能用得到: + +```ruby +I18n.locale = :eo + +UserMailer.welcome(@user).deliver_later # 使用世界语本地化电子邮件 +``` + + + +## GlobalID + +Active Job 支持参数使用 GlobalID。这样便可以把 Active Record 对象传给作业,而不用传递类和 ID,再自己反序列化。以前,要这么定义作业: + +```ruby +class TrashableCleanupJob < ApplicationJob + def perform(trashable_class, trashable_id, depth) + trashable = trashable_class.constantize.find(trashable_id) + trashable.cleanup(depth) + end +end +``` + +现在可以简化成这样: + +```ruby +class TrashableCleanupJob < ApplicationJob + def perform(trashable, depth) + trashable.cleanup(depth) + end +end +``` + +为此,模型类要混入 `GlobalID::Identification`。Active Record 模型类默认都混入了。 + + + +## 异常 + +Active Job 允许捕获执行作业过程中抛出的异常: + +```ruby +class GuestsCleanupJob < ApplicationJob + queue_as :default + + rescue_from(ActiveRecord::RecordNotFound) do |exception| + # 处理异常 + end + + def perform + # 稍后做些事情 + end +end +``` + + + +### 反序列化 + +有了 GlobalID,可以序列化传给 `#perform` 方法的整个 Active Record 对象。 + +如果在作业入队之后、调用 `#perform` 方法之前删除了传入的记录,Active Job 会抛出 `ActiveJob::DeserializationError` 异常。 + + + +## 测试作业 + +测试作业的详细说明参见 [测试作业](testing.html#testing-jobs)。 diff --git a/source/zh-CN/active_model_basics.md b/source/zh-CN/active_model_basics.md index 3eaeeff..046188d 100644 --- a/source/zh-CN/active_model_basics.md +++ b/source/zh-CN/active_model_basics.md @@ -1,20 +1,29 @@ -Active Model Basics -=================== +# Active Model 基础 -This guide should provide you with all you need to get started using model classes. Active Model allows for Action Pack helpers to interact with non-Active Record models. Active Model also helps building custom ORMs for use outside of the Rails framework. +本文简述模型类。Active Model 允许使用 Action Pack 辅助方法与普通的 Ruby 类交互。Active Model 还协助构建自定义的 ORM,可在 Rails 框架外部使用。 -After reading this guide, you will know: +读完本文后,您将学到: --------------------------------------------------------------------------------- +* Active Record 模型的行为; +* 回调和数据验证的工作方式; +* 序列化程序的工作方式; +* Active Model 与 Rails 国际化(i18n)框架的集成。 -Introduction ------------- +----------------------------------------------------------------------------- -Active Model is a library containing various modules used in developing frameworks that need to interact with the Rails Action Pack library. Active Model provides a known set of interfaces for usage in classes. Some of modules are explained below. +NOTE: 本文原文尚未完工! -### AttributeMethods + -The AttributeMethods module can add custom prefixes and suffixes on methods of a class. It is used by defining the prefixes and suffixes and which methods on the object will use them. +## 简介 + +Active Model 库包含很多模块,用于开发要在 Active Record 中存储的类。下面说明其中部分模块。 + + + +### 属性方法 + +`ActiveModel::AttributeMethods` 模块可以为类中的方法添加自定义的前缀和后缀。它用于定义前缀和后缀,对象中的方法将使用它们。 ```ruby class Person @@ -38,14 +47,16 @@ end person = Person.new person.age = 110 -person.age_highest? # true -person.reset_age # 0 -person.age_highest? # false +person.age_highest? # => true +person.reset_age # => 0 +person.age_highest? # => false ``` -### Callbacks + + +### 回调 -Callbacks gives Active Record style callbacks. This provides an ability to define callbacks which run at appropriate times. After defining callbacks, you can wrap them with before, after and around custom methods. +`ActiveModel::Callbacks` 模块为 Active Record 提供回调,在某个时刻运行。定义回调之后,可以使用前置、后置和环绕方法包装。 ```ruby class Person @@ -57,19 +68,22 @@ class Person def update run_callbacks(:update) do - # This method is called when update is called on an object. + # 在对象上调用 update 时执行这个方法 end end def reset_me - # This method is called when update is called on an object as a before_update callback is defined. + # 在对象上调用 update 方法时执行这个方法 + # 因为把它定义为 before_update 回调了 end end ``` -### Conversion + -If a class defines `persisted?` and `id` methods, then you can include the `Conversion` module in that class and call the Rails conversion methods on objects of that class. +### 转换 + +如果一个类定义了 `persisted?` 和 `id` 方法,可以在那个类中引入 `ActiveModel::Conversion` 模块,这样便能在类的对象上调用 Rails 提供的转换方法。 ```ruby class Person @@ -90,13 +104,13 @@ person.to_key # => nil person.to_param # => nil ``` -### Dirty + -An object becomes dirty when it has gone through one or more changes to its attributes and has not been saved. This gives the ability to check whether an object has been changed or not. It also has attribute based accessor methods. Let's consider a Person class with attributes `first_name` and `last_name`: +### 弄脏 -```ruby -require 'active_model' +如果修改了对象的一个或多个属性,但是没有保存,此时就把对象弄脏了。`ActiveModel::Dirty` 模块提供检查对象是否被修改的功能。它还提供了基于属性的存取方法。假如有个 `Person` 类,它有两个属性,`first_name` 和 `last_name`: +```ruby class Person include ActiveModel::Dirty define_attribute_methods :first_name, :last_name @@ -120,13 +134,15 @@ class Person end def save - # do save work... + # 执行保存操作…… changes_applied end end ``` -#### Querying object directly for its list of all changed attributes. + + +#### 直接查询对象,获取所有被修改的属性列表 ```ruby person = Person.new @@ -135,22 +151,24 @@ person.changed? # => false person.first_name = "First Name" person.first_name # => "First Name" -# returns if any attribute has changed. +# 如果修改属性后未保存,返回 true person.changed? # => true -# returns a list of attributes that have changed before saving. +# 返回修改之后没有保存的属性列表 person.changed # => ["first_name"] -# returns a hash of the attributes that have changed with their original values. +# 返回一个属性散列,指明原来的值 person.changed_attributes # => {"first_name"=>nil} -# returns a hash of changes, with the attribute names as the keys, and the values will be an array of the old and new value for that field. +# 返回一个散列,键为修改的属性名,值是一个数组,包含旧值和新值 person.changes # => {"first_name"=>[nil, "First Name"]} ``` -#### Attribute based accessor methods + -Track whether the particular attribute has been changed or not. +#### 基于属性的存取方法 + +判断具体的属性是否被修改了: ```ruby # attr_name_changed? @@ -158,24 +176,24 @@ person.first_name # => "First Name" person.first_name_changed? # => true ``` -Track what was the previous value of the attribute. +查看属性之前的值: ```ruby -# attr_name_was accessor -person.first_name_was # => "First Name" +person.first_name_was # => nil ``` -Track both previous and current value of the changed attribute. Returns an array if changed, else returns nil. +查看属性修改前后的值。如果修改了,返回一个数组,否则返回 `nil`: ```ruby -# attr_name_change person.first_name_change # => [nil, "First Name"] person.last_name_change # => nil ``` -### Validations + + +### 数据验证 -Validations module adds the ability to class objects to validate them in Active Record style. +`ActiveModel::Validations` 模块提供数据验证功能,这与 Active Record 中的类似。 ```ruby class Person @@ -188,7 +206,8 @@ class Person validates! :token, presence: true end -person = Person.new(token: "2b1f325") +person = Person.new +person.token = "2b1f325" person.valid? # => false person.name = 'vishnu' person.email = 'me' @@ -199,11 +218,11 @@ person.token = nil person.valid? # => raises ActiveModel::StrictValidationFailed ``` -### ActiveModel::Naming + -Naming adds a number of class methods which make the naming and routing -easier to manage. The module defines the `model_name` class method which -will define a number of accessors using some `ActiveSupport::Inflector` methods. +### 命名 + +`ActiveModel::Naming` 添加一些类方法,便于管理命名和路由。这个模块定义了 `model_name` 类方法,它使用 `ActiveSupport::Inflector` 中的一些方法定义一些存取方法。 ```ruby class Person @@ -221,3 +240,261 @@ Person.model_name.i18n_key # => :person Person.model_name.route_key # => "people" Person.model_name.singular_route_key # => "person" ``` + + + +### 模型 + +`ActiveModel::Model` 模块能让一个类立即能与 Action Pack 和 Action View 集成。 + +```ruby +class EmailContact + include ActiveModel::Model + + attr_accessor :name, :email, :message + validates :name, :email, :message, presence: true + + def deliver + if valid? + # 发送电子邮件 + end + end +end +``` + +引入 `ActiveModel::Model` 后,将获得以下功能: + +* 模型名称内省 +* 转换 +* 翻译 +* 数据验证 + +还能像 Active Record 对象那样使用散列指定属性,初始化对象。 + +```ruby +email_contact = EmailContact.new(name: 'David', + email: 'david@example.com', + message: 'Hello World') +email_contact.name # => 'David' +email_contact.email # => 'david@example.com' +email_contact.valid? # => true +email_contact.persisted? # => false +``` + +只要一个类引入了 `ActiveModel::Model`,它就能像 Active Record 对象那样使用 `form_for`、`render` 和任何 Action View 辅助方法。 + + + +### 序列化 + +`ActiveModel::Serialization` 模块为对象提供基本的序列化支持。你要定义一个属性散列,包含想序列化的属性。属性名必须使用字符串,不能使用符号。 + +```ruby +class Person + include ActiveModel::Serialization + + attr_accessor :name + + def attributes + {'name' => nil} + end +end +``` + +这样就可以使用 `serializable_hash` 方法访问对象的序列化散列: + +```ruby +person = Person.new +person.serializable_hash # => {"name"=>nil} +person.name = "Bob" +person.serializable_hash # => {"name"=>"Bob"} +``` + + + +#### `ActiveModel::Serializers` + +Rails 还提供了用于序列化和反序列化 JSON 的 `ActiveModel::Serializers::JSON`。这个模块自动引入前文介绍过的 `ActiveModel::Serialization` 模块。 + + + +##### `ActiveModel::Serializers::JSON` + +若想使用 `ActiveModel::Serializers::JSON`,只需把 `ActiveModel::Serialization` 换成 `ActiveModel::Serializers::JSON`。 + +```ruby +class Person + include ActiveModel::Serializers::JSON + + attr_accessor :name + + def attributes + {'name' => nil} + end +end +``` + +`as_json` 方法与 `serializable_hash` 方法相似,用于提供模型的散列表示形式。 + +```ruby +person = Person.new +person.as_json # => {"name"=>nil} +person.name = "Bob" +person.as_json # => {"name"=>"Bob"} +``` + +还可以使用 JSON 字符串定义模型的属性。然后,要在类中定义 `attributes=` 方法: + +```ruby +class Person + include ActiveModel::Serializers::JSON + + attr_accessor :name + + def attributes=(hash) + hash.each do |key, value| + send("#{key}=", value) + end + end + + def attributes + {'name' => nil} + end +end +``` + +现在,可以使用 `from_json` 方法创建 `Person` 实例,并且设定属性: + +```ruby +json = { name: 'Bob' }.to_json +person = Person.new +person.from_json(json) # => # +person.name # => "Bob" +``` + + + +### 翻译 + +`ActiveModel::Translation` 模块把对象与 Rails 国际化(i18n)框架集成起来。 + +```ruby +class Person + extend ActiveModel::Translation +end +``` + +使用 `human_attribute_name` 方法可以把属性名称变成对人类友好的格式。对人类友好的格式在本地化文件中定义。 + +* `config/locales/app.pt-BR.yml` + + ```ruby + pt-BR: + activemodel: + attributes: + person: + name: 'Nome' + ``` + + + +```ruby +Person.human_attribute_name('name') # => "Nome" +``` + + + +### lint 测试 + +`ActiveModel::Lint::Tests` 模块测试对象是否符合 Active Model API。 + +* `app/models/person.rb` + + ```ruby + class Person + include ActiveModel::Model + end + ``` + + +* `test/models/person_test.rb` + + ```ruby + require 'test_helper' + + class PersonTest < ActiveSupport::TestCase + include ActiveModel::Lint::Tests + + setup do + @model = Person.new + end + end + ``` + + + +```sh +$ rails test + +Run options: --seed 14596 + +# Running: + +...... + +Finished in 0.024899s, 240.9735 runs/s, 1204.8677 assertions/s. + +6 runs, 30 assertions, 0 failures, 0 errors, 0 skips +``` + +为了使用 Action Pack,对象无需实现所有 API。这个模块只是提供一种指导,以防你需要全部功能。 + + + +### 安全密码 + +`ActiveModel::SecurePassword` 提供安全加密密码的功能。这个模块提供了 `has_secure_password` 类方法,它定义了一个名为 `password` 的存取方法,而且有相应的数据验证。 + + + +#### 要求 + +`ActiveModel::SecurePassword` 依赖 [bcrypt](https://github.com/codahale/bcrypt-ruby),因此要在 `Gemfile` 中加入这个 gem,`ActiveModel::SecurePassword` 才能正确运行。为了使用安全密码,模型中必须定义一个名为 `password_digest` 的存取方法。`has_secure_password` 类方法会为 `password` 存取方法添加下述数据验证: + +1. 密码应该存在 +1. 密码应该等于密码确认(前提是有密码确认) +1. 密码的最大长度为 72(`ActiveModel::SecurePassword` 依赖的 `bcrypt` 的要求) + + + +#### 示例 + +```ruby +class Person + include ActiveModel::SecurePassword + has_secure_password + attr_accessor :password_digest +end + +person = Person.new + +# 密码为空时 +person.valid? # => false + +# 密码确认与密码不匹配时 +person.password = 'aditya' +person.password_confirmation = 'nomatch' +person.valid? # => false + +# 密码长度超过 72 时 +person.password = person.password_confirmation = 'a' * 100 +person.valid? # => false + +# 只有密码,没有密码确认 +person.password = 'aditya' +person.valid? # => true + +# 所有数据验证都通过时 +person.password = person.password_confirmation = 'aditya' +person.valid? # => true +``` diff --git a/source/zh-CN/active_record_basics.md b/source/zh-CN/active_record_basics.md index 1318b34..fa362c2 100644 --- a/source/zh-CN/active_record_basics.md +++ b/source/zh-CN/active_record_basics.md @@ -1,86 +1,99 @@ -Active Record 基础 -================== +# Active Record 基础 -本文介绍 Active Record。 +本文简介 Active Record。 -读完本文,你将学到: +读完本文后,您将学到: -* 对象关系映射(Object Relational Mapping,ORM)和 Active Record 是什么,以及如何在 Rails 中使用; -* Active Record 在 MVC 中的作用; -* 如何使用 Active Record 模型处理保存在关系型数据库中的数据; -* Active Record 模式(schema)命名约定; -* 数据库迁移,数据验证和回调; +* 对象关系映射(Object Relational Mapping,ORM)和 Active Record 是什么,以及如何在 Rails 中使用; +* Active Record 在 MVC 中的作用; +* 如何使用 Active Record 模型处理保存在关系型数据库中的数据; +* Active Record 模式(schema)的命名约定; +* 数据库迁移,数据验证和回调。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -Active Record 是什么? ---------------------- + -Active Record 是 [MVC](getting_started.html#the-mvc-architecture) 中的 M(模型),处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。 +## Active Record 是什么? + +Active Record 是 [MVC](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 中的 M(模型),负责处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。 + + ### Active Record 模式 -Active Record 模式出自 [Martin Fowler](http://www.martinfowler.com/eaaCatalog/activeRecord.html) 的《企业应用架构模式》一书。在 Active Record 模式中,对象中既有持久存储的数据,也有针对数据的操作。Active Record 模式把数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,以及从数据库中读出数据。 +[Active Record 模式](http://www.martinfowler.com/eaaCatalog/activeRecord.html)出自 Martin Fowler 写的《[企业应用架构模式](https://book.douban.com/subject/4826290/)》一书。在 Active Record 模式中,对象中既有持久存储的数据,也有针对数据的操作。Active Record 模式把数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,还知道如何从数据库中读出数据。 + + ### 对象关系映射 -对象关系映射(ORM)是一种技术手段,把程序中的对象和关系型数据库中的数据表连接起来。使用 ORM,程序中对象的属性和对象之间的关系可以通过一种简单的方法从数据库获取,无需直接编写 SQL 语句,也不过度依赖特定的数据库种类。 +对象关系映射(ORM)是一种技术手段,把应用中的对象和关系型数据库中的数据表连接起来。使用 ORM,应用中对象的属性和对象之间的关系可以通过一种简单的方法从数据库中获取,无需直接编写 SQL 语句,也不过度依赖特定的数据库种类。 -### Active Record 用作 ORM 框架 + + +### 用作 ORM 框架的 Active Record Active Record 提供了很多功能,其中最重要的几个如下: -* 表示模型和其中的数据; -* 表示模型之间的关系; -* 通过相关联的模型表示集成关系; -* 持久存入数据库之前,验证模型; -* 以面向对象的方式处理数据库操作; +* 表示模型和其中的数据; +* 表示模型之间的关系; +* 通过相关联的模型表示继承层次结构; +* 持久存入数据库之前,验证模型; +* 以面向对象的方式处理数据库操作。 + + -Active Record 中的“多约定少配置”原则 ----------------------------------- +## Active Record 中的“多约定少配置”原则 -使用其他编程语言或框架开发程序时,可能必须要编写很多配置代码。大多数的 ORM 框架都是这样。但是,如果遵循 Rails 的约定,创建 Active Record 模型时不用做多少配置(有时甚至完全不用配置)。Rails 的理念是,如果大多数情况下都要使用相同的方式配置程序,那么就应该把这定为默认的方法。所以,只有常规的方法无法满足要求时,才要额外的配置。 +使用其他编程语言或框架开发应用时,可能必须要编写很多配置代码。大多数 ORM 框架都是这样。但是,如果遵循 Rails 的约定,创建 Active Record 模型时不用做多少配置(有时甚至完全不用配置)。Rails 的理念是,如果大多数情况下都要使用相同的方式配置应用,那么就应该把这定为默认的方式。所以,只有约定无法满足要求时,才要额外配置。 + + ### 命名约定 -默认情况下,Active Record 使用一些命名约定,查找模型和数据表之间的映射关系。Rails 把模型的类名转换成复数,然后查找对应的数据表。例如,模型类名为 `Book`,数据表就是 `books`。Rails 提供的单复数变形功能很强大,常见和不常见的变形方式都能处理。如果类名由多个单词组成,应该按照 Ruby 的约定,使用驼峰式命名法,这时对应的数据表将使用下划线分隔各单词。因此: +默认情况下,Active Record 使用一些命名约定,查找模型和数据库表之间的映射关系。Rails 把模型的类名转换成复数,然后查找对应的数据表。例如,模型类名为 `Book`,数据表就是 `books`。Rails 提供的单复数转换功能很强大,常见和不常见的转换方式都能处理。如果类名由多个单词组成,应该按照 Ruby 的约定,使用驼峰式命名法,这时对应的数据库表将使用下划线分隔各单词。因此: + +* 数据库表名:复数,下划线分隔单词(例如 `book_clubs`) +* 模型类名:单数,每个单词的首字母大写(例如 `BookClub`) -* 数据表名:复数,下划线分隔单词(例如 `book_clubs`) -* 模型类名:单数,每个单词的首字母大写(例如 `BookClub`) +| 模型/类 | 表/模式 | +|---|---| +| `Article` | `articles` | +| `LineItem` | `line_items` | +| `Deer` | `deers` | +| `Mouse` | `mice` | +| `Person` | `people` | -| 模型 / 类 | 数据表 / 模式 | -| ------------- | -------------- | -| `Post` | `posts` | -| `LineItem` | `line_items` | -| `Deer` | `deers` | -| `Mouse` | `mice` | -| `Person` | `people` | + ### 模式约定 -根据字段的作用不同,Active Record 对数据表中的字段命名也做了相应的约定: +根据字段的作用不同,Active Record 对数据库表中的字段命名也做了相应的约定: -* **外键** - 使用 `singularized_table_name_id` 形式命名,例如 `item_id`,`order_id`。创建模型关联后,Active Record 会查找这个字段; -* **主键** - 默认情况下,Active Record 使用整数字段 `id` 作为表的主键。使用 [Active Record 迁移]({{ site.baseurl}}/migrations.html)创建数据表时,会自动创建这个字段; +* **外键**:使用 `singularized_table_name_id` 形式命名,例如 `item_id`,`order_id`。创建模型关联后,Active Record 会查找这个字段; +* **主键**:默认情况下,Active Record 使用整数字段 `id` 作为表的主键。使用 [Active Record 迁移](active_record_migrations.html)创建数据库表时,会自动创建这个字段; 还有一些可选的字段,能为 Active Record 实例添加更多的功能: -* `created_at` - 创建记录时,自动设为当前的时间戳; -* `updated_at` - 更新记录时,自动设为当前的时间戳; -* `lock_version` - 在模型中添加[乐观锁定](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html)功能; -* `type` - 让模型使用[单表继承](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance); -* `(association_name)_type` - [多态关联](association_basics.html#polymorphic-associations)的类型; -* `(table_name)_count` - 缓存关联对象的数量。例如,`posts` 表中的 `comments_count` 字段,缓存每篇文章的评论数; +* `created_at`:创建记录时,自动设为当前的日期和时间; +* `updated_at`:更新记录时,自动设为当前的日期和时间; +* `lock_version`:在模型中添加[乐观锁](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html); +* `type`:让模型使用[单表继承](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance); +* `(association_name)_type`:存储[多态关联](association_basics.html#polymorphic-associations)的类型; +* `(table_name)_count`:缓存所关联对象的数量。比如说,一个 `Article` 有多个 `Comment`,那么 `comments_count` 列存储各篇文章现有的评论数量; + +NOTE: 虽然这些字段是可选的,但在 Active Record 中是被保留的。如果想使用相应的功能,就不要把这些保留字段用作其他用途。例如,`type` 这个保留字段是用来指定数据库表使用单表继承(Single Table Inheritance,STI)的。如果不用单表继承,请使用其他的名称,例如“context”,这也能表明数据的作用。 -NOTE: 虽然这些字段是可选的,但在 Active Record 中是被保留的。如果想使用相应的功能,就不要把这些保留字段用作其他用途。例如,`type` 这个保留字段是用来指定数据表使用“单表继承”(STI)的,如果不用 STI,请使用其他的名字,例如“context”,这也能表明该字段的作用。 -创建 Active Record 模型 ------------------------ + -创建 Active Record 模型的过程很简单,只要继承 `ActiveRecord::Base` 类就行了: +## 创建 Active Record 模型 + +创建 Active Record 模型的过程很简单,只要继承 `ApplicationRecord` 类就行了: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord end ``` @@ -94,7 +107,7 @@ CREATE TABLE products ( ); ``` -按照这样的数据表结构,可以编写出下面的代码: +按照这样的数据表结构,可以编写下面的代码: ```ruby p = Product.new @@ -102,53 +115,57 @@ p.name = "Some Book" puts p.name # "Some Book" ``` -不用默认的命名约定 ----------------- + + +## 覆盖命名约定 -如果想使用其他的命名约定,或者在 Rails 程序中使用即有的数据库可以吗?没问题,不用默认的命名约定也很简单。 +如果想使用其他的命名约定,或者在 Rails 应用中使用即有的数据库可以吗?没问题,默认的约定能轻易覆盖。 -使用 `ActiveRecord::Base.table_name=` 方法可以指定数据表的名字: +`ApplicationRecord` 继承自 `ActiveRecord::Base`,后者定义了一系列有用的方法。使用 `ActiveRecord::Base.table_name=` 方法可以指定要使用的表名: ```ruby -class Product < ActiveRecord::Base - self.table_name = "PRODUCT" +class Product < ApplicationRecord + self.table_name = "my_products" end ``` -如果这么做,还要在测试中调用 `set_fixture_class` 方法,手动指定固件(`class_name.yml`)的类名: +如果这么做,还要调用 `set_fixture_class` 方法,手动指定固件(my_products.yml)的类名: ```ruby -class FunnyJoke < ActiveSupport::TestCase - set_fixture_class funny_jokes: Joke - fixtures :funny_jokes +class ProductTest < ActiveSupport::TestCase + set_fixture_class my_products: Product + fixtures :my_products ... end ``` -还可以使用 `ActiveRecord::Base.primary_key=` 方法指定数据表的主键: +还可以使用 `ActiveRecord::Base.primary_key=` 方法指定表的主键: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord self.primary_key = "product_id" end ``` -CRUD:读写数据 -------------- + + +## CRUD:读写数据 CURD 是四种数据操作的简称:C 表示创建,R 表示读取,U 表示更新,D 表示删除。Active Record 自动创建了处理数据表中数据的方法。 + + ### 创建 -Active Record 对象可以使用 Hash 创建,在块中创建,或者创建后手动设置属性。`new` 方法创建一个新对象,`create` 方法创建新对象,并将其存入数据库。 +Active Record 对象可以使用散列创建,在块中创建,或者创建后手动设置属性。`new` 方法创建一个新对象,`create` 方法创建新对象,并将其存入数据库。 -例如,`User` 模型中有两个属性,`name` 和 `occupation`。调用 `create` 方法会创建一个新纪录,并存入数据库: +例如,`User` 模型中有两个属性,`name` 和 `occupation`。调用 `create` 方法会创建一个新记录,并将其存入数据库: ```ruby user = User.create(name: "David", occupation: "Code Artist") ``` -使用 `new` 方法,可以实例化一个新对象,但不会保存: +`new` 方法实例化一个新对象,但不保存: ```ruby user = User.new @@ -158,7 +175,7 @@ user.occupation = "Code Artist" 调用 `user.save` 可以把记录存入数据库。 -如果在 `create` 和 `new` 方法中使用块,会把新创建的对象拉入块中: +最后,如果在 `create` 和 `new` 方法中使用块,会把新创建的对象拉入块中,初始化对象: ```ruby user = User.new do |u| @@ -167,36 +184,39 @@ user = User.new do |u| end ``` + + ### 读取 Active Record 为读取数据库中的数据提供了丰富的 API。下面举例说明。 ```ruby -# return a collection with all users +# 返回所有用户组成的集合 users = User.all ``` ```ruby -# return the first user +# 返回第一个用户 user = User.first ``` ```ruby -# return the first user named David +# 返回第一个名为 David 的用户 david = User.find_by(name: 'David') ``` ```ruby -# find all users named David who are Code Artists and sort by created_at -# in reverse chronological order -users = User.where(name: 'David', occupation: 'Code Artist').order('created_at DESC') +# 查找所有名为 David,职业为 Code Artists 的用户,而且按照 created_at 反向排列 +users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc) ``` -[Active Record 查询](active_record_querying.html)一文会详细介绍查询 Active Record 模型的方法。 +[Active Record 查询接口](active_record_querying.html)会详细介绍查询 Active Record 模型的方法。 + + ### 更新 -得到 Active Record 对象后,可以修改其属性,然后再存入数据库。 +检索到 Active Record 对象后,可以修改其属性,然后再将其存入数据库。 ```ruby user = User.find_by(name: 'David') @@ -204,58 +224,64 @@ user.name = 'Dave' user.save ``` -还有个简写方式,使用 Hash,指定属性名和属性值,例如: +还有种使用散列的简写方式,指定属性名和属性值,例如: ```ruby user = User.find_by(name: 'David') user.update(name: 'Dave') ``` -一次更新多个属性时使用这种方法很方便。如果想批量更新多个记录,可以使用类方法 `update_all`: +一次更新多个属性时使用这种方法最方便。如果想批量更新多个记录,可以使用类方法 `update_all`: ```ruby User.update_all "max_login_attempts = 3, must_change_password = 'true'" ``` + + ### 删除 -类似地,得到 Active Record 对象后还可以将其销毁,从数据库中删除。 +类似地,检索到 Active Record 对象后还可以将其销毁,从数据库中删除。 ```ruby user = User.find_by(name: 'David') user.destroy ``` -数据验证 -------- + -在存入数据库之前,Active Record 还可以验证模型。模型验证有很多方法,可以检查属性值是否不为空、是否是唯一的,或者没有在数据库中出现过,等等。 +## 数据验证 -把数据存入数据库之前进行验证是十分重要的步骤,所以调用 `create`、`save`、`update` 这三个方法时会做数据验证,验证失败时返回 `false`,此时不会对数据库做任何操作。这三个方法都要对应的爆炸方法(`create!`,`save!`,`update!`),爆炸方法要严格一些,如果验证失败,会抛出 `ActiveRecord::RecordInvalid` 异常。下面是个简单的例子: +在存入数据库之前,Active Record 还可以验证模型。模型验证有很多方法,可以检查属性值是否不为空,是否是唯一的、没有在数据库中出现过,等等。 + +把数据存入数据库之前进行验证是十分重要的步骤,所以调用 `save` 和 `update` 方法时会做数据验证。验证失败时返回 `false`,此时不会对数据库做任何操作。这两个方法都有对应的爆炸方法(`save!` 和 `update!`)。爆炸方法要严格一些,如果验证失败,抛出 `ActiveRecord::RecordInvalid` 异常。下面举个简单的例子: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :name, presence: true end -User.create # => false -User.create! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank +user = User.new +user.save # => false +user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank ``` -[Active Record 数据验证](active_record_validations.html)一文会详细介绍数据验证。 +[Active Record 数据验证](active_record_validations.html)会详细介绍数据验证。 + + + +## 回调 -回调 ----- +Active Record 回调用于在模型生命周期的特定事件上绑定代码,相应的事件发生时,执行绑定的代码。例如创建新纪录时、更新记录时、删除记录时,等等。[Active Record 回调](active_record_callbacks.html)会详细介绍回调。 -Active Record 回调可以在模型声明周期的特定事件上绑定代码,相应的事件发生时,执行这些代码。例如创建新纪录时,更新记录时,删除记录时,等等。[Active Record 回调](active_record_callbacks.html)一文会详细介绍回调。 + -迁移 ----- +## 迁移 -Rails 提供了一个 DSL 用来处理数据库模式,叫做“迁移”。迁移的代码存储在特定的文件中,通过 `rake` 调用,可以用在 Active Record 支持的所有数据库上。下面这个迁移会新建一个数据表: +Rails 提供了一个 DSL(Domain-Specific Language)用来处理数据库模式,叫做“迁移”。迁移的代码存储在特定的文件中,通过 `rails` 命令执行,可以用在 Active Record 支持的所有数据库上。下面这个迁移新建一个表: ```ruby -class CreatePublications < ActiveRecord::Migration +class CreatePublications < ActiveRecord::Migration[5.0] def change create_table :publications do |t| t.string :title @@ -272,6 +298,6 @@ class CreatePublications < ActiveRecord::Migration end ``` -Rails 会跟踪哪些迁移已经应用到数据库中,还提供了回滚功能。创建数据表要执行 `rake db:migrate` 命令;回滚操作要执行 `rake db:rollback` 命令。 +Rails 会跟踪哪些迁移已经应用到数据库上,还提供了回滚功能。为了创建表,要执行 `rails db:migrate` 命令。如果想回滚,则执行 `rails db:rollback` 命令。 -注意,上面的代码和具体的数据库种类无关,可用于 MySQL、PostgreSQL、Oracle 等数据库。关于迁移的详细介绍,参阅 [Active Record 迁移](migrations.html)一文。 +注意,上面的代码与具体的数据库种类无关,可用于 MySQL、PostgreSQL、Oracle 等数据库。关于迁移的详细介绍,参阅[Active Record 迁移](active_record_migrations.html)。 diff --git a/source/zh-CN/active_record_callbacks.md b/source/zh-CN/active_record_callbacks.md index 069de1b..3915e4d 100644 --- a/source/zh-CN/active_record_callbacks.md +++ b/source/zh-CN/active_record_callbacks.md @@ -1,39 +1,42 @@ -Active Record 回调 -================== +# Active Record 回调 本文介绍如何介入 Active Record 对象的生命周期。 -读完本文,你将学到: +读完本文后,您将学到: -* Active Record 对象的生命周期; -* 如何编写回调方法响应对象声明周期内发生的事件; -* 如何把常用的回调封装到特殊的类中; +* Active Record 对象的生命周期; +* 如何创建用于响应对象生命周期内事件的回调方法; +* 如何把常用的回调封装到特殊的类中。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -对象的声明周期 ------------- + -在 Rails 程序运行过程中,对象可以被创建、更新和销毁。Active Record 为对象的生命周期提供了很多钩子,让你控制程序及其数据。 +## 对象的生命周期 -回调可以在对象的状态改变之前或之后触发指定的逻辑操作。 +在 Rails 应用正常运作期间,对象可以被创建、更新或删除。Active Record 为对象的生命周期提供了钩子,使我们可以控制应用及其数据。 -回调简介 -------- +回调使我们可以在对象状态更改之前或之后触发逻辑。 -回调是在对象生命周期的特定时刻执行的方法。回调方法可以在 Active Record 对象创建、保存、更新、删除、验证或从数据库中读出时执行。 + + +## 回调概述 + +回调是在对象生命周期的某些时刻被调用的方法。通过回调,我们可以编写在创建、保存、更新、删除、验证或从数据库中加载 Active Record 对象时执行的代码。 + + ### 注册回调 -在使用回调之前,要先注册。回调方法的定义和普通的方法一样,然后使用类方法注册: +回调在使用之前需要注册。我们可以先把回调定义为普通方法,然后使用宏式类方法把这些普通方法注册为回调: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :login, :email, presence: true before_validation :ensure_login_has_a_value - protected + private def ensure_login_has_a_value if login.nil? self.login = email unless email.blank? @@ -42,10 +45,10 @@ class User < ActiveRecord::Base end ``` -这种类方法还可以接受一个代码块。如果操作可以使用一行代码表述,可以考虑使用代码块形式。 +宏式类方法也接受块。如果块中的代码短到可以放在一行里,可以考虑使用这种编程风格: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :login, :email, presence: true before_create do @@ -54,18 +57,18 @@ class User < ActiveRecord::Base end ``` -注册回调时可以指定只在对象生命周期的特定事件发生时执行: +回调也可以注册为仅被某些生命周期事件触发: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord before_validation :normalize_name, on: :create - # :on takes an array as well + # :on 选项的值也可以是数组 after_validation :set_location, on: [ :create, :update ] - protected + private def normalize_name - self.name = self.name.downcase.titleize + self.name = name.downcase.titleize end def set_location @@ -74,53 +77,65 @@ class User < ActiveRecord::Base end ``` -一般情况下,都把回调方法定义为受保护的方法或私有方法。如果定义成公共方法,回调就可以在模型外部调用,违背了对象封装原则。 +通常应该把回调定义为私有方法。如果把回调定义为公共方法,就可以从模型外部调用回调,这样做违反了对象封装原则。 + + -可用的回调 ---------- +## 可用的回调 -下面列出了所有可用的 Active Record 回调,按照执行各操作时触发的顺序: +下面按照回调在 Rails 应用正常运作期间被调用的顺序,列出所有可用的 Active Record 回调。 + + ### 创建对象 -* `before_validation` -* `after_validation` -* `before_save` -* `around_save` -* `before_create` -* `around_create` -* `after_create` -* `after_save` +* `before_validation` +* `after_validation` +* `before_save` +* `around_save` +* `before_create` +* `around_create` +* `after_create` +* `after_save` +* `after_commit/after_rollback` + + ### 更新对象 -* `before_validation` -* `after_validation` -* `before_save` -* `around_save` -* `before_update` -* `around_update` -* `after_update` -* `after_save` +* `before_validation` +* `after_validation` +* `before_save` +* `around_save` +* `before_update` +* `around_update` +* `after_update` +* `after_save` +* `after_commit/after_rollback` + + -### 销毁对象 +### 删除对象 -* `before_destroy` -* `around_destroy` -* `after_destroy` +* `before_destroy` +* `around_destroy` +* `after_destroy` +* `after_commit/after_rollback` -WARNING: 创建和更新对象时都会触发 `after_save`,但不管注册的顺序,总在 `after_create` 和 `after_update` 之后执行。 +WARNING: 无论按什么顺序注册回调,在创建和更新对象时,`after_save` 回调总是在更明确的 `after_create` 和 `after_update` 回调之后被调用。 -### `after_initialize` 和 `after_find` + -`after_initialize` 回调在 Active Record 对象初始化时执行,包括直接使用 `new` 方法初始化和从数据库中读取记录。`after_initialize` 回调不用直接重定义 Active Record 的 `initialize` 方法。 +### `after_initialize` 和 `after_find` 回调 -`after_find` 回调在从数据库中读取记录时执行。如果同时注册了 `after_find` 和 `after_initialize` 回调,`after_find` 会先执行。 +当 Active Record 对象被实例化时,不管是通过直接使用 `new` 方法还是从数据库加载记录,都会调用 `after_initialize` 回调。使用这个回调可以避免直接覆盖 Active Record 的 `initialize` 方法。 -`after_initialize` 和 `after_find` 没有对应的 `before_*` 回调,但可以像其他回调一样注册。 +当 Active Record 从数据库中加载记录时,会调用 `after_find` 回调。如果同时定义了 `after_initialize` 和 `after_find` 回调,会先调用 `after_find` 回调。 + +`after_initialize` 和 `after_find` 回调没有对应的 `before_*` 回调,这两个回调的注册方式和其他 Active Record 回调一样。 ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord after_initialize do |user| puts "You have initialized an object!" end @@ -129,7 +144,9 @@ class User < ActiveRecord::Base puts "You have found an object!" end end +``` +```irb >> User.new You have initialized an object! => # @@ -140,17 +157,21 @@ You have initialized an object! => # ``` -### `after_touch` + -`after_touch` 回调在触碰 Active Record 对象时执行。 +### `after_touch` 回调 + +当我们在 Active Record 对象上调用 `touch` 方法时,会调用 `after_touch` 回调。 ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord after_touch do |user| puts "You have touched an object" end end +``` +```irb >> u = User.create(name: 'Kuldeep') => # @@ -159,17 +180,17 @@ You have touched an object => true ``` -可以结合 `belongs_to` 一起使用: +`after_touch` 回调可以和 `belongs_to` 一起使用: ```ruby -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord belongs_to :company, touch: true after_touch do puts 'An Employee was touched' end end -class Company < ActiveRecord::Base +class Company < ApplicationRecord has_many :employees after_touch :log_when_employees_or_company_touched @@ -178,7 +199,9 @@ class Company < ActiveRecord::Base puts 'Employee/Company was touched' end end +``` +```irb >> @employee = Employee.last => # @@ -189,151 +212,159 @@ An Employee was touched => true ``` -执行回调 -------- + -下面的方法会触发执行回调: +## 调用回调 -* `create` -* `create!` -* `decrement!` -* `destroy` -* `destroy!` -* `destroy_all` -* `increment!` -* `save` -* `save!` -* `save(validate: false)` -* `toggle!` -* `update_attribute` -* `update` -* `update!` -* `valid?` +下面这些方法会触发回调: -`after_find` 回调由以下查询方法触发执行: +* `create` +* `create!` +* `decrement!` +* `destroy` +* `destroy!` +* `destroy_all` +* `increment!` +* `save` +* `save!` +* `save(validate: false)` +* `toggle!` +* `update_attribute` +* `update` +* `update!` +* `valid?` -* `all` -* `first` -* `find` -* `find_by` -* `find_by_*` -* `find_by_*!` -* `find_by_sql` -* `last` +此外,下面这些查找方法会触发 `after_find` 回调: -`after_initialize` 回调在新对象初始化时触发执行。 +* `all` +* `first` +* `find` +* `find_by` +* `find_by_*` +* `find_by_*!` +* `find_by_sql` +* `last` -NOTE: `find_by_*` 和 `find_by_*!` 是为每个属性生成的动态查询方法,详情参见“[动态查询方法](active_record_querying.html#dynamic-finders)”一节。 +每次初始化类的新对象时都会触发 `after_initialize` 回调。 -跳过回调 --------- +NOTE: `find_by_*` 和 `find_by_*!` 方法是为每个属性自动生成的动态查找方法。关于动态查找方法的更多介绍,请参阅 [动态查找方法](active_record_querying.html#dynamic-finders)。 -和数据验证一样,回调也可跳过,使用下列方法即可: + -* `decrement` -* `decrement_counter` -* `delete` -* `delete_all` -* `increment` -* `increment_counter` -* `toggle` -* `touch` -* `update_column` -* `update_columns` -* `update_all` -* `update_counters` +## 跳过回调 -使用这些方法是要特别留心,因为重要的业务逻辑可能在回调中完成。如果没弄懂回调的作用直接跳过,可能导致数据不合法。 +和验证一样,我们可以跳过回调。使用下面这些方法可以跳过回调: -终止执行 --------- +* `decrement` +* `decrement_counter` +* `delete` +* `delete_all` +* `increment` +* `increment_counter` +* `toggle` +* `touch` +* `update_column` +* `update_columns` +* `update_all` +* `update_counters` -在模型中注册回调后,回调会加入一个执行队列。这个队列中包含模型的数据验证,注册的回调,以及要执行的数据库操作。 +请慎重地使用这些方法,因为有些回调包含了重要的业务规则和应用逻辑,在不了解潜在影响的情况下就跳过回调,可能导致无效数据。 -整个回调链包含在一个事务中。如果任何一个 `before_*` 回调方法返回 `false` 或抛出异常,整个回调链都会终止执行,撤销事务;而 `after_*` 回调只有抛出异常才能达到相同的效果。 + -WARNING: `ActiveRecord::Rollback` 之外的异常在回调链终止之后,还会由 Rails 再次抛出。抛出 `ActiveRecord::Rollback` 之外的异常,可能导致不应该抛出异常的方法(例如 `save` 和 `update_attributes`,应该返回 `true` 或 `false`)无法执行。 +## 停止执行 -关联回调 -------- +回调在模型中注册后,将被加入队列等待执行。这个队列包含了所有模型的验证、已注册的回调和将要执行的数据库操作。 -回调能在模型关联中使用,甚至可由关联定义。假如一个用户发布了多篇文章,如果用户删除了,他发布的文章也应该删除。下面我们在 `Post` 模型中注册一个 `after_destroy` 回调,应用到 `User` 模型上: +整个回调链包装在一个事务中。只要有回调抛出异常,回调链随即停止,并且发出 `ROLLBACK` 消息。如果想故意停止回调链,可以这么做: ```ruby -class User < ActiveRecord::Base - has_many :posts, dependent: :destroy +throw :abort +``` + +WARNING: 当回调链停止后,Rails 会重新抛出除了 `ActiveRecord::Rollback` 和 `ActiveRecord::RecordInvalid` 之外的其他异常。这可能导致那些预期 `save` 和 `update_attributes` 等方法(通常返回 `true` 或 `false` )不会引发异常的代码出错。 + + + +## 关联回调 + +回调不仅可以在模型关联中使用,还可以通过模型关联定义。假设有一个用户在博客中发表了多篇文章,现在我们要删除这个用户,那么这个用户的所有文章也应该删除,为此我们通过 `Article` 模型和 `User` 模型的关联来给 `User` 模型添加一个 `after_destroy` 回调: + +```ruby +class User < ApplicationRecord + has_many :articles, dependent: :destroy end -class Post < ActiveRecord::Base +class Article < ApplicationRecord after_destroy :log_destroy_action def log_destroy_action - puts 'Post destroyed' + puts 'Article destroyed' end end +``` +```irb >> user = User.first => # ->> user.posts.create! -=> # +>> user.articles.create! +=> #
>> user.destroy -Post destroyed +Article destroyed => # ``` -条件回调 -------- + -和数据验证类似,也可以在满足指定条件时再调用回调方法。条件通过 `:if` 和 `:unless` 选项指定,选项的值可以是 Symbol、字符串、`Proc` 或数组。`:if` 选项指定什么时候调用回调。如果要指定何时不调用回调,使用 `:unless` 选项。 +## 条件回调 -### 使用 Symbol +和验证一样,我们可以在满足指定条件时再调用回调方法。为此,我们可以使用 `:if` 和 `:unless` 选项,选项的值可以是符号、`Proc` 或数组。要想指定在哪些条件下调用回调,可以使用 `:if` 选项。要想指定在哪些条件下不调用回调,可以使用 `:unless` 选项。 -:if 和 :unless 选项的值为 Symbol 时,表示要在调用回调之前执行对应的判断方法。使用 `:if` 选项时,如果判断方法返回 `false`,就不会调用回调;使用 `:unless` 选项时,如果判断方法返回 `true`,就不会调用回调。Symbol 是最常用的设置方式。使用这种方式注册回调时,可以使用多个判断方法检查是否要调用回调。 + -```ruby -class Order < ActiveRecord::Base - before_save :normalize_card_number, if: :paid_with_card? -end -``` - -### 使用字符串 +### 使用符号作为 `:if` 和 `:unless` 选项的值 -`:if` 和 `:unless` 选项的值还可以是字符串,但必须是 RUby 代码,传入 `eval` 方法中执行。当字符串表示的条件非常短时才应该是使用这种形式。 +可以使用符号作为 `:if` 和 `:unless` 选项的值,这个符号用于表示先于回调调用的断言方法。当使用 `:if` 选项时,如果断言方法返回 `false` 就不会调用回调;当使用 `:unless` 选项时,如果断言方法返回 `true` 就不会调用回调。使用符号作为 `:if` 和 `:unless` 选项的值是最常见的方式。在使用这种方式注册回调时,我们可以同时使用几个不同的断言,用于检查是否应该调用回调。 ```ruby -class Order < ActiveRecord::Base - before_save :normalize_card_number, if: "paid_with_card?" +class Order < ApplicationRecord + before_save :normalize_card_number, if: :paid_with_card? end ``` -### 使用 Proc + -`:if` 和 `:unless` 选项的值还可以是 Proc 对象。这种形式最适合用在一行代码能表示的条件上。 +### 使用 Proc 作为 `:if` 和 `:unless` 选项的值 + +最后,可以使用 Proc 作为 `:if` 和 `:unless` 选项的值。在验证方法非常短时最适合使用这种方式,这类验证方法通常只有一行代码: ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord before_save :normalize_card_number, if: Proc.new { |order| order.paid_with_card? } end ``` -### 回调的多重条件 + + +### 在条件回调中使用多个条件 -注册条件回调时,可以同时使用 `:if` 和 `:unless` 选项: +在编写条件回调时,我们可以在同一个回调声明中混合使用 `:if` 和 `:unless` 选项: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord after_create :send_email_to_author, if: :author_wants_emails?, - unless: Proc.new { |comment| comment.post.ignore_comments? } + unless: Proc.new { |comment| comment.article.ignore_comments? } end ``` -回调类 ------- + + +## 回调类 -有时回调方法可以在其他模型中重用,我们可以将其封装在类中。 +有时需要在其他模型中重用已有的回调方法,为了解决这个问题,Active Record 允许我们用类来封装回调方法。有了回调类,回调方法的重用就变得非常容易。 -在下面这个例子中,我们为 `PictureFile` 模型定义了一个 `after_destroy` 回调: +在下面的例子中,我们为 `PictureFile` 模型创建了 `PictureFileCallbacks` 回调类,在这个回调类中包含了 `after_destroy` 回调方法: ```ruby class PictureFileCallbacks @@ -345,15 +376,15 @@ class PictureFileCallbacks end ``` -在类中定义回调方法时(如上),可把模型对象作为参数传入。然后可以在模型中使用这个回调: +在上面的代码中我们可以看到,当在回调类中声明回调方法时,回调方法接受模型对象作为参数。回调类定义之后就可以在模型中使用了: ```ruby -class PictureFile < ActiveRecord::Base +class PictureFile < ApplicationRecord after_destroy PictureFileCallbacks.new end ``` -注意,因为回调方法被定义成实例方法,所以要实例化 `PictureFileCallbacks`。如果回调要使用实例化对象的状态,使用这种定义方式很有用。不过,一般情况下,定义为类方法更说得通: +请注意,上面我们把回调声明为实例方法,因此需要实例化新的 `PictureFileCallbacks` 对象。当回调想要使用实例化的对象的状态时,这种声明方式特别有用。尽管如此,一般我们会把回调声明为类方法: ```ruby class PictureFileCallbacks @@ -365,22 +396,23 @@ class PictureFileCallbacks end ``` -如果按照这种方式定义回调方法,就不用实例化 `PictureFileCallbacks`: +如果把回调声明为类方法,就不需要实例化新的 `PictureFileCallbacks` 对象。 ```ruby -class PictureFile < ActiveRecord::Base +class PictureFile < ApplicationRecord after_destroy PictureFileCallbacks end ``` -在回调类中可以定义任意数量的回调方法。 +我们可以根据需要在回调类中声明任意多个回调。 -事务回调 -------- + -还有两个回调会在数据库事务完成时触发:`after_commit` 和 `after_rollback`。这两个回调和 `after_save` 很像,只不过在数据库操作提交或回滚之前不会执行。如果模型要和数据库事务之外的系统交互,就可以使用这两个回调。 +## 事务回调 -例如,在前面的例子中,`PictureFile` 模型中的记录删除后,还要删除相应的文件。如果执行 `after_destroy` 回调之后程序抛出了异常,事务就会回滚,文件会被删除,但模型的状态前后不一致。假设在下面的代码中,`picture_file_2` 是不合法的,那么调用 `save!` 方法会抛出异常。 +`after_commit` 和 `after_rollback` 这两个回调会在数据库事务完成时触发。它们和 `after_save` 回调非常相似,区别在于它们在数据库变更已经提交或回滚后才会执行,常用于 Active Record 模型需要和数据库事务之外的系统交互的场景。 + +例如,在前面的例子中,`PictureFile` 模型中的记录删除后,还要删除相应的文件。如果 `after_destroy` 回调执行后应用引发异常,事务就会回滚,文件会被删除,模型会保持不一致的状态。例如,假设在下面的代码中,`picture_file_2` 对象是无效的,那么调用 `save!` 方法会引发错误: ```ruby PictureFile.transaction do @@ -389,11 +421,11 @@ PictureFile.transaction do end ``` -使用 `after_commit` 回调可以解决这个问题。 +通过使用 `after_commit` 回调,我们可以解决这个问题: ```ruby -class PictureFile < ActiveRecord::Base - after_commit :delete_picture_file_from_disk, on: [:destroy] +class PictureFile < ApplicationRecord + after_commit :delete_picture_file_from_disk, on: :destroy def delete_picture_file_from_disk if File.exist?(filepath) @@ -403,6 +435,24 @@ class PictureFile < ActiveRecord::Base end ``` -NOTE: `:on` 选项指定什么时候出发回调。如果不设置 `:on` 选项,每各个操作都会触发回调。 +NOTE: `:on` 选项说明什么时候触发回调。如果不提供 `:on` 选项,那么每个动作都会触发回调。 + +由于只在执行创建、更新或删除动作时触发 `after_commit` 回调是很常见的,这些操作都拥有别名: + +* `after_create_commit` +* `after_update_commit` +* `after_destroy_commit` + +```ruby +class PictureFile < ApplicationRecord + after_destroy_commit :delete_picture_file_from_disk + + def delete_picture_file_from_disk + if File.exist?(filepath) + File.delete(filepath) + end + end +end +``` -WARNING: `after_commit` 和 `after_rollback` 回调确保模型的创建、更新和销毁等操作在事务中完成。如果这两个回调抛出了异常,会被忽略,因此不会干扰其他回调。因此,如果回调可能抛出异常,就要做适当的补救和处理。 +WARNING: 在事务中创建、更新或删除模型时会调用 `after_commit` 和 `after_rollback` 回调。然而,如果其中有一个回调引发异常,异常会向上冒泡,后续 `after_commit` 和 `after_rollback` 回调不再执行。因此,如果回调代码可能引发异常,就需要在回调中救援并进行适当处理,以便让其他回调继续运行。 diff --git a/source/zh-CN/active_record_migrations.md b/source/zh-CN/active_record_migrations.md index 49404b7..0ec25dd 100644 --- a/source/zh-CN/active_record_migrations.md +++ b/source/zh-CN/active_record_migrations.md @@ -1,28 +1,29 @@ -Active Record 数据库迁移 -======================= +# Active Record 迁移 -迁移是 Active Record 提供的一个功能,按照时间顺序管理数据库模式。使用迁移,无需编写 SQL,使用简单的 Ruby DSL 就能修改数据表。 +迁移是 Active Record 的一个特性,允许我们按时间顺序管理数据库模式。有了迁移,就不必再用纯 SQL 来修改数据库模式,而是可以使用简单的 Ruby DSL 来描述对数据表的修改。 -读完本文,你将学到: +读完本文后,您将学到: -* 生成迁移文件的生成器; -* Active Record 提供用来修改数据库的方法; -* 管理迁移和数据库模式的 Rake 任务; -* 迁移和 `schema.rb` 文件的关系; +* 用于创建迁移的生成器; +* Active Record 提供的用于操作数据库的方法; +* 用于操作迁移和数据库模式的 `bin/rails` 任务; +* 迁移和 `schema.rb` 文件的关系。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -迁移简介 -------- + -迁移使用一种统一、简单的方式,按照时间顺序修改数据库的模式。迁移使用 Ruby DSL 编写,因此不用手动编写 SQL 语句,对数据库的操作和所用的数据库种类无关。 +## 迁移概述 -你可以把每个迁移看做数据库的一个修订版本。数据库中一开始什么也没有,各个迁移会添加或删除数据表、字段或记录。Active Record 知道如何按照时间线更新数据库,不管数据库现在的模式如何,都能更新到最新结构。同时,Active Record 还会更新 `db/schema.rb` 文件,匹配最新的数据库结构。 +迁移是以一致和轻松的方式按时间顺序修改数据库模式的实用方法。它使用 Ruby DSL,因此不必手动编写 SQL,从而实现了数据库无关的数据库模式的创建和修改。 -下面是一个迁移示例: +我们可以把迁移看做数据库的新“版本”。数据库模式一开始并不包含任何内容,之后通过一个个迁移来添加或删除数据表、字段和记录。 +Active Record 知道如何沿着时间线更新数据库模式,使其从任何历史版本更新为最新版本。Active Record 还会更新 `db/schema.rb` 文件,以匹配最新的数据库结构。 + +下面是一个迁移的示例: ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name @@ -34,18 +35,18 @@ class CreateProducts < ActiveRecord::Migration end ``` -这个迁移创建了一个名为 `products` 的表,然后在表中创建字符串字段 `name` 和文本字段 `description`。名为 `id` 的主键字段会被自动创建。`id` 字段是所有 Active Record 模型的默认主键。`timestamps` 方法创建两个字段:`created_at` 和 `updated_at`。如果数据表中有这两个字段,Active Record 会负责操作。 +这个迁移用于添加 `products` 数据表,数据表中包含 `name` 字符串字段和 `description` 文本字段。同时隐式添加了 `id` 主键字段,这是所有 Active Record 模型的默认主键。`timestamps` 宏添加了 `created_at` 和 `updated_at` 两个字段。后面这几个特殊字段只要存在就都由 Active Record 自动管理。 -注意,对数据库的改动按照时间向前 推移。运行迁移之前,数据表还不存在。运行迁移后,才会创建数据表。Active Record 知道如何撤销迁移,如果回滚这次迁移,数据表会被删除。 +注意这里定义的对数据库的修改是按时间进行的。在这个迁移运行之前,数据表还不存在。在这个迁移运行之后,数据表就被创建了。Active Record 还知道如何撤销这个迁移:如果我们回滚这个迁移,数据表就会被删除。 -在支持事务的数据库中,对模式的改动会在一个事务中执行。如果数据库不支持事务,迁移失败时,成功执行的操作将无法回滚。如要回滚,必须手动改回来。 +对于支持事务并提供了用于修改数据库模式的语句的数据库,迁移被包装在事务中。如果数据库不支持事务,那么当迁移失败时,已成功的那部分操作将无法回滚。这种情况下只能手动完成相应的回滚操作。 -NOTE: 某些查询无法在事务中运行。如果适配器支持 DDL 事务,可以在某个迁移中调用 `disable_ddl_transaction!` 方法禁用。 +NOTE: 某些查询不能在事务内部运行。如果数据库适配器支持 DDL 事务,就可以使用 `disable_ddl_transaction!` 方法在某个迁移中临时禁用事务。 -如果想在迁移中执行 Active Record 不知如何撤销的操作,可以使用 `reversible` 方法: +如果想在迁移中完成一些 Active Record 不知如何撤销的操作,可以使用 `reversible` 方法: ```ruby -class ChangeProductsPrice < ActiveRecord::Migration +class ChangeProductsPrice < ActiveRecord::Migration[5.0] def change reversible do |dir| change_table :products do |t| @@ -57,10 +58,10 @@ class ChangeProductsPrice < ActiveRecord::Migration end ``` -或者不用 `change` 方法,分别使用 `up` 和 `down` 方法: +或者用 `up` 和 `down` 方法来代替 `change` 方法: ```ruby -class ChangeProductsPrice < ActiveRecord::Migration +class ChangeProductsPrice < ActiveRecord::Migration[5.0] def up change_table :products do |t| t.change :price, :string @@ -75,54 +76,57 @@ class ChangeProductsPrice < ActiveRecord::Migration end ``` -创建迁移 -------- + + +## 创建迁移 + + -### 单独创建迁移 +### 创建独立的迁移 -迁移文件存储在 `db/migrate` 文件夹中,每个迁移保存在一个文件中。文件名采用 `YYYYMMDDHHMMSS_create_products.rb` 形式,即一个 UTC 时间戳后加以下划线分隔的迁移名。迁移的类名(驼峰式)要和文件名时间戳后面的部分匹配。例如,在 `20080906120000_create_products.rb` 文件中要定义 `CreateProducts` 类;在 `20080906120001_add_details_to_products.rb` 文件中要定义 `AddDetailsToProducts` 类。文件名中的时间戳决定要运行哪个迁移,以及按照什么顺序运行。从其他程序中复制迁移,或者自己生成迁移时,要注意运行的顺序。 +迁移文件储存在 `db/migrate` 文件夹中,一个迁移文件包含一个迁移类。文件名采用 `YYYYMMDDHHMMSS_create_products.rb` 形式,即 UTC 时间戳加上下划线再加上迁移的名称。迁移类的名称(驼峰式)应该匹配文件名中迁移的名称。例如,在 `20080906120000_create_products.rb` 文件中应该定义 `CreateProducts` 类,在 `20080906120001_add_details_to_products.rb` 文件中应该定义 `AddDetailsToProducts` 类。Rails 根据文件名的时间戳部分确定要运行的迁移和迁移运行的顺序,因此当需要把迁移文件复制到其他 Rails 应用,或者自己生成迁移文件时,一定要注意迁移运行的顺序。 -自己计算时间戳不是件简单的事,所以 Active Record 提供了一个生成器: +当然,计算时间戳不是什么有趣的事,因此 Active Record 提供了生成器: -```bash -$ rails generate migration AddPartNumberToProducts +```sh +$ bin/rails generate migration AddPartNumberToProducts ``` -这个命令生成一个空的迁移,但名字已经起好了: +上面的命令会创建空的迁移,并进行适当命名: ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change end end ``` -如果迁移的名字是“AddXXXToYYY”或者“RemoveXXXFromYYY”这种格式,而且后面跟着一个字段名和类型列表,那么迁移中会生成合适的 `add_column` 或 `remove_column` 语句。 +如果迁移名称是 `AddXXXToYYY` 或 `RemoveXXXFromYYY` 的形式,并且后面跟着字段名和类型列表,那么会生成包含合适的 `add_column` 或 `remove_column` 语句的迁移。 -```bash -$ rails generate migration AddPartNumberToProducts part_number:string +```sh +$ bin/rails generate migration AddPartNumberToProducts part_number:string ``` -这个命令生成的迁移如下: +上面的命令会生成: ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string end end ``` -如果想为新建的字段创建添加索引,可以这么做: +还可以像下面这样在新建字段上添加索引: -```bash -$ rails generate migration AddPartNumberToProducts part_number:string:index +```sh +$ bin/rails generate migration AddPartNumberToProducts part_number:string:index ``` -这个命令生成的迁移如下: +上面的命令会生成: ```ruby -class AddPartNumberToProducts < ActiveRecord::Migration +class AddPartNumberToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string add_index :products, :part_number @@ -130,32 +134,32 @@ class AddPartNumberToProducts < ActiveRecord::Migration end ``` -类似地,还可以生成删除字段的迁移: +类似地,还可以生成用于删除字段的迁移: -```bash -$ rails generate migration RemovePartNumberFromProducts part_number:string +```sh +$ bin/rails generate migration RemovePartNumberFromProducts part_number:string ``` -这个命令生成的迁移如下: +上面的命令会生成: ```ruby -class RemovePartNumberFromProducts < ActiveRecord::Migration +class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0] def change remove_column :products, :part_number, :string end end ``` -迁移生成器不单只能创建一个字段,例如: +还可以生成用于添加多个字段的迁移,例如: -```bash -$ rails generate migration AddDetailsToProducts part_number:string price:decimal +```sh +$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal ``` -生成的迁移如下: +上面的命令会生成: ```ruby -class AddDetailsToProducts < ActiveRecord::Migration +class AddDetailsToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :part_number, :string add_column :products, :price, :decimal @@ -163,16 +167,16 @@ class AddDetailsToProducts < ActiveRecord::Migration end ``` -如果迁移名是“CreateXXX”形式,后面跟着一串字段名和类型声明,迁移就会创建名为“XXX”的表,以及相应的字段。例如: +如果迁移名称是 `CreateXXX` 的形式,并且后面跟着字段名和类型列表,那么会生成用于创建包含指定字段的 `XXX` 数据表的迁移。例如: -```bash -$ rails generate migration CreateProducts name:string part_number:string +```sh +$ bin/rails generate migration CreateProducts name:string part_number:string ``` -生成的迁移如下: +上面的命令会生成: ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name @@ -182,36 +186,36 @@ class CreateProducts < ActiveRecord::Migration end ``` -生成器生成的只是一些基础代码,你可以根据需要修改 `db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` 文件,增删代码。 +和往常一样,上面的命令生成的代码只是一个起点,我们可以修改 `db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` 文件,根据需要增删代码。 -在生成器中还可把字段类型设为 `references`(还可使用 `belongs_to`)。例如: +生成器也接受 `references` 字段类型作为参数(还可使用 `belongs_to`),例如: -```bash -$ rails generate migration AddUserRefToProducts user:references +```sh +$ bin/rails generate migration AddUserRefToProducts user:references ``` -生成的迁移如下: +上面的命令会生成: ```ruby -class AddUserRefToProducts < ActiveRecord::Migration +class AddUserRefToProducts < ActiveRecord::Migration[5.0] def change - add_reference :products, :user, index: true + add_reference :products, :user, foreign_key: true end end ``` -这个迁移会创建 `user_id` 字段,并建立索引。 +这个迁移会创建 `user_id` 字段并添加索引。关于 `add_reference` 选项的更多介绍,请参阅 [API 文档](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference)。 -如果迁移名中包含 `JoinTable`,生成器还会创建联合数据表: +如果迁移名称中包含 `JoinTable`,生成器会创建联结数据表: -```bash -rails g migration CreateJoinTableCustomerProduct customer product +```sh +$ bin/rails g migration CreateJoinTableCustomerProduct customer product ``` -生成的迁移如下: +上面的命令会生成: ```ruby -class CreateJoinTableCustomerProduct < ActiveRecord::Migration +class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0] def change create_join_table :customers, :products do |t| # t.index [:customer_id, :product_id] @@ -221,18 +225,20 @@ class CreateJoinTableCustomerProduct < ActiveRecord::Migration end ``` + + ### 模型生成器 -模型生成器和脚手架生成器会生成合适的迁移,创建模型。迁移中会包含创建所需数据表的代码。如果在生成器中指定了字段,还会生成创建字段的代码。例如,运行下面的命令: +模型和脚手架生成器会生成适用于添加新模型的迁移。这些迁移中已经包含用于创建有关数据表的指令。如果我们告诉 Rails 想要哪些字段,那么添加这些字段所需的语句也会被创建。例如,运行下面的命令: -```bash -$ rails generate model Product name:string description:text +```sh +$ bin/rails generate model Product name:string description:text ``` -会生成如下的迁移: +上面的命令会创建下面的迁移: ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.string :name @@ -244,43 +250,42 @@ class CreateProducts < ActiveRecord::Migration end ``` -字段的名字和类型数量不限。 - -### 支持的类型修饰符 +我们可以根据需要添加“字段名称/类型”对,没有数量限制。 -在字段类型后面,可以在花括号中添加选项。可用的修饰符如下: + -* `limit`:设置 `string/text/binary/integer` 类型字段的最大值; -* `precision`:设置 `decimal` 类型字段的精度,即数字的位数; -* `scale`:设置 `decimal` 类型字段小数点后的数字位数; -* `polymorphic`:为 `belongs_to` 关联添加 `type` 字段; -* `null`:是否允许该字段的值为 `NULL`; +### 传递修饰符 -例如,执行下面的命令: +可以直接在命令行中传递常用的[类型修饰符](#column-modifiers)。这些类型修饰符用大括号括起来,放在字段类型之后。例如,运行下面的命令: -```bash -$ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic} +```sh +$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic} ``` -生成的迁移如下: +上面的命令会创建下面的迁移: ```ruby -class AddDetailsToProducts < ActiveRecord::Migration +class AddDetailsToProducts < ActiveRecord::Migration[5.0] def change add_column :products, :price, :decimal, precision: 5, scale: 2 - add_reference :products, :supplier, polymorphic: true, index: true + add_reference :products, :supplier, polymorphic: true end end ``` -编写迁移 -------- +TIP: 关于传递修饰符的更多介绍,请参阅生成器的命令行帮助信息。 + + -使用前面介绍的生成器生成迁移后,就可以开始写代码了。 +## 编写迁移 + +使用生成器创建迁移后,就可以开始写代码了。 + + ### 创建数据表 -`create_table` 方法最常用,大多数时候都会由模型或脚手架生成器生成。典型的用例如下: +`create_table` 方法是最基础、最常用的方法,其代码通常是由模型或脚手架生成器生成的。典型的用法像下面这样: ```ruby create_table :products do |t| @@ -288,9 +293,9 @@ create_table :products do |t| end ``` -这个迁移会创建 `products` 数据表,在数据表中创建 `name` 字段(后面会介绍,还会自动创建 `id` 字段)。 +上面的命令会创建包含 `name` 字段的 `products` 数据表(后面会介绍,数据表还包含自动创建的 `id` 字段)。 -默认情况下,`create_table` 方法会创建名为 `id` 的主键。通过 `:primary_key` 选项可以修改主键名(修改后别忘了修改相应的模型)。如果不想生成主键,可以传入 `id: false` 选项。如果设置数据库的选项,可以在 `:options` 选择中使用 SQL。例如: +默认情况下,`create_table` 方法会创建 `id` 主键。可以用 `:primary_key` 选项来修改主键名称,还可以传入 `id: false` 选项以禁用主键。如果需要传递数据库特有的选项,可以在 `:options` 选项中使用 SQL 代码片段。例如: ```ruby create_table :products, options: "ENGINE=BLACKHOLE" do |t| @@ -298,33 +303,35 @@ create_table :products, options: "ENGINE=BLACKHOLE" do |t| end ``` -这样设置之后,会在创建数据表的 SQL 语句后面加上 `ENGINE=BLACKHOLE`。(MySQL 默认的选项是 `ENGINE=InnoDB`) +上面的代码会在用于创建数据表的 SQL 语句末尾加上 `ENGINE=BLACKHOLE`(如果使用 MySQL 或 MarialDB,默认选项是 `ENGINE=InnoDB`)。 + +还可以传递带有数据表描述信息的 `:comment` 选项,这些注释会被储存在数据库中,可以使用 MySQL Workbench、PgAdmin III 等数据库管理工具查看。对于大型数据库,强列推荐在应用的迁移中添加注释。目前只有 MySQL 和 PostgreSQL 适配器支持注释功能。 -### 创建联合数据表 + -`create_join_table` 方法用来创建 HABTM 联合数据表。典型的用例如下: +### 创建联结数据表 + +`create_join_table` 方法用于创建 HABTM(has and belongs to many)联结数据表。典型的用法像下面这样: ```ruby create_join_table :products, :categories ``` -这段代码会创建一个名为 `categories_products` 的数据表,包含两个字段:`category_id` 和 `product_id`。这两个字段的 `:null` 选项默认情况都是 `false`,不过可在 `:column_options` 选项中设置。 +上面的代码会创建包含 `category_id` 和 `product_id` 字段的 `categories_products` 数据表。这两个字段的 `:null` 选项默认设置为 `false`,可以通过 `:column_options` 选项覆盖这一设置: ```ruby -create_join_table :products, :categories, column_options: {null: true} +create_join_table :products, :categories, column_options: { null: true } ``` -这段代码会把 `product_id` 和 `category_id` 字段的 `:null` 选项设为 `true`。 - -如果想修改数据表的名字,可以传入 `:table_name` 选项。例如: +联结数据表的名称默认由 `create_join_table` 方法的前两个参数按字母顺序组合而来。可以传入 `:table_name` 选项来自定义联结数据表的名称: ```ruby create_join_table :products, :categories, table_name: :categorization ``` -创建的数据表名为 `categorization`。 +上面的代码会创建 `categorization` 数据表。 -`create_join_table` 还可接受代码库,用来创建索引(默认无索引)或其他字段。 +`create_join_table` 方法也接受块作为参数,用于添加索引(默认未创建的索引)或附加字段: ```ruby create_join_table :products, :categories do |t| @@ -333,9 +340,11 @@ create_join_table :products, :categories do |t| end ``` + + ### 修改数据表 -有一个和 `create_table` 类似地方法,名为 `change_table`,用来修改现有的数据表。其用法和 `create_table` 类似,不过传入块的参数知道更多技巧。例如: +`change_table` 方法和 `create_table` 非常类似,用于修改现有的数据表。它的用法和 `create_table` 方法风格类似,但传入块的对象有更多用法。例如: ```ruby change_table :products do |t| @@ -346,69 +355,153 @@ change_table :products do |t| end ``` -这段代码删除了 `description` 和 `name` 字段,创建 `part_number` 字符串字段,并建立索引,最后重命名 `upccode` 字段。 +上面的代码删除 `description` 和 `name` 字段,创建 `part_number` 字符串字段并添加索引,最后重命名 `upccode` 字段。 + + + +### 修改字段 + +Rails 提供了与 `remove_column` 和 `add_column` 类似的 `change_column` 迁移方法。 + +```ruby +change_column :products, :part_number, :text +``` -### 如果帮助方法不够用 +上面的代码把 `products` 数据表的 `part_number` 字段修改为 `:text` 字段。请注意 `change_column` 命令是无法撤销的。 -如果 Active Record 提供的帮助方法不够用,可以使用 `excute` 方法,执行任意的 SQL 语句: +除 `change_column` 方法之外,还有 `change_column_null` 和 `change_column_default` 方法,前者专门用于设置字段可以为空或不可以为空,后者专门用于修改字段的默认值。 ```ruby -Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1') +change_column_null :products, :name, false +change_column_default :products, :approved, from: true, to: false ``` -各方法的详细用法请查阅 API 文档: +上面的代码把 `products` 数据表的 `:name` 字段设置为 `NOT NULL` 字段,把 `:approved` 字段的默认值由 `true` 修改为 `false`。 + +注意:也可以把上面的 `change_column_default` 迁移写成 `change_column_default :products, :approved, false`,但这种写法是无法撤销的。 + + + +### 字段修饰符 + +字段修饰符可以在创建或修改字段时使用: + +* `limit` 修饰符:设置 `string/text/binary/integer` 字段的最大长度。 +* `precision` 修饰符:定义 `decimal` 字段的精度,表示数字的总位数。 +* `scale` 修饰符:定义 `decimal` 字段的标度,表示小数点后的位数。 +* `polymorphic` 修饰符:为 `belongs_to` 关联添加 `type` 字段。 +* `null` 修饰符:设置字段能否为 `NULL` 值。 +* `default` 修饰符:设置字段的默认值。请注意,如果使用动态值(如日期)作为默认值,那么默认值只会在第一次使时(如应用迁移的日期)计算一次。 +* `index` 修饰符:为字段添加索引。 +* `comment` 修饰符:为字段添加注释。 + +有的适配器可能支持附加选项,更多介绍请参阅相应适配器的 API 文档。 + + + +### 外键 + +尽管不是必需的,但有时我们需要使用外键约束以保证引用完整性。 + +```ruby +add_foreign_key :articles, :authors +``` + +上面的代码为 `articles` 数据表的 `author_id` 字段添加外键,这个外键会引用 `authors` 数据表的 `id` 字段。如果字段名不能从表名称推导出来,我们可以使用 `:column` 和 `:primary_key` 选项。 + +Rails 会为每一个外键生成以 `fk_rails_` 开头并且后面紧跟着 10 个字符的外键名,外键名是根据 `from_table` 和 `column` 推导出来的。需要时可以使用 `:name` 来指定外键名。 + +NOTE: Active Record 只支持单字段外键,要想使用复合外键就需要 `execute` 方法和 `structure.sql`。更多介绍请参阅 [数据库模式转储](#schema-dumping-and-you)。 + +删除外键也很容易: + +```ruby +# 让 Active Record 找出列名 +remove_foreign_key :accounts, :branches + +# 删除特定列上的外键 +remove_foreign_key :accounts, column: :owner_id + +# 通过名称删除外键 +remove_foreign_key :accounts, name: :special_fk_name +``` + + + +### 如果辅助方法不够用 -- [`ActiveRecord::ConnectionAdapters::SchemaStatements`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html):包含可在 `change`,`up` 和 `down` 中使用的方法; -- [`ActiveRecord::ConnectionAdapters::TableDefinition`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html):包含可在 `create_table` 方法的块参数上调用的方法; -- [`ActiveRecord::ConnectionAdapters::Table`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html):包含可在 `change_table` 方法的块参数上调用的方法; +如果 Active Record 提供的辅助方法不够用,可以使用 `excute` 方法执行任意 SQL 语句: + +```ruby +Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1") +``` + +关于各个方法的更多介绍和例子,请参阅 API 文档。尤其是 [`ActiveRecord::ConnectionAdapters::SchemaStatements`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html) 的文档(在 `change`、`up` 和 `down` 方法中可以使用的方法)、[`ActiveRecord::ConnectionAdapters::TableDefinition`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html) 的文档(在 `create_table` 方法的块中可以使用的方法)和 [`ActiveRecord::ConnectionAdapters::Table`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html) 的文档(在 `change_table` 方法的块中可以使用的方法)。 + + ### 使用 `change` 方法 -`change` 是迁移中最常用的方法,大多数情况下都能完成指定的操作,而且 Active Record 知道如何撤这些操作。目前,在 `change` 方法中只能使用下面的方法: +`change` 方法是编写迁移时最常用的。在大多数情况下,Active Record 知道如何自动撤销用 `change` 方法编写的迁移。目前,在 `change` 方法中只能使用下面这些方法: + +* `add_column` +* `add_foreign_key` +* `add_index` +* `add_reference` +* `add_timestamps` +* `change_column_default`(必须提供 `:from` 和 `:to` 选项) +* `change_column_null` +* `create_join_table` +* `create_table` +* `disable_extension` +* `drop_join_table` +* `drop_table`(必须提供块) +* `enable_extension` +* `remove_column`(必须提供字段类型) +* `remove_foreign_key`(必须提供第二个数据表) +* `remove_index` +* `remove_reference` +* `remove_timestamps` +* `rename_column` +* `rename_index` +* `rename_table` + +如果在块中不使用 `change`、`change_default` 和 `remove` 方法,那么 `change_table` 方法也是可撤销的。 + +如果提供了字段类型作为第三个参数,那么 `remove_column` 是可撤销的。别忘了提供原来字段的选项,否则 Rails 在回滚时就无法准确地重建字段了: -* `add_column` -* `add_index` -* `add_reference` -* `add_timestamps` -* `create_table` -* `create_join_table` -* `drop_table`(必须提供代码块) -* `drop_join_table`(必须提供代码块) -* `remove_timestamps` -* `rename_column` -* `rename_index` -* `remove_reference` -* `rename_table` +```ruby +remove_column :posts, :slug, :string, null: false, default: '', index: true +``` -只要在块中不使用 `change`、`change_default` 或 `remove` 方法,`change_table` 中的操作也是可逆的。 +如果需要使用其他方法,可以用 `reversible` 方法或者 `up` 和 `down` 方法来代替 `change` 方法。 -如果要使用任何其他方法,可以使用 `reversible` 方法,或者不定义 `change` 方法,而分别定义 `up` 和 `down` 方法。 + ### 使用 `reversible` 方法 -Active Record 可能不知如何撤销复杂的迁移操作,这时可以使用 `reversible` 方法指定运行迁移和撤销迁移时怎么操作。例如: +撤销复杂迁移所需的操作有一些是 Rails 无法自动完成的,这时可以使用 `reversible` 方法指定运行和撤销迁移所需的操作。例如: ```ruby -class ExampleMigration < ActiveRecord::Migration +class ExampleMigration < ActiveRecord::Migration[5.0] def change - create_table :products do |t| - t.references :category + create_table :distributors do |t| + t.string :zipcode end reversible do |dir| dir.up do - #add a foreign key + # 添加 CHECK 约束 execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5) NO INHERIT; SQL end dir.down do execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL end end @@ -416,29 +509,31 @@ class ExampleMigration < ActiveRecord::Migration add_column :users, :home_page_url, :string rename_column :users, :email, :email_address end +end ``` -使用 `reversible` 方法还能确保操作按顺序执行。在上面的例子中,如果撤销迁移,`down` 代码块会在 `home_page_url` 字段删除后、`products` 数据表删除前运行。 +使用 `reversible` 方法可以确保指令按正确的顺序执行。在上面的代码中,撤销迁移时,`down` 块会在删除 `home_page_url` 字段之后、删除 `distributors` 数据表之前运行。 + +有时,迁移执行的操作是无法撤销的,例如删除数据。在这种情况下,我们可以在 `down` 块中抛出 `ActiveRecord::IrreversibleMigration` 异常。这样一旦尝试撤销迁移,就会显示无法撤销迁移的出错信息。 -有时,迁移的操作根本无法撤销,例如删除数据。这是,可以在 `down` 代码块中抛出 `ActiveRecord::IrreversibleMigration` 异常。如果有人尝试撤销迁移,会看到一个错误消息,告诉他无法撤销。 + ### 使用 `up` 和 `down` 方法 -在迁移中可以不用 `change` 方法,而用 `up` 和 `down` 方法。`up` 方法定义要对数据库模式做哪些操作,`down` 方法用来撤销这些操作。也就是说,如果执行 `up` 后立即执行 `down`,数据库的模式应该没有任何变化。例如,在 `up` 中创建了数据表,在 `down` 方法中就要将其删除。撤销时最好按照添加的相反顺序进行。前一节中的 `reversible` 用法示例代码可以改成: +可以使用 `up` 和 `down` 方法以传统风格编写迁移而不使用 `change` 方法。`up` 方法用于描述对数据库模式所做的改变,`down` 方法用于撤销 `up` 方法所做的改变。换句话说,如果调用 `up` 方法之后紧接着调用 `down` 方法,数据库模式不会发生任何改变。例如用 `up` 方法创建数据表,就应该用 `down` 方法删除这个数据表。在 `down` 方法中撤销迁移时,明智的做法是按照和 `up` 方法中操作相反的顺序执行操作。下面的例子和上一节中的例子的功能完全相同: ```ruby -class ExampleMigration < ActiveRecord::Migration +class ExampleMigration < ActiveRecord::Migration[5.0] def up - create_table :products do |t| - t.references :category + create_table :distributors do |t| + t.string :zipcode end - # add a foreign key + # 添加 CHECK 约束 execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5); SQL add_column :users, :home_page_url, :string @@ -450,25 +545,27 @@ class ExampleMigration < ActiveRecord::Migration remove_column :users, :home_page_url execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL - drop_table :products + drop_table :distributors end end ``` -如果迁移不可撤销,应该在 `down` 方法中抛出 `ActiveRecord::IrreversibleMigration` 异常。如果有人尝试撤销迁移,会看到一个错误消息,告诉他无法撤销。 +对于无法撤销的迁移,应该在 `down` 方法中抛出 `ActiveRecord::IrreversibleMigration` 异常。这样一旦尝试撤销迁移,就会显示无法撤销迁移的出错信息。 + + ### 撤销之前的迁移 -Active Record 提供了撤销迁移的功能,通过 `revert` 方法实现: +Active Record 提供了 `revert` 方法用于回滚迁移: ```ruby -require_relative '2012121212_example_migration' +require_relative '20121212123456_example_migration' -class FixupExampleMigration < ActiveRecord::Migration +class FixupExampleMigration < ActiveRecord::Migration[5.0] def change revert ExampleMigration @@ -479,146 +576,138 @@ class FixupExampleMigration < ActiveRecord::Migration end ``` -`revert` 方法还可接受一个块,定义撤销操作。`revert` 方法可用来撤销以前迁移的部分操作。例如,`ExampleMigration` 已经执行,但后来觉得最好还是序列化产品列表。那么,可以编写下面的代码: +`revert` 方法也接受块,在块中可以定义用于撤销迁移的指令。如果只是想要撤销之前迁移的部分操作,就可以使用块。例如,假设有一个 `ExampleMigration` 迁移已经执行,但后来发现应该用 ActiveRecord 验证代替 `CHECK` 约束来验证邮编,那么可以像下面这样编写迁移: ```ruby -class SerializeProductListMigration < ActiveRecord::Migration +class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0] def change - add_column :categories, :product_list - - reversible do |dir| - dir.up do - # transfer data from Products to Category#product_list - end - dir.down do - # create Products from Category#product_list - end - end - revert do - # copy-pasted code from ExampleMigration - create_table :products do |t| - t.references :category - end - + # 从 ExampleMigration 中复制粘贴代码 reversible do |dir| dir.up do - #add a foreign key + # 添加 CHECK 约束 execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5); SQL end dir.down do execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL end end - # The rest of the migration was ok + # ExampleMigration 中的其他操作无需撤销 end end end ``` -上面这个迁移也可以不用 `revert` 方法,不过步骤就多了:调换 `create_table` 和 `reversible` 的顺序,把 `create_table` 换成 `drop_table`,还要对调 `up` 和 `down` 中的代码。这些操作都可交给 `revert` 方法完成。 +不使用 `revert` 方法也可以编写出和上面的迁移功能相同的迁移,但需要更多步骤:调换 `create_table` 方法和 `reversible` 方法的顺序,用 `drop_table` 方法代替 `create_table` 方法,最后对调 `up` 和 `down` 方法。换句话说,这么多步骤用一个 `revert` 方法就可以代替。 -运行迁移 -------- +NOTE: 要想像上面的例子一样添加 `CHECK` 约束,必须使用 `structure.sql` 作为转储方式。请参阅 [数据库模式转储](#schema-dumping-and-you)。 -Rails 提供了很多 Rake 任务,用来执行指定的迁移。 + -其中最常使用的是 `rake db:migrate`,执行还没执行的迁移中的 `change` 或 `up` 方法。如果没有未运行的迁移,直接退出。`rake db:migrate` 按照迁移文件名中时间戳顺序执行迁移。 +## 运行迁移 -注意,执行 `db:migrate` 时还会执行 `db:schema:dump`,更新 `db/schema.rb` 文件,匹配数据库的结构。 +Rails 提供了一套用于运行迁移的 `bin/rails` 任务。其中最常用的是 `rails db:migrate` 任务,用于调用所有未运行的迁移中的 `chagne` 或 `up` 方法。如果没有未运行的迁移,任务会直接退出。调用顺序是根据迁移文件名的时间戳确定的。 -如果指定了版本,Active Record 会运行该版本之前的所有迁移。版本就是迁移文件名前的数字部分。例如,要运行 20080906120000 这个迁移,可以执行下面的命令: +请注意,执行 `db:migrate` 任务时会自动执行 `db:schema:dump` 任务,这个任务用于更新 `db/schema.rb` 文件,以匹配数据库结构。 -```bash -$ rake db:migrate VERSION=20080906120000 +如果指定了目标版本,Active Record 会运行该版本之前的所有迁移(调用其中的 `change`、`up` 和 `down` 方法),其中版本指的是迁移文件名的数字前缀。例如,下面的命令会运行 `20080906120000` 版本之前的所有迁移: + +```sh +$ bin/rails db:migrate VERSION=20080906120000 ``` -如果 20080906120000 比当前的版本高,上面的命令就会执行所有 20080906120000 之前(包括 20080906120000)的迁移中的 `change` 或 `up` 方法,但不会运行 20080906120000 之后的迁移。如果回滚迁移,则会执行 20080906120000 之前(不包括 20080906120000)的迁移中的 `down` 方法。 +如果版本 `20080906120000` 高于当前版本(换句话说,是向上迁移),上面的命令会按顺序运行迁移直到运行完 `20080906120000` 版本,之后的版本都不会运行。如果是向下迁移(即版本 `20080906120000` 低于当前版本),上面的命令会按顺序运行 `20080906120000` 版本之前的所有迁移,不包括 `20080906120000` 版本。 + + ### 回滚 -还有一个常用的操作时回滚到之前的迁移。例如,迁移代码写错了,想纠正。我们无须查找迁移的版本号,直接执行下面的命令即可: +另一个常用任务是回滚最后一个迁移。例如,当发现最后一个迁移中有错误需要修正时,就可以执行回滚任务。回滚最后一个迁移不需要指定这个迁移的版本,直接执行下面的命令即可: -```bash -$ rake db:rollback +```sh +$ bin/rails db:rollback ``` -这个命令会回滚上一次迁移,撤销 `change` 方法中的操作,或者执行 `down` 方法。如果想撤销多个迁移,可以使用 `STEP` 参数: +上面的命令通过撤销 `change` 方法或调用 `down` 方法来回滚最后一个迁移。要想取消多个迁移,可以使用 `STEP` 参数: -```bash -$ rake db:rollback STEP=3 +```sh +$ bin/rails db:rollback STEP=3 ``` -这个命令会撤销前三次迁移。 +上面的命令会撤销最后三个迁移。 -`db:migrate:redo` 命令可以回滚上一次迁移,然后再次执行迁移。和 `db:rollback` 一样,如果想重做多次迁移,可以使用 `STEP` 参数。例如: +`db:migrate:redo` 任务用于回滚最后一个迁移并再次运行这个迁移。和 `db:rollback` 任务一样,如果需要重做多个迁移,可以使用 `STEP` 参数,例如: -```bash -$ rake db:migrate:redo STEP=3 +```sh +$ bin/rails db:migrate:redo STEP=3 ``` -这些 Rake 任务的作用和 `db:migrate` 一样,只是用起来更方便,因为无需查找特定的迁移版本号。 +这些 `bin/rails` 任务可以完成的操作,通过 `db:migrate` 也都能完成,区别在于这些任务使用起来更方便,无需显式指定迁移的版本。 -### 搭建数据库 + -`rake db:setup` 任务会创建数据库,加载模式,并填充种子数据。 +### 安装数据库 -### 重建数据库 +`rails db:setup` 任务用于创建数据库,加载数据库模式,并使用种子数据初始化数据库。 -`rake db:reset` 任务会删除数据库,然后重建,等价于 `rake db:drop db:setup`。 + -NOTE: 这个任务和执行所有迁移的作用不同。`rake db:reset` 使用的是 `schema.rb` 文件中的内容。如果迁移无法回滚,`rake db:reset` 起不了作用。详细介绍参见“[导出模式](#schema-dumping-and-you)”一节。 +### 重置数据库 -### 运行指定的迁移 +`rails db:reset` 任务用于删除并重新创建数据库,其功能相当于 `rails db:drop db:setup`。 -如果想执行指定迁移,或者撤销指定迁移,可以使用 `db:migrate:up` 和 `db:migrate:down` 任务,指定相应的版本号,就会根据需求调用 `change`、`up` 或 `down` 方法。例如: +NOTE: 重置数据库和运行所有迁移是不一样的。重置数据库只使用当前的 `db/schema.rb` 或 `db/structure.sql` 文件的内容。如果迁移无法回滚,使用 `rails db:reset` 任务可能也没用。关于转储数据库模式的更多介绍,请参阅 [数据库模式转储](#schema-dumping-and-you)。 -```bash -$ rake db:migrate:up VERSION=20080906120000 + + +### 运行指定迁移 + +要想运行或撤销指定迁移,可以使用 `db:migrate:up` 和 `db:migrate:down` 任务。只需指定版本,对应迁移就会调用它的 `change` 、`up` 或 `down` 方法,例如: + +```sh +$ bin/rails db:migrate:up VERSION=20080906120000 ``` -这个命令会执行 20080906120000 迁移中的 `change` 方法或 `up` 方法。`db:migrate:up` 首先会检测指定的迁移是否已经运行,如果 Active Record 任务已经执行,就不会做任何操作。 +上面的命令会运行 `20080906120000` 这个迁移,调用它的 `change` 或 `up` 方法。`db:migrate:up` 任务会检查指定迁移是否已经运行过,如果已经运行过就不会执行任何操作。 -### 在不同的环境中运行迁移 + -默认情况下,`rake db:migrate` 任务在 `development` 环境中执行。要在其他环境中运行迁移,执行命令时可以使用环境变量 `RAILS_ENV` 指定环境。例如,要在 `test` 环境中运行迁移,可以执行下面的命令: +### 在不同环境中运行迁移 -```bash -$ rake db:migrate RAILS_ENV=test +`bin/rails db:migrate` 任务默认在开发环境中运行迁移。要想在其他环境中运行迁移,可以在执行任务时使用 `RAILS_ENV` 环境变量说明所需环境。例如,要想在测试环境中运行迁移,可以执行下面的命令: + +```sh +$ bin/rails db:migrate RAILS_ENV=test ``` -### 修改运行迁移时的输出 + + +### 修改迁移运行时的输出 -默认情况下,运行迁移时,会输出操作了哪些操作,以及花了多长时间。创建数据表并添加索引的迁移产生的输出如下: +运行迁移时,默认会输出正在进行的操作,以及操作所花费的时间。例如,创建数据表并添加索引的迁移在运行时会生成下面的输出: -```bash +``` == CreateProducts: migrating ================================================= -- create_table(:products) -> 0.0028s == CreateProducts: migrated (0.0028s) ======================================== ``` -在迁移中可以使用很多方法,控制输出: +在迁移中提供了几种方法,允许我们修改迁移运行时的输出: -| 方法 | 作用 | -| -------------------- | ------- | -| suppress_messages | 接受一个代码块,禁止代码块中所有操作的输出 | -| say | 接受一个消息字符串作为参数,将其输出。第二个参数是布尔值,指定输出结果是否缩进 | -| say_with_time | 输出文本,以及执行代码块中操作所用时间。如果代码块的返回结果是整数,会当做操作的记录数量 | -例如,下面这个迁移: +例如,下面的迁移: ```ruby -class CreateProducts < ActiveRecord::Migration +class CreateProducts < ActiveRecord::Migration[5.0] def change suppress_messages do create_table :products do |t| @@ -641,9 +730,9 @@ class CreateProducts < ActiveRecord::Migration end ``` -输出结果是: +会生成下面的输出: -```bash +``` == CreateProducts: migrating ================================================= -- Created a table -> and an index! @@ -653,35 +742,41 @@ end == CreateProducts: migrated (10.0054s) ======================================= ``` -如果不想让 Active Record 输出任何结果,可以使用 `rake db:migrate VERBOSE=false`。 +要是不想让 Active Record 生成任何输出,可以使用 `rails db:migrate VERBOSE=false`。 + + + +## 修改现有的迁移 + +在编写迁移时我们偶尔也会犯错误。如果已经运行过存在错误的迁移,那么直接修正迁移中的错误并重新运行这个迁移并不能解决问题:Rails 知道这个迁移已经运行过,因此执行 `rails db:migrate` 任务时不会执行任何操作。必须先回滚这个迁移(例如通过执行 `bin/rails db:rollback` 任务),再修正迁移中的错误,然后执行 `rails db:migrate` 任务来运行这个迁移的正确版本。 -修改现有的迁移 ------------- +通常,直接修改现有的迁移不是个好主意。这样做会给我们和同事带来额外的工作量,如果这个迁移已经在生产服务器上运行过,还可能带来大麻烦。作为替代,可以编写一个新的迁移来执行我们想要的操作。修改还未提交到源代版本码控制系统(或者更一般地,还未传播到开发设备之外)的新生成的迁移是相对无害的。 -有时编写的迁移中可能有错误,如果已经运行了迁移,不能直接编辑迁移文件再运行迁移。Rails 认为这个迁移已经运行,所以执行 `rake db:migrate` 任务时什么也不会做。这种情况必须先回滚迁移(例如,执行 `rake db:rollback` 任务),编辑迁移文件后再执行 `rake db:migrate` 任务执行改正后的版本。 +在编写新的迁移来完全或部分撤销之前的迁移时,可以使用 `revert` 方法(请参阅前面 [撤销之前的迁移](#reverting-previous-migrations))。 -一般来说,直接修改现有的迁移不是个好主意。这么做会为你以及你的同事带来额外的工作量,如果这个迁移已经在生产服务器上运行过,还可能带来不必要的麻烦。你应该编写一个新的迁移,做所需的改动。编辑新生成还未纳入版本控制的迁移(或者更宽泛地说,还没有出现在开发设备之外),相对来说是安全的。 + -在新迁移中撤销之前迁移中的全部操作或者部分操作可以使用 `revert` 方法。(参见前面的 [撤销之前的迁移](#reverting-previous-migrations) 一节) +## 数据库模式转储 -导出模式 -------- + -### 模式文件的作用 +### 数据库模式文件有什么用? -迁移的作用并不是为数据库模式提供可信的参考源。`db/schema.rb` 或由 Active Record 生成的 SQL 文件才有这个作用。`db/schema.rb` 这些文件不可修改,其目的是表示数据库的当前结构。 +迁移尽管很强大,但并非数据库模式的可信来源。Active Record 通过检查数据库生成的 `db/schema.rb` 文件或 SQL 文件才是数据库模式的可信来源。这两个可信来源不应该被修改,它们仅用于表示数据库的当前状态。 -部署新程序时,无需运行全部的迁移。直接加载数据库结构要简单快速得多。 +当需要部署 Rails 应用的新实例时,不必把所有迁移重新运行一遍,直接加载当前数据库的模式文件要简单和快速得多。 -例如,测试数据库是这样创建的:导出开发数据库的结构(存入文件 `db/schema.rb` 或 `db/structure.sql`),然后导入测试数据库。 +例如,我们可以这样创建测试数据库:把当前的开发数据库转储为 `db/schema.rb` 或 `db/structure.sql` 文件,然后加载到测试数据库。 -模式文件还可以用来快速查看 Active Record 中有哪些属性。模型中没有属性信息,而且迁移会频繁修改属性,但是模式文件中有最终的结果。[annotate_models](https://github.com/ctran/annotate_models) gem 会在模型文件的顶部加入注释,自动添加并更新模型的模式。 +数据库模式文件还可以用于快速查看 Active Record 对象具有的属性。这些属性信息不仅在模型代码中找不到,而且经常分散在几个迁移文件中,还好在数据库模式文件中可以很容易地查看这些信息。[annotate_models](https://github.com/ctran/annotate_models) gem 会在每个模型文件的顶部自动添加和更新注释,这些注释是对当前数据库模式的概述,如果需要可以使用这个 gem。 -### 导出的模式文件类型 + -导出模式有两种方法,由 `config/application.rb` 文件中的 `config.active_record.schema_format` 选项设置,可以是 `:sql` 或 `:ruby`。 +### 数据库模式转储的类型 -如果设为 `:ruby`,导出的模式保存在 `db/schema.rb` 文件中。打开这个文件,你会发现内容很多,就像一个很大的迁移: +数据库模式转储有两种方式,可以通过 `config/application.rb` 文件的 `config.active_record.schema_format` 选项来设置想要采用的方式,即 `:sql` 或 `:ruby`。 + +如果选择 `:ruby`,那么数据库模式会储存在 `db/schema.rb` 文件中。打开这个文件,会看到内容很多,就像一个巨大的迁移: ```ruby ActiveRecord::Schema.define(version: 20080906171750) do @@ -693,42 +788,48 @@ ActiveRecord::Schema.define(version: 20080906171750) do create_table "products", force: true do |t| t.string "name" - t.text "description" + t.text "description" t.datetime "created_at" t.datetime "updated_at" - t.string "part_number" + t.string "part_number" end end ``` -大多数情况下,文件的内容都是这样。这个文件使用 `create_table`、`add_index` 等方法审查数据库的结构。这个文件盒使用的数据库类型无关,可以导入任何一种 Active Record 支持的数据库。如果开发的程序需要兼容多种数据库,可以使用这个文件。 +在很多情况下,我们看到的数据库模式文件就是上面这个样子。这个文件是通过检查数据库生成的,使用 `create_table`、`add_index` 等方法来表达数据库结构。这个文件是数据库无关的,因此可以加载到 Active Record 支持的任何一种数据库。如果想要分发使用多数据库的 Rails 应用,数据库无关这一特性就非常有用了。 + +尽管如此,`db/schema.rb` 在设计上也有所取舍:它不能表达数据库的特定项目,如触发器、存储过程或检查约束。尽管我们可以在迁移中执行定制的 SQL 语句,但是数据库模式转储工具无法从数据库中复原这些语句。如果我们使用了这类特性,就应该把数据库模式的格式设置为 `:sql`。 + +在把数据库模式转储到 `db/structure.sql` 文件时,我们不使用数据库模式转储工具,而是使用数据库特有的工具(通过执行 `db:structure:dump` 任务)。例如,对于 PostgreSQL,使用的是 `pg_dump` 实用程序。对于 MySQL 和 MariaDB,`db/structure.sql` 文件将包含各种数据表的 `SHOW CREATE TABLE` 语句的输出。 + +加载数据库模式实际上就是执行其中包含的 SQL 语句。根据定义,加载数据库模式会创建数据库结构的完美拷贝。`:sql` 格式的数据库模式,只能加载到和原有数据库类型相同的数据库,而不能加载到其他类型的数据库。 + + -不过 `db/schema.rb` 也有缺点:无法执行数据库的某些操作,例如外键约束,触发器,存储过程。在迁移文件中可以执行 SQL 语句,但导出模式的程序无法从数据库中重建这些语句。如果你的程序用到了前面提到的数据库操作,可以把模式文件的格式设为 `:sql`。 +### 数据库模式转储和源码版本控制 -`:sql` 格式的文件不使用 Active Record 的模式导出程序,而使用数据库自带的导出工具(执行 `db:structure:dump` 任务),把数据库模式导入 `db/structure.sql` 文件。例如,PostgreSQL 使用 `pg_dump` 导出模式。如果使用 MySQL,`db/structure.sql` 文件中会出现多个 `SHOW CREATE TABLE` 用来创建数据表的语句。 +数据库模式转储是数据库模式的可信来源,因此强烈建议将其纳入源码版本控制。 -加载模式时,只要执行其中的 SQL 语句即可。按预期,导入后会创建一个完整的数据库结构。使用 `:sql` 格式,就不能把模式导入其他类型的数据库中了。 +`db/schema.rb` 文件包含数据库的当前版本号,这样可以确保在合并两个包含数据库模式文件的分支时会发生冲突。一旦出现这种情况,就需要手动解决冲突,保留版本较高的那个数据库模式文件。 -### 模式导出和版本控制 + -因为导出的模式文件时数据库模式的可信源,强烈推荐将其纳入版本控制。 +## Active Record 和引用完整性 -Active Record 和引用完整性 -------------------------- +Active Record 在模型而不是数据库中声明关联。因此,像触发器、约束这些依赖数据库的特性没有被大量使用。 -Active Record 在模型中,而不是数据库中设置关联。因此,需要在数据库中实现的功能,例如触发器、外键约束,不太常用。 +验证,如 `validates :foreign_key, uniqueness: true`,是模型强制数据完整性的一种方式。在关联中设置 `:dependent` 选项,可以保证父对象删除后,子对象也会被删除。和其他应用层的操作一样,这些操作无法保证引用完整性,因此有些人会在数据库中使用[外键约束](#foreign-keys)以加强数据完整性。 -`validates :foreign_key, uniqueness: true` 这个验证是模型保证数据完整性的一种方法。在关联中设置 `:dependent` 选项,可以保证父对象删除后,子对象也会被删除。和任何一种程序层的操作一样,这些无法完全保证引用完整性,所以很多人还是会在数据库中使用外键约束。 +尽管 Active Record 并未提供用于直接处理这些特性的工具,但 `execute` 方法可以用于执行任意 SQL。 -Active Record 并没有为使用这些功能提供任何工具,不过 `execute` 方法可以执行任意的 SQL 语句。还可以使用 [foreigner](https://github.com/matthuhiggins/foreigner) 等 gem,为 Active Record 添加外键支持(还能把外键导出到 `db/schema.rb` 文件)。 + -迁移和种子数据 -------------- +## 迁移和种子数据 -有些人使用迁移把数据存入数据库: +Rails 迁移特性的主要用途是使用一致的进程调用修改数据库模式的命令。迁移还可以用于添加或修改数据。对于不能删除和重建的数据库,如生产数据库,这些功能非常有用。 ```ruby -class AddInitialProducts < ActiveRecord::Migration +class AddInitialProducts < ActiveRecord::Migration[5.0] def up 5.times do |i| Product.create(name: "Product ##{i}", description: "A product.") @@ -741,7 +842,7 @@ class AddInitialProducts < ActiveRecord::Migration end ``` -Rails 提供了“种子”功能,可以把初始化数据存入数据库。这个功能用起来很简单,在 `db/seeds.rb` 文件中写一些 Ruby 代码,然后执行 `rake db:seed` 命令即可: +使用 Rails 内置的“种子”特性可以快速简便地完成创建数据库后添加初始数据的任务。在开发和测试环境中,经常需要重新加载数据库,这时“种子”特性就更有用了。使用“种子”特性很容易,只要用 Ruby 代码填充 `db/seeds.rb` 文件,然后执行 `rails db:seed` 命令即可: ```ruby 5.times do |i| @@ -749,4 +850,4 @@ Rails 提供了“种子”功能,可以把初始化数据存入数据库。 end ``` -填充新建程序的数据库,使用这种方法操作起来简洁得多。 +相比之下,这种设置新建应用数据库的方法更加干净利落。 diff --git a/source/zh-CN/active_record_postgresql.md b/source/zh-CN/active_record_postgresql.md index a5649e3..5eb19f5 100644 --- a/source/zh-CN/active_record_postgresql.md +++ b/source/zh-CN/active_record_postgresql.md @@ -1,3 +1,5 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** + Active Record and PostgreSQL ============================ @@ -12,7 +14,7 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- -In order to use the PostgreSQL adapter you need to have at least version 8.2 +In order to use the PostgreSQL adapter you need to have at least version 9.1 installed. Older versions are not supported. To get started with PostgreSQL have a look at the @@ -27,8 +29,8 @@ that are supported by the PostgreSQL adapter. ### Bytea -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-binary.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-binarystring.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-binary.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-binarystring.html) ```ruby # db/migrate/20140207133952_create_documents.rb @@ -37,7 +39,7 @@ create_table :documents do |t| end # app/models/document.rb -class Document < ActiveRecord::Base +class Document < ApplicationRecord end # Usage @@ -47,8 +49,8 @@ Document.create payload: data ### Array -* [type definition](http://www.postgresql.org/docs/9.3/static/arrays.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-array.html) +* [type definition](http://www.postgresql.org/docs/current/static/arrays.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-array.html) ```ruby # db/migrate/20140207133952_create_books.rb @@ -61,7 +63,7 @@ add_index :books, :tags, using: 'gin' add_index :books, :ratings, using: 'gin' # app/models/book.rb -class Book < ActiveRecord::Base +class Book < ApplicationRecord end # Usage @@ -81,18 +83,22 @@ Book.where("array_length(ratings, 1) >= 3") ### Hstore -* [type definition](http://www.postgresql.org/docs/9.3/static/hstore.html) +* [type definition](http://www.postgresql.org/docs/current/static/hstore.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/hstore.html#AEN167712) + +NOTE: You need to enable the `hstore` extension to use hstore. ```ruby # db/migrate/20131009135255_create_profiles.rb ActiveRecord::Schema.define do + enable_extension 'hstore' unless extension_enabled?('hstore') create_table :profiles do |t| t.hstore 'settings' end end # app/models/profile.rb -class Profile < ActiveRecord::Base +class Profile < ApplicationRecord end # Usage @@ -104,16 +110,14 @@ profile.settings # => {"color"=>"blue", "resolution"=>"800x600"} profile.settings = {"color" => "yellow", "resolution" => "1280x1024"} profile.save! -## you need to call _will_change! if you are editing the store in place -profile.settings["color"] = "green" -profile.settings_will_change! -profile.save! +Profile.where("settings->'color' = ?", "yellow") +#=> #"yellow", "resolution"=>"1280x1024"}>]> ``` ### JSON -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-json.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-json.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-json.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-json.html) ```ruby # db/migrate/20131220144913_create_events.rb @@ -122,7 +126,7 @@ create_table :events do |t| end # app/models/event.rb -class Event < ActiveRecord::Base +class Event < ApplicationRecord end # Usage @@ -132,15 +136,16 @@ event = Event.first event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]} ## Query based on JSON document -Event.where("payload->'kind' = ?", "user_renamed") +# The -> operator returns the original JSON type (which might be an object), whereas ->> returns text +Event.where("payload->>'kind' = ?", "user_renamed") ``` ### Range Types -* [type definition](http://www.postgresql.org/docs/9.3/static/rangetypes.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-range.html) +* [type definition](http://www.postgresql.org/docs/current/static/rangetypes.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-range.html) -This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.1.1/Range.html) objects. +This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.2.2/Range.html) objects. ```ruby # db/migrate/20130923065404_create_events.rb @@ -149,7 +154,7 @@ create_table :events do |t| end # app/models/event.rb -class Event < ActiveRecord::Base +class Event < ApplicationRecord end # Usage @@ -172,7 +177,7 @@ event.ends_at # => Thu, 13 Feb 2014 ### Composite Types -* [type definition](http://www.postgresql.org/docs/9.3/static/rowtypes.html) +* [type definition](http://www.postgresql.org/docs/current/static/rowtypes.html) Currently there is no special support for composite types. They are mapped to normal text columns: @@ -199,7 +204,7 @@ create_table :contacts do |t| end # app/models/contact.rb -class Contact < ActiveRecord::Base +class Contact < ApplicationRecord end # Usage @@ -212,22 +217,33 @@ contact.save! ### Enumerated Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-enum.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-enum.html) Currently there is no special support for enumerated types. They are mapped as normal text columns: ```ruby -# db/migrate/20131220144913_create_events.rb -execute <<-SQL - CREATE TYPE article_status AS ENUM ('draft', 'published'); -SQL -create_table :articles do |t| - t.column :status, :article_status +# db/migrate/20131220144913_create_articles.rb +def up + execute <<-SQL + CREATE TYPE article_status AS ENUM ('draft', 'published'); + SQL + create_table :articles do |t| + t.column :status, :article_status + end +end + +# NOTE: It's important to drop table before dropping enum. +def down + drop_table :articles + + execute <<-SQL + DROP TYPE article_status; + SQL end # app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord end # Usage @@ -239,20 +255,50 @@ article.status = "published" article.save! ``` +To add a new value before/after existing one you should use [ALTER TYPE](http://www.postgresql.org/docs/current/static/sql-altertype.html): + +```ruby +# db/migrate/20150720144913_add_new_state_to_articles.rb +# NOTE: ALTER TYPE ... ADD VALUE cannot be executed inside of a transaction block so here we are using disable_ddl_transaction! +disable_ddl_transaction! + +def up + execute <<-SQL + ALTER TYPE article_status ADD VALUE IF NOT EXISTS 'archived' AFTER 'published'; + SQL +end +``` + +NOTE: ENUM values can't be dropped currently. You can read why [here](http://www.postgresql.org/message-id/29F36C7C98AB09499B1A209D48EAA615B7653DBC8A@mail2a.alliedtesting.com). + +Hint: to show all the values of the all enums you have, you should call this query in `bin/rails db` or `psql` console: + +```sql +SELECT n.nspname AS enum_schema, + t.typname AS enum_name, + e.enumlabel AS enum_value + FROM pg_type t + JOIN pg_enum e ON t.oid = e.enumtypid + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace +``` + ### UUID -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html) -* [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-uuid.html) +* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN159361) +* [uuid-ossp generator functions](http://www.postgresql.org/docs/current/static/uuid-ossp.html) +NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to use uuid. ```ruby # db/migrate/20131220144913_create_revisions.rb create_table :revisions do |t| - t.column :identifier, :uuid + t.uuid :identifier end # app/models/revision.rb -class Revision < ActiveRecord::Base +class Revision < ApplicationRecord end # Usage @@ -262,10 +308,35 @@ revision = Revision.first revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" ``` +You can use `uuid` type to define references in migrations: + +```ruby +# db/migrate/20150418012400_create_blog.rb +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :posts, id: :uuid, default: 'gen_random_uuid()' + +create_table :comments, id: :uuid, default: 'gen_random_uuid()' do |t| + # t.belongs_to :post, type: :uuid + t.references :post, type: :uuid +end + +# app/models/post.rb +class Post < ApplicationRecord + has_many :comments +end + +# app/models/comment.rb +class Comment < ApplicationRecord + belongs_to :post +end +``` + +See [this section](#uuid-primary-keys) for more details on using UUIDs as primary key. + ### Bit String Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-bit.html) -* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-bitstring.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-bit.html) +* [functions and operators](http://www.postgresql.org/docs/current/static/functions-bitstring.html) ```ruby # db/migrate/20131220144913_create_users.rb @@ -274,13 +345,13 @@ create_table :users, force: true do |t| end # app/models/device.rb -class User < ActiveRecord::Base +class User < ApplicationRecord end # Usage User.create settings: "01010011" user = User.first -user.settings # => "(Paris,Champs-Élysées)" +user.settings # => "01010011" user.settings = "0xAF" user.settings # => 10101111 user.save! @@ -288,10 +359,10 @@ user.save! ### Network Address Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-net-types.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-net-types.html) The types `inet` and `cidr` are mapped to Ruby -[`IPAddr`](http://www.ruby-doc.org/stdlib-2.1.1/libdoc/ipaddr/rdoc/IPAddr.html) +[`IPAddr`](http://www.ruby-doc.org/stdlib-2.2.2/libdoc/ipaddr/rdoc/IPAddr.html) objects. The `macaddr` type is mapped to normal text. ```ruby @@ -303,7 +374,7 @@ create_table(:devices, force: true) do |t| end # app/models/device.rb -class Device < ActiveRecord::Base +class Device < ApplicationRecord end # Usage @@ -323,7 +394,7 @@ macbook.address ### Geometric Types -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-geometric.html) +* [type definition](http://www.postgresql.org/docs/current/static/datatype-geometric.html) All geometric types, with the exception of `points` are mapped to normal text. A point is casted to an array containing `x` and `y` coordinates. @@ -332,17 +403,18 @@ A point is casted to an array containing `x` and `y` coordinates. UUID Primary Keys ----------------- -NOTE: you need to enable the `uuid-ossp` extension to generate UUIDs. +NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to generate random UUIDs. ```ruby # db/migrate/20131220144913_create_devices.rb -enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp') -create_table :devices, id: :uuid, default: 'uuid_generate_v4()' do |t| +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :devices, id: :uuid, default: 'gen_random_uuid()' do |t| t.string :kind end # app/models/device.rb -class Device < ActiveRecord::Base +class Device < ApplicationRecord end # Usage @@ -350,6 +422,9 @@ device = Device.create device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e" ``` +NOTE: `uuid_generate_v4()` (from `uuid-ossp`) is assumed if no `:default` option was +passed to `create_table`. + Full Text Search ---------------- @@ -363,7 +438,7 @@ end execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));" # app/models/document.rb -class Document < ActiveRecord::Base +class Document < ApplicationRecord end # Usage @@ -377,7 +452,7 @@ Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)", Database Views -------------- -* [view creation](http://www.postgresql.org/docs/9.3/static/sql-createview.html) +* [view creation](http://www.postgresql.org/docs/current/static/sql-createview.html) Imagine you need to work with a legacy database containing the following table: @@ -413,7 +488,7 @@ CREATE VIEW articles AS SQL # app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord self.primary_key = "id" def archive! update_attribute :archived, true diff --git a/source/zh-CN/active_record_querying.md b/source/zh-CN/active_record_querying.md index 1017af1..3fe0d08 100644 --- a/source/zh-CN/active_record_querying.md +++ b/source/zh-CN/active_record_querying.md @@ -1,28 +1,28 @@ -Active Record 查询 -================== +# Active Record 查询接口 -本文介绍使用 Active Record 从数据库中获取数据的不同方法。 +本文介绍使用 Active Record 从数据库中检索数据的不同方法。 -读完本文,你将学到: +读完本文后,您将学到: -* 如何使用各种方法查找满足条件的记录; -* 如何指定查找记录的排序方式,获取哪些属性,分组等; -* 获取数据时如何使用按需加载介绍数据库查询数; -* 如何使用动态查询方法; -* 如何检查某个记录是否存在; -* 如何在 Active Record 模型中做各种计算; -* 如何执行 EXPLAIN 命令; +* 如何使用各种方法和条件查找记录; +* 如何指定所查找记录的排序方式、想要检索的属性、分组方式和其他特性; +* 如何使用预先加载以减少数据检索所需的数据库查询的数量; +* 如何使用动态查找方法; +* 如何通过方法链来连续使用多个 Active Record 方法; +* 如何检查某个记录是否存在; +* 如何在 Active Record 模型上做各种计算; +* 如何在关联上执行 `EXPLAIN` 命令。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -如果习惯使用 SQL 查询数据库,会发现在 Rails 中执行相同的查询有更好的方式。大多数情况下,在 Active Record 中无需直接使用 SQL。 +如果你习惯直接使用 SQL 来查找数据库记录,那么你通常会发现 Rails 为执行相同操作提供了更好的方式。在大多数情况下,Active Record 使你无需使用 SQL。 -文中的实例代码会用到下面一个或多个模型: +本文中的示例代码会用到下面的一个或多个模型: -TIP: 下面所有的模型除非有特别说明之外,都使用 `id` 做主键。 +TIP: 除非另有说明,下面所有模型都使用 `id` 作为主键。 ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord has_one :address has_many :orders has_and_belongs_to_many :roles @@ -30,628 +30,697 @@ end ``` ```ruby -class Address < ActiveRecord::Base +class Address < ApplicationRecord belongs_to :client end ``` ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord belongs_to :client, counter_cache: true end ``` ```ruby -class Role < ActiveRecord::Base +class Role < ApplicationRecord has_and_belongs_to_many :clients end ``` -Active Record 会代你执行数据库查询,可以兼容大多数数据库(MySQL,PostgreSQL 和 SQLite 等)。不管使用哪种数据库,所用的 Active Record 方法都是一样的。 +Active Record 会为你执行数据库查询,它和大多数数据库系统兼容,包括 MySQL、MariaDB、PostgreSQL 和 SQLite。不管使用哪个数据库系统,Active Record 方法的用法总是相同的。 -从数据库中获取对象 ---------------- + -Active Record 提供了很多查询方法,用来从数据库中获取对象。每个查询方法都接可接受参数,不用直接写 SQL 就能在数据库中执行指定的查询。 +## 从数据库中检索对象 -这些方法是: +Active Record 提供了几个用于从数据库中检索对象的查找方法。查找方法接受参数并执行指定的数据库查询,使我们无需直接编写 SQL。 -* `bind` -* `create_with` -* `distinct` -* `eager_load` -* `extending` -* `from` -* `group` -* `having` -* `includes` -* `joins` -* `limit` -* `lock` -* `none` -* `offset` -* `order` -* `preload` -* `readonly` -* `references` -* `reorder` -* `reverse_order` -* `select` -* `uniq` -* `where` +下面列出这些查找方法: -上述所有方法都返回一个 `ActiveRecord::Relation` 实例。 +* `find` +* `create_with` +* `distinct` +* `eager_load` +* `extending` +* `from` +* `group` +* `having` +* `includes` +* `joins` +* `left_outer_joins` +* `limit` +* `lock` +* `none` +* `offset` +* `order` +* `preload` +* `readonly` +* `references` +* `reorder` +* `reverse_order` +* `select` +* `where` -`Model.find(options)` 方法执行的主要操作概括如下: +返回集合的查找方法,如 `where` 和 `group`,返回一个 `ActiveRecord::Relation` 实例。查找单个记录的方法,如 `find` 和 `first`,返回相应模型的一个实例。 -* 把指定的选项转换成等价的 SQL 查询语句; -* 执行 SQL 查询,从数据库中获取结果; -* 为每个查询结果实例化一个对应的模型对象; -* 如果有 `after_find` 回调,再执行 `after_find` 回调; +`Model.find(options)` 执行的主要操作可以概括为: -### 获取单个对象 +* 把提供的选项转换为等价的 SQL 查询。 +* 触发 SQL 查询并从数据库中检索对应的结果。 +* 为每个查询结果实例化对应的模型对象。 +* 当存在回调时,先调用 `after_find` 回调再调用 `after_initialize` 回调。 -在 Active Record 中获取单个对象有好几种方法。 + -#### 使用主键 +### 检索单个对象 -使用 `Model.find(primary_key)` 方法可以获取指定主键对应的对象。例如: +Active Record 为检索单个对象提供了几个不同的方法。 + + + +#### `find` 方法 + +可以使用 `find` 方法检索指定主键对应的对象,指定主键时可以使用多个选项。例如: ```ruby -# Find the client with primary key (id) 10. +# 查找主键(ID)为 10 的客户 client = Client.find(10) # => # ``` -和上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1 ``` -如果未找到匹配的记录,`Model.find(primary_key)` 会抛出 `ActiveRecord::RecordNotFound` 异常。 +如果没有找到匹配的记录,`find` 方法抛出 `ActiveRecord::RecordNotFound` 异常。 -#### `take` - -`Model.take` 方法会获取一个记录,不考虑任何顺序。例如: +还可以使用 `find` 方法查询多个对象,方法是调用 `find` 方法并传入主键构成的数组。返回值是包含所提供的主键的所有匹配记录的数组。例如: ```ruby -client = Client.take -# => # +# 查找主键为 1 和 10 的客户 +client = Client.find([1, 10]) # Or even Client.find(1, 10) +# => [#, #] ``` -和上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients WHERE (clients.id IN (1,10)) ``` -如果没找到记录,`Model.take` 不会抛出异常,而是返回 `nil`。 +WARNING: 如果所提供的主键都没有匹配记录,那么 `find` 方法会抛出 `ActiveRecord::RecordNotFound` 异常。 -TIP: 获取的记录根据所用的数据库引擎会有所不同。 + -#### `first` +#### `take` 方法 -`Model.first` 获取按主键排序得到的第一个记录。例如: +`take` 方法检索一条记录而不考虑排序。例如: ```ruby -client = Client.first +client = Client.take # => # ``` -和上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql -SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 +SELECT * FROM clients LIMIT 1 ``` -`Model.first` 如果没找到匹配的记录,不会抛出异常,而是返回 `nil`。 +如果没有找到记录,`take` 方法返回 `nil`,而不抛出异常。 -#### `last` - -`Model.last` 获取按主键排序得到的最后一个记录。例如: +`take` 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如: ```ruby -client = Client.last -# => # +client = Client.take(2) +# => [ +# #, +# # +# ] ``` -和上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql -SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 +SELECT * FROM clients LIMIT 2 ``` -`Model.last` 如果没找到匹配的记录,不会抛出异常,而是返回 `nil`。 +`take!` 方法的行为和 `take` 方法类似,区别在于如果没有找到匹配的记录,`take!` 方法抛出 `ActiveRecord::RecordNotFound` 异常。 + +TIP: 对于不同的数据库引擎,`take` 方法检索的记录可能不一样。 + + -#### `find_by` +#### `first` 方法 -`Model.find_by` 获取满足条件的第一个记录。例如: +`first` 方法默认查找按主键排序的第一条记录。例如: ```ruby -Client.find_by first_name: 'Lifo' +client = Client.first # => # - -Client.find_by first_name: 'Jon' -# => nil ``` -等价于: +和上面的代码等价的 SQL 是: -```ruby -Client.where(first_name: 'Lifo').take +```sql +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 ``` -#### `take!` +如果没有找到匹配的记录,`first` 方法返回 `nil`,而不抛出异常。 -`Model.take!` 方法会获取一个记录,不考虑任何顺序。例如: +如果默认作用域 (请参阅 [应用默认作用域](#applying-a-default-scope))包含排序方法,`first` 方法会返回按照这个顺序排序的第一条记录。 + +`first` 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如: ```ruby -client = Client.take! -# => # +client = Client.first(3) +# => [ +# #, +# #, +# # +# ] ``` -和上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3 ``` -如果未找到匹配的记录,`Model.take!` 会抛出 `ActiveRecord::RecordNotFound` 异常。 - -#### `first!` - -`Model.first!` 获取按主键排序得到的第一个记录。例如: +对于使用 `order` 排序的集合,`first` 方法返回按照指定属性排序的第一条记录。例如: ```ruby -client = Client.first! -# => # +client = Client.order(:first_name).first +# => # ``` -和上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql -SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 +SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1 ``` -如果未找到匹配的记录,`Model.first!` 会抛出 `ActiveRecord::RecordNotFound` 异常。 +`first!` 方法的行为和 `first` 方法类似,区别在于如果没有找到匹配的记录,`first!` 方法会抛出 `ActiveRecord::RecordNotFound` 异常。 + + -#### `last!` +#### `last` 方法 -`Model.last!` 获取按主键排序得到的最后一个记录。例如: +`last` 方法默认查找按主键排序的最后一条记录。例如: ```ruby -client = Client.last! +client = Client.last # => # ``` -和上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 ``` -如果未找到匹配的记录,`Model.last!` 会抛出 `ActiveRecord::RecordNotFound` 异常。 +如果没有找到匹配的记录,`last` 方法返回 `nil`,而不抛出异常。 -#### `find_by!` +如果默认作用域 (请参阅 [应用默认作用域](#applying-a-default-scope))包含排序方法,`last` 方法会返回按照这个顺序排序的最后一条记录。 -`Model.find_by!` 获取满足条件的第一个记录。如果没找到匹配的记录,会抛出 `ActiveRecord::RecordNotFound` 异常。例如: +`last` 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如: ```ruby -Client.find_by! first_name: 'Lifo' -# => # - -Client.find_by! first_name: 'Jon' -# => ActiveRecord::RecordNotFound +client = Client.last(3) +# => [ +# #, +# #, +# # +# ] ``` -等价于: +和上面的代码等价的 SQL 是: -```ruby -Client.where(first_name: 'Lifo').take! +```sql +SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3 ``` -### 获取多个对象 - -#### 使用多个主键 - -`Model.find(array_of_primary_key)` 方法可接受一个由主键组成的数组,返回一个由主键对应记录组成的数组。例如: +对于使用 `order` 排序的集合,`last` 方法返回按照指定属性排序的最后一条记录。例如: ```ruby -# Find the clients with primary keys 1 and 10. -client = Client.find([1, 10]) # Or even Client.find(1, 10) -# => [#, #] +client = Client.order(:first_name).last +# => # ``` -上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql -SELECT * FROM clients WHERE (clients.id IN (1,10)) +SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1 ``` -WARNING: 只要有一个主键的对应的记录未找到,`Model.find(array_of_primary_key)` 方法就会抛出 `ActiveRecord::RecordNotFound` 异常。 +`last!` 方法的行为和 `last` 方法类似,区别在于如果没有找到匹配的记录,`last!` 方法会抛出 `ActiveRecord::RecordNotFound` 异常。 -#### take + -`Model.take(limit)` 方法获取 `limit` 个记录,不考虑任何顺序: +#### `find_by` 方法 -```ruby -Client.take(2) -# => [#, - #] -``` +`find_by` 方法查找匹配指定条件的第一条记录。 例如: -和上述方法等价的 SQL 查询是: +```ruby +Client.find_by first_name: 'Lifo' +# => # -```sql -SELECT * FROM clients LIMIT 2 +Client.find_by first_name: 'Jon' +# => nil ``` -#### first - -`Model.first(limit)` 方法获取按主键排序的前 `limit` 个记录: +上面的代码等价于: ```ruby -Client.first(2) -# => [#, - #] +Client.where(first_name: 'Lifo').take ``` -和上述方法等价的 SQL 查询是: +和上面的代码等价的 SQL 是: ```sql -SELECT * FROM clients ORDER BY id ASC LIMIT 2 +SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1 ``` -#### last - -`Model.last(limit)` 方法获取按主键降序排列的前 `limit` 个记录: +`find_by!` 方法的行为和 `find_by` 方法类似,区别在于如果没有找到匹配的记录,`find_by!` 方法会抛出 `ActiveRecord::RecordNotFound` 异常。例如: ```ruby -Client.last(2) -# => [#, - #] +Client.find_by! first_name: 'does not exist' +# => ActiveRecord::RecordNotFound ``` -和上述方法等价的 SQL 查询是: +上面的代码等价于: -```sql -SELECT * FROM clients ORDER BY id DESC LIMIT 2 +```ruby +Client.where(first_name: 'does not exist').take! ``` -### 批量获取多个对象 + + +### 批量检索多个对象 -我们经常需要遍历由很多记录组成的集合,例如给大量用户发送邮件列表,或者导出数据。 +我们常常需要遍历大量记录,例如向大量用户发送时事通讯、导出数据等。 -我们可能会直接写出如下的代码: +处理这类问题的方法看起来可能很简单: ```ruby -# This is very inefficient when the users table has thousands of rows. +# 如果表中记录很多,可能消耗大量内存 User.all.each do |user| - NewsLetter.weekly_deliver(user) + NewsMailer.weekly(user).deliver_now end ``` -但这种方法在数据表很大时就有点不现实了,因为 `User.all.each` 会一次读取整个数据表,一行记录创建一个模型对象,然后把整个模型对象数组存入内存。如果记录数非常多,可能会用完内存。 +但随着数据表越来越大,这种方法越来越行不通,因为 `User.all.each` 会使 Active Record 一次性取回整个数据表,为每条记录创建模型对象,并把整个模型对象数组保存在内存中。事实上,如果我们有大量记录,整个模型对象数组需要占用的空间可能会超过可用的内存容量。 + +Rails 提供了两种方法来解决这个问题,两种方法都是把整个记录分成多个对内存友好的批处理。第一种方法是通过 `find_each` 方法每次检索一批记录,然后逐一把每条记录作为模型传入块。第二种方法是通过 `find_in_batches` 方法每次检索一批记录,然后把这批记录整个作为模型数组传入块。 -Rails 为了解决这种问题提供了两个方法,把记录分成几个批次,不占用过多内存。第一个方法是 `find_each`,获取一批记录,然后分别把每个记录传入代码块。第二个方法是 `find_in_batches`,获取一批记录,然后把整批记录作为数组传入代码块。 +TIP: `find_each` 和 `find_in_batches` 方法用于大量记录的批处理,这些记录数量很大以至于不适合一次性保存在内存中。如果只需要循环 1000 条记录,那么应该首选常规的 `find` 方法。 -TIP: `find_each` 和 `find_in_batches` 方法的目的是分批处理无法一次载入内存的巨量记录。如果只想遍历几千个记录,更推荐使用常规的查询方法。 + -#### `find_each` +#### `find_each` 方法 -`find_each` 方法获取一批记录,然后分别把每个记录传入代码块。在下面的例子中,`find_each` 获取 1000 各记录,然后把每个记录传入代码块,知道所有记录都处理完为止: +`find_each` 方法批量检索记录,然后逐一把每条记录作为模型传入块。在下面的例子中,`find_each` 方法取回 1000 条记录,然后逐一把每条记录作为模型传入块。 ```ruby User.find_each do |user| - NewsLetter.weekly_deliver(user) + NewsMailer.weekly(user).deliver_now end ``` -##### `find_each` 方法的选项 +这一过程会不断重复,直到处理完所有记录。 -在 `find_each` 方法中可使用 `find` 方法的大多数选项,但不能使用 `:order` 和 `:limit`,因为这两个选项是保留给 `find_each` 内部使用的。 +如前所述,`find_each` 能处理模型类,此外它还能处理关系: -`find_each` 方法还可使用另外两个选项:`:batch_size` 和 `:start`。 +```ruby +User.where(weekly_subscriber: true).find_each do |user| + NewsMailer.weekly(user).deliver_now +end +``` + +前提是关系不能有顺序,因为这个方法在迭代时有既定的顺序。 + +如果接收者定义了顺序,具体行为取决于 `config.active_record.error_on_ignored_order` 旗标。设为 `true` 时,抛出 `ArgumentError` 异常,否则忽略顺序,发出提醒(这是默认设置)。这一行为可使用 `:error_on_ignore` 选项覆盖,详情参见下文。 **`:batch_size`** -`:batch_size` 选项指定在把各记录传入代码块之前,各批次获取的记录数量。例如,一个批次获取 5000 个记录: +`:batch_size` 选项用于指明批量检索记录时一次检索多少条记录。例如,一次检索 5000 条记录: ```ruby User.find_each(batch_size: 5000) do |user| - NewsLetter.weekly_deliver(user) + NewsMailer.weekly(user).deliver_now end ``` **`:start`** -默认情况下,按主键的升序方式获取记录,其中主键的类型必须是整数。如果不想用最小的 ID,可以使用 `:start` 选项指定批次的起始 ID。例如,前面的批量处理中断了,但保存了中断时的 ID,就可以使用这个选项继续处理。 +记录默认是按主键的升序方式取回的,这里的主键必须是整数。`:start` 选项用于配置想要取回的记录序列的第一个 ID,比这个 ID 小的记录都不会取回。这个选项有时候很有用,例如当需要恢复之前中断的批处理时,只需从最后一个取回的记录之后开始继续处理即可。 -例如,在有 5000 个记录的批次中,只向主键大于 2000 的用户发送邮件列表,可以这么做: +下面的例子把时事通讯发送给主键从 2000 开始的用户: ```ruby -User.find_each(start: 2000, batch_size: 5000) do |user| - NewsLetter.weekly_deliver(user) +User.find_each(start: 2000) do |user| + NewsMailer.weekly(user).deliver_now end ``` -还有一个例子是,使用多个 worker 处理同一个进程队列。如果需要每个 worker 处理 10000 个记录,就可以在每个 worker 中设置相应的 `:start` 选项。 +**`:finish`** -#### `find_in_batches` +和 `:start` 选项类似,`:finish` 选项用于配置想要取回的记录序列的最后一个 ID,比这个 ID 大的记录都不会取回。这个选项有时候很有用,例如可以通过配置 `:start` 和 `:finish` 选项指明想要批处理的子记录集。 -`find_in_batches` 方法和 `find_each` 类似,都获取一批记录。二者的不同点是,`find_in_batches` 把整批记录作为一个数组传入代码块,而不是单独传入各记录。在下面的例子中,会把 1000 个单据一次性传入代码块,让代码块后面的程序处理剩下的单据: +下面的例子把时事通讯发送给主键从 2000 到 10000 的用户: ```ruby -# Give add_invoices an array of 1000 invoices at a time -Invoice.find_in_batches(include: :invoice_lines) do |invoices| +User.find_each(start: 2000, finish: 10000) do |user| + NewsMailer.weekly(user).deliver_now +end +``` + +另一个例子是使用多个职程(worker)处理同一个进程队列。通过分别配置 `:start` 和 `:finish` 选项可以让每个职程每次都处理 10000 条记录。 + +**`:error_on_ignore`** + +覆盖应用的配置,指定有顺序的关系是否抛出异常。 + + + +#### `find_in_batches` 方法 + +`find_in_batches` 方法和 `find_each` 方法类似,两者都是批量检索记录。区别在于,`find_in_batches` 方法会把一批记录作为模型数组传入块,而不是像 `find_each` 方法那样逐一把每条记录作为模型传入块。下面的例子每次把 1000 张发票的数组一次性传入块(最后一次传入块的数组中的发票数量可能不到 1000): + +```ruby +# 一次把 1000 张发票组成的数组传给 add_invoices +Invoice.find_in_batches do |invoices| export.add_invoices(invoices) end ``` -NOTE: `:include` 选项可以让指定的关联和模型一同加载。 +如前所述,`find_in_batches` 能处理模型,也能处理关系: + +```ruby +Invoice.pending.find_in_batches do |invoice| + pending_invoices_export.add_invoices(invoices) +end +``` + +但是关系不能有顺序,因为这个方法在迭代时有既定的顺序。 + + ##### `find_in_batches` 方法的选项 -`find_in_batches` 方法和 `find_each` 方法一样,可以使用 `:batch_size` 和 `:start` 选项,还可使用常规的 `find` 方法中的大多数选项,但不能使用 `:order` 和 `:limit` 选项,因为这两个选项保留给 `find_in_batches` 方法内部使用。 +`find_in_batches` 方法接受的选项与 `find_each` 方法一样。 + + -条件查询 -------- +## 条件查询 -`where` 方法用来指定限制获取记录的条件,用于 SQL 语句的 `WHERE` 子句。条件可使用字符串、数组或 Hash 指定。 +`where` 方法用于指明限制返回记录所使用的条件,相当于 SQL 语句的 `WHERE` 部分。条件可以使用字符串、数组或散列指定。 + + ### 纯字符串条件 -如果查询时要使用条件,可以直接指定。例如 `Client.where("orders_count = '2'")`,获取 `orders_count` 字段为 `2` 的客户记录。 +可以直接用纯字符串为查找添加条件。例如,`Client.where("orders_count = '2'")` 会查找所有 `orders_count` 字段的值为 2 的客户记录。 + +WARNING: 使用纯字符串创建条件存在容易受到 SQL 注入攻击的风险。例如,`Client.where("first_name LIKE '%#{params[:first_name]}%'")` 是不安全的。在下一节中我们会看到,使用数组创建条件是推荐的做法。 -WARNING: 使用纯字符串指定条件可能导致 SQL 注入漏洞。例如,`Client.where("first_name LIKE '%#{params[:first_name]}%'")`,这里的条件就不安全。推荐使用的条件指定方式是数组,请阅读下一节。 + ### 数组条件 -如果数字是在别处动态生成的话应该怎么处理呢?可用下面的查询: +如果 `Client.where("orders_count = '2'")` 这个例子中的数字是变化的,比如说是从别处传递过来的参数,那么可以像下面这样进行查找: ```ruby Client.where("orders_count = ?", params[:orders]) ``` -Active Record 会先处理第一个元素中的条件,然后使用后续元素替换第一个元素中的问号(`?`)。 +Active Record 会把第一个参数作为条件字符串,并用之后的其他参数来替换条件字符串中的问号(`?`)。 -指定多个条件的方式如下: +我们还可以指定多个条件: ```ruby Client.where("orders_count = ? AND locked = ?", params[:orders], false) ``` -在这个例子中,第一个问号会替换成 `params[:orders]` 的值;第二个问号会替换成 `false` 在 SQL 中对应的值,具体的值视所用的适配器而定。 +在上面的例子中,第一个问号会被替换为 `params[:orders]` 的值,第二个问号会被替换为 `false` 在 SQL 中对应的值,这个值是什么取决于所使用的数据库适配器。 -下面这种形式 +强烈推荐使用下面这种写法: ```ruby Client.where("orders_count = ?", params[:orders]) ``` -要比这种形式好 +而不是: ```ruby Client.where("orders_count = #{params[:orders]}") ``` -因为前者传入的参数更安全。直接在条件字符串中指定的条件会原封不动的传给数据库。也就是说,即使用户不怀好意,条件也会转义。如果这么做,整个数据库就处在一个危险境地,只要用户发现可以接触数据库,就能做任何想做的事。所以,千万别直接在条件字符串中使用参数。 +原因是出于参数的安全性考虑。把变量直接放入条件字符串会导致变量原封不动地传递给数据库,这意味着即使是恶意用户提交的变量也不会被转义。这样一来,整个数据库就处于风险之中,因为一旦恶意用户发现自己能够滥用数据库,他就可能做任何事情。所以,永远不要把参数直接放入条件字符串。 -TIP: 关于 SQL 注入更详细的介绍,请阅读“[Ruby on Rails 安全指南](security.html#sql-injection)” +TIP: 关于 SQL 注入的危险性的更多介绍,请参阅 [SQL 注入](security.html#sql-injection)。 + + #### 条件中的占位符 -除了使用问号占位之外,在数组条件中还可使用键值对 Hash 形式的占位符: +和问号占位符(`?`)类似,我们还可以在条件字符串中使用符号占位符,并通过散列提供符号对应的值: ```ruby Client.where("created_at >= :start_date AND created_at <= :end_date", {start_date: params[:start_date], end_date: params[:end_date]}) ``` -如果条件中有很多参数,使用这种形式可读性更高。 +如果条件中有很多变量,那么上面这种写法的可读性更高。 + + + +### 散列条件 -### Hash 条件 +Active Record 还允许使用散列条件,以提高条件语句的可读性。使用散列条件时,散列的键指明需要限制的字段,键对应的值指明如何进行限制。 -Active Record 还允许使用 Hash 条件,提高条件语句的可读性。使用 Hash 条件时,传入 Hash 的键是要设定条件的字段,值是要设定的条件。 +NOTE: 在散列条件中,只能进行相等性、范围和子集检查。 -NOTE: 在 Hash 条件中只能指定相等。范围和子集这三种条件。 + -#### 相等 +#### 相等性条件 ```ruby Client.where(locked: true) ``` -字段的名字还可使用字符串表示: +上面的代码会生成下面的 SQL 语句: + +```sql +SELECT * FROM clients WHERE (clients.locked = 1) +``` + +其中字段名也可以是字符串: ```ruby Client.where('locked' => true) ``` -在 `belongs_to` 关联中,如果条件中的值是模型对象,可用关联键表示。这种条件指定方式也可用于多态关联。 +对于 `belongs_to` 关联来说,如果使用 Active Record 对象作为值,就可以使用关联键来指定模型。这种方法也适用于多态关联。 ```ruby -Post.where(author: author) -Author.joins(:posts).where(posts: { author: author }) +Article.where(author: author) +Author.joins(:articles).where(articles: { author: author }) ``` -NOTE: 条件的值不能为 Symbol。例如,不能这么指定条件:`Client.where(status: :active)`。 +NOTE: 相等性条件中的值不能是符号。例如,`Client.where(status: :active)` 这种写法是错误的。 -#### 范围 + + +#### 范围条件 ```ruby Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight) ``` -指定这个条件后,会使用 SQL `BETWEEN` 子句查询昨天创建的客户: +上面的代码会使用 `BETWEEN` SQL 表达式查找所有昨天创建的客户记录: ```sql SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00') ``` -这段代码演示了[数组条件](#array-conditions)的简写形式。 +这是 [数组条件](#array-conditions)中那个示例代码的更简短的写法。 + + -#### 子集 +#### 子集条件 -如果想使用 `IN` 子句查询记录,可以在 Hash 条件中使用数组: +要想用 `IN` 表达式来查找记录,可以在散列条件中使用数组: ```ruby Client.where(orders_count: [1,3,5]) ``` -上述代码生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) ``` -### `NOT` 条件 + -SQL `NOT` 查询可用 `where.not` 方法构建。 +### NOT 条件 + +可以用 `where.not` 创建 `NOT` SQL 查询: ```ruby -Post.where.not(author: author) +Client.where.not(locked: true) +``` + +也就是说,先调用没有参数的 `where` 方法,然后马上链式调用 `not` 方法,就可以生成这个查询。上面的代码会生成下面的 SQL 语句: + +```sql +SELECT * FROM clients WHERE (clients.locked != 1) ``` -也即是说,这个查询首先调用没有参数的 `where` 方法,然后再调用 `not` 方法。 + -排序 ----- +## 排序 -要想按照特定的顺序从数据库中获取记录,可以使用 `order` 方法。 +要想按特定顺序从数据库中检索记录,可以使用 `order` 方法。 -例如,想按照 `created_at` 的升序方式获取一些记录,可以这么做: +例如,如果想按 `created_at` 字段的升序方式取回记录: ```ruby Client.order(:created_at) -# OR +# 或 Client.order("created_at") ``` -还可使用 `ASC` 或 `DESC` 指定排序方式: +还可以使用 `ASC`(升序) 或 `DESC`(降序) 指定排序方式: ```ruby Client.order(created_at: :desc) -# OR +# 或 Client.order(created_at: :asc) -# OR +# 或 Client.order("created_at DESC") -# OR +# 或 Client.order("created_at ASC") ``` -或者使用多个字段排序: +或按多个字段排序: ```ruby Client.order(orders_count: :asc, created_at: :desc) -# OR +# 或 Client.order(:orders_count, created_at: :desc) -# OR +# 或 Client.order("orders_count ASC, created_at DESC") -# OR +# 或 Client.order("orders_count ASC", "created_at DESC") ``` -如果想在不同的上下文中多次调用 `order`,可以在前一个 `order` 后再调用一次: +如果多次调用 `order` 方法,后续排序会在第一次排序的基础上进行: -```ruby +```sql Client.order("orders_count ASC").order("created_at DESC") # SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC ``` -查询指定字段 ------------ +WARNING: 使用 **MySQL 5.7.5** 及以上版本时,若想从结果集合中选择字段,要使用 `select`、`pluck` 和 `ids` 等方法。如果 `order` 子句中使用的字段不在选择列表中,`order` 方法抛出 `ActiveRecord::StatementInvalid` 异常。从结果集合中选择字段的方法参见下一节。 + + + -默认情况下,`Model.find` 使用 `SELECT *` 查询所有字段。 +## 选择特定字段 -要查询部分字段,可使用 `select` 方法。 +`Model.find` 默认使用 `select *` 从结果集中选择所有字段。 -例如,只查询 `viewable_by` 和 `locked` 字段: +可以使用 `select` 方法从结果集中选择字段的子集。 + +例如,只选择 `viewable_by` 和 `locked` 字段: ```ruby Client.select("viewable_by, locked") ``` -上述查询使用的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT viewable_by, locked FROM clients ``` -使用时要注意,因为模型对象只会使用选择的字段初始化。如果字段不能初始化模型对象,会得到以下异常: +请注意,上面的代码初始化的模型对象只包含了所选择的字段,这时如果访问这个模型对象未包含的字段就会抛出异常: -```bash +``` ActiveModel::MissingAttributeError: missing attribute: ``` -其中 `` 是所查询的字段。`id` 字段不会抛出 `ActiveRecord::MissingAttributeError` 异常,所以在关联中使用时要注意,因为关联需要 `id` 字段才能正常使用。 +其中 `` 是我们想要访问的字段。`id` 方法不会引发 `ActiveRecord::MissingAttributeError` 异常,因此在使用关联时一定要小心,因为只有当 `id` 方法正常工作时关联才能正常工作。 -如果查询时希望指定字段的同值记录只出现一次,可以使用 `distinct` 方法: +在查询时如果想让某个字段的同值记录只出现一次,可以使用 `distinct` 方法添加唯一性约束: ```ruby Client.select(:name).distinct ``` -上述方法生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT DISTINCT name FROM clients ``` -查询后还可以删除唯一性限制: +唯一性约束在添加之后还可以删除: ```ruby query = Client.select(:name).distinct -# => Returns unique names +# => 返回无重复的名字 query.distinct(false) -# => Returns all names, even if there are duplicates +# => 返回所有名字,即使有重复 ``` -限量和偏移 ---------- + + +## 限量和偏移量 -要想在 `Model.find` 方法中使用 SQL `LIMIT` 子句,可使用 `limit` 和 `offset` 方法。 +要想在 `Model.find` 生成的 SQL 语句中使用 `LIMIT` 子句,可以在关联上使用 `limit` 和 `offset` 方法。 -`limit` 方法指定获取的记录数量,`offset` 方法指定在返回结果之前跳过多少个记录。例如: +`limit` 方法用于指明想要取回的记录数量,`offset` 方法用于指明取回记录时在第一条记录之前要跳过多少条记录。例如: ```ruby Client.limit(5) ``` -上述查询最大只会返回 5 各客户对象,因为没指定偏移,多以会返回数据表中的前 5 个记录。生成的 SQL 语句如下: +上面的代码会返回 5 条客户记录,因为没有使用 `offset` 方法,所以返回的这 5 条记录就是前 5 条记录。生成的 SQL 语句如下: ```sql SELECT * FROM clients LIMIT 5 ``` -再加上 `offset` 方法: +如果使用 `offset` 方法: ```ruby Client.limit(5).offset(30) ``` -这时会从第 31 个记录开始,返回最多 5 个客户对象。生成的 SQL 语句如下: +这时会返回从第 31 条记录开始的 5 条记录。生成的 SQL 语句如下: ```sql SELECT * FROM clients LIMIT 5 OFFSET 30 ``` -分组 ----- + -要想在查询时使用 SQL `GROUP BY` 子句,可以使用 `group` 方法。 +## 分组 -例如,如果想获取一组订单的创建日期,可以这么做: +要想在查找方法生成的 SQL 语句中使用 `GROUP BY` 子句,可以使用 `group` 方法。 + +例如,如果我们想根据订单创建日期查找订单记录: ```ruby Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)") ``` -上述查询会只会为相同日期下的订单创建一个 `Order` 对象。 - -生成的 SQL 语句如下: +上面的代码会为数据库中同一天创建的订单创建 `Order` 对象。生成的 SQL 语句如下: ```sql SELECT date(created_at) as ordered_date, sum(price) as total_price @@ -659,19 +728,37 @@ FROM orders GROUP BY date(created_at) ``` -分组筛选 -------- + + +### 分组项目的总数 + +要想得到一次查询中分组项目的总数,可以在调用 `group` 方法后调用 `count` 方法。 + +```ruby +Order.group(:status).count +# => { 'awaiting_approval' => 7, 'paid' => 12 } +``` + +上面的代码会生成下面的 SQL 语句: + +```sql +SELECT COUNT (*) AS count_all, status AS status +FROM "orders" +GROUP BY status +``` -SQL 使用 `HAVING` 子句指定 `GROUP BY` 分组的条件。在 `Model.find` 方法中可使用 `:having` 选项指定 `HAVING` 子句。 + -例如: +## `having` 方法 + +SQL 语句用 `HAVING` 子句指明 `GROUP BY` 字段的约束条件。要想在 `Model.find` 生成的 SQL 语句中使用 `HAVING` 子句,可以使用 `having` 方法。例如: ```ruby Order.select("date(created_at) as ordered_date, sum(price) as total_price"). group("date(created_at)").having("sum(price) > ?", 100) ``` -生成的 SQL 如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT date(created_at) as ordered_date, sum(price) as total_price @@ -680,168 +767,183 @@ GROUP BY date(created_at) HAVING sum(price) > 100 ``` -这个查询只会为同一天下的订单创建一个 `Order` 对象,而且这一天的订单总额要大于 $100。 +上面的查询会返回每个 `Order` 对象的日期和总价,查询结果按日期分组并排序,并且总价必须高于 100。 + + -条件覆盖 -------- +## 条件覆盖 -### `unscope` + -如果要删除某个条件可使用 `unscope` 方法。例如: +### `unscope` 方法 + +可以使用 `unscope` 方法删除某些条件。 例如: ```ruby -Post.where('id > 10').limit(20).order('id asc').unscope(:order) +Article.where('id > 10').limit(20).order('id asc').unscope(:order) ``` -生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql -SELECT * FROM posts WHERE id > 10 LIMIT 20 +SELECT * FROM articles WHERE id > 10 LIMIT 20 -# Original query without `unscope` -SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20 +# 没使用 `unscope` 之前的查询 +SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20 ``` -`unscope` 还可删除 `WHERE` 子句中的条件。例如: +还可以使用 `unscope` 方法删除 `where` 方法中的某些条件。例如: ```ruby -Post.where(id: 10, trashed: false).unscope(where: :id) -# SELECT "posts".* FROM "posts" WHERE trashed = 0 +Article.where(id: 10, trashed: false).unscope(where: :id) +# SELECT "articles".* FROM "articles" WHERE trashed = 0 ``` -`unscope` 还可影响合并后的查询: +在关联中使用 `unscope` 方法,会对整个关联造成影响: ```ruby -Post.order('id asc').merge(Post.unscope(:order)) -# SELECT "posts".* FROM "posts" +Article.order('id asc').merge(Article.unscope(:order)) +# SELECT "articles".* FROM "articles" ``` -### `only` + + +### `only` 方法 -查询条件还可使用 `only` 方法覆盖。例如: +可以使用 `only` 方法覆盖某些条件。例如: ```ruby -Post.where('id > 10').limit(20).order('id desc').only(:order, :where) +Article.where('id > 10').limit(20).order('id desc').only(:order, :where) ``` -执行的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql -SELECT * FROM posts WHERE id > 10 ORDER BY id DESC +SELECT * FROM articles WHERE id > 10 ORDER BY id DESC -# Original query without `only` -SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20 +# 没使用 `only` 之前的查询 +SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20 ``` -### `reorder` + -`reorder` 方法覆盖原来的 `order` 条件。例如: +### `reorder` 方法 + +可以使用 `reorder` 方法覆盖默认作用域中的排序方式。例如: ```ruby -class Post < ActiveRecord::Base - .. - .. +class Article < ApplicationRecord has_many :comments, -> { order('posted_at DESC') } end +``` -Post.find(10).comments.reorder('name') +```ruby +Article.find(10).comments.reorder('name') ``` -执行的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql -SELECT * FROM posts WHERE id = 10 ORDER BY name +SELECT * FROM articles WHERE id = 10 +SELECT * FROM comments WHERE article_id = 10 ORDER BY name ``` -没用 `reorder` 方法时执行的 SQL 语句如下: +如果不使用 `reorder` 方法,那么会生成下面的 SQL 语句: ```sql -SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC +SELECT * FROM articles WHERE id = 10 +SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC ``` -### `reverse_order` + -`reverse_order` 方法翻转 `ORDER` 子句的条件。 +### `reverse_order` 方法 -```ruby +可以使用 `reverse_order` 方法反转排序条件。 + +```sql Client.where("orders_count > 10").order(:name).reverse_order ``` -执行的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC ``` -如果查询中没有使用 `ORDER` 子句,`reverse_order` 方法会按照主键的逆序查询: +如果查询时没有使用 `order` 方法,那么 `reverse_order` 方法会使查询结果按主键的降序方式排序。 ```ruby Client.where("orders_count > 10").reverse_order ``` -执行的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC ``` -这个方法**没有**参数。 +`reverse_order` 方法不接受任何参数。 + + -### `rewhere` +### `rewhere` 方法 -`rewhere` 方法覆盖前面的 `where` 条件。例如: +可以使用 `rewhere` 方法覆盖 `where` 方法中指定的条件。例如: ```ruby -Post.where(trashed: true).rewhere(trashed: false) +Article.where(trashed: true).rewhere(trashed: false) ``` -执行的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql -SELECT * FROM posts WHERE `trashed` = 0 +SELECT * FROM articles WHERE `trashed` = 0 ``` -如果不使用 `rewhere` 方法,写成: +如果不使用 `rewhere` 方法而是再次使用 `where` 方法: -```ruby -Post.where(trashed: true).where(trashed: false) +```sql +Article.where(trashed: true).where(trashed: false) ``` -执行的 SQL 语句如下: +会生成下面的 SQL 语句: ```sql -SELECT * FROM posts WHERE `trashed` = 1 AND `trashed` = 0 +SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0 ``` -空关系 ------- + -`none` 返回一个可链接的关系,没有相应的记录。`none` 方法返回对象的后续条件查询,得到的还是空关系。如果想以可链接的方式响应可能无返回结果的方法或者作用域,可使用 `none` 方法。 +## 空关系 + +`none` 方法返回可以在链式调用中使用的、不包含任何记录的空关系。在这个空关系上应用后续条件链,会继续生成空关系。对于可能返回零结果、但又需要在链式调用中使用的方法或作用域,可以使用 `none` 方法来提供返回值。 ```ruby -Post.none # returns an empty Relation and fires no queries. +Article.none # 返回一个空 Relation 对象,而且不执行查询 ``` ```ruby -# The visible_posts method below is expected to return a Relation. -@posts = current_user.visible_posts.where(name: params[:name]) +# 下面的 visible_articles 方法期待返回一个空 Relation 对象 +@articles = current_user.visible_articles.where(name: params[:name]) -def visible_posts +def visible_articles case role when 'Country Manager' - Post.where(country: country) + Article.where(country: country) when 'Reviewer' - Post.published + Article.published when 'Bad User' - Post.none # => returning [] or nil breaks the caller code in this case + Article.none # => 如果这里返回 [] 或 nil,会导致调用方出错 end end ``` -只读对象 -------- + + +## 只读对象 -Active Record 提供了 `readonly` 方法,禁止修改获取的对象。试图修改只读记录的操作不会成功,而且会抛出 `ActiveRecord::ReadOnlyRecord` 异常。 +在关联中使用 Active Record 提供的 `readonly` 方法,可以显式禁止修改任何返回对象。如果尝试修改只读对象,不但不会成功,还会抛出 `ActiveRecord::ReadOnlyRecord` 异常。 ```ruby client = Client.readonly.first @@ -849,25 +951,30 @@ client.visits += 1 client.save ``` -因为把 `client` 设为了只读对象,所以上述代码调用 `client.save` 方法修改 `visits` 的值时会抛出 `ActiveRecord::ReadOnlyRecord` 异常。 +在上面的代码中,`client` 被显式设置为只读对象,因此在更新 `client.visits` 的值后调用 `client.save` 会抛出 `ActiveRecord::ReadOnlyRecord` 异常。 -更新时锁定记录 ------------- + -锁定可以避免更新记录时的条件竞争,也能保证原子更新。 +## 在更新时锁定记录 + +在数据库中,锁定用于避免更新记录时的条件竞争,并确保原子更新。 Active Record 提供了两种锁定机制: -* 乐观锁定 -* 悲观锁定 +* 乐观锁定 +* 悲观锁定 + + ### 乐观锁定 -乐观锁定允许多个用户编辑同一个记录,假设数据发生冲突的可能性最小。Rails 会检查读取记录后是否有其他程序在修改这个记录。如果检测到有其他程序在修改,就会抛出 `ActiveRecord::StaleObjectError` 异常,忽略改动。 +乐观锁定允许多个用户访问并编辑同一记录,并假设数据发生冲突的可能性最小。其原理是检查读取记录后是否有其他进程尝试更新记录,如果有就抛出 `ActiveRecord::StaleObjectError` 异常,并忽略该更新。 -**乐观锁定字段** + -为了使用乐观锁定,数据表中要有一个类型为整数的 `lock_version` 字段。每次更新记录时,Active Record 都会增加 `lock_version` 字段的值。如果更新请求中的 `lock_version` 字段值比数据库中的 `lock_version` 字段值小,会抛出 `ActiveRecord::StaleObjectError` 异常,更新失败。例如: +#### 字段的乐观锁定 + +为了使用乐观锁定,数据表中需要有一个整数类型的 `lock_version` 字段。每次更新记录时,Active Record 都会增加 `lock_version` 字段的值。如果更新请求中 `lock_version` 字段的值比当前数据库中 `lock_version` 字段的值小,更新请求就会失败,并抛出 `ActiveRecord::StaleObjectError` 异常。例如: ```ruby c1 = Client.find(1) @@ -877,36 +984,36 @@ c1.first_name = "Michael" c1.save c2.name = "should fail" -c2.save # Raises an ActiveRecord::StaleObjectError +c2.save # 抛出 ActiveRecord::StaleObjectError ``` -抛出异常后,你要负责处理冲突,可以回滚操作、合并操作或者使用其他业务逻辑处理。 +抛出异常后,我们需要救援异常并处理冲突,或回滚,或合并,或应用其他业务逻辑来解决冲突。 -乐观锁定可以使用 `ActiveRecord::Base.lock_optimistically = false` 关闭。 +通过设置 `ActiveRecord::Base.lock_optimistically = false` 可以关闭乐观锁定。 -要想修改 `lock_version` 字段的名字,可以使用 `ActiveRecord::Base` 提供的 `locking_column` 类方法: +可以使用 `ActiveRecord::Base` 提供的 `locking_column` 类属性来覆盖 `lock_version` 字段名: ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord self.locking_column = :lock_client_column end ``` -### 悲观锁定 + -悲观锁定使用底层数据库提供的锁定机制。使用 `lock` 方法构建的关系在所选记录上生成一个“互斥锁”(exclusive lock)。使用 `lock` 方法构建的关系一般都放入事务中,避免死锁。 +### 悲观锁定 -例如: +悲观锁定使用底层数据库提供的锁定机制。在创建关联时使用 `lock` 方法,会在选定字段上生成互斥锁。使用 `lock` 方法的关联通常被包装在事务中,以避免发生死锁。例如: ```ruby Item.transaction do i = Item.lock.first i.name = 'Jones' - i.save + i.save! end ``` -在 MySQL 中,上述代码生成的 SQL 如下: +对于 MySQL 后端,上面的会话会生成下面的 SQL 语句: ```sql SQL (0.2ms) BEGIN @@ -915,7 +1022,7 @@ Item Update (0.4ms) UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', ` SQL (0.8ms) COMMIT ``` -`lock` 方法还可以接受 SQL 语句,使用其他锁定类型。例如,MySQL 中有一个语句是 `LOCK IN SHARE MODE`,会锁定记录,但还是允许其他查询读取记录。要想使用这个语句,直接传入 `lock` 方法即可: +要想支持其他锁定类型,可以直接传递 SQL 给 `lock` 方法。例如,MySQL 的 `LOCK IN SHARE MODE` 表达式在锁定记录时允许其他查询读取记录,这个表达式可以用作锁定选项: ```ruby Item.transaction do @@ -924,160 +1031,203 @@ Item.transaction do end ``` -如果已经创建了模型实例,可以在事务中加上这种锁定,如下所示: +对于已有模型实例,可以启动事务并一次性获取锁: ```ruby item = Item.first item.with_lock do - # This block is called within a transaction, - # item is already locked. + # 这个块在事务中调用 + # item 已经锁定 item.increment!(:views) end ``` -连接数据表 ---------- + + +## 联结表 -Active Record 提供了一个查询方法名为 `joins`,用来指定 SQL `JOIN` 子句。`joins` 方法的用法有很多种。 +Active Record 提供了 `joins` 和 `left_outer_joins` 这两个查找方法,用于指明生成的 SQL 语句中的 `JOIN` 子句。其中,`joins` 方法用于 `INNER JOIN` 查询或定制查询,`left_outer_joins` 用于 `LEFT OUTER JOIN` 查询。 -### 使用字符串形式的 SQL 语句 + -在 `joins` 方法中可以直接使用 `JOIN` 子句的 SQL: +### `joins` 方法 + +`joins` 方法有多种用法。 + + + +#### 使用字符串 SQL 片段 + +在 `joins` 方法中可以直接用 SQL 指明 `JOIN` 子句: ```ruby -Client.joins('LEFT OUTER JOIN addresses ON addresses.client_id = clients.id') +Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'") ``` -生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql -SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id +SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't' ``` -### 使用数组或 Hash 指定具名关联 + -WARNING: 这种方法只用于 `INNER JOIN`。 +#### 使用具名关联数组或散列 -使用 `joins` 方法时,可以使用声明[关联](association_basics.html)时使用的关联名指定 `JOIN` 子句。 +使用 `joins` 方法时,Active Record 允许我们使用在模型上定义的关联的名称,作为指明这些关联的 `JOIN` 子句的快捷方式。 -例如,假如按照如下方式定义 `Category`、`Post`、`Comment`、`Guest` 和 `Tag` 模型: +例如,假设有 `Category`、`Article`、`Comment`、`Guest` 和 `Tag` 这几个模型: ```ruby -class Category < ActiveRecord::Base - has_many :posts +class Category < ApplicationRecord + has_many :articles end -class Post < ActiveRecord::Base +class Article < ApplicationRecord belongs_to :category has_many :comments has_many :tags end -class Comment < ActiveRecord::Base - belongs_to :post +class Comment < ApplicationRecord + belongs_to :article has_one :guest end -class Guest < ActiveRecord::Base +class Guest < ApplicationRecord belongs_to :comment end -class Tag < ActiveRecord::Base - belongs_to :post +class Tag < ApplicationRecord + belongs_to :article end ``` -下面各种用法能都使用 `INNER JOIN` 子句生成正确的连接查询: +下面几种用法都会使用 `INNER JOIN` 生成我们想要的关联查询。 + +(译者注:原文此处开始出现编号错误,由译者根据内容逻辑关系进行了修正。) -#### 连接单个关联 + + +##### 单个关联的联结 ```ruby -Category.joins(:posts) +Category.joins(:articles) ``` -生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT categories.* FROM categories - INNER JOIN posts ON posts.category_id = categories.id + INNER JOIN articles ON articles.category_id = categories.id ``` -用人类语言表达,上述查询的意思是,“使用文章的分类创建分类对象”。注意,分类对象可能有重复,因为多篇文章可能属于同一分类。如果不想出现重复,可使用 `Category.joins(:posts).uniq` 方法。 +这个查询的意思是把所有包含了文章的(非空)分类作为一个 `Category` 对象返回。请注意,如果多篇文章同属于一个分类,那么这个分类会在 `Category` 对象中出现多次。要想让每个分类只出现一次,可以使用 `Category.joins(:articles).distinct`。 + + -#### 连接多个关联 +##### 多个关联的联结 ```ruby -Post.joins(:category, :comments) +Article.joins(:category, :comments) ``` -生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql -SELECT posts.* FROM posts - INNER JOIN categories ON posts.category_id = categories.id - INNER JOIN comments ON comments.post_id = posts.id +SELECT articles.* FROM articles + INNER JOIN categories ON articles.category_id = categories.id + INNER JOIN comments ON comments.article_id = articles.id ``` -用人类语言表达,上述查询的意思是,“返回指定分类且至少有一个评论的所有文章”。注意,如果文章有多个评论,同个文章对象会出现多次。 +这个查询的意思是把所有属于某个分类并至少拥有一条评论的文章作为一个 `Article` 对象返回。同样请注意,拥有多条评论的文章会在 `Article` 对象中出现多次。 -#### 连接一层嵌套关联 + + +##### 单层嵌套关联的联结 ```ruby -Post.joins(comments: :guest) +Article.joins(comments: :guest) ``` -生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql -SELECT posts.* FROM posts - INNER JOIN comments ON comments.post_id = posts.id +SELECT articles.* FROM articles + INNER JOIN comments ON comments.article_id = articles.id INNER JOIN guests ON guests.comment_id = comments.id ``` -用人类语言表达,上述查询的意思是,“返回有一个游客发布评论的所有文章”。 +这个查询的意思是把所有拥有访客评论的文章作为一个 `Article` 对象返回。 + + -#### 连接多层嵌套关联 +##### 多层嵌套关联的联结 ```ruby -Category.joins(posts: [{ comments: :guest }, :tags]) +Category.joins(articles: [{ comments: :guest }, :tags]) ``` -生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT categories.* FROM categories - INNER JOIN posts ON posts.category_id = categories.id - INNER JOIN comments ON comments.post_id = posts.id + INNER JOIN articles ON articles.category_id = categories.id + INNER JOIN comments ON comments.article_id = articles.id INNER JOIN guests ON guests.comment_id = comments.id - INNER JOIN tags ON tags.post_id = posts.id + INNER JOIN tags ON tags.article_id = articles.id ``` -### 指定用于连接数据表上的条件 +这个查询的意思是把所有包含文章的分类作为一个 `Category` 对象返回,其中这些文章都拥有访客评论并且带有标签。 -作用在连接数据表上的条件可以使用[数组](#array-conditions)和[字符串](#pure-string-conditions)指定。[Hash 形式的条件]((#hash-conditions)使用的句法有点特殊: + + +#### 为联结表指明条件 + +可以使用普通的数组和字符串条件作为关联数据表的条件。但如果想使用散列条件作为关联数据表的条件,就需要使用特殊语法了: ```ruby time_range = (Time.now.midnight - 1.day)..Time.now.midnight Client.joins(:orders).where('orders.created_at' => time_range) ``` -还有一种更简洁的句法是使用嵌套 Hash: +还有一种更干净的替代语法,即嵌套使用散列条件: ```ruby time_range = (Time.now.midnight - 1.day)..Time.now.midnight Client.joins(:orders).where(orders: { created_at: time_range }) ``` -上述查询会获取昨天下订单的所有客户对象,再次用到了 SQL `BETWEEN` 语句。 +这个查询会查找所有在昨天创建过订单的客户,在生成的 SQL 语句中同样使用了 `BETWEEN` SQL 表达式。 + + + +### `left_outer_joins` 方法 + +如果想要选择一组记录,而不管它们是否具有关联记录,可以使用 `left_outer_joins` 方法。 + +```ruby +Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id') +``` + +上面的代码会生成下面的 SQL 语句: -按需加载关联 ------------ +```sql +SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors" +LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id +``` + +这个查询的意思是返回所有作者和每位作者的帖子数,而不管这些作者是否发过帖子。 + + + +## 及早加载关联 -使用 `Model.find` 方法获取对象的关联记录时,按需加载机制会使用尽量少的查询次数。 +及早加载是一种用于加载 `Model.find` 返回对象的关联记录的机制,目的是尽可能减少查询次数。 **N + 1 查询问题** -假设有如下的代码,获取 10 个客户对象,并把客户的邮编打印出来 +假设有如下代码,查找 10 条客户记录并打印这些客户的邮编: ```ruby clients = Client.limit(10) @@ -1087,13 +1237,13 @@ clients.each do |client| end ``` -上述代码初看起来很好,但问题在于查询的总次数。上述代码总共会执行 1(获取 10 个客户记录)+ 10(分别获取 10 个客户的地址)= *11* 次查询。 +上面的代码第一眼看起来不错,但实际上存在查询总次数较高的问题。这段代码总共需要执行 1(查找 10 条客户记录)+ 10(每条客户记录都需要加载地址)= 11 次查询。 -**N + 1 查询的解决办法** +**N + 1 查询问题的解决办法** -在 Active Record 中可以进一步指定要加载的所有关联,调用 `Model.find` 方法是使用 `includes` 方法实现。使用 `includes` 后,Active Record 会使用尽可能少的查询次数加载所有指定的关联。 +Active Record 允许我们提前指明需要加载的所有关联,这是通过在调用 `Model.find` 时指明 `includes` 方法实现的。通过指明 `includes` 方法,Active Record 会使用尽可能少的查询来加载所有已指明的关联。 -我们可以使用按需加载机制加载客户的地址,把 `Client.limit(10)` 改写成: +回到之前 N + 1 查询问题的例子,我们重写其中的 `Client.limit(10)` 来使用及早加载: ```ruby clients = Client.includes(:address).limit(10) @@ -1103,7 +1253,7 @@ clients.each do |client| end ``` -和前面的 **11** 次查询不同,上述代码只会执行 **2** 次查询: +上面的代码只执行 2 次查询,而不是之前的 11 次查询: ```sql SELECT * FROM clients LIMIT 10 @@ -1111,129 +1261,213 @@ SELECT addresses.* FROM addresses WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10)) ``` -### 按需加载多个关联 + -调用 `Model.find` 方法时,使用 `includes` 方法可以一次加载任意数量的关联,加载的关联可以通过数组、Hash、嵌套 Hash 指定。 +### 及早加载多个关联 -#### 用数组指定多个关联 +通过在 `includes` 方法中使用数组、散列或嵌套散列,Active Record 允许我们在一次 `Model.find` 调用中及早加载任意数量的关联。 + + + +#### 多个关联的数组 ```ruby -Post.includes(:category, :comments) +Article.includes(:category, :comments) ``` -上述代码会加载所有文章,以及和每篇文章关联的分类和评论。 +上面的代码会加载所有文章、所有关联的分类和每篇文章的所有评论。 -#### 使用 Hash 指定嵌套关联 + + +#### 嵌套关联的散列 ```ruby -Category.includes(posts: [{ comments: :guest }, :tags]).find(1) +Category.includes(articles: [{ comments: :guest }, :tags]).find(1) ``` -上述代码会获取 ID 为 1 的分类,按需加载所有关联的文章,文章的标签和评论,以及每个评论的 `guest` 关联。 +上面的代码会查找 ID 为 1 的分类,并及早加载所有关联的文章、这些文章关联的标签和评论,以及这些评论关联的访客。 + + -### 指定用于按需加载关联上的条件 +### 为关联的及早加载指明条件 -虽然 Active Record 允许使用 `joins` 方法指定用于按需加载关联上的条件,但是推荐的做法是使用[连接数据表](#joining-tables)。 +尽管 Active Record 允许我们像 `joins` 方法那样为关联的及早加载指明条件,但推荐的方式是使用[联结](#joining-tables)。 -如果非要这么做,可以按照常规方式使用 `where` 方法。 +尽管如此,在必要时仍然可以用 `where` 方法来为关联的及早加载指明条件。 ```ruby -Post.includes(:comments).where("comments.visible" => true) +Article.includes(:comments).where(comments: { visible: true }) ``` -上述代码生成的查询中会包含 `LEFT OUTER JOIN` 子句,而 `joins` 方法生成的查询使用的是 `INNER JOIN` 子句。 +上面的代码会生成使用 `LEFT OUTER JOIN` 子句的 SQL 语句,而 `joins` 方法会成生使用 `INNER JOIN` 子句的 SQL 语句。 + +```sql +SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1) +``` + +如果上面的代码没有使用 `where` 方法,就会生成常规的一组两条查询语句。 + +NOTE: 要想像上面的代码那样使用 `where` 方法,必须在 `where` 方法中使用散列。如果想要在 `where` 方法中使用字符串 SQL 片段,就必须用 `references` 方法强制使用联结表: ```ruby -SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible = 1) +Article.includes(:comments).where("comments.visible = true").references(:comments) ``` -如果没指定 `where` 条件,上述代码会生成两个查询语句。 -如果像上面的代码一样使用 `includes`,即使所有文章都没有评论,也会加载所有文章。使用 `joins` 方法(`INNER JOIN`)时,必须满足连接条件,否则不会得到任何记录。 +通过在 `where` 方法中使用字符串 SQL 片段并使用 `references` 方法这种方式,即使一条评论都没有,所有文章仍然会被加载。而在使用 `joins` 方法(`INNER JOIN`)时,必须匹配关联条件,否则一条记录都不会返回。 -作用域 ------- + -作用域把常用的查询定义成方法,在关联对象或模型上调用。在作用域中可以使用前面介绍的所有方法,例如 `where`、`joins` 和 `includes`。所有作用域方法都会返回一个 `ActiveRecord::Relation` 对象,允许继续调用其他方法(例如另一个作用域方法)。 +## 作用域 -要想定义简单的作用域,可在类中调用 `scope` 方法,传入执行作用域时运行的代码: +作用域允许我们把常用查询定义为方法,然后通过在关联对象或模型上调用方法来引用这些查询。fotnote:[“作用域”和“作用域方法”在本文中是一个意思。——译者注]在作用域中,我们可以使用之前介绍过的所有方法,如 `where`、`join` 和 `includes` 方法。所有作用域都会返回 `ActiveRecord::Relation` 对象,这样就可以继续在这个对象上调用其他方法(如其他作用域)。 + +要想定义简单的作用域,我们可以在类中通过 `scope` 方法定义作用域,并传入调用这个作用域时执行的查询。 ```ruby -class Post < ActiveRecord::Base +class Article < ApplicationRecord scope :published, -> { where(published: true) } end ``` -上述方式和直接定义类方法的作用一样,使用哪种方式只是个人喜好: +通过上面这种方式定义作用域和通过定义类方法来定义作用域效果完全相同,至于使用哪种方式只是个人喜好问题: ```ruby -class Post < ActiveRecord::Base +class Article < ApplicationRecord def self.published where(published: true) end end ``` -作用域可以链在一起调用: +在作用域中可以链接其他作用域: ```ruby -class Post < ActiveRecord::Base +class Article < ApplicationRecord scope :published, -> { where(published: true) } scope :published_and_commented, -> { published.where("comments_count > 0") } end ``` -可以在模型类上调用 `published` 作用域: +我们可以在模型上调用 `published` 作用域: ```ruby -Post.published # => [published posts] +Article.published # => [published articles] ``` -也可以在包含 `Post` 对象的关联上调用: +或在多个 `Article` 对象组成的关联对象上调用 `published` 作用域: ```ruby category = Category.first -category.posts.published # => [published posts belonging to this category] +category.articles.published # => [published articles belonging to this category] ``` + + ### 传入参数 -作用域可接受参数: +作用域可以接受参数: ```ruby -class Post < ActiveRecord::Base +class Article < ApplicationRecord scope :created_before, ->(time) { where("created_at < ?", time) } end ``` -作用域的调用方法和类方法一样: +调用作用域和调用类方法一样: ```ruby -Post.created_before(Time.zone.now) +Article.created_before(Time.zone.now) ``` -不过这就和类方法的作用一样了。 +不过这只是复制了本该通过类方法提供给我们的的功能。 ```ruby -class Post < ActiveRecord::Base +class Article < ApplicationRecord def self.created_before(time) where("created_at < ?", time) end end ``` -如果作用域要接受参数,推荐直接使用类方法。有参数的作用域也可在关联对象上调用: +当作用域需要接受参数时,推荐改用类方法。使用类方法时,这些方法仍然可以在关联对象上访问: + +```ruby +category.articles.created_before(time) +``` + + + +### 使用条件 + +我们可以在作用域中使用条件: + +```ruby +class Article < ApplicationRecord + scope :created_before, ->(time) { where("created_at < ?", time) if time.present? } +end +``` + +和之前的例子一样,作用域的这一行为也和类方法类似。 + +```ruby +class Article < ApplicationRecord + def self.created_before(time) + where("created_at < ?", time) if time.present? + end +end +``` + +不过有一点需要特别注意:不管条件的值是 `true` 还是 `false`,作用域总是返回 `ActiveRecord::Relation` 对象,而当条件是 `false` 时,类方法返回的是 `nil`。因此,当链接带有条件的类方法时,如果任何一个条件的值是 `false`,就会引发 `NoMethodError` 异常。 + + + +### 应用默认作用域 + +要想在模型的所有查询中应用作用域,我们可以在这个模型上使用 `default_scope` 方法。 + +```ruby +class Client < ApplicationRecord + default_scope { where("removed_at IS NULL") } +end +``` + +应用默认作用域后,在这个模型上执行查询,会生成下面这样的 SQL 语句: + +```sql +SELECT * FROM clients WHERE removed_at IS NULL +``` + +如果想用默认作用域做更复杂的事情,我们也可以把它定义为类方法: ```ruby -category.posts.created_before(time) +class Client < ApplicationRecord + def self.default_scope + # 应该返回一个 ActiveRecord::Relation 对象 + end +end ``` +NOTE: 默认作用域在创建记录时同样起作用,但在更新记录时不起作用。例如: + +```ruby +class Client < ApplicationRecord + default_scope { where(active: true) } +end + +Client.new # => # +Client.unscoped.new # => # +``` + + + + ### 合并作用域 -和 `where` 方法一样,作用域也可通过 `AND` 合并查询条件: +和 `WHERE` 子句一样,我们用 `AND` 来合并作用域。 ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord scope :active, -> { where state: 'active' } scope :inactive, -> { where state: 'inactive' } end @@ -1242,24 +1476,24 @@ User.active.inactive # SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive' ``` -作用域还可以 `where` 一起使用,生成的 SQL 语句会使用 `AND` 连接所有条件。 +我们可以混合使用 `scope` 和 `where` 方法,这样最后生成的 SQL 语句会使用 `AND` 连接所有条件。 ```ruby User.active.where(state: 'finished') # SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished' ``` -如果不想让最后一个 `WHERE` 子句获得优先权,可以使用 `Relation#merge` 方法。 +如果使用 `Relation#merge` 方法,那么在发生条件冲突时总是最后的 `WHERE` 子句起作用。 ```ruby User.active.merge(User.inactive) # SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive' ``` -使用作用域时要注意,`default_scope` 会添加到作用域和 `where` 方法指定的条件之前。 +有一点需要特别注意,`default_scope` 总是在所有 `scope` 和 `where` 之前起作用。 ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord default_scope { where state: 'pending' } scope :active, -> { where state: 'active' } scope :inactive, -> { where state: 'inactive' } @@ -1275,80 +1509,149 @@ User.where(state: 'inactive') # SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive' ``` -如上所示,`default_scope` 中的条件添加到了 `active` 和 `where` 之前。 +在上面的代码中我们可以看到,在 `scope` 条件和 `where` 条件中都合并了 `default_scope` 条件。 -### 指定默认作用域 + -如果某个作用域要用在模型的所有查询中,可以在模型中使用 `default_scope` 方法指定。 +### 删除所有作用域 + +在需要时,可以使用 `unscoped` 方法删除作用域。如果在模型中定义了默认作用域,但在某次查询中又不想应用默认作用域,这时就可以使用 `unscoped` 方法。 ```ruby -class Client < ActiveRecord::Base - default_scope { where("removed_at IS NULL") } -end +Client.unscoped.load ``` -执行查询时使用的 SQL 语句如下: +`unscoped` 方法会删除所有作用域,仅在数据表上执行常规查询。 -```sql -SELECT * FROM clients WHERE removed_at IS NULL +```ruby +Client.unscoped.all +# SELECT "clients".* FROM "clients" + +Client.where(published: false).unscoped.all +# SELECT "clients".* FROM "clients" ``` -如果默认作用域中的条件比较复杂,可以使用类方法的形式定义: +`unscoped` 方法也接受块作为参数。 ```ruby -class Client < ActiveRecord::Base - def self.default_scope - # Should return an ActiveRecord::Relation. - end +Client.unscoped { + Client.created_before(Time.zone.now) +} +``` + + + +## 动态查找方法 + +Active Record 为数据表中的每个字段(也称为属性)都提供了查找方法(也就是动态查找方法)。例如,对于 `Client` 模型的 `first_name` 字段,Active Record 会自动生成 `find_by_first_name` 查找方法。对于 `Client` 模型的 `locked` 字段,Active Record 会自动生成 `find_by_locked` 查找方法。 + +在调用动态查找方法时可以在末尾加上感叹号(`!`),例如 `Client.find_by_name!("Ryan")`,这样如果动态查找方法没有返回任何记录,就会抛出 `ActiveRecord::RecordNotFound` 异常。 + +如果想同时查询 `first_name` 和 `locked` 字段,可以在动态查找方法中用 `and` 把这两个字段连起来,例如 `Client.find_by_first_name_and_locked("Ryan", true)`。 + + + +## `enum` 宏 + +`enum` 宏把整数字段映射为一组可能的值。 + +```ruby +class Book < ApplicationRecord + enum availability: [:available, :unavailable] end ``` -### 删除所有作用域 +上面的代码会自动创建用于查询模型的对应作用域,同时会添加用于转换状态和查询当前状态的方法。 + +```ruby +# 下面的示例只查询可用的图书 +Book.available +# 或 +Book.where(availability: :available) + +book = Book.new(availability: :available) +book.available? # => true +book.unavailable! # => true +book.available? # => false +``` + +请访问 [Rails API 文档](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html),查看 `enum` 宏的完整文档。 -如果基于某些原因想删除作用域,可以使用 `unscoped` 方法。如果模型中定义了 `default_scope`,而在这个作用域中不需要使用,就可以使用 `unscoped` 方法。 + + +## 理解方法链 + +Active Record 实现[方法链](http://en.wikipedia.org/wiki/Method_chaining)的方式既简单又直接,有了方法链我们就可以同时使用多个 Active Record 方法。 + +当之前的方法调用返回 `ActiveRecord::Relation` 对象时,例如 `all`、`where` 和 `joins` 方法,我们就可以在语句中把方法连接起来。返回单个对象的方法(请参阅 [检索单个对象](#retrieving-a-single-object))必须位于语句的末尾。 + +下面给出了一些例子。本文无法涵盖所有的可能性,这里给出的只是很少的一部分例子。在调用 Active Record 方法时,查询不会立即生成并发送到数据库,这些操作只有在实际需要数据时才会执行。下面的每个例子都会生成一次查询。 + + + +### 从多个数据表中检索过滤后的数据 ```ruby -Client.unscoped.load +Person + .select('people.id, people.name, comments.text') + .joins(:comments) + .where('comments.created_at > ?', 1.week.ago) +``` + +上面的代码会生成下面的 SQL 语句: + +```sql +SELECT people.id, people.name, comments.text +FROM people +INNER JOIN comments + ON comments.person_id = people.id +WHERE comments.created_at > '2015-01-01' ``` -`unscoped` 方法会删除所有作用域,在数据表中执行常规查询。 + -注意,不能在作用域后链式调用 `unscoped`,这时可以使用代码块形式的 `unscoped` 方法: +### 从多个数据表中检索特定的数据 ```ruby -Client.unscoped { - Client.created_before(Time.zone.now) -} +Person + .select('people.id, people.name, companies.name') + .joins(:company) + .find_by('people.name' => 'John') # this should be the last ``` -动态查询方法 ------------ +上面的代码会生成下面的 SQL 语句: -Active Record 为数据表中的每个字段都提供了一个查询方法。例如,在 `Client` 模型中有个 `first_name` 字段,那么 Active Record 就会生成 `find_by_first_name` 方法。如果在 `Client` 模型中有个 `locked` 字段,就有一个 `find_by_locked` 方法。 +```sql +SELECT people.id, people.name, companies.name +FROM people +INNER JOIN companies + ON companies.person_id = people.id +WHERE people.name = 'John' +LIMIT 1 +``` -在这些动态生成的查询方法后,可以加上感叹号(`!`),例如 `Client.find_by_name!("Ryan")`。此时,如果找不到记录就会抛出 `ActiveRecord::RecordNotFound` 异常。 +NOTE: 请注意,如果查询匹配多条记录,`find_by` 方法会取回第一条记录并忽略其他记录(如上面的 SQL 语句中的 `LIMIT 1`)。 -如果想同时查询 `first_name` 和 `locked` 字段,可以用 `and` 把两个字段连接起来,获得所需的查询方法,例如 `Client.find_by_first_name_and_locked("Ryan", true)`。 + -查找或构建新对象 --------------- +## 查找或创建新对象 -NOTE: 某些动态查询方法在 Rails 4.0 中已经启用,会在 Rails 4.1 中删除。推荐的做法是使用 Active Record 作用域。废弃的方法可以在这个 gem 中查看:。 +我们经常需要查找记录并在找不到记录时创建记录,这时我们可以使用 `find_or_create_by` 和 `find_or_create_by!` 方法。 -我们经常需要在查询不到记录时创建一个新记录。这种需求可以使用 `find_or_create_by` 或 `find_or_create_by!` 方法实现。 + -### `find_or_create_by` +### `find_or_create_by` 方法 -`find_or_create_by` 方法首先检查指定属性对应的记录是否存在,如果不存在就调用 `create` 方法。我们来看一个例子。 +`find_or_create_by` 方法检查具有指定属性的记录是否存在。如果记录不存在,就调用 `create` 方法创建记录。让我们看一个例子。 -假设你想查找一个名为“Andy”的客户,如果这个客户不存在就新建。这个需求可以使用下面的代码完成: +假设我们在查找名为“Andy”的用户记录,但是没找到,因此要创建这条记录。这时我们可以执行下面的代码: ```ruby Client.find_or_create_by(first_name: 'Andy') # => # ``` -上述方法生成的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1 @@ -1357,19 +1660,19 @@ INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) V COMMIT ``` -`find_or_create_by` 方法返回现有的记录或者新建的记录。在上面的例子中,名为“Andy”的客户不存在,所以会新建一个记录,然后将其返回。 +`find_or_create_by` 方法会返回已存在的记录或新建的记录。在本例中,名为“Andy”的客户记录并不存在,因此会创建并返回这条记录。 -新纪录可能没有存入数据库,这取决于是否能通过数据验证(就像 `create` 方法一样)。 +新建记录不一定会保存到数据库,是否保存取决于验证是否通过(就像 `create` 方法那样)。 -假设创建新记录时,要把 `locked` 属性设为 `false`,但不想在查询中设置。例如,我们要查询一个名为“Andy”的客户,如果这个客户不存在就新建一个,而且 `locked` 属性为 `false`。 +假设我们想在新建记录时把 `locked` 字段设置为 `false`,但又不想在查询中进行设置。例如,我们想查找名为“Andy”的客户记录,但这条记录并不存在,因此要创建这条记录并把 `locked` 字段设置为 `false`。 -这种需求有两种实现方法。第一种,使用 `create_with` 方法: +要完成这一操作有两种方式。第一种方式是使用 `create_with` 方法: ```ruby Client.create_with(locked: false).find_or_create_by(first_name: 'Andy') ``` -第二种,使用代码块: +第二种方式是使用块: ```ruby Client.find_or_create_by(first_name: 'Andy') do |c| @@ -1377,30 +1680,34 @@ Client.find_or_create_by(first_name: 'Andy') do |c| end ``` -代码块中的代码只会在创建客户之后执行。再次运行这段代码时,会忽略代码块中的代码。 +只有在创建客户记录时才会执行该块。第二次运行这段代码时(此时客户记录已创建),块会被忽略。 -### `find_or_create_by!` + -还可使用 `find_or_create_by!` 方法,如果新纪录不合法,会抛出异常。本文不涉及数据验证,假设已经在 `Client` 模型中定义了下面的验证: +### `find_or_create_by!` 方法 + +我们也可以使用 `find_or_create_by!` 方法,这样如果新建记录是无效的就会抛出异常。本文不涉及数据验证,不过这里我们暂且假设已经在 `Client` 模型中添加了下面的数据验证: ```ruby validates :orders_count, presence: true ``` -如果创建新 `Client` 对象时没有指定 `orders_count` 属性的值,这个对象就是不合法的,会抛出以下异常: +如果我们尝试新建客户记录,但忘了传递 `orders_count` 字段的值,新建记录就是无效的,因而会抛出下面的异常: ```ruby Client.find_or_create_by!(first_name: 'Andy') # => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank ``` -### `find_or_initialize_by` + + +### `find_or_initialize_by` 方法 -`find_or_initialize_by` 方法和 `find_or_create_by` 的作用差不多,但不调用 `create` 方法,而是 `new` 方法。也就是说新建的模型实例在内存中,没有存入数据库。继续使用前面的例子,现在我们要查询的客户名为“Nick”: +`find_or_initialize_by` 方法的工作原理和 `find_or_create_by` 方法类似,区别之处在于前者调用的是 `new` 方法而不是 `create` 方法。这意味着新建模型实例在内存中创建,但没有保存到数据库。下面继续使用介绍 `find_or_create_by` 方法时使用的例子,我们现在想查找名为“Nick”的客户记录: ```ruby nick = Client.find_or_initialize_by(first_name: 'Nick') -# => +# => # nick.persisted? # => false @@ -1409,43 +1716,57 @@ nick.new_record? # => true ``` -因为对象不会存入数据库,上述代码生成的 SQL 语句如下: +出现上面的执行结果是因为 `nick` 对象还没有保存到数据库。在上面的代码中,`find_or_initialize_by` 方法会生成下面的 SQL 语句: ```sql SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1 ``` -如果想把对象存入数据库,调用 `save` 方法即可: +要想把 `nick` 对象保存到数据库,只需调用 `save` 方法: ```ruby nick.save # => true ``` -使用 SQL 语句查询 ----------------- + -如果想使用 SQL 语句查询数据表中的记录,可以使用 `find_by_sql` 方法。就算只找到一个记录,`find_by_sql` 方法也会返回一个由记录组成的数组。例如,可以运行下面的查询: +## 使用 SQL 语句进行查找 + +要想直接使用 SQL 语句在数据表中查找记录,可以使用 `find_by_sql` 方法。`find_by_sql` 方法总是返回对象的数组,即使底层查询只返回了一条记录也是如此。例如,我们可以执行下面的查询: ```ruby Client.find_by_sql("SELECT * FROM clients INNER JOIN orders ON clients.id = orders.client_id ORDER BY clients.created_at desc") +# => [ +# #, +# #, +# ... +# ] ``` -`find_by_sql` 方法提供了一种定制查询的简单方式。 +`find_by_sql` 方法提供了对数据库进行定制查询并取回实例化对象的简单方式。 + + -### `select_all` +### `select_all` 方法 -`find_by_sql` 方法有一个近亲,名为 `connection#select_all`。和 `find_by_sql` 一样,`select_all` 方法会使用 SQL 语句查询数据库,获取记录,但不会初始化对象。`select_all` 返回的结果是一个由 Hash 组成的数组,每个 Hash 表示一个记录。 +`find_by_sql` 方法有一个名为 `connection#select_all` 的近亲。和 `find_by_sql` 方法一样,`select_all` 方法也会使用定制的 SQL 语句从数据库中检索对象,区别在于 `select_all` 方法不会对这些对象进行实例化,而是返回一个散列构成的数组,其中每个散列表示一条记录。 ```ruby -Client.connection.select_all("SELECT * FROM clients WHERE id = '1'") +Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'") +# => [ +# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, +# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"} +# ] ``` -### `pluck` + + +### `pluck` 方法 -`pluck` 方法可以在模型对应的数据表中查询一个或多个字段,其参数是一组字段名,返回结果是由各字段的值组成的数组。 +`pluck` 方法用于在模型对应的底层数据表中查询单个或多个字段。它接受字段名的列表作为参数,并返回这些字段的值的数组,数组中的每个值都具有对应的数据类型。 ```ruby Client.where(active: true).pluck(:id) @@ -1461,28 +1782,28 @@ Client.pluck(:id, :name) # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] ``` -如下的代码: +使用 `pluck` 方法,我们可以把下面的代码: ```ruby Client.select(:id).map { |c| c.id } -# or +# 或 Client.select(:id).map(&:id) -# or +# 或 Client.select(:id, :name).map { |c| [c.id, c.name] } ``` -可用 `pluck` 方法实现: +替换为: ```ruby Client.pluck(:id) -# or +# 或 Client.pluck(:id, :name) ``` -和 `select` 方法不一样,`pluck` 直接把查询结果转换成 Ruby 数组,不生成 Active Record 对象,可以提升大型查询或常用查询的执行效率。但 `pluck` 方法不会使用重新定义的属性方法处理查询结果。例如: +和 `select` 方法不同,`pluck` 方法把数据库查询结果直接转换为 Ruby 数组,而不是构建 Active Record 对象。这意味着对于大型查询或常用查询,`pluck` 方法的性能更好。不过对于 `pluck` 方法,模型方法重载是不可用的。例如: ```ruby -class Client < ActiveRecord::Base +class Client < ApplicationRecord def name "I am #{super}" end @@ -1495,7 +1816,7 @@ Client.pluck(:name) # => ["David", "Jeremy", "Jose"] ``` -而且,与 `select` 和其他 `Relation` 作用域不同的是,`pluck` 方法会直接执行查询,因此后面不能和其他作用域链在一起,但是可以链接到已经执行的作用域之后: +此外,和 `select` 方法及其他 `Relation` 作用域不同,`pluck` 方法会触发即时查询,因此在 `pluck` 方法之前可以链接作用域,但在 `pluck` 方法之后不能链接作用域: ```ruby Client.pluck(:name).limit(1) @@ -1505,9 +1826,11 @@ Client.limit(1).pluck(:name) # => ["David"] ``` -### `ids` + -`ids` 方法可以直接获取数据表的主键。 +### `ids` 方法 + +使用 `ids` 方法可以获得关联的所有 ID,也就是数据表的主键。 ```ruby Person.ids @@ -1515,7 +1838,7 @@ Person.ids ``` ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord self.primary_key = "person_id" end @@ -1523,205 +1846,241 @@ Person.ids # SELECT person_id FROM people ``` -检查对象是否存在 --------------- + + +## 检查对象是否存在 -如果只想检查对象是否存在,可以使用 `exists?` 方法。这个方法使用的数据库查询和 `find` 方法一样,但不会返回对象或对象集合,而是返回 `true` 或 `false`。 +要想检查对象是否存在,可以使用 `exists?` 方法。`exists?` 方法查询数据库的工作原理和 `find` 方法相同,但是 `find` 方法返回的是对象或对象集合,而 `exists?` 方法返回的是 `true` 或 `false`。 ```ruby Client.exists?(1) ``` -`exists?` 方法可以接受多个值,但只要其中一个记录存在,就会返回 `true`。 +`exists?` 方法也接受多个值作为参数,并且只要有一条对应记录存在就会返回 `true`。 ```ruby Client.exists?(id: [1,2,3]) -# or +# 或 Client.exists?(name: ['John', 'Sergei']) ``` -在模型或关系上调用 `exists?` 方法时,可以不指定任何参数。 +我们还可以在模型或关联上调用 `exists?` 方法,这时不需要任何参数。 ```ruby Client.where(first_name: 'Ryan').exists? ``` -在上述代码中,只要有一个客户的 `first_name` 字段值为 `'Ryan'`,就会返回 `true`,否则返回 `false`。 +只要存在一条名为“Ryan”的客户记录,上面的代码就会返回 `true`,否则返回 `false`。 ```ruby Client.exists? ``` -在上述代码中,如果 `clients` 表是空的,会返回 `false`,否则返回 `true`。 +如果 `clients` 数据表是空的,上面的代码返回 `false`,否则返回 `true`。 -在模型或关系中检查存在性时还可使用 `any?` 和 `many?` 方法。 +我们还可以在模型或关联上调用 `any?` 和 `many?` 方法来检查对象是否存在。 ```ruby -# via a model -Post.any? -Post.many? +# 通过模型 +Article.any? +Article.many? -# via a named scope -Post.recent.any? -Post.recent.many? +# 通过指定的作用域 +Article.recent.any? +Article.recent.many? -# via a relation -Post.where(published: true).any? -Post.where(published: true).many? +# 通过关系 +Article.where(published: true).any? +Article.where(published: true).many? -# via an association -Post.first.categories.any? -Post.first.categories.many? +# 通过关联 +Article.first.categories.any? +Article.first.categories.many? ``` -计算 ----- + -这里先以 `count` 方法为例,所有的选项都可在后面各方法中使用。 +## 计算 -所有计算型方法都可直接在模型上调用: +在本节的前言中我们以 `count` 方法为例,例子中提到的所有选项对本节的各小节都适用。 + +所有用于计算的方法都可以直接在模型上调用: ```ruby Client.count # SELECT count(*) AS count_all FROM clients ``` -或者在关系上调用: +或者在关联上调用: ```ruby Client.where(first_name: 'Ryan').count # SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan') ``` -执行复杂计算时还可使用各种查询方法: +我们还可以在关联上执行各种查找方法来执行复杂的计算: ```ruby Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count ``` -上述代码执行的 SQL 语句如下: +上面的代码会生成下面的 SQL 语句: ```sql SELECT count(DISTINCT clients.id) AS count_all FROM clients - LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE + LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE (clients.first_name = 'Ryan' AND orders.status = 'received') ``` -### 计数 + + +### `count` 方法 -如果想知道模型对应的数据表中有多少条记录,可以使用 `Client.count` 方法。如果想更精确的计算设定了 `age` 字段的记录数,可以使用 `Client.count(:age)`。 +要想知道模型对应的数据表中有多少条记录,可以使用 `Client.count` 方法,这个方法的返回值就是记录条数。如果想要知道特定记录的条数,例如具有 `age` 字段值的所有客户记录的条数,可以使用 `Client.count(:age)`。 -`count` 方法可用的选项[如前所述](#calculations)。 +关于 `count` 方法的选项的更多介绍,请参阅 [计算](#calculations)。 -### 平均值 + -如果想查看某个字段的平均值,可以使用 `average` 方法。用法如下: +### `average` 方法 + +要想知道数据表中某个字段的平均值,可以在数据表对应的类上调用 `average` 方法。例如: ```ruby Client.average("orders_count") ``` -这个方法会返回指定字段的平均值,得到的有可能是浮点数,例如 3.14159265。 +上面的代码会返回表示 `orders_count` 字段平均值的数字(可能是浮点数,如 3.14159265)。 + +关于 `average` 方法的选项的更多介绍,请参阅 [计算](#calculations)。 -`average` 方法可用的选项[如前所述](#calculations)。 + -### 最小值 +### `minimum` 方法 -如果想查看某个字段的最小值,可以使用 `minimum` 方法。用法如下: +要想查找数据表中某个字段的最小值,可以在数据表对应的类上调用 `minimum` 方法。例如: ```ruby Client.minimum("age") ``` -`minimum` 方法可用的选项[如前所述](#calculations)。 +关于 `minimum` 方法的选项的更多介绍,请参阅 [计算](#calculations)。 -### 最大值 + -如果想查看某个字段的最大值,可以使用 `maximum` 方法。用法如下: +### `maximum` 方法 + +要想查找数据表中某个字段的最大值,可以在数据表对应的类上调用 `maximum` 方法。例如: ```ruby Client.maximum("age") ``` -`maximum` 方法可用的选项[如前所述](#calculations)。 +关于 `maximum` 方法的选项的更多介绍,请参阅 [计算](#calculations)。 + + -### 求和 +### `sum` 方法 -如果想查看所有记录中某个字段的总值,可以使用 `sum` 方法。用法如下: +要想知道数据表中某个字段的所有字段值之和,可以在数据表对应的类上调用 `sum` 方法。例如: ```ruby Client.sum("orders_count") ``` -`sum` 方法可用的选项[如前所述](#calculations)。 +关于 `sum` 方法的选项的更多介绍,请参阅 [计算](#calculations)。 -执行 EXPLAIN 命令 ----------------- + -可以在关系执行的查询中执行 EXPLAIN 命令。例如: +## 执行 `EXPLAIN` 命令 + +我们可以在关联触发的查询上执行 `EXPLAIN` 命令。例如: ```ruby -User.where(id: 1).joins(:posts).explain +User.where(id: 1).joins(:articles).explain ``` -在 MySQL 中得到的输出如下: +对于 MySQL 和 MariaDB 数据库后端,上面的代码会产生下面的输出结果: ``` -EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ -| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | -| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ +EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 ++----+-------------+----------+-------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+----------+-------+---------------+ +| 1 | SIMPLE | users | const | PRIMARY | +| 1 | SIMPLE | articles | ALL | NULL | ++----+-------------+----------+-------+---------------+ ++---------+---------+-------+------+-------------+ +| key | key_len | ref | rows | Extra | ++---------+---------+-------+------+-------------+ +| PRIMARY | 4 | const | 1 | | +| NULL | NULL | NULL | 1 | Using where | ++---------+---------+-------+------+-------------+ + 2 rows in set (0.00 sec) ``` -Active Record 会按照所用数据库 shell 的方式输出结果。所以,相同的查询在 PostgreSQL 中得到的输出如下: +Active Record 会模拟对应数据库的 shell 来打印输出结果。因此对于 PostgreSQL 数据库后端,同样的代码会产生下面的输出结果: ``` -EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."id" = 1 +EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 QUERY PLAN ------------------------------------------------------------------------------ Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - Join Filter: (posts.user_id = users.id) + Join Filter: (articles.user_id = users.id) -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) Index Cond: (id = 1) - -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - Filter: (posts.user_id = 1) + -> Seq Scan on articles (cost=0.00..28.88 rows=8 width=4) + Filter: (articles.user_id = 1) (6 rows) ``` -按需加载会触发多次查询,而且有些查询要用到之前查询的结果。鉴于此,`explain` 方法会真正执行查询,然后询问查询计划。例如: +及早加载在底层可能会触发多次查询,有的查询可能需要使用之前查询的结果。因此,`explain` 方法实际上先执行了查询,然后询问查询计划。例如: ```ruby -User.where(id: 1).includes(:posts).explain +User.where(id: 1).includes(:articles).explain ``` -在 MySQL 中得到的输出如下: +对于 MySQL 和 MariaDB 数据库后端,上面的代码会产生下面的输出结果: ``` EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ -| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | -+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ ++----+-------------+-------+-------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+-------+-------+---------------+ +| 1 | SIMPLE | users | const | PRIMARY | ++----+-------------+-------+-------+---------------+ ++---------+---------+-------+------+-------+ +| key | key_len | ref | rows | Extra | ++---------+---------+-------+------+-------+ +| PRIMARY | 4 | const | 1 | | ++---------+---------+-------+------+-------+ + 1 row in set (0.00 sec) -EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1) -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ -| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ -| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | -+----+-------------+-------+------+---------------+------+---------+------+------+-------------+ +EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN (1) ++----+-------------+----------+------+---------------+ +| id | select_type | table | type | possible_keys | ++----+-------------+----------+------+---------------+ +| 1 | SIMPLE | articles | ALL | NULL | ++----+-------------+----------+------+---------------+ ++------+---------+------+------+-------------+ +| key | key_len | ref | rows | Extra | ++------+---------+------+------+-------------+ +| NULL | NULL | NULL | 1 | Using where | ++------+---------+------+------+-------------+ + + 1 row in set (0.00 sec) ``` -### 解读 EXPLAIN 命令的输出结果 + + +### 对 `EXPLAIN` 命令输出结果的解释 -解读 EXPLAIN 命令的输出结果不在本文的范畴之内。下面列出的链接可以帮助你进一步了解相关知识: +对 `EXPLAIN` 命令输出结果的解释超出了本文的范畴。下面提供了一些有用链接: -* SQLite3: [EXPLAIN QUERY PLAN](http://www.sqlite.org/eqp.html) -* MySQL: [EXPLAIN 的输出格式](http://dev.mysql.com/doc/refman/5.6/en/explain-output.html) -* PostgreSQL: [使用 EXPLAIN](http://www.postgresql.org/docs/current/static/using-explain.html) +* SQLite3:[对查询计划的解释](http://www.sqlite.org/eqp.html) +* MySQL:[EXPLAIN 输出格式](http://dev.mysql.com/doc/refman/5.7/en/explain-output.html) +* MariaDB:[EXPLAIN](https://mariadb.com/kb/en/mariadb/explain/) +* PostgreSQL:[使用 EXPLAIN](http://www.postgresql.org/docs/current/static/using-explain.html) diff --git a/source/zh-CN/active_record_validations.md b/source/zh-CN/active_record_validations.md index af92116..6aa2335 100644 --- a/source/zh-CN/active_record_validations.md +++ b/source/zh-CN/active_record_validations.md @@ -1,23 +1,23 @@ -Active Record 数据验证 -===================== +# Active Record 数据验证 -本文介绍如何使用 Active Record 提供的数据验证功能在数据存入数据库之前验证对象的状态。 +本文介绍如何使用 Active Record 提供的数据验证功能,在数据存入数据库之前验证对象的状态。 -读完本文,你将学到: +读完本文后,您将学到: -* 如何使用 Active Record 内建的数据验证帮助方法; -* 如果编写自定义的数据验证方法; -* 如何处理验证时产生的错误消息; +* 如何使用 Active Record 内置的数据验证辅助方法; +* 如果自定义数据验证方法; +* 如何处理验证过程产生的错误消息。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -数据验证简介 ----------- + -下面演示一个非常简单的数据验证: +## 数据验证概览 + +下面是一个非常简单的数据验证: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -25,35 +25,39 @@ Person.create(name: "John Doe").valid? # => true Person.create(name: nil).valid? # => false ``` -如上所示,如果 `Person` 没有 `name` 属性,验证就会将其视为不合法对象。创建的第二个 `Person` 对象不会存入数据库。 +可以看出,如果 `Person` 没有 `name` 属性,验证就会将其视为无效对象。第二个 `Person` 对象不会存入数据库。 + +在深入探讨之前,我们先来了解数据验证在应用中的作用。 -在深入探讨之前,我们先来介绍数据验证在整个程序中的作用。 + ### 为什么要做数据验证? -数据验证能确保只有合法的数据才会存入数据库。例如,程序可能需要用户提供一个合法的 Email 地址和邮寄地址。在模型中做验证是最有保障的,只有通过验证的数据才能存入数据库。数据验证和使用的数据库种类无关,终端用户也无法跳过,而且容易测试和维护。在 Rails 中做数据验证很简单,Rails 内置了很多帮助方法,能满足常规的需求,而且还可以编写自定义的验证方法。 +数据验证确保只有有效的数据才能存入数据库。例如,应用可能需要用户提供一个有效的电子邮件地址和邮寄地址。在模型中做验证是最有保障的,只有通过验证的数据才能存入数据库。数据验证和使用的数据库种类无关,终端用户也无法跳过,而且容易测试和维护。在 Rails 中做数据验证很简单,Rails 内置了很多辅助方法,能满足常规的需求,而且还可以编写自定义的验证方法。 + +在数据存入数据库之前,也有几种验证数据的方法,包括数据库原生的约束、客户端验证和控制器层验证。下面列出这几种验证方法的优缺点: -数据存入数据库之前的验证方法还有其他几种,包括数据库内建的约束,客户端验证和控制器层验证。下面列出了这几种验证方法的优缺点: +* 数据库约束和存储过程无法兼容多种数据库,而且难以测试和维护。然而,如果其他应用也要使用这个数据库,最好在数据库层做些约束。此外,数据库层的某些验证(例如在使用量很高的表中做唯一性验证)通过其他方式实现起来有点困难。 +* 客户端验证很有用,但单独使用时可靠性不高。如果使用 JavaScript 实现,用户在浏览器中禁用 JavaScript 后很容易跳过验证。然而,客户端验证和其他验证方式相结合,可以为用户提供实时反馈。 +* 控制器层验证很诱人,但一般都不灵便,难以测试和维护。只要可能,就要保证控制器的代码简洁,这样才有利于长远发展。 -* 数据库约束和“存储过程”无法兼容多种数据库,而且测试和维护较为困难。不过,如果其他程序也要使用这个数据库,最好在数据库层做些约束。数据库层的某些验证(例如在使用量很高的数据表中做唯一性验证)通过其他方式实现起来有点困难。 -* 客户端验证很有用,但单独使用时可靠性不高。如果使用 JavaScript 实现,用户在浏览器中禁用 JavaScript 后很容易跳过验证。客户端验证和其他验证方式结合使用,可以为用户提供实时反馈。 -* 控制器层验证很诱人,但一般都不灵便,难以测试和维护。只要可能,就要保证控制器的代码简洁性,这样才有利于长远发展。 +你可以根据实际需求选择使用合适的验证方式。Rails 团队认为,模型层数据验证最具普适性。 -你可以根据实际的需求选择使用哪种验证方式。Rails 团队认为,模型层数据验证最具普适性。 + -### 什么时候做数据验证? +### 数据在何时验证? -在 Active Record 中对象有两种状态:一种在数据库中有对应的记录,一种没有。新建的对象(例如,使用 `new` 方法)还不属于数据库。在对象上调用 `save` 方法后,才会把对象存入相应的数据表。Active Record 使用实例方法 `new_record?` 判断对象是否已经存入数据库。加入有下面这个简单的 Active Record 类: +Active Record 对象分为两种:一种在数据库中有对应的记录,一种没有。新建的对象(例如,使用 `new` 方法)还不属于数据库。在对象上调用 `save` 方法后,才会把对象存入相应的数据库表。Active Record 使用实例方法 `new_record?` 判断对象是否已经存入数据库。假如有下面这个简单的 Active Record 类: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord end ``` 我们可以在 `rails console` 中看一下到底怎么回事: -```ruby -$ rails console +```irb +$ bin/rails console >> p = Person.new(name: "John Doe") => # >> p.new_record? @@ -64,47 +68,54 @@ $ rails console => false ``` -新建并保存记录会在数据库中执行 SQL `INSERT` 操作。更新现有的记录会在数据库上执行 SQL `UPDATE` 操作。一般情况下,数据验证发生在这些 SQL 操作执行之前。如果验证失败,对象会被标记为不合法,Active Record 不会向数据库发送 `INSERT` 或 `UPDATE` 指令。这样就可以避免把不合法的数据存入数据库。你可以选择在对象创建、保存或更新时执行哪些数据验证。 +新建并保存记录会在数据库中执行 SQL `INSERT` 操作。更新现有的记录会在数据库中执行 SQL `UPDATE` 操作。一般情况下,数据验证发生在这些 SQL 操作执行之前。如果验证失败,对象会被标记为无效,Active Record 不会向数据库发送 `INSERT` 或 `UPDATE` 指令。这样就可以避免把无效的数据存入数据库。你可以选择在对象创建、保存或更新时执行特定的数据验证。 + +WARNING: 修改数据库中对象的状态有多种方式。有些方法会触发数据验证,有些则不会。所以,如果不小心处理,还是有可能把无效的数据存入数据库。 -WARNING: 修改数据库中对象的状态有很多方法。有些方法会做数据验证,有些则不会。所以,如果不小心处理,还是有可能把不合法的数据存入数据库。 -下列方法会做数据验证,如果验证失败就不会把对象存入数据库: +下列方法会触发数据验证,如果验证失败就不把对象存入数据库: -* `create` -* `create!` -* `save` -* `save!` -* `update` -* `update!` +* `create` +* `create!` +* `save` +* `save!` +* `update` +* `update!` 爆炸方法(例如 `save!`)会在验证失败后抛出异常。验证失败后,非爆炸方法不会抛出异常,`save` 和 `update` 返回 `false`,`create` 返回对象本身。 + + ### 跳过验证 -下列方法会跳过验证,不过验证是否通过都会把对象存入数据库,使用时要特别留意。 +下列方法会跳过验证,不管验证是否通过都会把对象存入数据库,使用时要特别留意。 + +* `decrement!` +* `decrement_counter` +* `increment!` +* `increment_counter` +* `toggle!` +* `touch` +* `update_all` +* `update_attribute` +* `update_column` +* `update_columns` +* `update_counters` -* `decrement!` -* `decrement_counter` -* `increment!` -* `increment_counter` -* `toggle!` -* `touch` -* `update_all` -* `update_attribute` -* `update_column` -* `update_columns` -* `update_counters` +注意,使用 `save` 时如果传入 `validate: false` 参数,也会跳过验证。使用时要特别留意。 -注意,使用 `save` 时如果传入 `validate: false`,也会跳过验证。使用时要特别留意。 +* `save(validate: false)` -* `save(validate: false)` + ### `valid?` 和 `invalid?` -Rails 使用 `valid?` 方法检查对象是否合法。`valid?` 方法会触发数据验证,如果对象上没有错误,就返回 `true`,否则返回 `false`。前面我们已经用过了: +Rails 在保存 Active Record 对象之前验证数据。如果验证过程产生错误,Rails 不会保存对象。 + +你还可以自己执行数据验证。`valid?` 方法会触发数据验证,如果对象上没有错误,返回 `true`,否则返回 `false`。前面我们已经用过了: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -112,12 +123,12 @@ Person.create(name: "John Doe").valid? # => true Person.create(name: nil).valid? # => false ``` -Active Record 验证结束后,所有发现的错误都可以通过实例方法 `errors.messages` 获取,该方法返回一个错误集合。如果数据验证后,这个集合为空,则说明对象是合法的。 +Active Record 执行验证后,所有发现的错误都可以通过实例方法 `errors.messages` 获取。该方法返回一个错误集合。如果数据验证后,这个集合为空,说明对象是有效的。 -注意,使用 `new` 方法初始化对象时,即使不合法也不会报错,因为这时还没做数据验证。 +注意,使用 `new` 方法初始化对象时,即使无效也不会报错,因为只有保存对象时才会验证数据,例如调用 `create` 或 `save` 方法。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -146,16 +157,18 @@ end # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank ``` -`invalid?` 是 `valid?` 的逆测试,会触发数据验证,如果找到错误就返回 `true`,否则返回 `false`。 +`invalid?` 的作用与 `valid?` 相反,它会触发数据验证,如果找到错误就返回 `true`,否则返回 `false`。 + + ### `errors[]` -要检查对象的某个属性是否合法,可以使用 `errors[:attribute]`。`errors[:attribute]` 中包含 `:attribute` 的所有错误。如果某个属性没有错误,就会返回空数组。 +若想检查对象的某个属性是否有效,可以使用 `errors[:attribute]`。`errors[:attribute]` 中包含与 `:attribute` 有关的所有错误。如果某个属性没有错误,就会返回空数组。 -这个方法只在数据验证之后才能使用,因为它只是用来收集错误信息的,并不会触发验证。而且,和前面介绍的 `ActiveRecord::Base#invalid?` 方法不一样,因为 `errors[:attribute]` 不会验证整个对象,值检查对象的某个属性是否出错。 +这个方法只在数据验证之后才能使用,因为它只是用来收集错误信息的,并不会触发验证。与前面介绍的 `ActiveRecord::Base#invalid?` 方法不一样,`errors[:attribute]` 不会验证整个对象,只检查对象的某个属性是否有错。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true end @@ -163,131 +176,184 @@ end >> Person.create.errors[:name].any? # => true ``` -我们会在“[处理验证错误](#working-with-validation-errors)”一节详细介绍验证错误。现在,我们来看一下 Rails 默认提供的数据验证帮助方法。 +我们会在 [处理验证错误](#working-with-validation-errors)详细说明验证错误。 + + + +### `errors.details` + +若想查看是哪个验证导致属性无效的,可以使用 `errors.details[:attribute]`。它的返回值是一个由散列组成的数组,`:error` 键的值是一个符号,指明出错的数据验证。 + +```ruby +class Person < ApplicationRecord + validates :name, presence: true +end + +>> person = Person.new +>> person.valid? +>> person.errors.details[:name] # => [{error: :blank}] +``` + +[处理验证错误](#working-with-validation-errors)会说明如何在自定义的数据验证中使用 `details`。 + + -数据验证帮助方法 --------------- +## 数据验证辅助方法 -Active Record 预先定义了很多数据验证帮助方法,可以直接在模型类定义中使用。这些帮助方法提供了常用的验证规则。每次验证失败后,都会向对象的 `errors` 集合中添加一个消息,这些消息和所验证的属性是关联的。 +Active Record 预先定义了很多数据验证辅助方法,可以直接在模型类定义中使用。这些辅助方法提供了常用的验证规则。每次验证失败后,都会向对象的 `errors` 集合中添加一个消息,而且这些消息与所验证的属性是关联的。 -每个帮助方法都可以接受任意数量的属性名,所以一行代码就能在多个属性上做同一种验证。 +每个辅助方法都可以接受任意个属性名,所以一行代码就能在多个属性上做同一种验证。 -所有的帮助方法都可指定 `:on` 和 `:message` 选项,指定何时做验证,以及验证失败后向 `errors` 集合添加什么消息。`:on` 选项的可选值是 `:create` 和 `:update`。每个帮助函数都有默认的错误消息,如果没有通过 `:message` 选项指定,则使用默认值。下面分别介绍各帮助方法。 +所有辅助方法都可指定 `:on` 和 `:message` 选项,分别指定何时做验证,以及验证失败后向 `errors` 集合添加什么消息。`:on` 选项的可选值是 `:create` 或 `:update`。每个辅助函数都有默认的错误消息,如果没有通过 `:message` 选项指定,则使用默认值。下面分别介绍各个辅助方法。 + + ### `acceptance` -这个方法检查表单提交时,用户界面中的复选框是否被选中。这个功能一般用来要求用户接受程序的服务条款,阅读一些文字,等等。这种验证只针对网页程序,不会存入数据库(如果没有对应的字段,该方法会创建一个虚拟属性)。 +这个方法检查表单提交时,用户界面中的复选框是否被选中。这个功能一般用来要求用户接受应用的服务条款、确保用户阅读了一些文本,等等。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :terms_of_service, acceptance: true end ``` -这个帮助方法的默认错误消息是“must be accepted”。 +仅当 `terms_of_service` 不为 `nil` 时才会执行这个检查。这个辅助方法的默认错误消息是“must be accepted”。通过 `message` 选项可以传入自定义的消息。 -这个方法可以指定 `:accept` 选项,决定可接受什么值。默认为“1”,很容易修改: +```ruby +class Person < ApplicationRecord + validates :terms_of_service, acceptance: { message: 'must be abided' } +end +``` + +这个辅助方法还接受 `:accept` 选项,指定把哪些值视作“接受”。默认为 `['1', true]`,不过可以轻易修改: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :terms_of_service, acceptance: { accept: 'yes' } + validates :eula, acceptance: { accept: ['TRUE', 'accepted'] } end ``` +这种验证只针对 Web 应用,接受与否无需存入数据库。如果没有对应的字段,该方法会创建一个虚拟属性。如果数据库中有对应的字段,必须把 `accept` 选项的值设为或包含 `true`,否则验证不会执行。 + + + ### `validates_associated` -如果模型和其他模型有关联,也要验证关联的模型对象,可以使用这个方法。保存对象是,会在相关联的每个对象上调用 `valid?` 方法。 +如果模型和其他模型有关联,而且关联的模型也要验证,要使用这个辅助方法。保存对象时,会在相关联的每个对象上调用 `valid?` 方法。 ```ruby -class Library < ActiveRecord::Base +class Library < ApplicationRecord has_many :books validates_associated :books end ``` -这个帮助方法可用于所有关联类型。 +这种验证支持所有关联类型。 + +WARNING: 不要在关联的两端都使用 `validates_associated`,这样会变成无限循环。 -WARNING: 不要在关联的两端都使用 `validates_associated`,这样会生成一个循环。 `validates_associated` 的默认错误消息是“is invalid”。注意,相关联的每个对象都有各自的 `errors` 集合,错误消息不会都集中在调用该方法的模型对象上。 + + ### `confirmation` -如果要检查两个文本字段的值是否完全相同,可以使用这个帮助方法。例如,确认 Email 地址或密码。这个帮助方法会创建一个虚拟属性,其名字为要验证的属性名后加 `_confirmation`。 +如果要检查两个文本字段的值是否完全相同,使用这个辅助方法。例如,确认电子邮件地址或密码。这个验证创建一个虚拟属性,其名字为要验证的属性名后加 `_confirmation`。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, confirmation: true end ``` -在视图中可以这么写: +在视图模板中可以这么写: ```erb <%= text_field :person, :email %> <%= text_field :person, :email_confirmation %> ``` -只有 `email_confirmation` 的值不是 `nil` 时才会做这个验证。所以要为确认属性加上存在性验证(后文会介绍 `presence` 验证)。 +只有 `email_confirmation` 的值不是 `nil` 时才会检查。所以要为确认属性加上存在性验证(后文会介绍 `presence` 验证)。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, confirmation: true validates :email_confirmation, presence: true end ``` -这个帮助方法的默认错误消息是“doesn't match confirmation”。 +此外,还可以使用 `:case_sensitive` 选项指定确认时是否区分大小写。这个选项的默认值是 `true`。 + +```ruby +class Person < ApplicationRecord + validates :email, confirmation: { case_sensitive: false } +end +``` + +这个辅助方法的默认错误消息是“doesn’t match confirmation”。 + + ### `exclusion` -这个帮助方法检查属性的值是否不在指定的集合中。集合可以是任何一种可枚举的对象。 +这个辅助方法检查属性的值是否不在指定的集合中。集合可以是任何一种可枚举的对象。 ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :subdomain, exclusion: { in: %w(www us ca jp), message: "%{value} is reserved." } end ``` -`exclusion` 方法要指定 `:in` 选项,设置哪些值不能作为属性的值。`:in` 选项有个别名 `:with`,作用相同。上面的例子设置了 `:message` 选项,演示如何获取属性的值。 +`exclusion` 方法要指定 `:in` 选项,设置哪些值不能作为属性的值。`:in` 选项有个别名 `:with`,作用相同。上面的例子设置了 `:message` 选项,演示如何获取属性的值。`:message` 选项的完整参数参见 [`:message`](#message)。 默认的错误消息是“is reserved”。 + + ### `format` -这个帮助方法检查属性的值是否匹配 `:with` 选项指定的正则表达式。 +这个辅助方法检查属性的值是否匹配 `:with` 选项指定的正则表达式。 ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/, message: "only allows letters" } end ``` +或者,使用 `:without` 选项,指定属性的值不能匹配正则表达式。 + 默认的错误消息是“is invalid”。 + + ### `inclusion` -这个帮助方法检查属性的值是否在指定的集合中。集合可以是任何一种可枚举的对象。 +这个辅助方法检查属性的值是否在指定的集合中。集合可以是任何一种可枚举的对象。 ```ruby -class Coffee < ActiveRecord::Base +class Coffee < ApplicationRecord validates :size, inclusion: { in: %w(small medium large), message: "%{value} is not a valid size" } end ``` -`inclusion` 方法要指定 `:in` 选项,设置可接受哪些值。`:in` 选项有个别名 `:within`,作用相同。上面的例子设置了 `:message` 选项,演示如何获取属性的值。 +`inclusion` 方法要指定 `:in` 选项,设置可接受哪些值。`:in` 选项有个别名 `:within`,作用相同。上面的例子设置了 `:message` 选项,演示如何获取属性的值。`:message` 选项的完整参数参见 [`:message`](#message)。 该方法的默认错误消息是“is not included in the list”。 + + ### `length` -这个帮助方法验证属性值的长度,有多个选项,可以使用不同的方法指定长度限制: +这个辅助方法验证属性值的长度,有多个选项,可以使用不同的方法指定长度约束: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, length: { minimum: 2 } validates :bio, length: { maximum: 500 } validates :password, length: { in: 6..20 } @@ -295,54 +361,42 @@ class Person < ActiveRecord::Base end ``` -可用的长度限制选项有: +可用的长度约束选项有: -* `:minimum`:属性的值不能比指定的长度短; -* `:maximum`:属性的值不能比指定的长度长; -* `:in`(或 `:within`):属性值的长度在指定值之间。该选项的值必须是一个范围; -* `:is`:属性值的长度必须等于指定值; +* `:minimum`:属性的值不能比指定的长度短; +* `:maximum`:属性的值不能比指定的长度长; +* `:in`(或 `:within`):属性值的长度在指定的范围内。该选项的值必须是一个范围; +* `:is`:属性值的长度必须等于指定值; -默认的错误消息根据长度验证类型而有所不同,还是可以 `:message` 定制。定制消息时,可以使用 `:wrong_length`、`:too_long` 和 `:too_short` 选项,`%{count}` 表示长度限制的值。 +默认的错误消息根据长度验证的约束类型而有所不同,不过可以使用 `:message` 选项定制。定制消息时,可以使用 `:wrong_length`、`:too_long` 和 `:too_short` 选项,`%{count}` 表示长度限制的值。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :bio, length: { maximum: 1000, too_long: "%{count} characters is the maximum allowed" } end ``` -这个帮助方法默认统计字符数,但可以使用 `:tokenizer` 选项设置其他的统计方式: +这个辅助方法默认统计字符数,但可以使用 `:tokenizer` 选项设置其他的统计方式: -```ruby -class Essay < ActiveRecord::Base - validates :content, length: { - minimum: 300, - maximum: 400, - tokenizer: lambda { |str| str.scan(/\w+/) }, - too_short: "must have at least %{count} words", - too_long: "must have at most %{count} words" - } -end -``` +注意,默认的错误消息使用复数形式(例如,“is too short (minimum is %{count} characters”),所以如果长度限制是 `minimum: 1`,就要提供一个定制的消息,或者使用 `presence: true` 代替。`:in` 或 `:within` 的下限值比 1 小时,要提供一个定制的消息,或者在 `length` 之前调用 `presence` 方法。 -注意,默认的错误消息使用复数形式(例如,“is too short (minimum is %{count} characters”),所以如果长度限制是 `minimum: 1`,就要提供一个定制的消息,或者使用 `presence: true` 代替。`:in` 或 `:within` 的值比 1 小时,都要提供一个定制的消息,或者在 `length` 之前,调用 `presence` 方法。 + ### `numericality` -这个帮助方法检查属性的值是否值包含数字。默认情况下,匹配的值是可选的正负符号后加整数或浮点数。如果只接受整数,可以把 `:only_integer` 选项设为 `true`。 +这个辅助方法检查属性的值是否只包含数字。默认情况下,匹配的值是可选的正负符号后加整数或浮点数。如果只接受整数,把 `:only_integer` 选项设为 `true`。 -如果 `:only_integer` 为 `true`,则使用下面的正则表达式验证属性的值。 +如果把 `:only_integer` 的值设为 `true`,使用下面的正则表达式验证属性的值: ```ruby -/\A[+-]?\d+\Z/ +/\A[+-]?\d+\z/ ``` 否则,会尝试使用 `Float` 把值转换成数字。 -WARNING: 注意上面的正则表达式允许最后出现换行符。 - ```ruby -class Player < ActiveRecord::Base +class Player < ApplicationRecord validates :points, numericality: true validates :games_played, numericality: { only_integer: true } end @@ -350,30 +404,36 @@ end 除了 `:only_integer` 之外,这个方法还可指定以下选项,限制可接受的值: -* `:greater_than`:属性值必须比指定的值大。该选项默认的错误消息是“must be greater than %{count}”; -* `:greater_than_or_equal_to`:属性值必须大于或等于指定的值。该选项默认的错误消息是“must be greater than or equal to %{count}”; -* `:equal_to`:属性值必须等于指定的值。该选项默认的错误消息是“must be equal to %{count}”; -* `:less_than`:属性值必须比指定的值小。该选项默认的错误消息是“must be less than %{count}”; -* `:less_than_or_equal_to`:属性值必须小于或等于指定的值。该选项默认的错误消息是“must be less than or equal to %{count}”; -* `:odd`:如果设为 `true`,属性值必须是奇数。该选项默认的错误消息是“must be odd”; -* `:even`:如果设为 `true`,属性值必须是偶数。该选项默认的错误消息是“must be even”; +* `:greater_than`:属性值必须比指定的值大。该选项默认的错误消息是“must be greater than %{count}”; +* `:greater_than_or_equal_to`:属性值必须大于或等于指定的值。该选项默认的错误消息是“must be greater than or equal to %{count}”; +* `:equal_to`:属性值必须等于指定的值。该选项默认的错误消息是“must be equal to %{count}”; +* `:less_than`:属性值必须比指定的值小。该选项默认的错误消息是“must be less than %{count}”; +* `:less_than_or_equal_to`:属性值必须小于或等于指定的值。该选项默认的错误消息是“must be less than or equal to %{count}”; +* `:other_than`:属性值必须与指定的值不同。该选项默认的错误消息是“must be other than %{count}”。 +* `:odd`:如果设为 `true`,属性值必须是奇数。该选项默认的错误消息是“must be odd”; +* `:even`:如果设为 `true`,属性值必须是偶数。该选项默认的错误消息是“must be even”; + +NOTE: `numericality` 默认不接受 `nil` 值。可以使用 `allow_nil: true` 选项允许接受 `nil`。 + 默认的错误消息是“is not a number”。 + + ### `presence` -这个帮助方法检查指定的属性是否为非空值,调用 `blank?` 方法检查只是否为 `nil` 或空字符串,即空字符串或只包含空白的字符串。 +这个辅助方法检查指定的属性是否为非空值。它调用 `blank?` 方法检查值是否为 `nil` 或空字符串,即空字符串或只包含空白的字符串。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, :login, :email, presence: true end ``` -如果要确保关联对象存在,需要测试关联的对象本身是够存在,而不是用来映射关联的外键。 +如果要确保关联对象存在,需要测试关联的对象本身是否存在,而不是用来映射关联的外键。 ```ruby -class LineItem < ActiveRecord::Base +class LineItem < ApplicationRecord belongs_to :order validates :order, presence: true end @@ -382,31 +442,40 @@ end 为了能验证关联的对象是否存在,要在关联中指定 `:inverse_of` 选项。 ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord has_many :line_items, inverse_of: :order end ``` 如果验证 `has_one` 或 `has_many` 关联的对象是否存在,会在关联的对象上调用 `blank?` 和 `marked_for_destruction?` 方法。 -因为 `false.blank?` 的返回值是 `true`,所以如果要验证布尔值字段是否存在要使用 `validates :field_name, inclusion: { in: [true, false] }`。 +因为 `false.blank?` 的返回值是 `true`,所以如果要验证布尔值字段是否存在,要使用下述验证中的一个: + +```ruby +validates :boolean_field_name, inclusion: { in: [true, false] } +validates :boolean_field_name, exclusion: { in: [nil] } +``` + +上述验证确保值不是 `nil`;在多数情况下,即验证不是 `NULL`。 + +默认的错误消息是“can’t be blank”。 -默认的错误消息是“can't be blank”。 + ### `absence` -这个方法验证指定的属性值是否为空,使用 `present?` 方法检测值是否为 `nil` 或空字符串,即空字符串或只包含空白的字符串。 +这个辅助方法验证指定的属性值是否为空。它使用 `present?` 方法检测值是否为 `nil` 或空字符串,即空字符串或只包含空白的字符串。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, :login, :email, absence: true end ``` -如果要确保关联对象为空,需要测试关联的对象本身是够为空,而不是用来映射关联的外键。 +如果要确保关联对象为空,要测试关联的对象本身是否为空,而不是用来映射关联的外键。 ```ruby -class LineItem < ActiveRecord::Base +class LineItem < ApplicationRecord belongs_to :order validates :order, absence: true end @@ -415,7 +484,7 @@ end 为了能验证关联的对象是否为空,要在关联中指定 `:inverse_of` 选项。 ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord has_many :line_items, inverse_of: :order end ``` @@ -426,42 +495,49 @@ end 默认的错误消息是“must be blank”。 + + ### `uniqueness` -这个帮助方法会在保存对象之前验证属性值是否是唯一的。该方法不会在数据库中创建唯一性约束,所以有可能两个数据库连接创建的记录字段的值是相同的。为了避免出现这种问题,要在数据库的字段上建立唯一性索引。关于多字段所以的详细介绍,参阅 [MySQL 手册](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html)。 +这个辅助方法在保存对象之前验证属性值是否是唯一的。该方法不会在数据库中创建唯一性约束,所以有可能两次数据库连接创建的记录具有相同的字段值。为了避免出现这种问题,必须在数据库的字段上建立唯一性索引。 ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :email, uniqueness: true end ``` -这个验证会在模型对应的数据表中执行一个 SQL 查询,检查现有的记录中该字段是否已经出现过相同的值。 +这个验证会在模型对应的表中执行一个 SQL 查询,检查现有的记录中该字段是否已经出现过相同的值。 -`:scope` 选项可以指定其他属性,用来约束唯一性验证: +`:scope` 选项用于指定检查唯一性时使用的一个或多个属性: ```ruby -class Holiday < ActiveRecord::Base +class Holiday < ApplicationRecord validates :name, uniqueness: { scope: :year, message: "should happen once per year" } end ``` -还有个 `:case_sensitive` 选项,指定唯一性验证是否要区分大小写,默认值为 `true`。 +如果想确保使用 `:scope` 选项的唯一性验证严格有效,必须在数据库中为多列创建唯一性索引。多列索引的详情参见 [MySQL 手册](http://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html),[PostgreSQL 手册](http://www.postgresql.org/docs/current/static/ddl-constraints.html)中有些示例,说明如何为一组列创建唯一性约束。 + +还有个 `:case_sensitive` 选项,指定唯一性验证是否区分大小写,默认值为 `true`。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, uniqueness: { case_sensitive: false } end ``` -WARNING: 注意,有些数据库的设置是,查询时不区分大小写。 +WARNING: 注意,不管怎样设置,有些数据库查询时始终不区分大小写。 + 默认的错误消息是“has already been taken”。 + + ### `validates_with` -这个帮助方法把记录交给其他的类做验证。 +这个辅助方法把记录交给其他类做验证。 ```ruby class GoodnessValidator < ActiveModel::Validator @@ -472,18 +548,19 @@ class GoodnessValidator < ActiveModel::Validator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_with GoodnessValidator end ``` NOTE: `record.errors[:base]` 中的错误针对整个对象,而不是特定的属性。 -`validates_with` 方法的参数是一个类,或一组类,用来做验证。`validates_with` 方法没有默认的错误消息。在做验证的类中要手动把错误添加到记录的错误集合中。 + +`validates_with` 方法的参数是一个类或一组类,用来做验证。`validates_with` 方法没有默认的错误消息。在做验证的类中要手动把错误添加到记录的错误集合中。 实现 `validate` 方法时,必须指定 `record` 参数,这是要做验证的记录。 -和其他验证一样,`validates_with` 也可指定 `:if`、`:unless` 和 `:on` 选项。如果指定了其他选项,会包含在 `options` 中传递给做验证的类。 +与其他验证一样,`validates_with` 也可指定 `:if`、`:unless` 和 `:on` 选项。如果指定了其他选项,会包含在 `options` 中传递给做验证的类。 ```ruby class GoodnessValidator < ActiveModel::Validator @@ -494,17 +571,17 @@ class GoodnessValidator < ActiveModel::Validator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_with GoodnessValidator, fields: [:first_name, :last_name] end ``` -注意,做验证的类在整个程序的生命周期内只会初始化一次,而不是每次验证时都初始化,所以使用实例变量时要特别小心。 +注意,做验证的类在整个应用的生命周期内只会初始化一次,而不是每次验证时都初始化,所以使用实例变量时要特别小心。 如果做验证的类很复杂,必须要用实例变量,可以用纯粹的 Ruby 对象代替: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validate do |person| GoodnessValidator.new(person).validate end @@ -525,42 +602,51 @@ class GoodnessValidator end ``` + + ### `validates_each` -这个帮助方法会把属性值传入代码库做验证,没有预先定义验证的方式,你应该在代码库中定义验证方式。要验证的每个属性都会传入块中做验证。在下面的例子中,我们确保名和姓都不能以小写字母开头: +这个辅助方法使用代码块中的代码验证属性。它没有预先定义验证函数,你要在代码块中定义验证方式。要验证的每个属性都会传入块中做验证。在下面的例子中,我们确保名和姓都不能以小写字母开头: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates_each :name, :surname do |record, attr, value| - record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/ + record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/ end end ``` -代码块的参数是记录,属性名和属性值。在代码块中可以做任何检查,确保数据合法。如果验证失败,要向模型添加一个错误消息,把数据标记为不合法。 +代码块的参数是记录、属性名和属性值。在代码块中可以做任何检查,确保数据有效。如果验证失败,应该向模型添加一个错误消息,把数据标记为无效。 + + -常用的验证选项 -------------- +## 常用的验证选项 -常用的验证选项包括: +下面介绍常用的验证选项。 + + ### `:allow_nil` -指定 `:allow_nil` 选项后,如果要验证的值为 `nil` 就会跳过验证。 +指定 `:allow_nil` 选项后,如果要验证的值为 `nil` 就跳过验证。 ```ruby -class Coffee < ActiveRecord::Base +class Coffee < ApplicationRecord validates :size, inclusion: { in: %w(small medium large), message: "%{value} is not a valid size" }, allow_nil: true end ``` +`:message` 选项的完整参数参见 [`:message`](#message)。 + + + ### `:allow_blank` -`:allow_blank` 选项和 `:allow_nil` 选项类似。如果要验证的值为空(调用 `blank?` 方法,例如 `nil` 或空字符串),就会跳过验证。 +`:allow_blank` 选项和 `:allow_nil` 选项类似。如果要验证的值为空(调用 `blank?` 方法判断,例如 `nil` 或空字符串),就跳过验证。 ```ruby -class Topic < ActiveRecord::Base +class Topic < ApplicationRecord validates :title, length: { is: 5 }, allow_blank: true end @@ -568,61 +654,107 @@ Topic.create(title: "").valid? # => true Topic.create(title: nil).valid? # => true ``` + + ### `:message` -前面已经介绍过,如果验证失败,会把 `:message` 选项指定的字符串添加到 `errors` 集合中。如果没指定这个选项,Active Record 会使用各种验证帮助方法的默认错误消息。 +前面已经介绍过,如果验证失败,会把 `:message` 选项指定的字符串添加到 `errors` 集合中。如果没指定这个选项,Active Record 使用各个验证辅助方法的默认错误消息。`:message` 选项的值是一个字符串或一个 `Proc` 对象。 + +字符串消息中可以包含 `%{value}`、`%{attribute}` 和 `%{model}`,在验证失败时它们会被替换成具体的值。替换通过 I18n gem 实现,而且占位符必须精确匹配,不能有空格。 + +`Proc` 形式的消息有两个参数:验证的对象,以及包含 `:model`、`:attribute` 和 `:value` 键值对的散列。 + +```ruby +class Person < ApplicationRecord + # 直接写消息 + validates :name, presence: { message: "must be given please" } + + # 带有动态属性值的消息。%{value} 会被替换成属性的值 + # 此外还可以使用 %{attribute} 和 %{model} + validates :age, numericality: { message: "%{value} seems wrong" } + + # Proc + validates :username, + uniqueness: { + # object = 要验证的 person 对象 + # data = { model: "Person", attribute: "Username", value: } + message: ->(object, data) do + "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}" + end + } +end +``` + + ### `:on` -`:on` 选项指定什么时候做验证。所有内建的验证帮助方法默认都在保存时(新建记录或更新记录)做验证。如果想修改,可以使用 `on: :create`,指定只在创建记录时做验证;或者使用 `on: :update`,指定只在更新记录时做验证。 +`:on` 选项指定什么时候验证。所有内置的验证辅助方法默认都在保存时(新建记录或更新记录)验证。如果想修改,可以使用 `on: :create`,指定只在创建记录时验证;或者使用 `on: :update`,指定只在更新记录时验证。 ```ruby -class Person < ActiveRecord::Base - # it will be possible to update email with a duplicated value +class Person < ApplicationRecord + # 更新时允许电子邮件地址重复 validates :email, uniqueness: true, on: :create - # it will be possible to create the record with a non-numerical age + # 创建记录时允许年龄不是数字 validates :age, numericality: true, on: :update - # the default (validates on both create and update) + # 默认行为(创建和更新时都验证) validates :name, presence: true end ``` -严格验证 -------- +此外,还可以使用 `on:` 定义自定义的上下文。必须把上下文的名称传给 `valid?`、`invalid?` 或 `save` 才能触发自定义的上下文。 + +```ruby +class Person < ApplicationRecord + validates :email, uniqueness: true, on: :account_setup + validates :age, numericality: true, on: :account_setup +end + +person = Person.new +``` + +`person.valid?(:account_setup)` 会执行上述两个验证,但不保存记录。`person.save(context: :account_setup)` 在保存之前在 `account_setup` 上下文中验证 `person`。显式触发时,可以只使用某个上下文验证,也可以不使用某个上下文验证。 + + -数据验证还可以使用严格模式,失败后会抛出 `ActiveModel::StrictValidationFailed` 异常。 +## 严格验证 + +数据验证还可以使用严格模式,当对象无效时抛出 `ActiveModel::StrictValidationFailed` 异常。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: { strict: true } end Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank ``` -通过 `:strict` 选项,还可以指定抛出什么异常: +还可以通过 `:strict` 选项指定抛出什么异常: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :token, presence: true, uniqueness: true, strict: TokenGenerationException end Person.new.valid? # => TokenGenerationException: Token can't be blank ``` -条件验证 -------- + + +## 条件验证 -有时只有满足特定条件时做验证才说得通。条件可通过 `:if` 和 `:unless` 选项指定,这两个选项的值可以是 Symbol、字符串、`Proc` 或数组。`:if` 选项指定何时做验证。如果要指定何时不做验证,可以使用 `:unless` 选项。 +有时,只有满足特定条件时做验证才说得通。条件可通过 `:if` 和 `:unless` 选项指定,这两个选项的值可以是符号、字符串、`Proc` 或数组。`:if` 选项指定何时做验证。如果要指定何时不做验证,使用 `:unless` 选项。 -### 指定 Symbol + -`:if` 和 `:unless` 选项的值为 Symbol 时,表示要在验证之前执行对应的方法。这是最常用的设置方法。 +### 使用符号 + +`:if` 和 `:unless` 选项的值为符号时,表示要在验证之前执行对应的方法。这是最常用的设置方法。 ```ruby -class Order < ActiveRecord::Base +class Order < ApplicationRecord validates :card_number, presence: true, if: :paid_with_card? def paid_with_card? @@ -631,33 +763,27 @@ class Order < ActiveRecord::Base end ``` -### 指定字符串 - -`:if` 和 `:unless` 选项的值还可以是字符串,但必须是 Ruby 代码,传入 `eval` 方法中执行。当字符串表示的条件非常短时才应该使用这种形式。 - -```ruby -class Person < ActiveRecord::Base - validates :surname, presence: true, if: "name.nil?" -end -``` + -### 指定 Proc +### 使用 Proc `:if` and `:unless` 选项的值还可以是 Proc。使用 Proc 对象可以在行间编写条件,不用定义额外的方法。这种形式最适合用在一行代码能表示的条件上。 ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord validates :password, confirmation: true, unless: Proc.new { |a| a.password.blank? } end ``` + + ### 条件组合 -有时同一个条件会用在多个验证上,这时可以使用 `with_options` 方法: +有时,同一个条件会用在多个验证上,这时可以使用 `with_options` 方法: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord with_options if: :is_admin? do |admin| admin.validates :password, length: { minimum: 10 } admin.validates :email, presence: true @@ -667,28 +793,33 @@ end `with_options` 代码块中的所有验证都会使用 `if: :is_admin?` 这个条件。 + + ### 联合条件 -另一方面,如果是否做某个验证要满足多个条件时,可以使用数组。而且,都一个验证可以同时指定 `:if` 和 `:unless` 选项。 +另一方面,如果是否做某个验证要满足多个条件时,可以使用数组。而且,一个验证可以同时指定 `:if` 和 `:unless` 选项。 ```ruby -class Computer < ActiveRecord::Base +class Computer < ApplicationRecord validates :mouse, presence: true, - if: ["market.retail?", :desktop?] + if: ["market.retail?", :desktop?], unless: Proc.new { |c| c.trackpad.present? } end ``` 只有当 `:if` 选项的所有条件都返回 `true`,且 `:unless` 选项中的条件返回 `false` 时才会做验证。 -自定义验证方式 ------------- + + +## 自定义验证 -如果内建的数据验证帮助方法无法满足需求时,可以选择自己定义验证使用的类或方法。 +如果内置的数据验证辅助方法无法满足需求,可以选择自己定义验证使用的类或方法。 -### 自定义验证使用的类 + -自定义的验证类继承自 `ActiveModel::Validator`,必须实现 `validate` 方法,传入的参数是要验证的记录,然后验证这个记录是否合法。自定义的验证类通过 `validates_with` 方法调用。 +### 自定义验证类 + +自定义的验证类继承自 `ActiveModel::Validator`,必须实现 `validate` 方法,其参数是要验证的记录,然后验证这个记录是否有效。自定义的验证类通过 `validates_with` 方法调用。 ```ruby class MyValidator < ActiveModel::Validator @@ -705,7 +836,7 @@ class Person end ``` -在自定义的验证类中验证单个属性,最简单的方法是集成 `ActiveModel::EachValidator` 类。此时,自定义的验证类中要实现 `validate_each` 方法。这个方法接受三个参数:记录,属性名和属性值。 +在自定义的验证类中验证单个属性,最简单的方法是继承 `ActiveModel::EachValidator` 类。此时,自定义的验证类必须实现 `validate_each` 方法。这个方法接受三个参数:记录、属性名和属性值。它们分别对应模型实例、要验证的属性及其值。 ```ruby class EmailValidator < ActiveModel::EachValidator @@ -716,21 +847,25 @@ class EmailValidator < ActiveModel::EachValidator end end -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :email, presence: true, email: true end ``` -如上面的代码所示,可以同时使用内建的验证方法和自定义的验证类。 +如上面的代码所示,可以同时使用内置的验证方法和自定义的验证类。 + + -### 自定义验证使用的方法 +### 自定义验证方法 -还可以自定义方法验证模型的状态,如果验证失败,向 `erros` 集合添加错误消息。然后还要使用类方法 `validate` 注册这些方法,传入自定义验证方法名的 Symbol 形式。 +你还可以自定义方法,验证模型的状态,如果验证失败,向 `erros` 集合添加错误消息。验证方法必须使用类方法 `validate`([API](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate))注册,传入自定义验证方法名的符号形式。 -类方法可以接受多个 Symbol,自定义的验证方法会按照注册的顺序执行。 +这个类方法可以接受多个符号,自定义的验证方法会按照注册的顺序执行。 + +`valid?` 方法会验证错误集合是否为空,因此若想让验证失败,自定义的验证方法要把错误添加到那个集合中。 ```ruby -class Invoice < ActiveRecord::Base +class Invoice < ApplicationRecord validate :expiration_date_cannot_be_in_the_past, :discount_cannot_be_greater_than_total_value @@ -748,10 +883,10 @@ class Invoice < ActiveRecord::Base end ``` -默认情况下,每次调用 `valid?` 方法时都会执行自定义的验证方法。使用 `validate` 方法注册自定义验证方法时可以设置 `:on` 选项,执行什么时候运行。`:on` 的可选值为 `:create` 和 `:update`。 +默认情况下,每次调用 `valid?` 方法或保存对象时都会执行自定义的验证方法。不过,使用 `validate` 方法注册自定义验证方法时可以设置 `:on` 选项,指定什么时候验证。`:on` 的可选值为 `:create` 和 `:update`。 ```ruby -class Invoice < ActiveRecord::Base +class Invoice < ApplicationRecord validate :active_customer, on: :create def active_customer @@ -760,19 +895,22 @@ class Invoice < ActiveRecord::Base end ``` -处理验证错误 ------------ + + +## 处理验证错误 -除了前面介绍的 `valid?` 和 `invalid?` 方法之外,Rails 还提供了很多方法用来处理 `errors` 集合,以及查询对象的合法性。 +除了前面介绍的 `valid?` 和 `invalid?` 方法之外,Rails 还提供了很多方法用来处理 `errors` 集合,以及查询对象的有效性。 -下面介绍其中一些常用的方法。所有可用的方法请查阅 `ActiveModel::Errors` 的文档。 +下面介绍其中一些最常用的方法。所有可用的方法请查阅 `ActiveModel::Errors` 的文档。 + + ### `errors` -`ActiveModel::Errors` 的实例包含所有的错误。其键是每个属性的名字,值是一个数组,包含错误消息字符串。 +`ActiveModel::Errors` 的实例包含所有的错误。键是每个属性的名称,值是一个数组,包含错误消息字符串。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -786,12 +924,14 @@ person.valid? # => true person.errors.messages # => {} ``` + + ### `errors[]` -`errors[]` 用来获取某个属性上的错误消息,返回结果是一个由该属性所有错误消息字符串组成的数组,每个字符串表示一个错误消息。如果字段上没有错误,则返回空数组。 +`errors[]` 用于获取某个属性上的错误消息,返回结果是一个由该属性所有错误消息字符串组成的数组,每个字符串表示一个错误消息。如果字段上没有错误,则返回空数组。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -809,12 +949,16 @@ person.errors[:name] # => ["can't be blank", "is too short (minimum is 3 characters)"] ``` + + ### `errors.add` -`add` 方法可以手动添加某属性的错误消息。使用 `errors.full_messages` 或 `errors.to_a` 方法会以最终显示给用户的形式显示错误消息。这些错误消息的前面都会加上字段名可读形式(并且首字母大写)。`add` 方法接受两个参数:错误消息要添加到的字段名和错误消息本身。 +`add` 方法用于手动添加某属性的错误消息,它的参数是属性和错误消息。 + +使用 `errors.full_messages`(或等价的 `errors.to_a`)方法以对用户友好的格式显示错误消息。这些错误消息的前面都会加上属性名(首字母大写),如下述示例所示。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord def a_method_used_for_validation_purposes errors.add(:name, "cannot contain the characters !@#%*()_-+=") end @@ -829,42 +973,82 @@ person.errors.full_messages # => ["Name cannot contain the characters !@#%*()_-+="] ``` -还有一种方法可以实现同样地效果,使用 `[]=` 设置方法: +`<<` 的作用与 `errors#add` 一样:把一个消息追加到 `errors.messages` 数组中。 ```ruby - class Person < ActiveRecord::Base - def a_method_used_for_validation_purposes - errors[:name] = "cannot contain the characters !@#%*()_-+=" - end +class Person < ApplicationRecord + def a_method_used_for_validation_purposes + errors.messages[:name] << "cannot contain the characters !@#%*()_-+=" end +end - person = Person.create(name: "!@#") +person = Person.create(name: "!@#") - person.errors[:name] - # => ["cannot contain the characters !@#%*()_-+="] +person.errors[:name] + # => ["cannot contain the characters !@#%*()_-+="] - person.errors.to_a - # => ["Name cannot contain the characters !@#%*()_-+="] +person.errors.to_a + # => ["Name cannot contain the characters !@#%*()_-+="] ``` + + +### `errors.details` + +使用 `errors.add` 方法可以为返回的错误详情散列指定验证程序类型。 + +```ruby +class Person < ApplicationRecord + def a_method_used_for_validation_purposes + errors.add(:name, :invalid_characters) + end +end + +person = Person.create(name: "!@#") + +person.errors.details[:name] +# => [{error: :invalid_characters}] +``` + +如果想提升错误详情的信息量,可以为 `errors.add` 方法提供额外的键,指定不允许的字符。 + +```ruby +class Person < ApplicationRecord + def a_method_used_for_validation_purposes + errors.add(:name, :invalid_characters, not_allowed: "!@#%*()_-+=") + end +end + +person = Person.create(name: "!@#") + +person.errors.details[:name] +# => [{error: :invalid_characters, not_allowed: "!@#%*()_-+="}] +``` + +Rails 内置的验证程序生成的错误详情散列都有对应的验证程序类型。 + + + ### `errors[:base]` -错误消息可以添加到整个对象上,而不是针对某个属性。如果不想管是哪个属性导致对象不合法,指向把对象标记为不合法状态,就可以使用这个方法。`errors[:base]` 是个数字,可以添加字符串作为错误消息。 +错误消息可以添加到整个对象上,而不是针对某个属性。如果不想管是哪个属性导致对象无效,只想把对象标记为无效状态,就可以使用这个方法。`errors[:base]` 是个数组,可以添加字符串作为错误消息。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord def a_method_used_for_validation_purposes errors[:base] << "This person is invalid because ..." end end ``` + + ### `errors.clear` -如果想清除 `errors` 集合中的所有错误消息,可以使用 `clear` 方法。当然了,在不合法的对象上调用 `errors.clear` 方法后,这个对象还是不合法的,虽然 `errors` 集合为空了,但下次调用 `valid?` 方法,或调用其他把对象存入数据库的方法时, 会再次进行验证。如果任何一个验证失败了,`errors` 集合中就再次出现值了。 +如果想清除 `errors` 集合中的所有错误消息,可以使用 `clear` 方法。当然,在无效的对象上调用 `errors.clear` 方法后,对象还是无效的,虽然 `errors` 集合为空了,但下次调用 `valid?` 方法,或调用其他把对象存入数据库的方法时, 会再次进行验证。如果任何一个验证失败了,`errors` 集合中就再次出现值了。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -876,18 +1060,20 @@ person.errors[:name] person.errors.clear person.errors.empty? # => true -p.save # => false +person.save # => false -p.errors[:name] +person.errors[:name] # => ["can't be blank", "is too short (minimum is 3 characters)"] ``` + + ### `errors.size` `size` 方法返回对象上错误消息的总数。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end @@ -900,22 +1086,23 @@ person.valid? # => true person.errors.size # => 0 ``` -在视图中显示验证错误 ------------------ + + +## 在视图中显示验证错误 在模型中加入数据验证后,如果在表单中创建模型,出错时,你或许想把错误消息显示出来。 -因为每个程序显示错误消息的方式不同,所以 Rails 没有直接提供用来显示错误消息的视图帮助方法。不过,Rails 提供了这么多方法用来处理验证,自己编写一个也不难。使用脚手架时,Rails 会在生成的 `_form.html.erb` 中加入一些 ERB 代码,显示模型错误消息的完整列表。 +因为每个应用显示错误消息的方式不同,所以 Rails 没有直接提供用于显示错误消息的视图辅助方法。不过,Rails 提供了这么多方法用来处理验证,自己编写一个也不难。使用脚手架时,Rails 会在生成的 `_form.html.erb` 中加入一些 ERB 代码,显示模型错误消息的完整列表。 -假设有个模型对象存储在实例变量 `@post` 中,视图的代码可以这么写: +假如有个模型对象存储在实例变量 `@article` 中,视图的代码可以这么写: -```ruby -<% if @post.errors.any? %> +```erb +<% if @article.errors.any? %>
-

<%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:

+

<%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:

    - <% @post.errors.full_messages.each do |msg| %> + <% @article.errors.full_messages.each do |msg| %>
  • <%= msg %>
  • <% end %>
@@ -923,17 +1110,17 @@ person.errors.size # => 0 <% end %> ``` -而且,如果使用 Rails 的表单帮助方法生成表单,如果某个表单字段验证失败,会把字段包含在一个 `
` 中: +此外,如果使用 Rails 的表单辅助方法生成表单,如果某个表单字段验证失败,会把字段包含在一个 `
` 中: -``` +```html
- +
``` -然后可以根据需求为这个 `div` 添加样式。脚手架默认添加的 CSS 如下: +然后,你可以根据需求为这个 `div` 添加样式。脚手架默认添加的 CSS 规则如下: -``` +```css .field_with_errors { padding: 2px; background-color: red; @@ -941,4 +1128,4 @@ person.errors.size # => 0 } ``` -所有出错的表单字段都会放入一个内边距为 2 像素的红色框内。 +上述样式把所有出错的表单字段放入一个内边距为 2 像素的红色框内。 diff --git a/source/zh-CN/active_support_core_extensions.md b/source/zh-CN/active_support_core_extensions.md index 4f37bf9..44d1c31 100644 --- a/source/zh-CN/active_support_core_extensions.md +++ b/source/zh-CN/active_support_core_extensions.md @@ -1,105 +1,117 @@ -Active Support Core Extensions -============================== +# Active Support 核心扩展 -Active Support is the Ruby on Rails component responsible for providing Ruby language extensions, utilities, and other transversal stuff. +Active Support 是 Ruby on Rails 的一个组件,扩展了 Ruby 语言,提供了一些实用功能。 -It offers a richer bottom-line at the language level, targeted both at the development of Rails applications, and at the development of Ruby on Rails itself. +Active Support 丰富了 Rails 使用的编程语言,目的是便于开发 Rails 应用以及 Rails 本身。 -After reading this guide, you will know: +读完本文后,您将学到: -* What Core Extensions are. -* How to load all extensions. -* How to cherry-pick just the extensions you want. -* What extensions Active Support provides. +* 核心扩展是什么; +* 如何加载所有扩展; +* 如何按需加载想用的扩展; +* Active Support 提供了哪些扩展。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -How to Load Core Extensions ---------------------------- + -### Stand-Alone Active Support +## 如何加载核心扩展 -In order to have a near-zero default footprint, Active Support does not load anything by default. It is broken in small pieces so that you can load just what you need, and also has some convenience entry points to load related extensions in one shot, even everything. + -Thus, after a simple require like: +### 独立的 Active Support + +为了减轻应用的负担,默认情况下 Active Support 不会加载任何功能。Active Support 中的各部分功能是相对独立的,可以只加载需要的功能,也可以方便地加载相互联系的功能,或者加载全部功能。 + +因此,只编写下面这个 `require` 语句,对象甚至无法响应 `blank?` 方法: ```ruby require 'active_support' ``` -objects do not even respond to `blank?`. Let's see how to load its definition. +我们来看一下到底应该如何加载。 + + -#### Cherry-picking a Definition +#### 按需加载 -The most lightweight way to get `blank?` is to cherry-pick the file that defines it. +获取 `blank?` 方法最轻便的做法是按需加载其定义所在的文件。 -For every single method defined as a core extension this guide has a note that says where such a method is defined. In the case of `blank?` the note reads: +本文为核心扩展中的每个方法都做了说明,告知是在哪个文件中定义的。对 `blank?` 方法而言,说明如下: -NOTE: Defined in `active_support/core_ext/object/blank.rb`. +NOTE: 在 `active_support/core_ext/object/blank.rb` 文件中定义。 -That means that you can require it like this: +因此 `blank?` 方法要这么加载: ```ruby require 'active_support' require 'active_support/core_ext/object/blank' ``` -Active Support has been carefully revised so that cherry-picking a file loads only strictly needed dependencies, if any. +Active Support 的设计方式精良,确保按需加载时真的只加载所需的扩展。 -#### Loading Grouped Core Extensions + -The next level is to simply load all extensions to `Object`. As a rule of thumb, extensions to `SomeClass` are available in one shot by loading `active_support/core_ext/some_class`. +#### 成组加载核心扩展 -Thus, to load all extensions to `Object` (including `blank?`): +下一层级是加载 `Object` 对象的所有扩展。一般来说,对 `SomeClass` 的扩展都保存在 `active_support/core_ext/some_class` 文件夹中。 + +因此,加载 `Object` 对象的所有扩展(包括 `balnk?` 方法)可以这么做: ```ruby require 'active_support' require 'active_support/core_ext/object' ``` -#### Loading All Core Extensions + + +#### 加载所有扩展 -You may prefer just to load all core extensions, there is a file for that: +如果想加载所有核心扩展,可以这么做: ```ruby require 'active_support' require 'active_support/core_ext' ``` -#### Loading All Active Support + + +#### 加载 Active Support 提供的所有功能 -And finally, if you want to have all Active Support available just issue: +最后,如果想使用 Active Support 提供的所有功能,可以这么做: ```ruby require 'active_support/all' ``` -That does not even put the entire Active Support in memory upfront indeed, some stuff is configured via `autoload`, so it is only loaded if used. +其实,这么做并不会把整个 Active Support 载入内存,有些功能通过 `autoload` 加载,所以真正使用时才会加载。 -### Active Support Within a Ruby on Rails Application + -A Ruby on Rails application loads all Active Support unless `config.active_support.bare` is true. In that case, the application will only load what the framework itself cherry-picks for its own needs, and can still cherry-pick itself at any granularity level, as explained in the previous section. +### 在 Rails 应用中使用 Active Support -Extensions to All Objects -------------------------- +除非把 `config.active_support.bare` 设为 `true`,否则 Rails 应用不会加载 Active Support 提供的所有功能。即便全部加载,应用也会根据框架的设置按需加载所需功能,而且应用开发者还可以根据需要做更细化的选择,方法如前文所述。 -### `blank?` and `present?` + -The following values are considered to be blank in a Rails application: +## 所有对象皆可使用的扩展 -* `nil` and `false`, + -* strings composed only of whitespace (see note below), +### `blank?` 和 `present?` -* empty arrays and hashes, and +在 Rails 应用中,下面这些值表示空值: -* any other object that responds to `empty?` and is empty. +* `nil` 和 `false`; +* 只有空白的字符串(注意下面的说明); +* 空数组和空散列; +* 其他能响应 `empty?` 方法,而且返回值为 `true` 的对象; -INFO: The predicate for strings uses the Unicode-aware character class `[:space:]`, so for example U+2029 (paragraph separator) is considered to be whitespace. +TIP: 判断字符串是否为空使用的是能理解 Unicode 字符的 `[:space:]`,所以 `U+2029`(分段符)会被视为空白。 -WARNING: Note that numbers are not mentioned. In particular, 0 and 0.0 are **not** blank. +WARNING: 注意,这里并没有提到数字。特别说明,`0` 和 `0.0` 不是空值。 -For example, this method from `ActionController::HttpAuthentication::Token::ControllerMethods` uses `blank?` for checking whether a token is present: +例如,`ActionController::HttpAuthentication::Token::ControllerMethods` 定义的这个方法使用 `blank?` 检查是否有令牌: ```ruby def authenticate(controller, &login_procedure) @@ -110,7 +122,7 @@ def authenticate(controller, &login_procedure) end ``` -The method `present?` is equivalent to `!blank?`. This example is taken from `ActionDispatch::Http::Cache::Response`: +`present?` 方法等价于 `!blank?`。下面这个方法摘自 `ActionDispatch::Http::Cache::Response`: ```ruby def set_conditional_cache_control! @@ -119,41 +131,45 @@ def set_conditional_cache_control! end ``` -NOTE: Defined in `active_support/core_ext/object/blank.rb`. +NOTE: 在 `active_support/core_ext/object/blank.rb` 文件中定义。 + + ### `presence` -The `presence` method returns its receiver if `present?`, and `nil` otherwise. It is useful for idioms like this: +如果 `present?` 方法返回 `true`,`presence` 方法的返回值为调用对象,否则返回 `nil`。惯用法如下: ```ruby host = config[:host].presence || 'localhost' ``` -NOTE: Defined in `active_support/core_ext/object/blank.rb`. +NOTE: 在 `active_support/core_ext/object/blank.rb` 文件中定义。 + + ### `duplicable?` -A few fundamental objects in Ruby are singletons. For example, in the whole life of a program the integer 1 refers always to the same instance: +Ruby 中很多基本的对象是单例。例如,在应用的整个生命周期内,整数 1 始终表示同一个实例: ```ruby 1.object_id # => 3 Math.cos(0).to_i.object_id # => 3 ``` -Hence, there's no way these objects can be duplicated through `dup` or `clone`: +因此,这些对象无法通过 `dup` 或 `clone` 方法复制: ```ruby true.dup # => TypeError: can't dup TrueClass ``` -Some numbers which are not singletons are not duplicable either: +有些数字虽然不是单例,但也不能复制: ```ruby 0.0.clone # => allocator undefined for Float (2**1024).clone # => allocator undefined for Bignum ``` -Active Support provides `duplicable?` to programmatically query an object about this property: +Active Support 提供的 `duplicable?` 方法用于查询对象是否可以复制: ```ruby "foo".duplicable? # => true @@ -162,15 +178,17 @@ Active Support provides `duplicable?` to programmatically query an object about false.duplicable? # => false ``` -By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, and module objects. +按照定义,除了 `nil`、`false`、`true`、符号、数字、类、模块和方法对象之外,其他对象都可以复制。 + +WARNING: 任何类都可以禁止对象复制,只需删除 `dup` 和 `clone` 两个方法,或者在这两个方法中抛出异常。因此只能在 `rescue` 语句中判断对象是否可复制。`duplicable?` 方法直接检查对象是否在上述列表中,因此比 `rescue` 的速度快。仅当你知道上述列表能满足需求时才应该使用 `duplicable?` 方法。 -WARNING: Any class can disallow duplication by removing `dup` and `clone` or raising exceptions from them. Thus only `rescue` can tell whether a given arbitrary object is duplicable. `duplicable?` depends on the hard-coded list above, but it is much faster than `rescue`. Use it only if you know the hard-coded list is enough in your use case. +NOTE: 在 `active_support/core_ext/object/duplicable.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/object/duplicable.rb`. + ### `deep_dup` -The `deep_dup` method returns deep copy of a given object. Normally, when you `dup` an object that contains other objects, Ruby does not `dup` them, so it creates a shallow copy of the object. If you have an array with a string, for example, it will look like this: +`deep_dup` 方法深拷贝指定的对象。一般情况下,复制包含其他对象的对象时,Ruby 不会复制内部对象,这叫做浅拷贝。假如有一个由字符串组成的数组,浅拷贝的行为如下: ```ruby array = ['string'] @@ -178,20 +196,20 @@ duplicate = array.dup duplicate.push 'another-string' -# the object was duplicated, so the element was added only to the duplicate +# 创建了对象副本,因此元素只添加到副本中 array # => ['string'] duplicate # => ['string', 'another-string'] duplicate.first.gsub!('string', 'foo') -# first element was not duplicated, it will be changed in both arrays +# 第一个元素没有副本,因此两个数组都会变 array # => ['foo'] duplicate # => ['foo', 'another-string'] ``` -As you can see, after duplicating the `Array` instance, we got another object, therefore we can modify it and the original object will stay unchanged. This is not true for array's elements, however. Since `dup` does not make deep copy, the string inside the array is still the same object. +如上所示,复制数组后得到了一个新对象,修改新对象后原对象没有变化。但对数组中的元素来说情况就不一样了。因为 `dup` 方法不是深拷贝,所以数组中的字符串是同一个对象。 -If you need a deep copy of an object, you should use `deep_dup`. Here is an example: +如果想深拷贝一个对象,应该使用 `deep_dup` 方法。举个例子: ```ruby array = ['string'] @@ -203,7 +221,7 @@ array # => ['string'] duplicate # => ['foo'] ``` -If the object is not duplicable, `deep_dup` will just return it: +如果对象不可复制,`deep_dup` 方法直接返回对象本身: ```ruby number = 1 @@ -211,25 +229,27 @@ duplicate = number.deep_dup number.object_id == duplicate.object_id # => true ``` -NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`. +NOTE: 在 `active_support/core_ext/object/deep_dup.rb` 文件中定义。 + + ### `try` -When you want to call a method on an object only if it is not `nil`, the simplest way to achieve it is with conditional statements, adding unnecessary clutter. The alternative is to use `try`. `try` is like `Object#send` except that it returns `nil` if sent to `nil`. +如果只想当对象不为 `nil` 时在其上调用方法,最简单的方式是使用条件语句,但这么做把代码变复杂了。你可以使用 `try` 方法。`try` 方法和 `Object#send` 方法类似,但如果在 `nil` 上调用,返回值为 `nil`。 -Here is an example: +举个例子: ```ruby -# without try +# 不使用 try unless @number.nil? @number.next end -# with try +# 使用 try @number.try(:next) ``` -Another example is this code from `ActiveRecord::ConnectionAdapters::AbstractAdapter` where `@logger` could be `nil`. You can see that the code uses `try` and avoids an unnecessary check. +下面这个例子摘自 `ActiveRecord::ConnectionAdapters::AbstractAdapter`,实例变量 `@logger` 有可能为 `nil`。可以看出,使用 `try` 方法可以避免不必要的检查。 ```ruby def log_info(sql, name, ms) @@ -240,17 +260,26 @@ def log_info(sql, name, ms) end ``` -`try` can also be called without arguments but a block, which will only be executed if the object is not nil: +`try` 方法也可接受代码块,仅当对象不为 `nil` 时才会执行其中的代码: ```ruby @person.try { |p| "#{p.first_name} #{p.last_name}" } ``` -NOTE: Defined in `active_support/core_ext/object/try.rb`. +注意,`try` 会吞没没有方法错误,返回 `nil`。如果想避免此类问题,应该使用 `try!`: + +```ruby +@number.try(:nest) # => nil +@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer +``` + +NOTE: 在 `active_support/core_ext/object/try.rb` 文件中定义。 + + ### `class_eval(*args, &block)` -You can evaluate code in the context of any object's singleton class using `class_eval`: +使用 `class_eval` 方法可以在对象的单例类上下文中执行代码: ```ruby class Proc @@ -267,52 +296,56 @@ class Proc end ``` -NOTE: Defined in `active_support/core_ext/kernel/singleton_class.rb`. +NOTE: 在 `active_support/core_ext/kernel/singleton_class.rb` 文件中定义。 + + ### `acts_like?(duck)` -The method `acts_like?` provides a way to check whether some class acts like some other class based on a simple convention: a class that provides the same interface as `String` defines +`acts_like?` 方法检查一个类的行为是否与另一个类相似。比较是基于一个简单的约定:如果在某个类中定义了下面这个方法,就说明其接口与字符串一样。 ```ruby def acts_like_string? end ``` -which is only a marker, its body or return value are irrelevant. Then, client code can query for duck-type-safeness this way: +这个方法只是一个标记,其定义体和返回值不影响效果。开发者可使用下面这种方式判断两个类的表现是否类似: ```ruby some_klass.acts_like?(:string) ``` -Rails has classes that act like `Date` or `Time` and follow this contract. +Rails 使用这种约定定义了行为与 `Date` 和 `Time` 相似的类。 + +NOTE: 在 `active_support/core_ext/object/acts_like.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/object/acts_like.rb`. + ### `to_param` -All objects in Rails respond to the method `to_param`, which is meant to return something that represents them as values in a query string, or as URL fragments. +Rails 中的所有对象都能响应 `to_param` 方法。`to_param` 方法的返回值表示查询字符串的值,或者 URL 片段。 -By default `to_param` just calls `to_s`: +默认情况下,`to_param` 方法直接调用 `to_s` 方法: ```ruby 7.to_param # => "7" ``` -The return value of `to_param` should **not** be escaped: +`to_param` 方法的返回值**不应该**转义: ```ruby "Tom & Jerry".to_param # => "Tom & Jerry" ``` -Several classes in Rails overwrite this method. +Rails 中的很多类都覆盖了这个方法。 -For example `nil`, `true`, and `false` return themselves. `Array#to_param` calls `to_param` on the elements and joins the result with "/": +例如,`nil`、`true` 和 `false` 返回自身。`Array#to_param` 在各个元素上调用 `to_param` 方法,然后使用 `"/"` 合并: ```ruby [0, true, String].to_param # => "0/true/String" ``` -Notably, the Rails routing system calls `to_param` on models to get a value for the `:id` placeholder. `ActiveRecord::Base#to_param` returns the `id` of a model, but you can redefine that method in your models. For example, given +注意,Rails 的路由系统在模型上调用 `to_param` 方法获取占位符 `:id` 的值。`ActiveRecord::Base#to_param` 返回模型的 `id`,不过可以在模型中重新定义。例如,按照下面的方式重新定义: ```ruby class User @@ -322,19 +355,21 @@ class User end ``` -we get: +效果如下: ```ruby user_path(@user) # => "/users/357-john-smith" ``` -WARNING. Controllers need to be aware of any redefinition of `to_param` because when a request like that comes in "357-john-smith" is the value of `params[:id]`. +WARNING: 应该让控制器知道重新定义了 `to_param` 方法,因为接收到上面这种请求后,`params[:id]` 的值为 `"357-john-smith"`。 + +NOTE: 在 `active_support/core_ext/object/to_param.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/object/to_param.rb`. + ### `to_query` -Except for hashes, given an unescaped `key` this method constructs the part of a query string that would map such key to what `to_param` returns. For example, given +除散列之外,传入未转义的 `key`,`to_query` 方法把 `to_param` 方法的返回值赋值给 `key`,组成查询字符串。例如,重新定义了 `to_param` 方法: ```ruby class User @@ -344,51 +379,53 @@ class User end ``` -we get: +效果如下: ```ruby current_user.to_query('user') # => user=357-john-smith ``` -This method escapes whatever is needed, both for the key and the value: +`to_query` 方法会根据需要转义键和值: ```ruby account.to_query('company[name]') # => "company%5Bname%5D=Johnson+%26+Johnson" ``` -so its output is ready to be used in a query string. +因此得到的值可以作为查询字符串使用。 -Arrays return the result of applying `to_query` to each element with `_key_[]` as key, and join the result with "&": +`Array#to_query` 方法在各个元素上调用 `to_query` 方法,键为 `key[]`,然后使用 `"&"` 合并: ```ruby [3.4, -45.6].to_query('sample') # => "sample%5B%5D=3.4&sample%5B%5D=-45.6" ``` -Hashes also respond to `to_query` but with a different signature. If no argument is passed a call generates a sorted series of key/value assignments calling `to_query(key)` on its values. Then it joins the result with "&": +散列也响应 `to_query` 方法,但处理方式不一样。如果不传入参数,先在各个元素上调用 `to_query(key)`,得到一系列键值对赋值字符串,然后按照键的顺序排列,再使用 `"&"` 合并: ```ruby {c: 3, b: 2, a: 1}.to_query # => "a=1&b=2&c=3" ``` -The method `Hash#to_query` accepts an optional namespace for the keys: +`Hash#to_query` 方法还有一个可选参数,用于指定键的命名空间: -```ruby +```rb {id: 89, name: "John Smith"}.to_query('user') # => "user%5Bid%5D=89&user%5Bname%5D=John+Smith" ``` -NOTE: Defined in `active_support/core_ext/object/to_query.rb`. +NOTE: 在 `active_support/core_ext/object/to_query.rb` 文件中定义。 + + ### `with_options` -The method `with_options` provides a way to factor out common options in a series of method calls. +`with_options` 方法把一系列方法调用中的通用选项提取出来。 -Given a default options hash, `with_options` yields a proxy object to a block. Within the block, methods called on the proxy are forwarded to the receiver with their options merged. For example, you get rid of the duplication in: +使用散列指定通用选项后,`with_options` 方法会把一个代理对象拽入代码块。在代码块中,代理对象调用的方法会转发给调用者,并合并选项。例如,如下的代码 ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord has_many :customers, dependent: :destroy has_many :products, dependent: :destroy has_many :invoices, dependent: :destroy @@ -396,10 +433,10 @@ class Account < ActiveRecord::Base end ``` -this way: +其中的重复可以使用 `with_options` 方法去除: ```ruby -class Account < ActiveRecord::Base +class Account < ApplicationRecord with_options dependent: :destroy do |assoc| assoc.has_many :customers assoc.has_many :products @@ -409,7 +446,7 @@ class Account < ActiveRecord::Base end ``` -That idiom may convey _grouping_ to the reader as well. For example, say you want to send a newsletter whose language depends on the user. Somewhere in the mailer you could group locale-dependent bits like this: +这种用法还可形成一种分组方式。假如想根据用户使用的语言发送不同的电子报,在邮件发送程序中可以根据用户的区域设置分组: ```ruby I18n.with_options locale: user.locale, scope: "newsletter" do |i18n| @@ -418,24 +455,29 @@ I18n.with_options locale: user.locale, scope: "newsletter" do |i18n| end ``` -TIP: Since `with_options` forwards calls to its receiver they can be nested. Each nesting level will merge inherited defaults in addition to their own. +TIP: `with_options` 方法会把方法调用转发给调用者,因此可以嵌套使用。每层嵌套都会合并上一层的选项。 + +NOTE: 在 `active_support/core_ext/object/with_options.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/object/with_options.rb`. + -### JSON support +### 对 JSON 的支持 -Active Support provides a better implementation of `to_json` than the `json` gem ordinarily provides for Ruby objects. This is because some classes, like `Hash`, `OrderedHash` and `Process::Status` need special handling in order to provide a proper JSON representation. +Active Support 实现的 `to_json` 方法比 `json` gem 更好用,这是因为 `Hash`、`OrderedHash` 和 `Process::Status` 等类转换成 JSON 时要做特别处理。 -NOTE: Defined in `active_support/core_ext/object/json.rb`. +NOTE: 在 `active_support/core_ext/object/json.rb` 文件中定义。 -### Instance Variables + -Active Support provides several methods to ease access to instance variables. +### 实例变量 + +Active Support 提供了很多便于访问实例变量的方法。 + + #### `instance_values` -The method `instance_values` returns a hash that maps instance variable names without "@" to their -corresponding values. Keys are strings: +`instance_values` 方法返回一个散列,把实例变量的名称(不含前面的 `@` 符号)映射到其值上,键是字符串: ```ruby class C @@ -447,11 +489,13 @@ end C.new(0, 1).instance_values # => {"x" => 0, "y" => 1} ``` -NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`. +NOTE: 在 `active_support/core_ext/object/instance_variables.rb` 文件中定义。 + + #### `instance_variable_names` -The method `instance_variable_names` returns an array. Each name includes the "@" sign. +`instance_variable_names` 方法返回一个数组,实例变量的名称前面包含 `@` 符号。 ```ruby class C @@ -463,48 +507,36 @@ end C.new(0, 1).instance_variable_names # => ["@x", "@y"] ``` -NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`. +NOTE: 在 `active_support/core_ext/object/instance_variables.rb` 文件中定义。 -### Silencing Warnings, Streams, and Exceptions + -The methods `silence_warnings` and `enable_warnings` change the value of `$VERBOSE` accordingly for the duration of their block, and reset it afterwards: +### 静默警告和异常 -```ruby -silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } -``` - -You can silence any stream while a block runs with `silence_stream`: - -```ruby -silence_stream(STDOUT) do - # STDOUT is silent here -end -``` - -The `quietly` method addresses the common use case where you want to silence STDOUT and STDERR, even in subprocesses: +`silence_warnings` 和 `enable_warnings` 方法修改各自代码块的 `$VERBOSE` 全局变量,代码块结束后恢复原值: ```ruby -quietly { system 'bundle install' } +silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } ``` -For example, the railties test suite uses that one in a few places to prevent command messages from being echoed intermixed with the progress status. - -Silencing exceptions is also possible with `suppress`. This method receives an arbitrary number of exception classes. If an exception is raised during the execution of the block and is `kind_of?` any of the arguments, `suppress` captures it and returns silently. Otherwise the exception is reraised: +异常消息也可静默,使用 `suppress` 方法即可。`suppress` 方法可接受任意个异常类。如果执行代码块的过程中抛出异常,而且异常属于(`kind_of?`)参数指定的类,`suppress` 方法会静默该异常类的消息,否则抛出异常: ```ruby -# If the user is locked the increment is lost, no big deal. +# 如果用户锁定了,访问次数不增加也没关系 suppress(ActiveRecord::StaleObjectError) do current_user.increment! :visits end ``` -NOTE: Defined in `active_support/core_ext/kernel/reporting.rb`. +NOTE: 在 `active_support/core_ext/kernel/reporting.rb` 文件中定义。 + + ### `in?` -The predicate `in?` tests if an object is included in another object. An `ArgumentError` exception will be raised if the argument passed does not respond to `include?`. +`in?` 方法测试某个对象是否在另一个对象中。如果传入的对象不能响应 `include?` 方法,抛出 `ArgumentError` 异常。 -Examples of `in?`: +`in?` 方法使用举例: ```ruby 1.in?([1,2]) # => true @@ -513,102 +545,59 @@ Examples of `in?`: 1.in?(1) # => ArgumentError ``` -NOTE: Defined in `active_support/core_ext/object/inclusion.rb`. - -Extensions to `Module` ----------------------- - -### `alias_method_chain` - -Using plain Ruby you can wrap methods with other methods, that's called _alias chaining_. - -For example, let's say you'd like params to be strings in functional tests, as they are in real requests, but still want the convenience of assigning integers and other kind of values. To accomplish that you could wrap `ActionController::TestCase#process` this way in `test/test_helper.rb`: - -```ruby -ActionController::TestCase.class_eval do - # save a reference to the original process method - alias_method :original_process, :process - - # now redefine process and delegate to original_process - def process(action, params=nil, session=nil, flash=nil, http_method='GET') - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - original_process(action, params, session, flash, http_method) - end -end -``` - -That's the method `get`, `post`, etc., delegate the work to. +NOTE: 在 `active_support/core_ext/object/inclusion.rb` 文件中定义。 -That technique has a risk, it could be the case that `:original_process` was taken. To try to avoid collisions people choose some label that characterizes what the chaining is about: + -```ruby -ActionController::TestCase.class_eval do - def process_with_stringified_params(...) - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(action, params, session, flash, http_method) - end - alias_method :process_without_stringified_params, :process - alias_method :process, :process_with_stringified_params -end -``` - -The method `alias_method_chain` provides a shortcut for that pattern: - -```ruby -ActionController::TestCase.class_eval do - def process_with_stringified_params(...) - params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten] - process_without_stringified_params(action, params, session, flash, http_method) - end - alias_method_chain :process, :stringified_params -end -``` +## `Module` 的扩展 -Rails uses `alias_method_chain` all over the code base. For example validations are added to `ActiveRecord::Base#save` by wrapping the method that way in a separate module specialized in validations. + -NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. +### 属性 -### Attributes + #### `alias_attribute` -Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (one mnemonic is that they go in the same order as if you did an assignment): +模型的属性有读值方法、设值方法和判断方法。`alias_attribute` 方法可以一次性为这三种方法创建别名。和其他创建别名的方法一样,`alias_attribute` 方法的第一个参数是新属性名,第二个参数是旧属性名(我是这样记的,参数的顺序和赋值语句一样): ```ruby -class User < ActiveRecord::Base - # You can refer to the email column as "login". - # This can be meaningful for authentication code. +class User < ApplicationRecord + # 可以使用 login 指代 email 列 + # 在身份验证代码中可以这样做 alias_attribute :login, :email end ``` -NOTE: Defined in `active_support/core_ext/module/aliasing.rb`. +NOTE: 在 `active_support/core_ext/module/aliasing.rb` 文件中定义。 + + -#### Internal Attributes +#### 内部属性 -When you are defining an attribute in a class that is meant to be subclassed, name collisions are a risk. That's remarkably important for libraries. +如果在父类中定义属性,有可能会出现命名冲突。代码库一定要注意这个问题。 -Active Support defines the macros `attr_internal_reader`, `attr_internal_writer`, and `attr_internal_accessor`. They behave like their Ruby built-in `attr_*` counterparts, except they name the underlying instance variable in a way that makes collisions less likely. +Active Support 提供了 `attr_internal_reader`、`attr_internal_writer` 和 `attr_internal_accessor` 三个方法,其行为与 Ruby 内置的 `attr_*` 方法类似,但使用其他方式命名实例变量,从而减少重名的几率。 -The macro `attr_internal` is a synonym for `attr_internal_accessor`: +`attr_internal` 方法是 `attr_internal_accessor` 方法的别名: ```ruby -# library +# 库 class ThirdPartyLibrary::Crawler attr_internal :log_level end -# client code +# 客户代码 class MyCrawler < ThirdPartyLibrary::Crawler attr_accessor :log_level end ``` -In the previous example it could be the case that `:log_level` does not belong to the public interface of the library and it is only used for development. The client code, unaware of the potential conflict, subclasses and defines its own `:log_level`. Thanks to `attr_internal` there's no collision. +在上面的例子中,`:log_level` 可能不属于代码库的公开接口,只在开发过程中使用。开发者并不知道潜在的重名风险,创建了子类,并在子类中定义了 `:log_level`。幸好用了 `attr_internal` 方法才不会出现命名冲突。 -By default the internal instance variable is named with a leading underscore, `@_log_level` in the example above. That's configurable via `Module.attr_internal_naming_format` though, you can pass any `sprintf`-like format string with a leading `@` and a `%s` somewhere, which is where the name will be placed. The default is `"@_%s"`. +默认情况下,内部变量的名字前面有个下划线,上例中的内部变量名为 `@_log_level`。不过可使用 `Module.attr_internal_naming_format` 重新设置,可以传入任何 `sprintf` 方法能理解的格式,开头加上 `@` 符号,并在某处放入 `%s`(代表原变量名)。默认的设置为 `"@_%s"`。 -Rails uses internal attributes in a few spots, for examples for views: +Rails 的代码很多地方都用到了内部属性,例如,在视图相关的代码中有如下代码: ```ruby module ActionView @@ -620,13 +609,15 @@ module ActionView end ``` -NOTE: Defined in `active_support/core_ext/module/attr_internal.rb`. +NOTE: 在 `active_support/core_ext/module/attr_internal.rb` 文件中定义。 + + -#### Module Attributes +#### 模块属性 -The macros `mattr_reader`, `mattr_writer`, and `mattr_accessor` are the same as the `cattr_*` macros defined for class. In fact, the `cattr_*` macros are just aliases for the `mattr_*` macros. Check [Class Attributes](#class-attributes). +方法 `mattr_reader`、`mattr_writer` 和 `mattr_accessor` 类似于为类定义的 `cattr_*` 方法。其实 `cattr_*` 方法就是 `mattr_*` 方法的别名。参见 [类属性](#class-attributes)。 -For example, the dependencies mechanism uses them: +例如,依赖机制就用到了这些方法: ```ruby module ActiveSupport @@ -639,21 +630,23 @@ module ActiveSupport mattr_accessor :load_once_paths mattr_accessor :autoloaded_constants mattr_accessor :explicitly_unloadable_constants - mattr_accessor :logger - mattr_accessor :log_activity mattr_accessor :constant_watch_stack mattr_accessor :constant_watch_stack_mutex end end ``` -NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`. +NOTE: 在 `active_support/core_ext/module/attribute_accessors.rb` 文件中定义。 -### Parents + + +### 父级 + + #### `parent` -The `parent` method on a nested named module returns the module that contains its corresponding constant: +在嵌套的具名模块上调用 `parent` 方法,返回包含对应常量的模块: ```ruby module X @@ -668,15 +661,17 @@ X::Y::Z.parent # => X::Y M.parent # => X::Y ``` -If the module is anonymous or belongs to the top-level, `parent` returns `Object`. +如果是匿名模块或者位于顶层,`parent` 方法返回 `Object`。 + +WARNING: 此时,`parent_name` 方法返回 `nil`。 -WARNING: Note that in that case `parent_name` returns `nil`. +NOTE: 在 `active_support/core_ext/module/introspection.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/module/introspection.rb`. + #### `parent_name` -The `parent_name` method on a nested named module returns the fully-qualified name of the module that contains its corresponding constant: +在嵌套的具名模块上调用 `parent_name` 方法,返回包含对应常量的完全限定模块名: ```ruby module X @@ -691,15 +686,17 @@ X::Y::Z.parent_name # => "X::Y" M.parent_name # => "X::Y" ``` -For top-level or anonymous modules `parent_name` returns `nil`. +如果是匿名模块或者位于顶层,`parent_name` 方法返回 `nil`。 -WARNING: Note that in that case `parent` returns `Object`. +WARNING: 注意,此时 `parent` 方法返回 `Object`。 -NOTE: Defined in `active_support/core_ext/module/introspection.rb`. +NOTE: 在 `active_support/core_ext/module/introspection.rb` 文件中定义。 + + #### `parents` -The method `parents` calls `parent` on the receiver and upwards until `Object` is reached. The chain is returned in an array, from bottom to top: +`parents` 方法在调用者上调用 `parent` 方法,直至 `Object` 为止。返回的结果是一个数组,由底而上: ```ruby module X @@ -714,94 +711,15 @@ X::Y::Z.parents # => [X::Y, X, Object] M.parents # => [X::Y, X, Object] ``` -NOTE: Defined in `active_support/core_ext/module/introspection.rb`. - -### Constants - -The method `local_constants` returns the names of the constants that have been -defined in the receiver module: - -```ruby -module X - X1 = 1 - X2 = 2 - module Y - Y1 = :y1 - X1 = :overrides_X1_above - end -end - -X.local_constants # => [:X1, :X2, :Y] -X::Y.local_constants # => [:Y1, :X1] -``` - -The names are returned as symbols. - -NOTE: Defined in `active_support/core_ext/module/introspection.rb`. - -#### Qualified Constant Names - -The standard methods `const_defined?`, `const_get` , and `const_set` accept -bare constant names. Active Support extends this API to be able to pass -relative qualified constant names. - -The new methods are `qualified_const_defined?`, `qualified_const_get`, and -`qualified_const_set`. Their arguments are assumed to be qualified constant -names relative to their receiver: - -```ruby -Object.qualified_const_defined?("Math::PI") # => true -Object.qualified_const_get("Math::PI") # => 3.141592653589793 -Object.qualified_const_set("Math::Phi", 1.618034) # => 1.618034 -``` - -Arguments may be bare constant names: - -```ruby -Math.qualified_const_get("E") # => 2.718281828459045 -``` - -These methods are analogous to their built-in counterparts. In particular, -`qualified_constant_defined?` accepts an optional second argument to be -able to say whether you want the predicate to look in the ancestors. -This flag is taken into account for each constant in the expression while -walking down the path. - -For example, given - -```ruby -module M - X = 1 -end - -module N - class C - include M - end -end -``` - -`qualified_const_defined?` behaves this way: - -```ruby -N.qualified_const_defined?("C::X", false) # => false -N.qualified_const_defined?("C::X", true) # => true -N.qualified_const_defined?("C::X") # => true -``` - -As the last example implies, the second argument defaults to true, -as in `const_defined?`. - -For coherence with the built-in methods only relative paths are accepted. -Absolute qualified constant names like `::Math::PI` raise `NameError`. +NOTE: 在 `active_support/core_ext/module/introspection.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/module/qualified_const.rb`. + -### Reachable +### 可达性 -A named module is reachable if it is stored in its corresponding constant. It means you can reach the module object via the constant. +如果把具名模块存储在相应的常量中,模块是可达的,意即可以通过常量访问模块对象。 -That is what ordinarily happens, if a module is called "M", the `M` constant exists and holds it: +通常,模块都是如此。如果有名为“M”的模块,`M` 常量就存在,指代那个模块: ```ruby module M @@ -810,7 +728,7 @@ end M.reachable? # => true ``` -But since constants and modules are indeed kind of decoupled, module objects can become unreachable: +但是,常量和模块其实是解耦的,因此模块对象也许不可达: ```ruby module M @@ -818,26 +736,28 @@ end orphan = Object.send(:remove_const, :M) -# The module object is orphan now but it still has a name. +# 现在模块对象是孤儿,但它仍有名称 orphan.name # => "M" -# You cannot reach it via the constant M because it does not even exist. +# 不能通过常量 M 访问,因为这个常量不存在 orphan.reachable? # => false -# Let's define a module called "M" again. +# 再定义一个名为“M”的模块 module M end -# The constant M exists now again, and it stores a module -# object called "M", but it is a new instance. +# 现在常量 M 存在了,而且存储名为“M”的常量对象 +# 但这是一个新实例 orphan.reachable? # => false ``` -NOTE: Defined in `active_support/core_ext/module/reachable.rb`. +NOTE: 在 `active_support/core_ext/module/reachable.rb` 文件中定义。 + + -### Anonymous +### 匿名 -A module may or may not have a name: +模块可能有也可能没有名称: ```ruby module M @@ -850,7 +770,7 @@ N.name # => "N" Module.new.name # => nil ``` -You can check whether a module has a name with the predicate `anonymous?`: +可以使用 `anonymous?` 方法判断模块有没有名称: ```ruby module M @@ -860,7 +780,7 @@ M.anonymous? # => false Module.new.anonymous? # => true ``` -Note that being unreachable does not imply being anonymous: +注意,不可达不意味着就是匿名的: ```ruby module M @@ -872,26 +792,28 @@ m.reachable? # => false m.anonymous? # => false ``` -though an anonymous module is unreachable by definition. +但是按照定义,匿名模块是不可达的。 -NOTE: Defined in `active_support/core_ext/module/anonymous.rb`. +NOTE: 在 `active_support/core_ext/module/anonymous.rb` 文件中定义。 -### Method Delegation + -The macro `delegate` offers an easy way to forward methods. +### 方法委托 -Let's imagine that users in some application have login information in the `User` model but name and other data in a separate `Profile` model: +`delegate` 方法提供一种便利的方法转发方式。 + +假设在一个应用中,用户的登录信息存储在 `User` 模型中,而名字和其他数据存储在 `Profile` 模型中: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile end ``` -With that configuration you get a user's name via their profile, `user.profile.name`, but it could be handy to still be able to access such attribute directly: +此时,要通过个人资料获取用户的名字,即 `user.profile.name`。不过,若能直接访问这些信息更为便利: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile def name @@ -900,82 +822,89 @@ class User < ActiveRecord::Base end ``` -That is what `delegate` does for you: +`delegate` 方法正是为这种需求而生的: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_one :profile delegate :name, to: :profile end ``` -It is shorter, and the intention more obvious. +这样写出的代码更简洁,而且意图更明显。 -The method must be public in the target. +委托的方法在目标中必须是公开的。 -The `delegate` macro accepts several methods: +`delegate` 方法可接受多个参数,委托多个方法: ```ruby delegate :name, :age, :address, :twitter, to: :profile ``` -When interpolated into a string, the `:to` option should become an expression that evaluates to the object the method is delegated to. Typically a string or symbol. Such an expression is evaluated in the context of the receiver: +内插到字符串中时,`:to` 选项的值应该能求值为方法委托的对象。通常,使用字符串或符号。这个选项的值在接收者的上下文中求值: ```ruby -# delegates to the Rails constant +# 委托给 Rails 常量 delegate :logger, to: :Rails -# delegates to the receiver's class +# 委托给接收者所属的类 delegate :table_name, to: :class ``` -WARNING: If the `:prefix` option is `true` this is less generic, see below. +WARNING: 如果 `:prefix` 选项的值为 `true`,不能这么做。参见下文。 -By default, if the delegation raises `NoMethodError` and the target is `nil` the exception is propagated. You can ask that `nil` is returned instead with the `:allow_nil` option: +默认情况下,如果委托导致 `NoMethodError` 抛出,而且目标是 `nil`,这个异常会向上冒泡。可以指定 `:allow_nil` 选项,遇到这种情况时返回 `nil`: ```ruby delegate :name, to: :profile, allow_nil: true ``` -With `:allow_nil` the call `user.name` returns `nil` if the user has no profile. +设定 `:allow_nil` 选项后,如果用户没有个人资料,`user.name` 返回 `nil`。 -The option `:prefix` adds a prefix to the name of the generated method. This may be handy for example to get a better name: +`:prefix` 选项在生成的方法前面添加一个前缀。如果想起个更好的名称,就可以使用这个选项: ```ruby delegate :street, to: :address, prefix: true ``` -The previous example generates `address_street` rather than `street`. +上述示例生成的方法是 `address_street`,而不是 `street`。 -WARNING: Since in this case the name of the generated method is composed of the target object and target method names, the `:to` option must be a method name. +WARNING: 此时,生成的方法名由目标对象和目标方法的名称构成,因此 `:to` 选项必须是一个方法名。 -A custom prefix may also be configured: +此外,还可以自定义前缀: ```ruby delegate :size, to: :attachment, prefix: :avatar ``` -In the previous example the macro generates `avatar_size` rather than `size`. +在这个示例中,生成的方法是 `avatar_size`,而不是 `size`。 + +NOTE: 在 `active_support/core_ext/module/delegation.rb` 文件中定义。 + + -NOTE: Defined in `active_support/core_ext/module/delegation.rb` +### 重新定义方法 -### Redefining Methods +有时需要使用 `define_method` 定义方法,但却不知道那个方法名是否已经存在。如果存在,而且启用了警告消息,会发出警告。这没什么,但却不够利落。 -There are cases where you need to define a method with `define_method`, but don't know whether a method with that name already exists. If it does, a warning is issued if they are enabled. No big deal, but not clean either. +`redefine_method` 方法能避免这种警告,如果需要,会把现有的方法删除。 -The method `redefine_method` prevents such a potential warning, removing the existing method before if needed. +NOTE: 在 `active_support/core_ext/module/remove_method.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/module/remove_method.rb` + -Extensions to `Class` ---------------------- +## `Class` 的扩展 -### Class Attributes + + +### 类属性 + + #### `class_attribute` -The method `class_attribute` declares one or more inheritable class attributes that can be overridden at any level down the hierarchy. +`class_attribute` 方法声明一个或多个可继承的类属性,它们可以在继承树的任一层级覆盖。 ```ruby class A @@ -999,7 +928,7 @@ A.x # => :a B.x # => :b ``` -For example `ActionMailer::Base` defines: +例如,`ActionMailer::Base` 定义了: ```ruby class_attribute :default_params @@ -1011,7 +940,7 @@ self.default_params = { }.freeze ``` -They can be also accessed and overridden at the instance level. +类属性还可以通过实例访问和覆盖: ```ruby A.x = 1 @@ -1024,7 +953,7 @@ a1.x # => 1, comes from A a2.x # => 2, overridden in a2 ``` -The generation of the writer instance method can be prevented by setting the option `:instance_writer` to `false`. +把 `:instance_writer` 选项设为 `false`,不生成设值实例方法: ```ruby module ActiveRecord @@ -1035,39 +964,42 @@ module ActiveRecord end ``` -A model may find that option useful as a way to prevent mass-assignment from setting the attribute. +模型可以使用这个选项,禁止批量赋值属性。 -The generation of the reader instance method can be prevented by setting the option `:instance_reader` to `false`. +把 `:instance_reader` 选项设为 `false`,不生成读值实例方法: ```ruby class A class_attribute :x, instance_reader: false end -A.new.x = 1 # NoMethodError +A.new.x = 1 +A.new.x # NoMethodError ``` -For convenience `class_attribute` also defines an instance predicate which is the double negation of what the instance reader returns. In the examples above it would be called `x?`. +为了方便,`class_attribute` 还会定义实例判断方法,对实例读值方法的返回值做双重否定。在上例中,判断方法是 `x?`。 + +如果 `:instance_reader` 的值是 `false`,实例判断方法与读值方法一样,返回 `NoMethodError`。 -When `:instance_reader` is `false`, the instance predicate returns a `NoMethodError` just like the reader method. +如果不想要实例判断方法,传入 `instance_predicate: false`,这样就不会定义了。 -If you do not want the instance predicate, pass `instance_predicate: false` and it will not be defined. +NOTE: 在 `active_support/core_ext/class/attribute.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/class/attribute.rb` + -#### `cattr_reader`, `cattr_writer`, and `cattr_accessor` +#### `cattr_reader`、`cattr_writer` 和 `cattr_accessor` -The macros `cattr_reader`, `cattr_writer`, and `cattr_accessor` are analogous to their `attr_*` counterparts but for classes. They initialize a class variable to `nil` unless it already exists, and generate the corresponding class methods to access it: +`cattr_reader`、`cattr_writer` 和 `cattr_accessor` 的作用与相应的 `attr_*` 方法类似,不过是针对类的。它们声明的类属性,初始值为 `nil`,除非在此之前类属性已经存在,而且会生成相应的访问方法: ```ruby class MysqlAdapter < AbstractAdapter - # Generates class methods to access @@emulate_booleans. + # 生成访问 @@emulate_booleans 的类方法 cattr_accessor :emulate_booleans self.emulate_booleans = true end ``` -Instance methods are created as well for convenience, they are just proxies to the class attribute. So, instances can change the class attribute, but cannot override it as it happens with `class_attribute` (see above). For example given +为了方便,也会生成实例方法,这些实例方法只是类属性的代理。因此,实例可以修改类属性,但是不能覆盖——这与 `class_attribute` 不同(参见上文)。例如: ```ruby module ActionView @@ -1078,41 +1010,45 @@ module ActionView end ``` -we can access `field_error_proc` in views. +这样,我们便可以在视图中访问 `field_error_proc`。 -Also, you can pass a block to `cattr_*` to set up the attribute with a default value: +此外,可以把一个块传给 `cattr_*` 方法,设定属性的默认值: ```ruby class MysqlAdapter < AbstractAdapter - # Generates class methods to access @@emulate_booleans with default value of true. + # 生成访问 @@emulate_booleans 的类方法,其默认值为 true cattr_accessor(:emulate_booleans) { true } end ``` -The generation of the reader instance method can be prevented by setting `:instance_reader` to `false` and the generation of the writer instance method can be prevented by setting `:instance_writer` to `false`. Generation of both methods can be prevented by setting `:instance_accessor` to `false`. In all cases, the value must be exactly `false` and not any false value. +把 `:instance_reader` 设为 `false`,不生成实例读值方法,把 `:instance_writer` 设为 `false`,不生成实例设值方法,把 `:instance_accessor` 设为 `false`,实例读值和设置方法都不生成。此时,这三个选项的值都必须是 `false`,而不能是假值。 ```ruby module A class B - # No first_name instance reader is generated. + # 不生成实例读值方法 first_name cattr_accessor :first_name, instance_reader: false - # No last_name= instance writer is generated. + # 不生成实例设值方法 last_name= cattr_accessor :last_name, instance_writer: false - # No surname instance reader or surname= writer is generated. + # 不生成实例读值方法 surname 和实例设值方法 surname= cattr_accessor :surname, instance_accessor: false end end ``` -A model may find it useful to set `:instance_accessor` to `false` as a way to prevent mass-assignment from setting the attribute. +在模型中可以把 `:instance_accessor` 设为 `false`,防止批量赋值属性。 -NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`. +NOTE: 在 `active_support/core_ext/module/attribute_accessors.rb` 文件中定义。 -### Subclasses & Descendants + + +### 子类和后代 + + #### `subclasses` -The `subclasses` method returns the subclasses of the receiver: +`subclasses` 方法返回接收者的子类: ```ruby class C; end @@ -1128,13 +1064,15 @@ class D < C; end C.subclasses # => [B, D] ``` -The order in which these classes are returned is unspecified. +返回的子类没有特定顺序。 + +NOTE: 在 `active_support/core_ext/class/subclasses.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/class/subclasses.rb`. + #### `descendants` -The `descendants` method returns all classes that are `<` than its receiver: +`descendants` 方法返回接收者的后代: ```ruby class C; end @@ -1150,37 +1088,44 @@ class D < C; end C.descendants # => [B, A, D] ``` -The order in which these classes are returned is unspecified. +返回的后代没有特定顺序。 -NOTE: Defined in `active_support/core_ext/class/subclasses.rb`. +NOTE: 在 `active_support/core_ext/class/subclasses.rb` 文件中定义。 -Extensions to `String` ----------------------- + -### Output Safety +## `String` 的扩展 -#### Motivation + -Inserting data into HTML templates needs extra care. For example, you can't just interpolate `@review.title` verbatim into an HTML page. For one thing, if the review title is "Flanagan & Matz rules!" the output won't be well-formed because an ampersand has to be escaped as "&amp;". What's more, depending on the application, that may be a big security hole because users can inject malicious HTML setting a hand-crafted review title. Check out the section about cross-site scripting in the [Security guide](security.html#cross-site-scripting-xss) for further information about the risks. +### 输出的安全性 -#### Safe Strings + -Active Support has the concept of (html) safe strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not. +#### 引子 -Strings are considered to be unsafe by default: +把数据插入 HTML 模板要格外小心。例如,不能原封不动地把 `@review.title` 内插到 HTML 页面中。假如标题是“Flanagan & Matz rules!”,得到的输出格式就不对,因为 & 会转义成“&amp;”。更糟的是,如果应用编写不当,这可能留下严重的安全漏洞,因为用户可以注入恶意的 HTML,设定精心编造的标题。关于这个问题的详情,请阅读 [跨站脚本(XSS)](security.html#cross-site-scripting-xss)对跨站脚本的说明。 + + + +#### 安全字符串 + +Active Support 提出了安全字符串(对 HTML 而言)这一概念。安全字符串是对字符串做的一种标记,表示可以原封不动地插入 HTML。这种字符串是可信赖的,不管会不会转义。 + +默认,字符串被认为是不安全的: ```ruby "".html_safe? # => false ``` -You can obtain a safe string from a given one with the `html_safe` method: +可以使用 `html_safe` 方法把指定的字符串标记为安全的: ```ruby s = "".html_safe s.html_safe? # => true ``` -It is important to understand that `html_safe` performs no escaping whatsoever, it is just an assertion: +注意,无论如何,`html_safe` 不会执行转义操作,它的作用只是一种断定: ```ruby s = "".html_safe @@ -1188,39 +1133,39 @@ s.html_safe? # => true s # => "" ``` -It is your responsibility to ensure calling `html_safe` on a particular string is fine. +你要自己确定该不该在某个字符串上调用 `html_safe`。 -If you append onto a safe string, either in-place with `concat`/`<<`, or with `+`, the result is a safe string. Unsafe arguments are escaped: +如果把字符串追加到安全字符串上,不管是就地修改,还是使用 `concat`/`<<` 或 `+`,结果都是一个安全字符串。不安全的字符会转义: ```ruby "".html_safe + "<" # => "<" ``` -Safe arguments are directly appended: +安全的字符直接追加: ```ruby "".html_safe + "<".html_safe # => "<" ``` -These methods should not be used in ordinary views. Unsafe values are automatically escaped: +在常规的视图中不应该使用这些方法。不安全的值会自动转义: ```erb -<%= @review.title %> <%# fine, escaped if needed %> +<%= @review.title %> <%# 可以这么做,如果需要会转义 %> ``` -To insert something verbatim use the `raw` helper rather than calling `html_safe`: +如果想原封不动地插入值,不能调用 `html_safe`,而要使用 `raw` 辅助方法: ```erb -<%= raw @cms.current_template %> <%# inserts @cms.current_template as is %> +<%= raw @cms.current_template %> <%# 原封不动地插入 @cms.current_template %> ``` -or, equivalently, use `<%==`: +或者,可以使用等效的 `<%==`: ```erb -<%== @cms.current_template %> <%# inserts @cms.current_template as is %> +<%== @cms.current_template %> <%# 原封不动地插入 @cms.current_template %> ``` -The `raw` helper calls `html_safe` for you: +`raw` 辅助方法已经调用 `html_safe` 了: ```ruby def raw(stringish) @@ -1228,69 +1173,81 @@ def raw(stringish) end ``` -NOTE: Defined in `active_support/core_ext/string/output_safety.rb`. +NOTE: 在 `active_support/core_ext/string/output_safety.rb` 文件中定义。 + + + +#### 转换 -#### Transformation +通常,修改字符串的方法都返回不安全的字符串,前文所述的拼接除外。例如,`downcase`、`gsub`、`strip`、`chomp`、`underscore`,等等。 -As a rule of thumb, except perhaps for concatenation as explained above, any method that may change a string gives you an unsafe string. These are `downcase`, `gsub`, `strip`, `chomp`, `underscore`, etc. +就地转换接收者,如 `gsub!`,其本身也变成不安全的了。 -In the case of in-place transformations like `gsub!` the receiver itself becomes unsafe. +TIP: 不管是否修改了自身,安全性都丧失了。 -INFO: The safety bit is lost always, no matter whether the transformation actually changed something. + -#### Conversion and Coercion +#### 类型转换和强制转换 -Calling `to_s` on a safe string returns a safe string, but coercion with `to_str` returns an unsafe string. +在安全字符串上调用 `to_s`,得到的还是安全字符串,但是使用 `to_str` 强制转换,得到的是不安全的字符串。 -#### Copying + -Calling `dup` or `clone` on safe strings yields safe strings. +#### 复制 + +在安全字符串上调用 `dup` 或 `clone`,得到的还是安全字符串。 + + ### `remove` -The method `remove` will remove all occurrences of the pattern: +`remove` 方法删除匹配模式的所有内容: ```ruby -"Hello World".remove(/Hello /) => "World" +"Hello World".remove(/Hello /) # => "World" ``` -There's also the destructive version `String#remove!`. +也有破坏性版本,`String#remove!`。 + +NOTE: 在 `active_support/core_ext/string/filters.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/string/filters.rb`. + ### `squish` -The method `squish` strips leading and trailing whitespace, and substitutes runs of whitespace with a single space each: +`squish` 方法把首尾的空白去掉,还会把多个空白压缩成一个: ```ruby " \n foo\n\r \t bar \n".squish # => "foo bar" ``` -There's also the destructive version `String#squish!`. +也有破坏性版本,`String#squish!`。 -Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E). +注意,既能处理 ASCII 空白,也能处理 Unicode 空白。 -NOTE: Defined in `active_support/core_ext/string/filters.rb`. +NOTE: 在 `active_support/core_ext/string/filters.rb` 文件中定义。 + + ### `truncate` -The method `truncate` returns a copy of its receiver truncated after a given `length`: +`truncate` 方法在指定长度处截断接收者,返回一个副本: ```ruby "Oh dear! Oh dear! I shall be late!".truncate(20) # => "Oh dear! Oh dear!..." ``` -Ellipsis can be customized with the `:omission` option: +省略号可以使用 `:omission` 选项自定义: ```ruby "Oh dear! Oh dear! I shall be late!".truncate(20, omission: '…') # => "Oh dear! Oh …" ``` -Note in particular that truncation takes into account the length of the omission string. +尤其要注意,截断长度包含省略字符串。 -Pass a `:separator` to truncate the string at a natural break: +设置 `:separator` 选项,以自然的方式截断: ```ruby "Oh dear! Oh dear! I shall be late!".truncate(18) @@ -1299,42 +1256,82 @@ Pass a `:separator` to truncate the string at a natural break: # => "Oh dear! Oh..." ``` -The option `:separator` can be a regexp: +`:separator` 选项的值可以是一个正则表达式: ```ruby "Oh dear! Oh dear! I shall be late!".truncate(18, separator: /\s/) # => "Oh dear! Oh..." ``` -In above examples "dear" gets cut first, but then `:separator` prevents it. +在上述示例中,本该在“dear”中间截断,但是 `:separator` 选项进行了阻止。 + +NOTE: 在 `active_support/core_ext/string/filters.rb` 文件中定义。 + + + +### `truncate_words` + +`truncate_words` 方法在指定个单词处截断接收者,返回一个副本: + +```ruby +"Oh dear! Oh dear! I shall be late!".truncate_words(4) +# => "Oh dear! Oh dear!..." +``` + +省略号可以使用 `:omission` 选项自定义: + +```ruby +"Oh dear! Oh dear! I shall be late!".truncate_words(4, omission: '…') +# => "Oh dear! Oh dear!…" +``` + +设置 `:separator` 选项,以自然的方式截断: + +```ruby +"Oh dear! Oh dear! I shall be late!".truncate_words(3, separator: '!') +# => "Oh dear! Oh dear! I shall be late..." +``` + +`:separator` 选项的值可以是一个正则表达式: + +```ruby +"Oh dear! Oh dear! I shall be late!".truncate_words(4, separator: /\s/) +# => "Oh dear! Oh dear!..." +``` + +NOTE: 在 `active_support/core_ext/string/filters.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/string/filters.rb`. + ### `inquiry` -The `inquiry` method converts a string into a `StringInquirer` object making equality checks prettier. +`inquiry` 方法把字符串转换成 `StringInquirer` 对象,这样可以使用漂亮的方式检查相等性: ```ruby "production".inquiry.production? # => true "active".inquiry.inactive? # => false ``` -### `starts_with?` and `ends_with?` + -Active Support defines 3rd person aliases of `String#start_with?` and `String#end_with?`: +### `starts_with?` 和 `ends_with?` + +Active Support 为 `String#start_with?` 和 `String#end_with?` 定义了第三人称版本: ```ruby "foo".starts_with?("f") # => true "foo".ends_with?("o") # => true ``` -NOTE: Defined in `active_support/core_ext/string/starts_ends_with.rb`. +NOTE: 在 `active_support/core_ext/string/starts_ends_with.rb` 文件中定义。 + + ### `strip_heredoc` -The method `strip_heredoc` strips indentation in heredocs. +`strip_heredoc` 方法去掉 here 文档中的缩进。 -For example in +例如: ```ruby if options[:usage] @@ -1348,16 +1345,17 @@ if options[:usage] end ``` -the user would see the usage message aligned against the left margin. +用户看到的消息会靠左边对齐。 + +从技术层面来说,这个方法寻找整个字符串中的最小缩进量,然后删除那么多的前导空白。 -Technically, it looks for the least indented line in the whole string, and removes -that amount of leading whitespace. +NOTE: 在 `active_support/core_ext/string/strip.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/string/strip.rb`. + ### `indent` -Indents the lines in the receiver: +按指定量缩进接收者: ```ruby < " foo" @@ -1379,24 +1377,28 @@ The second argument, `indent_string`, specifies which indent string to use. The "foo".indent(2, "\t") # => "\t\tfoo" ``` -While `indent_string` is typically one space or tab, it may be any string. +`indent_string` 的值虽然经常设为一个空格或一个制表符,但是可以使用任何字符串。 -The third argument, `indent_empty_lines`, is a flag that says whether empty lines should be indented. Default is false. +第三个参数,`indent_empty_lines`,是个旗标,指明是否缩进空行。默认值是 `false`。 ```ruby "foo\n\nbar".indent(2) # => " foo\n\n bar" "foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar" ``` -The `indent!` method performs indentation in-place. +`indent!` 方法就地执行缩进。 -NOTE: Defined in `active_support/core_ext/string/indent.rb`. +NOTE: 在 `active_support/core_ext/string/indent.rb` 文件中定义。 -### Access + + +### 访问 + + #### `at(position)` -Returns the character of the string at position `position`: +返回字符串中 `position` 位置上的字符: ```ruby "hello".at(0) # => "h" @@ -1405,24 +1407,28 @@ Returns the character of the string at position `position`: "hello".at(10) # => nil ``` -NOTE: Defined in `active_support/core_ext/string/access.rb`. +NOTE: 在 `active_support/core_ext/string/access.rb` 文件中定义。 + + #### `from(position)` -Returns the substring of the string starting at position `position`: +返回子串,从 `position` 位置开始: ```ruby "hello".from(0) # => "hello" "hello".from(2) # => "llo" "hello".from(-2) # => "lo" -"hello".from(10) # => "" if < 1.9, nil in 1.9 +"hello".from(10) # => nil ``` -NOTE: Defined in `active_support/core_ext/string/access.rb`. +NOTE: 在 `active_support/core_ext/string/access.rb` 文件中定义。 + + #### `to(position)` -Returns the substring of the string up to position `position`: +返回子串,到 `position` 位置为止: ```ruby "hello".to(0) # => "h" @@ -1431,25 +1437,33 @@ Returns the substring of the string up to position `position`: "hello".to(10) # => "hello" ``` -NOTE: Defined in `active_support/core_ext/string/access.rb`. +NOTE: 在 `active_support/core_ext/string/access.rb` 文件中定义。 + + #### `first(limit = 1)` -The call `str.first(n)` is equivalent to `str.to(n-1)` if `n` > 0, and returns an empty string for `n` == 0. +如果 `n` > 0,`str.first(n)` 的作用与 `str.to(n-1)` 一样;如果 `n` == 0,返回一个空字符串。 + +NOTE: 在 `active_support/core_ext/string/access.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/string/access.rb`. + #### `last(limit = 1)` -The call `str.last(n)` is equivalent to `str.from(-n)` if `n` > 0, and returns an empty string for `n` == 0. +如果 `n` > 0,`str.last(n)` 的作用与 `str.from(-n)` 一样;如果 `n` == 0,返回一个空字符串。 -NOTE: Defined in `active_support/core_ext/string/access.rb`. +NOTE: 在 `active_support/core_ext/string/access.rb` 文件中定义。 -### Inflections + + +### 词形变化 + + #### `pluralize` -The method `pluralize` returns the plural of its receiver: +`pluralize` 方法返回接收者的复数形式: ```ruby "table".pluralize # => "tables" @@ -1457,9 +1471,9 @@ The method `pluralize` returns the plural of its receiver: "equipment".pluralize # => "equipment" ``` -As the previous example shows, Active Support knows some irregular plurals and uncountable nouns. Built-in rules can be extended in `config/initializers/inflections.rb`. That file is generated by the `rails` command and has instructions in comments. +如上例所示,Active Support 知道如何处理不规则的复数形式和不可数名词。内置的规则可以在 `config/initializers/inflections.rb` 文件中扩展。那个文件是由 `rails` 命令生成的,里面的注释说明了该怎么做。 -`pluralize` can also take an optional `count` parameter. If `count == 1` the singular form will be returned. For any other value of `count` the plural form will be returned: +`pluralize` 还可以接受可选的 `count` 参数。如果 `count == 1`,返回单数形式。把 `count` 设为其他值,都会返回复数形式: ```ruby "dude".pluralize(0) # => "dudes" @@ -1467,7 +1481,7 @@ As the previous example shows, Active Support knows some irregular plurals and u "dude".pluralize(2) # => "dudes" ``` -Active Record uses this method to compute the default table name that corresponds to a model: +Active Record 使用这个方法计算模型对应的默认表名: ```ruby # active_record/model_schema.rb @@ -1477,11 +1491,13 @@ def undecorated_table_name(class_name = base_class.name) end ``` -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `singularize` -The inverse of `pluralize`: +作用与 `pluralize` 相反: ```ruby "tables".singularize # => "table" @@ -1489,7 +1505,7 @@ The inverse of `pluralize`: "equipment".singularize # => "equipment" ``` -Associations compute the name of the corresponding default associated class using this method: +关联使用这个方法计算默认的关联类: ```ruby # active_record/reflection.rb @@ -1500,24 +1516,26 @@ def derive_class_name end ``` -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `camelize` -The method `camelize` returns its receiver in camel case: +`camelize` 方法把接收者变成驼峰式: ```ruby "product".camelize # => "Product" "admin_user".camelize # => "AdminUser" ``` -As a rule of thumb you can think of this method as the one that transforms paths into Ruby class or module names, where slashes separate namespaces: +一般来说,你可以把这个方法的作用想象为把路径转换成 Ruby 类或模块名的方式(使用斜线分隔命名空间): ```ruby "backoffice/session".camelize # => "Backoffice::Session" ``` -For example, Action Pack uses this method to load the class that provides a certain session store: +例如,Action Pack 使用这个方法加载提供特定会话存储功能的类: ```ruby # action_controller/metal/session_management.rb @@ -1528,15 +1546,15 @@ def session_store=(store) end ``` -`camelize` accepts an optional argument, it can be `:upper` (default), or `:lower`. With the latter the first letter becomes lowercase: +`camelize` 接受一个可选的参数,其值可以是 `:upper`(默认值)或 `:lower`。设为后者时,第一个字母是小写的: ```ruby "visual_effect".camelize(:lower) # => "visualEffect" ``` -That may be handy to compute method names in a language that follows that convention, for example JavaScript. +为使用这种风格的语言计算方法名时可以这么设定,例如 JavaScript。 -INFO: As a rule of thumb you can think of `camelize` as the inverse of `underscore`, though there are cases where that does not hold: `"SSLError".underscore.camelize` gives back `"SslError"`. To support cases such as this, Active Support allows you to specify acronyms in `config/initializers/inflections.rb`: +TIP: 一般来说,可以把 `camelize` 视作 `underscore` 的逆操作,不过也有例外:`"SSLError".underscore.camelize` 的结果是 `"SslError"`。为了支持这种情况,Active Support 允许你在 `config/initializers/inflections.rb` 文件中指定缩略词。 ```ruby ActiveSupport::Inflector.inflections do |inflect| @@ -1546,34 +1564,37 @@ end "SSLError".underscore.camelize # => "SSLError" ``` -`camelize` is aliased to `camelcase`. -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +`camelcase` 是 `camelize` 的别名。 + +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `underscore` -The method `underscore` goes the other way around, from camel case to paths: +`underscore` 方法的作用相反,把驼峰式变成蛇底式: ```ruby "Product".underscore # => "product" "AdminUser".underscore # => "admin_user" ``` -Also converts "::" back to "/": +还会把 `"::"` 转换成 `"/"`: ```ruby "Backoffice::Session".underscore # => "backoffice/session" ``` -and understands strings that start with lowercase: +也能理解以小写字母开头的字符串: ```ruby "visualEffect".underscore # => "visual_effect" ``` -`underscore` accepts no argument though. +不过,`underscore` 不接受任何参数。 -Rails class and module autoloading uses `underscore` to infer the relative path without extension of a file that would define a given missing constant: +Rails 自动加载类和模块的机制使用 `underscore` 推断可能定义缺失的常量的文件的相对路径(不带扩展名): ```ruby # active_support/dependencies.rb @@ -1585,33 +1606,37 @@ def load_missing_constant(from_mod, const_name) end ``` -INFO: As a rule of thumb you can think of `underscore` as the inverse of `camelize`, though there are cases where that does not hold. For example, `"SSLError".underscore.camelize` gives back `"SslError"`. +TIP: 一般来说,可以把 `underscore` 视作 `camelize` 的逆操作,不过也有例外。例如,`"SSLError".underscore.camelize` 的结果是 `"SslError"`。 -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `titleize` -The method `titleize` capitalizes the words in the receiver: +`titleize` 方法把接收者中的单词首字母变成大写: ```ruby "alice in wonderland".titleize # => "Alice In Wonderland" "fermat's enigma".titleize # => "Fermat's Enigma" ``` -`titleize` is aliased to `titlecase`. +`titlecase` 是 `titleize` 的别名。 + +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. + #### `dasherize` -The method `dasherize` replaces the underscores in the receiver with dashes: +`dasherize` 方法把接收者中的下划线替换成连字符: ```ruby "name".dasherize # => "name" "contact_data".dasherize # => "contact-data" ``` -The XML serializer of models uses this method to dasherize node names: +模型的 XML 序列化程序使用这个方法处理节点名: ```ruby # active_model/serializers/xml.rb @@ -1621,11 +1646,13 @@ def reformat_name(name) end ``` -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `demodulize` -Given a string with a qualified constant name, `demodulize` returns the very constant name, that is, the rightmost part of it: +`demodulize` 方法返回限定常量名的常量名本身,即最右边那一部分: ```ruby "Product".demodulize # => "Product" @@ -1633,10 +1660,9 @@ Given a string with a qualified constant name, `demodulize` returns the very con "Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils" "::Inflections".demodulize # => "Inflections" "".demodulize # => "" - ``` -Active Record for example uses this method to compute the name of a counter cache column: +例如,Active Record 使用这个方法计算计数器缓存列的名称: ```ruby # active_record/reflection.rb @@ -1649,11 +1675,13 @@ def counter_cache_column end ``` -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `deconstantize` -Given a string with a qualified constant reference expression, `deconstantize` removes the rightmost segment, generally leaving the name of the constant's container: +`deconstantize` 方法去掉限定常量引用表达式的最右侧部分,留下常量的容器: ```ruby "Product".deconstantize # => "" @@ -1661,37 +1689,42 @@ Given a string with a qualified constant reference expression, `deconstantize` r "Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel" ``` -Active Support for example uses this method in `Module#qualified_const_set`: +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 -```ruby -def qualified_const_set(path, value) - QualifiedConstUtils.raise_if_absolute(path) - - const_name = path.demodulize - mod_name = path.deconstantize - mod = mod_name.empty? ? self : qualified_const_get(mod_name) - mod.const_set(const_name, value) -end -``` - -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. + #### `parameterize` -The method `parameterize` normalizes its receiver in a way that can be used in pretty URLs. +`parameterize` 方法对接收者做整形,以便在精美的 URL 中使用。 ```ruby "John Smith".parameterize # => "john-smith" "Kurt Gödel".parameterize # => "kurt-godel" ``` -In fact, the result string is wrapped in an instance of `ActiveSupport::Multibyte::Chars`. +如果想保留大小写,把 `preserve_case` 参数设为 `true`。这个参数的默认值是 `false`。 + +```ruby +"John Smith".parameterize(preserve_case: true) # => "John-Smith" +"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel" +``` + +如果想使用自定义的分隔符,覆盖 `separator` 参数。 + +```ruby +"John Smith".parameterize(separator: "_") # => "john\_smith" +"Kurt Gödel".parameterize(separator: "_") # => "kurt\_godel" +``` + +其实,得到的字符串包装在 `ActiveSupport::Multibyte::Chars` 实例中。 -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `tableize` -The method `tableize` is `underscore` followed by `pluralize`. +`tableize` 方法相当于先调用 `underscore`,再调用 `pluralize`。 ```ruby "Person".tableize # => "people" @@ -1699,13 +1732,15 @@ The method `tableize` is `underscore` followed by `pluralize`. "InvoiceLine".tableize # => "invoice_lines" ``` -As a rule of thumb, `tableize` returns the table name that corresponds to a given model for simple cases. The actual implementation in Active Record is not straight `tableize` indeed, because it also demodulizes the class name and checks a few options that may affect the returned string. +一般来说,`tableize` 返回简单模型对应的表名。Active Record 真正的实现方式不是只使用 `tableize`,还会使用 `demodulize`,再检查一些可能影响返回结果的选项。 + +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. + #### `classify` -The method `classify` is the inverse of `tableize`. It gives you the class name corresponding to a table name: +`classify` 方法的作用与 `tableize` 相反,返回表名对应的类名: ```ruby "people".classify # => "Person" @@ -1713,22 +1748,24 @@ The method `classify` is the inverse of `tableize`. It gives you the class name "invoice_lines".classify # => "InvoiceLine" ``` -The method understands qualified table names: +这个方法能处理限定的表名: ```ruby "highrise_production.companies".classify # => "Company" ``` -Note that `classify` returns a class name as a string. You can get the actual class object invoking `constantize` on it, explained next. +注意,`classify` 方法返回的类名是字符串。你可以调用 `constantize` 方法,得到真正的类对象,如下一节所述。 + +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. + #### `constantize` -The method `constantize` resolves the constant reference expression in its receiver: +`constantize` 方法解析接收者中的常量引用表达式: ```ruby -"Fixnum".constantize # => Fixnum +"Integer".constantize # => Integer module M X = 1 @@ -1736,9 +1773,9 @@ end "M::X".constantize # => 1 ``` -If the string evaluates to no known constant, or its content is not even a valid constant name, `constantize` raises `NameError`. +如果结果是未知的常量,或者根本不是有效的常量名,`constantize` 抛出 `NameError` 异常。 -Constant name resolution by `constantize` starts always at the top-level `Object` even if there is no leading "::". +即便开头没有 `::`,`constantize` 也始终从顶层的 `Object` 解析常量名。 ```ruby X = :in_Object @@ -1751,9 +1788,9 @@ module M end ``` -So, it is in general not equivalent to what Ruby would do in the same spot, had a real constant be evaluated. +因此,通常这与 Ruby 的处理方式不同,Ruby 会求值真正的常量。 -Mailer test cases obtain the mailer being tested from the name of the test class using `constantize`: +邮件程序测试用例使用 `constantize` 方法从测试用例的名称中获取要测试的邮件程序: ```ruby # action_mailer/test_case.rb @@ -1764,23 +1801,24 @@ rescue NameError => e end ``` -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `humanize` -The method `humanize` tweaks an attribute name for display to end users. +`humanize` 方法对属性名做调整,以便显示给终端用户查看。 -Specifically performs these transformations: +这个方法所做的转换如下: - * Applies human inflection rules to the argument. - * Deletes leading underscores, if any. - * Removes a "_id" suffix if present. - * Replaces underscores with spaces, if any. - * Downcases all words except acronyms. - * Capitalizes the first word. +* 根据参数做对人类友好的词形变化 +* 删除前导下划线(如果有) +* 删除“_id”后缀(如果有) +* 把下划线替换成空格(如果有) +* 把所有单词变成小写,缩略词除外 +* 把第一个单词的首字母变成大写 -The capitalization of the first word can be turned off by setting the -+:capitalize+ option to false (default is true). +把 `:capitalize` 选项设为 `false`(默认值为 `true`)可以禁止把第一个单词的首字母变成大写。 ```ruby "name".humanize # => "Name" @@ -1790,35 +1828,34 @@ The capitalization of the first word can be turned off by setting the "_id".humanize # => "Id" ``` -If "SSL" was defined to be an acronym: +如果把“SSL”定义为缩略词: ```ruby 'ssl_error'.humanize # => "SSL error" ``` -The helper method `full_messages` uses `humanize` as a fallback to include -attribute names: +`full_messages` 辅助方法使用 `humanize` 作为一种后备机制,以便包含属性名: ```ruby def full_messages - full_messages = [] - - each do |attribute, messages| - ... - attr_name = attribute.to_s.gsub('.', '_').humanize - attr_name = @base.class.human_attribute_name(attribute, default: attr_name) - ... - end + map { |attribute, message| full_message(attribute, message) } +end - full_messages +def full_message + ... + attr_name = attribute.to_s.tr('.', '_').humanize + attr_name = @base.class.human_attribute_name(attribute, default: attr_name) + ... end ``` -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + #### `foreign_key` -The method `foreign_key` gives a foreign key column name from a class name. To do so it demodulizes, underscores, and adds "_id": +`foreign_key` 方法根据类名计算外键列的名称。为此,它先调用 `demodulize`,再调用 `underscore`,最后加上“_id”: ```ruby "User".foreign_key # => "user_id" @@ -1826,54 +1863,61 @@ The method `foreign_key` gives a foreign key column name from a class name. To d "Admin::Session".foreign_key # => "session_id" ``` -Pass a false argument if you do not want the underscore in "_id": +如果不想添加“_id”中的下划线,传入 `false` 参数: ```ruby "User".foreign_key(false) # => "userid" ``` -Associations use this method to infer foreign keys, for example `has_one` and `has_many` do this: +关联使用这个方法推断外键,例如 `has_one` 和 `has_many` 是这么做的: ```ruby # active_record/associations.rb foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key ``` -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. +NOTE: 在 `active_support/core_ext/string/inflections.rb` 文件中定义。 + + -### Conversions +### 转换 -#### `to_date`, `to_time`, `to_datetime` + -The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wrappers around `Date._parse`: +#### `to_date`、`to_time`、`to_datetime` + +`to_date`、`to_time` 和 `to_datetime` 是对 `Date._parse` 的便利包装: ```ruby "2010-07-27".to_date # => Tue, 27 Jul 2010 -"2010-07-27 23:37:00".to_time # => Tue Jul 27 23:37:00 UTC 2010 +"2010-07-27 23:37:00".to_time # => 2010-07-27 23:37:00 +0200 "2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000 ``` -`to_time` receives an optional argument `:utc` or `:local`, to indicate which time zone you want the time in: +`to_time` 有个可选的参数,值为 `:utc` 或 `:local`,指明想使用的时区: ```ruby -"2010-07-27 23:42:00".to_time(:utc) # => Tue Jul 27 23:42:00 UTC 2010 -"2010-07-27 23:42:00".to_time(:local) # => Tue Jul 27 23:42:00 +0200 2010 +"2010-07-27 23:42:00".to_time(:utc) # => 2010-07-27 23:42:00 UTC +"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200 ``` -Default is `:utc`. +默认值是 `:utc`。 + +详情参见 `Date._parse` 的文档。 + +TIP: 参数为空时,这三个方法返回 `nil`。 -Please refer to the documentation of `Date._parse` for further details. +NOTE: 在 `active_support/core_ext/string/conversions.rb` 文件中定义。 -INFO: The three of them return `nil` for blank receivers. + -NOTE: Defined in `active_support/core_ext/string/conversions.rb`. +## `Numeric` 的扩展 -Extensions to `Numeric` ------------------------ + -### Bytes +### 字节 -All numbers respond to these methods: +所有数字都能响应下述方法: ```ruby bytes @@ -1885,7 +1929,7 @@ petabytes exabytes ``` -They return the corresponding amount of bytes, using a conversion factor of 1024: +这些方法返回相应的字节数,因子是 1024: ```ruby 2.kilobytes # => 2048 @@ -1894,55 +1938,42 @@ They return the corresponding amount of bytes, using a conversion factor of 1024 -4.exabytes # => -4611686018427387904 ``` -Singular forms are aliased so you are able to say: +这些方法都有单数别名,因此可以这样用: ```ruby 1.megabyte # => 1048576 ``` -NOTE: Defined in `active_support/core_ext/numeric/bytes.rb`. +NOTE: 在 `active_support/core_ext/numeric/bytes.rb` 文件中定义。 -### Time + -Enables the use of time calculations and declarations, like `45.minutes + 2.hours + 4.years`. +### 时间 -These methods use Time#advance for precise date calculations when using from_now, ago, etc. -as well as adding or subtracting their results from a Time object. For example: +用于计算和声明时间,例如 `45.minutes + 2.hours + 4.years`。 + +使用 `from_now`、`ago` 等精确计算日期,以及增减 `Time` 对象时使用 `Time#advance`。例如: ```ruby -# equivalent to Time.current.advance(months: 1) +# 等价于 Time.current.advance(months: 1) 1.month.from_now -# equivalent to Time.current.advance(years: 2) +# 等价于 Time.current.advance(years: 2) 2.years.from_now -# equivalent to Time.current.advance(months: 4, years: 5) +# 等价于 Time.current.advance(months: 4, years: 5) (4.months + 5.years).from_now ``` -While these methods provide precise calculation when used as in the examples above, care -should be taken to note that this is not true if the result of `months', `years', etc is -converted before use: - -```ruby -# equivalent to 30.days.to_i.from_now -1.month.to_i.from_now - -# equivalent to 365.25.days.to_f.from_now -1.year.to_f.from_now -``` - -In such cases, Ruby's core [Date](http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html) and -[Time](http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html) should be used for precision -date and time arithmetic. +NOTE: 在 `active_support/core_ext/numeric/time.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/numeric/time.rb`. + -### Formatting +### 格式化 -Enables the formatting of numbers in a variety of ways. +以各种形式格式化数字。 -Produce a string representation of a number as a telephone number: +把数字转换成字符串表示形式,表示电话号码: ```ruby 5551234.to_s(:phone) @@ -1959,7 +1990,7 @@ Produce a string representation of a number as a telephone number: # => +1-123-555-1234 ``` -Produce a string representation of a number as currency: +把数字转换成字符串表示形式,表示货币: ```ruby 1234567890.50.to_s(:currency) # => $1,234,567,890.50 @@ -1967,7 +1998,7 @@ Produce a string representation of a number as currency: 1234567890.506.to_s(:currency, precision: 3) # => $1,234,567,890.506 ``` -Produce a string representation of a number as a percentage: +把数字转换成字符串表示形式,表示百分比: ```ruby 100.to_s(:percentage) @@ -1980,7 +2011,7 @@ Produce a string representation of a number as a percentage: # => 302.24399% ``` -Produce a string representation of a number in delimited form: +把数字转换成字符串表示形式,以分隔符分隔: ```ruby 12345678.to_s(:delimited) # => 12,345,678 @@ -1990,7 +2021,7 @@ Produce a string representation of a number in delimited form: 12345678.05.to_s(:delimited, separator: " ") # => 12,345,678 05 ``` -Produce a string representation of a number rounded to a precision: +把数字转换成字符串表示形式,以指定精度四舍五入: ```ruby 111.2345.to_s(:rounded) # => 111.235 @@ -2000,18 +2031,20 @@ Produce a string representation of a number rounded to a precision: 111.2345.to_s(:rounded, significant: true) # => 111 ``` -Produce a string representation of a number as a human-readable number of bytes: +把数字转换成字符串表示形式,得到人类可读的字节数: ```ruby -123.to_s(:human_size) # => 123 Bytes -1234.to_s(:human_size) # => 1.21 KB -12345.to_s(:human_size) # => 12.1 KB -1234567.to_s(:human_size) # => 1.18 MB -1234567890.to_s(:human_size) # => 1.15 GB -1234567890123.to_s(:human_size) # => 1.12 TB +123.to_s(:human_size) # => 123 Bytes +1234.to_s(:human_size) # => 1.21 KB +12345.to_s(:human_size) # => 12.1 KB +1234567.to_s(:human_size) # => 1.18 MB +1234567890.to_s(:human_size) # => 1.15 GB +1234567890123.to_s(:human_size) # => 1.12 TB +1234567890123456.to_s(:human_size) # => 1.1 PB +1234567890123456789.to_s(:human_size) # => 1.07 EB ``` -Produce a string representation of a number in human-readable words: +把数字转换成字符串表示形式,得到人类可读的词: ```ruby 123.to_s(:human) # => "123" @@ -2023,25 +2056,30 @@ Produce a string representation of a number in human-readable words: 1234567890123456.to_s(:human) # => "1.23 Quadrillion" ``` -NOTE: Defined in `active_support/core_ext/numeric/conversions.rb`. +NOTE: 在 `active_support/core_ext/numeric/conversions.rb` 文件中定义。 -Extensions to `Integer` ------------------------ + + +## `Integer` 的扩展 + + ### `multiple_of?` -The method `multiple_of?` tests whether an integer is multiple of the argument: +`multiple_of?` 方法测试一个整数是不是参数的倍数: ```ruby 2.multiple_of?(1) # => true 1.multiple_of?(2) # => false ``` -NOTE: Defined in `active_support/core_ext/integer/multiple.rb`. +NOTE: 在 `active_support/core_ext/integer/multiple.rb` 文件中定义。 + + ### `ordinal` -The method `ordinal` returns the ordinal suffix string corresponding to the receiver integer: +`ordinal` 方法返回整数接收者的序数词后缀(字符串): ```ruby 1.ordinal # => "st" @@ -2052,11 +2090,13 @@ The method `ordinal` returns the ordinal suffix string corresponding to the rece -134.ordinal # => "th" ``` -NOTE: Defined in `active_support/core_ext/integer/inflections.rb`. +NOTE: 在 `active_support/core_ext/integer/inflections.rb` 文件中定义。 + + ### `ordinalize` -The method `ordinalize` returns the ordinal string corresponding to the receiver integer. In comparison, note that the `ordinal` method returns **only** the suffix string. +`ordinalize` 方法返回整数接收者的序数词(字符串)。注意,`ordinal` 方法只返回后缀。 ```ruby 1.ordinalize # => "1st" @@ -2067,98 +2107,101 @@ The method `ordinalize` returns the ordinal string corresponding to the receiver -134.ordinalize # => "-134th" ``` -NOTE: Defined in `active_support/core_ext/integer/inflections.rb`. +NOTE: 在 `active_support/core_ext/integer/inflections.rb` 文件中定义。 + + + +## `BigDecimal` 的扩展 + + -Extensions to `BigDecimal` --------------------------- ### `to_s` -The method `to_s` is aliased to `to_formatted_s`. This provides a convenient way to display a BigDecimal value in floating-point notation: +`to_s` 方法把默认的说明符设为“F”。这意味着,不传入参数时,`to_s` 返回浮点数表示形式,而不是工程计数法。 ```ruby BigDecimal.new(5.00, 6).to_s # => "5.0" ``` -### `to_formatted_s` - -Te method `to_formatted_s` provides a default specifier of "F". This means that a simple call to `to_formatted_s` or `to_s` will result in floating point representation instead of engineering notation: +说明符也可以使用符号: ```ruby -BigDecimal.new(5.00, 6).to_formatted_s # => "5.0" +BigDecimal.new(5.00, 6).to_s(:db) # => "5.0" ``` -and that symbol specifiers are also supported: +也支持工程计数法: ```ruby -BigDecimal.new(5.00, 6).to_formatted_s(:db) # => "5.0" +BigDecimal.new(5.00, 6).to_s("e") # => "0.5E1" ``` -Engineering notation is still supported: + -```ruby -BigDecimal.new(5.00, 6).to_formatted_s("e") # => "0.5E1" -``` +## `Enumerable` 的扩展 -Extensions to `Enumerable` --------------------------- + ### `sum` -The method `sum` adds the elements of an enumerable: +`sum` 方法计算可枚举对象的元素之和: ```ruby [1, 2, 3].sum # => 6 (1..100).sum # => 5050 ``` -Addition only assumes the elements respond to `+`: +只假定元素能响应 `+`: ```ruby [[1, 2], [2, 3], [3, 4]].sum # => [1, 2, 2, 3, 3, 4] %w(foo bar baz).sum # => "foobarbaz" -{a: 1, b: 2, c: 3}.sum # => [:b, 2, :c, 3, :a, 1] +{a: 1, b: 2, c: 3}.sum # => [:b, 2, :c, 3, :a, 1] ``` -The sum of an empty collection is zero by default, but this is customizable: +空集合的元素之和默认为零,不过可以自定义: ```ruby [].sum # => 0 [].sum(1) # => 1 ``` -If a block is given, `sum` becomes an iterator that yields the elements of the collection and sums the returned values: +如果提供块,`sum` 变成迭代器,把集合中的元素拽入块中,然后求返回值之和: ```ruby (1..5).sum {|n| n * 2 } # => 30 [2, 4, 6, 8, 10].sum # => 30 ``` -The sum of an empty receiver can be customized in this form as well: +空接收者之和也可以使用这种方式自定义: ```ruby [].sum(1) {|n| n**3} # => 1 ``` -NOTE: Defined in `active_support/core_ext/enumerable.rb`. +NOTE: 在 `active_support/core_ext/enumerable.rb` 文件中定义。 + + ### `index_by` -The method `index_by` generates a hash with the elements of an enumerable indexed by some key. +`index_by` 方法生成一个散列,使用某个键索引可枚举对象中的元素。 -It iterates through the collection and passes each element to a block. The element will be keyed by the value returned by the block: +它迭代集合,把各个元素传入块中。元素使用块的返回值为键: ```ruby invoices.index_by(&:number) # => {'2009-032' => , '2009-008' => , ...} ``` -WARNING. Keys should normally be unique. If the block returns the same value for different elements no collection is built for that key. The last item will win. +WARNING: 键一般是唯一的。如果块为不同的元素返回相同的键,不会使用那个键构建集合。最后一个元素胜出。 + +NOTE: 在 `active_support/core_ext/enumerable.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/enumerable.rb`. + ### `many?` -The method `many?` is shorthand for `collection.size > 1`: +`many?` 方法是 `collection.size > 1` 的简化: ```erb <% if pages.many? %> @@ -2166,37 +2209,66 @@ The method `many?` is shorthand for `collection.size > 1`: <% end %> ``` -If an optional block is given, `many?` only takes into account those elements that return true: +如果提供可选的块,`many?` 只考虑返回 `true` 的元素: ```ruby @see_more = videos.many? {|video| video.category == params[:category]} ``` -NOTE: Defined in `active_support/core_ext/enumerable.rb`. +NOTE: 在 `active_support/core_ext/enumerable.rb` 文件中定义。 + + ### `exclude?` -The predicate `exclude?` tests whether a given object does **not** belong to the collection. It is the negation of the built-in `include?`: +`exclude?` 方法测试指定对象是否不在集合中。这是内置方法 `include?` 的逆向判断。 ```ruby to_visit << node if visited.exclude?(node) ``` -NOTE: Defined in `active_support/core_ext/enumerable.rb`. +NOTE: 在 `active_support/core_ext/enumerable.rb` 文件中定义。 -Extensions to `Array` ---------------------- + -### Accessing +### `without` -Active Support augments the API of arrays to ease certain ways of accessing them. For example, `to` returns the subarray of elements up to the one at the passed index: +`without` 从可枚举对象中删除指定的元素,然后返回副本: + +```ruby +["David", "Rafael", "Aaron", "Todd"].without("Aaron", "Todd") # => ["David", "Rafael"] +``` + +NOTE: 在 `active_support/core_ext/enumerable.rb` 文件中定义。 + + + +### `pluck` + +`pluck` 方法基于指定的键返回一个数组: + +```ruby +[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"] +``` + +NOTE: 在 `active_support/core_ext/enumerable.rb` 文件中定义。 + + + +## `Array` 的扩展 + + + +### 访问 + +为了便于以多种方式访问数组,Active Support 增强了数组的 API。例如,若想获取到指定索引的子数组,可以这么做: ```ruby %w(a b c d).to(2) # => %w(a b c) [].to(7) # => [] ``` -Similarly, `from` returns the tail from the element at the passed index to the end. If the index is greater than the length of the array, it returns an empty array. +类似地,`from` 从指定索引一直获取到末尾。如果索引大于数组的长度,返回一个空数组。 ```ruby %w(a b c d).from(2) # => %w(c d) @@ -2204,54 +2276,62 @@ Similarly, `from` returns the tail from the element at the passed index to the e [].from(0) # => [] ``` -The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element (`first` is built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. +`second`、`third`、`fourth` 和 `fifth` 分别返回对应的元素,`second_to_last` 和 `third_to_last` 也是(`first` 和 `last` 是内置的)。得益于公众智慧和积极的建设性建议,还有 `forty_two` 可用。 ```ruby %w(a b c d).third # => c %w(a b c d).fifth # => nil ``` -NOTE: Defined in `active_support/core_ext/array/access.rb`. +NOTE: 在 `active_support/core_ext/array/access.rb` 文件中定义。 + + -### Adding Elements +### 添加元素 + + #### `prepend` -This method is an alias of `Array#unshift`. +这个方法是 `Array#unshift` 的别名。 ```ruby -%w(a b c d).prepend('e') # => %w(e a b c d) +%w(a b c d).prepend('e') # => ["e", "a", "b", "c", "d"] [].prepend(10) # => [10] ``` -NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. +NOTE: 在 `active_support/core_ext/array/prepend_and_append.rb` 文件中定义。 + + #### `append` -This method is an alias of `Array#<<`. +这个方法是 `Array#<<` 的别名。 ```ruby -%w(a b c d).append('e') # => %w(a b c d e) -[].append([1,2]) # => [[1,2]] +%w(a b c d).append('e') # => ["a", "b", "c", "d", "e"] +[].append([1,2]) # => [[1, 2]] ``` -NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. +NOTE: 在 `active_support/core_ext/array/prepend_and_append.rb` 文件中定义。 + + -### Options Extraction +### 选项提取 -When the last argument in a method call is a hash, except perhaps for a `&block` argument, Ruby allows you to omit the brackets: +如果方法调用的最后一个参数(不含 `&block` 参数)是散列,Ruby 允许省略花括号: ```ruby User.exists?(email: params[:email]) ``` -That syntactic sugar is used a lot in Rails to avoid positional arguments where there would be too many, offering instead interfaces that emulate named parameters. In particular it is very idiomatic to use a trailing hash for options. +Rails 大量使用这种语法糖,以此避免编写大量位置参数,用于模仿具名参数。Rails 经常在最后一个散列选项上使用这种惯用法。 -If a method expects a variable number of arguments and uses `*` in its declaration, however, such an options hash ends up being an item of the array of arguments, where it loses its role. +然而,如果方法期待任意个参数,在声明中使用 `*`,那么选项散列就会变成数组中一个元素,失去了应有的作用。 -In those cases, you may give an options hash a distinguished treatment with `extract_options!`. This method checks the type of the last item of an array. If it is a hash it pops it and returns it, otherwise it returns an empty hash. +此时,可以使用 `extract_options!` 特殊处理选项散列。这个方法检查数组最后一个元素的类型,如果是散列,把它提取出来,并返回;否则,返回一个空散列。 -Let's see for example the definition of the `caches_action` controller macro: +下面以控制器的 `caches_action` 方法的定义为例: ```ruby def caches_action(*actions) @@ -2261,15 +2341,19 @@ def caches_action(*actions) end ``` -This method receives an arbitrary number of action names, and an optional hash of options as last argument. With the call to `extract_options!` you obtain the options hash and remove it from `actions` in a simple and explicit way. +这个方法接收任意个动作名,最后一个参数是选项散列。`extract_options!` 方法获取选项散列,把它从 `actions` 参数中删除,这样简单便利。 -NOTE: Defined in `active_support/core_ext/array/extract_options.rb`. +NOTE: 在 `active_support/core_ext/array/extract_options.rb` 文件中定义。 -### Conversions + + +### 转换 + + #### `to_sentence` -The method `to_sentence` turns an array into a string containing a sentence that enumerates its items: +`to_sentence` 方法枚举元素,把数组变成一个句子(字符串): ```ruby %w().to_sentence # => "" @@ -2278,29 +2362,29 @@ The method `to_sentence` turns an array into a string containing a sentence that %w(Earth Wind Fire).to_sentence # => "Earth, Wind, and Fire" ``` -This method accepts three options: +这个方法接受三个选项: + +* `:two_words_connector`:数组长度为 2 时使用什么词。默认为“ and”。 +* `:words_connector`:数组元素数量为 3 个以上(含)时,使用什么连接除最后两个元素之外的元素。默认为“, ”。 +* `:last_word_connector`:数组元素数量为 3 个以上(含)时,使用什么连接最后两个元素。默认为“, and”。 -* `:two_words_connector`: What is used for arrays of length 2. Default is " and ". -* `:words_connector`: What is used to join the elements of arrays with 3 or more elements, except for the last two. Default is ", ". -* `:last_word_connector`: What is used to join the last items of an array with 3 or more elements. Default is ", and ". +这些选项的默认值可以本地化,相应的键为: -The defaults for these options can be localized, their keys are: +| 选项 | i18n 键 | +|---|---| +| `:two_words_connector` | `support.array.two_words_connector` | +| `:words_connector` | `support.array.words_connector` | +| `:last_word_connector` | `support.array.last_word_connector` | -| Option | I18n key | -| ---------------------- | ----------------------------------- | -| `:two_words_connector` | `support.array.two_words_connector` | -| `:words_connector` | `support.array.words_connector` | -| `:last_word_connector` | `support.array.last_word_connector` | +NOTE: 在 `active_support/core_ext/array/conversions.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/array/conversions.rb`. + #### `to_formatted_s` -The method `to_formatted_s` acts like `to_s` by default. +默认情况下,`to_formatted_s` 的行为与 `to_s` 一样。 -If the array contains items that respond to `id`, however, the symbol -`:db` may be passed as argument. That's typically used with -collections of Active Record objects. Returned strings are: +然而,如果数组中的元素能响应 `id` 方法,可以传入参数 `:db`。处理 Active Record 对象集合时经常如此。返回的字符串如下: ```ruby [].to_formatted_s(:db) # => "null" @@ -2308,13 +2392,15 @@ collections of Active Record objects. Returned strings are: invoice.lines.to_formatted_s(:db) # => "23,567,556,12" ``` -Integers in the example above are supposed to come from the respective calls to `id`. +在上述示例中,整数是在元素上调用 `id` 得到的。 -NOTE: Defined in `active_support/core_ext/array/conversions.rb`. +NOTE: 在 `active_support/core_ext/array/conversions.rb` 文件中定义。 + + #### `to_xml` -The method `to_xml` returns a string containing an XML representation of its receiver: +`to_xml` 方法返回接收者的 XML 表述: ```ruby Contributor.limit(2).order(:rank).to_xml @@ -2336,11 +2422,11 @@ Contributor.limit(2).order(:rank).to_xml # ``` -To do so it sends `to_xml` to every item in turn, and collects the results under a root node. All items must respond to `to_xml`, an exception is raised otherwise. +为此,它把 `to_xml` 分别发送给每个元素,然后收集结果,放在一个根节点中。所有元素都必须能响应 `to_xml`,否则抛出异常。 -By default, the name of the root element is the underscorized and dasherized plural of the name of the class of the first item, provided the rest of elements belong to that type (checked with `is_a?`) and they are not hashes. In the example above that's "contributors". +默认情况下,根元素的名称是第一个元素的类名的复数形式经过 `underscore` 和 `dasherize` 处理后得到的值——前提是余下的元素属于那个类型(使用 `is_a?` 检查),而且不是散列。在上例中,根元素是“contributors”。 -If there's any element that does not belong to the type of the first one the root node becomes "objects": +只要有不属于那个类型的元素,根元素就使用“objects”: ```ruby [Contributor.first, Commit.first].to_xml @@ -2368,7 +2454,7 @@ If there's any element that does not belong to the type of the first one the roo # ``` -If the receiver is an array of hashes the root element is by default also "objects": +如果接收者是由散列组成的数组,根元素默认也是“objects”: ```ruby [{a: 1, b: 2}, {c: 3}].to_xml @@ -2385,11 +2471,11 @@ If the receiver is an array of hashes the root element is by default also "objec # ``` -WARNING. If the collection is empty the root element is by default "nil-classes". That's a gotcha, for example the root element of the list of contributors above would not be "contributors" if the collection was empty, but "nil-classes". You may use the `:root` option to ensure a consistent root element. +WARNING: 如果集合为空,根元素默认为“nil-classes”。例如上述示例中的贡献者列表,如果集合为空,根元素不是“contributors”,而是“nil-classes”。可以使用 `:root` 选项确保根元素始终一致。 -The name of children nodes is by default the name of the root node singularized. In the examples above we've seen "contributor" and "object". The option `:children` allows you to set these node names. +子节点的名称默认为根节点的单数形式。在前面几个例子中,我们见到的是“contributor”和“object”。可以使用 `:children` 选项设定子节点的名称。 -The default XML builder is a fresh instance of `Builder::XmlMarkup`. You can configure your own builder via the `:builder` option. The method also accepts options like `:dasherize` and friends, they are forwarded to the builder: +默认的 XML 构建程序是一个新的 `Builder::XmlMarkup` 实例。可以使用 `:builder` 选项指定构建程序。这个方法还接受 `:dasherize` 等方法,它们会被转发给构建程序。 ```ruby Contributor.limit(2).order(:rank).to_xml(skip_types: true) @@ -2411,17 +2497,19 @@ Contributor.limit(2).order(:rank).to_xml(skip_types: true) # ``` -NOTE: Defined in `active_support/core_ext/array/conversions.rb`. +NOTE: 在 `active_support/core_ext/array/conversions.rb` 文件中定义。 + + -### Wrapping +### 包装 -The method `Array.wrap` wraps its argument in an array unless it is already an array (or array-like). +`Array.wrap` 方法把参数包装成一个数组,除非参数已经是数组(或与数组类似的结构)。 -Specifically: +具体而言: -* If the argument is `nil` an empty list is returned. -* Otherwise, if the argument responds to `to_ary` it is invoked, and if the value of `to_ary` is not `nil`, it is returned. -* Otherwise, an array with the argument as its single element is returned. +* 如果参数是 `nil`,返回一个空数组。 +* 否则,如果参数响应 `to_ary` 方法,调用之;如果 `to_ary` 返回值不是 `nil`,返回之。 +* 否则,把参数作为数组的唯一元素,返回之。 ```ruby Array.wrap(nil) # => [] @@ -2429,35 +2517,36 @@ Array.wrap([1, 2, 3]) # => [1, 2, 3] Array.wrap(0) # => [0] ``` -This method is similar in purpose to `Kernel#Array`, but there are some differences: +这个方法的作用与 `Kernel#Array` 类似,不过二者之间有些区别: -* If the argument responds to `to_ary` the method is invoked. `Kernel#Array` moves on to try `to_a` if the returned value is `nil`, but `Array.wrap` returns `nil` right away. -* If the returned value from `to_ary` is neither `nil` nor an `Array` object, `Kernel#Array` raises an exception, while `Array.wrap` does not, it just returns the value. -* It does not call `to_a` on the argument, though special-cases `nil` to return an empty array. +* 如果参数响应 `to_ary`,调用之。如果 `to_ary` 的返回值是 `nil`,`Kernel#Array` 接着调用 `to_a`,而 `Array.wrap` 把参数作为数组的唯一元素,返回之。 +* 如果 `to_ary` 的返回值既不是 `nil`,也不是 `Array` 对象,`Kernel#Array` 抛出异常,而 `Array.wrap` 不会,它返回那个值。 +* 如果参数不响应 `to_ary`,`Array.wrap` 不在参数上调用 `to_a`,而是把参数作为数组的唯一元素,返回之。 -The last point is particularly worth comparing for some enumerables: +对某些可枚举对象来说,最后一点尤为重要: ```ruby Array.wrap(foo: :bar) # => [{:foo=>:bar}] Array(foo: :bar) # => [[:foo, :bar]] ``` -There's also a related idiom that uses the splat operator: +还有一种惯用法是使用星号运算符: ```ruby [*object] ``` -which in Ruby 1.8 returns `[nil]` for `nil`, and calls to `Array(object)` otherwise. (Please if you know the exact behavior in 1.9 contact fxn.) +在 Ruby 1.8 中,如果参数是 `nil`,返回 `[nil]`,否则调用 `Array(object)`。(如果你知道在 Ruby 1.9 中的行为,请联系 fxn。) -Thus, in this case the behavior is different for `nil`, and the differences with `Kernel#Array` explained above apply to the rest of `object`s. +因此,参数为 `nil` 时二者的行为不同,前文对 `Kernel#Array` 的说明适用于其他对象。 -NOTE: Defined in `active_support/core_ext/array/wrap.rb`. +NOTE: 在 `active_support/core_ext/array/wrap.rb` 文件中定义。 -### Duplicating + -The method `Array.deep_dup` duplicates itself and all objects inside -recursively with Active Support method `Object#deep_dup`. It works like `Array#map` with sending `deep_dup` method to each object inside. +### 复制 + +`Array#deep_dup` 方法使用 Active Support 提供的 `Object#deep_dup` 方法复制数组自身和里面的对象。其工作方式相当于通过 `Array#map` 把 `deep_dup` 方法发给里面的各个对象。 ```ruby array = [1, [2, 3]] @@ -2466,21 +2555,25 @@ dup[1][2] = 4 array[1][2] == nil # => true ``` -NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`. +NOTE: 在 `active_support/core_ext/object/deep_dup.rb` 文件中定义。 + + + +### 分组 -### Grouping + #### `in_groups_of(number, fill_with = nil)` -The method `in_groups_of` splits an array into consecutive groups of a certain size. It returns an array with the groups: +`in_groups_of` 方法把数组拆分成特定长度的连续分组,返回由各分组构成的数组: ```ruby [1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]] ``` -or yields them in turn if a block is passed: +如果有块,把各分组拽入块中: -```html+erb +```erb <% sample.in_groups_of(3) do |a, b, c| %> <%= a %> @@ -2490,32 +2583,34 @@ or yields them in turn if a block is passed: <% end %> ``` -The first example shows `in_groups_of` fills the last group with as many `nil` elements as needed to have the requested size. You can change this padding value using the second optional argument: +第一个示例说明 `in_groups_of` 会使用 `nil` 元素填充最后一组,得到指定大小的分组。可以使用第二个参数(可选的)修改填充值: ```ruby [1, 2, 3].in_groups_of(2, 0) # => [[1, 2], [3, 0]] ``` -And you can tell the method not to fill the last group passing `false`: +如果传入 `false`,不填充最后一组: ```ruby [1, 2, 3].in_groups_of(2, false) # => [[1, 2], [3]] ``` -As a consequence `false` can't be a used as a padding value. +因此,`false` 不能作为填充值使用。 + +NOTE: 在 `active_support/core_ext/array/grouping.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/array/grouping.rb`. + #### `in_groups(number, fill_with = nil)` -The method `in_groups` splits an array into a certain number of groups. The method returns an array with the groups: +`in_groups` 方法把数组分成特定个分组。这个方法返回由分组构成的数组: ```ruby %w(1 2 3 4 5 6 7).in_groups(3) # => [["1", "2", "3"], ["4", "5", nil], ["6", "7", nil]] ``` -or yields them in turn if a block is passed: +如果有块,把分组拽入块中: ```ruby %w(1 2 3 4 5 6 7).in_groups(3) {|group| p group} @@ -2524,56 +2619,63 @@ or yields them in turn if a block is passed: ["6", "7", nil] ``` -The examples above show that `in_groups` fills some groups with a trailing `nil` element as needed. A group can get at most one of these extra elements, the rightmost one if any. And the groups that have them are always the last ones. +在上述示例中,`in_groups` 使用 `nil` 填充尾部的分组。一个分组至多有一个填充值,而且是最后一个元素。有填充值的始终是最后几个分组。 -You can change this padding value using the second optional argument: +可以使用第二个参数(可选的)修改填充值: ```ruby %w(1 2 3 4 5 6 7).in_groups(3, "0") # => [["1", "2", "3"], ["4", "5", "0"], ["6", "7", "0"]] ``` -And you can tell the method not to fill the smaller groups passing `false`: +如果传入 `false`,不填充较短的分组: ```ruby %w(1 2 3 4 5 6 7).in_groups(3, false) # => [["1", "2", "3"], ["4", "5"], ["6", "7"]] ``` -As a consequence `false` can't be a used as a padding value. +因此,`false` 不能作为填充值使用。 + +NOTE: 在 `active_support/core_ext/array/grouping.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/array/grouping.rb`. + #### `split(value = nil)` -The method `split` divides an array by a separator and returns the resulting chunks. +`split` 方法在指定的分隔符处拆分数组,返回得到的片段。 -If a block is passed the separators are those elements of the array for which the block returns true: +如果有块,使用块中表达式返回 `true` 的元素作为分隔符: ```ruby (-5..5).to_a.split { |i| i.multiple_of?(4) } # => [[-5], [-3, -2, -1], [1, 2, 3], [5]] ``` -Otherwise, the value received as argument, which defaults to `nil`, is the separator: +否则,使用指定的参数(默认为 `nil`)作为分隔符: ```ruby [0, 1, -5, 1, 1, "foo", "bar"].split(1) # => [[0], [-5], [], ["foo", "bar"]] ``` -TIP: Observe in the previous example that consecutive separators result in empty arrays. +TIP: 仔细观察上例,出现连续的分隔符时,得到的是空数组。 -NOTE: Defined in `active_support/core_ext/array/grouping.rb`. +NOTE: 在 `active_support/core_ext/array/grouping.rb` 文件中定义。 -Extensions to `Hash` --------------------- + -### Conversions +## `Hash` 的扩展 + + + +### 转换 + + #### `to_xml` -The method `to_xml` returns a string containing an XML representation of its receiver: +`to_xml` 方法返回接收者的 XML 表述(字符串): ```ruby {"foo" => 1, "bar" => 2}.to_xml @@ -2585,101 +2687,107 @@ The method `to_xml` returns a string containing an XML representation of its rec # ``` -To do so, the method loops over the pairs and builds nodes that depend on the _values_. Given a pair `key`, `value`: +为此,这个方法迭代各个键值对,根据值构建节点。假如键值对是 `key, value`: -* If `value` is a hash there's a recursive call with `key` as `:root`. +* 如果 `value` 是一个散列,递归调用,此时 `key` 作为 `:root`。 +* 如果 `value` 是一个数组,递归调用,此时 `key` 作为 `:root`,`key` 的单数形式作为 `:children`。 +* 如果 `value` 是可调用对象,必须能接受一个或两个参数。根据参数的数量,传给可调用对象的第一个参数是 `options` 散列,`key` 作为 `:root`,`key` 的单数形式作为第二个参数。它的返回值作为新节点。 +* 如果 `value` 响应 `to_xml`,调用这个方法时把 `key` 作为 `:root`。 +* 否则,使用 `key` 为标签创建一个节点,`value` 的字符串表示形式为文本作为节点的文本。如果 `value` 是 `nil`,添加“nil”属性,值为“true”。除非有 `:skip_type` 选项,而且值为 `true`,否则还会根据下述对应关系添加“type”属性: -* If `value` is an array there's a recursive call with `key` as `:root`, and `key` singularized as `:children`. + ```ruby + XML_TYPE_NAMES = { + "Symbol" => "symbol", + "Integer" => "integer", + "BigDecimal" => "decimal", + "Float" => "float", + "TrueClass" => "boolean", + "FalseClass" => "boolean", + "Date" => "date", + "DateTime" => "datetime", + "Time" => "datetime" + } + ``` -* If `value` is a callable object it must expect one or two arguments. Depending on the arity, the callable is invoked with the `options` hash as first argument with `key` as `:root`, and `key` singularized as second argument. Its return value becomes a new node. -* If `value` responds to `to_xml` the method is invoked with `key` as `:root`. -* Otherwise, a node with `key` as tag is created with a string representation of `value` as text node. If `value` is `nil` an attribute "nil" set to "true" is added. Unless the option `:skip_types` exists and is true, an attribute "type" is added as well according to the following mapping: - -```ruby -XML_TYPE_NAMES = { - "Symbol" => "symbol", - "Fixnum" => "integer", - "Bignum" => "integer", - "BigDecimal" => "decimal", - "Float" => "float", - "TrueClass" => "boolean", - "FalseClass" => "boolean", - "Date" => "date", - "DateTime" => "datetime", - "Time" => "datetime" -} -``` +默认情况下,根节点是“hash”,不过可以通过 `:root` 选项配置。 -By default the root node is "hash", but that's configurable via the `:root` option. +默认的 XML 构建程序是一个新的 `Builder::XmlMarkup` 实例。可以使用 `:builder` 选项配置构建程序。这个方法还接受 `:dasherize` 等选项,它们会被转发给构建程序。 -The default XML builder is a fresh instance of `Builder::XmlMarkup`. You can configure your own builder with the `:builder` option. The method also accepts options like `:dasherize` and friends, they are forwarded to the builder. +NOTE: 在 `active_support/core_ext/hash/conversions.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/hash/conversions.rb`. + -### Merging +### 合并 -Ruby has a built-in method `Hash#merge` that merges two hashes: +Ruby 有个内置的方法,`Hash#merge`,用于合并两个散列: ```ruby {a: 1, b: 1}.merge(a: 0, c: 2) # => {:a=>0, :b=>1, :c=>2} ``` -Active Support defines a few more ways of merging hashes that may be convenient. +为了方便,Active Support 定义了几个用于合并散列的方法。 -#### `reverse_merge` and `reverse_merge!` + -In case of collision the key in the hash of the argument wins in `merge`. You can support option hashes with default values in a compact way with this idiom: +#### `reverse_merge` 和 `reverse_merge!` + +如果键有冲突,`merge` 方法的参数中的键胜出。通常利用这一点为选项散列提供默认值: ```ruby options = {length: 30, omission: "..."}.merge(options) ``` -Active Support defines `reverse_merge` in case you prefer this alternative notation: +Active Support 定义了 `reverse_merge` 方法,以防你想使用相反的合并方式: ```ruby options = options.reverse_merge(length: 30, omission: "...") ``` -And a bang version `reverse_merge!` that performs the merge in place: +还有一个爆炸版本,`reverse_merge!`,就地执行合并: ```ruby options.reverse_merge!(length: 30, omission: "...") ``` -WARNING. Take into account that `reverse_merge!` may change the hash in the caller, which may or may not be a good idea. +WARNING: `reverse_merge!` 方法会就地修改调用方,这可能不是个好主意。 + +NOTE: 在 `active_support/core_ext/hash/reverse_merge.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/hash/reverse_merge.rb`. + #### `reverse_update` -The method `reverse_update` is an alias for `reverse_merge!`, explained above. +`reverse_update` 方法是 `reverse_merge!` 的别名,作用参见前文。 + +WARNING: 注意,`reverse_update` 方法的名称中没有感叹号。 -WARNING. Note that `reverse_update` has no bang. +NOTE: 在 `active_support/core_ext/hash/reverse_merge.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/hash/reverse_merge.rb`. + -#### `deep_merge` and `deep_merge!` +#### `deep_merge` 和 `deep_merge!` -As you can see in the previous example if a key is found in both hashes the value in the one in the argument wins. +如前面的示例所示,如果两个散列中有相同的键,参数中的散列胜出。 -Active Support defines `Hash#deep_merge`. In a deep merge, if a key is found in both hashes and their values are hashes in turn, then their _merge_ becomes the value in the resulting hash: +Active Support 定义了 `Hash#deep_merge` 方法。在深度合并中,如果两个散列中有相同的键,而且它们的值都是散列,那么在得到的散列中,那个键的值是合并后的结果: ```ruby {a: {b: 1}}.deep_merge(a: {c: 2}) # => {:a=>{:b=>1, :c=>2}} ``` -The method `deep_merge!` performs a deep merge in place. +`deep_merge!` 方法就地执行深度合并。 -NOTE: Defined in `active_support/core_ext/hash/deep_merge.rb`. +NOTE: 在 `active_support/core_ext/hash/deep_merge.rb` 文件中定义。 -### Deep duplicating + -The method `Hash.deep_dup` duplicates itself and all keys and values -inside recursively with Active Support method `Object#deep_dup`. It works like `Enumerator#each_with_object` with sending `deep_dup` method to each pair inside. +### 深度复制 + +`Hash#deep_dup` 方法使用 Active Support 提供的 `Object#deep_dup` 方法复制散列自身及里面的键值对。其工作方式相当于通过 `Enumerator#each_with_object` 把 `deep_dup` 方法发给各个键值对。 ```ruby hash = { a: 1, b: { c: 2, d: [3, 4] } } @@ -2692,49 +2800,55 @@ hash[:b][:e] == nil # => true hash[:b][:d] == [3, 4] # => true ``` -NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`. +NOTE: 在 `active_support/core_ext/object/deep_dup.rb` 文件中定义。 + + + +### 处理键 -### Working with Keys + -#### `except` and `except!` +#### `except` 和 `except!` -The method `except` returns a hash with the keys in the argument list removed, if present: +`except` 方法返回一个散列,从接收者中把参数中列出的键删除(如果有的话): ```ruby {a: 1, b: 2}.except(:a) # => {:b=>2} ``` -If the receiver responds to `convert_key`, the method is called on each of the arguments. This allows `except` to play nice with hashes with indifferent access for instance: +如果接收者响应 `convert_key` 方法,会在各个参数上调用它。这样 `except` 能更好地处理不区分键类型的散列,例如: ```ruby {a: 1}.with_indifferent_access.except(:a) # => {} {a: 1}.with_indifferent_access.except("a") # => {} ``` -There's also the bang variant `except!` that removes keys in the very receiver. +还有爆炸版本,`except!`,就地从接收者中删除键。 -NOTE: Defined in `active_support/core_ext/hash/except.rb`. +NOTE: 在 `active_support/core_ext/hash/except.rb` 文件中定义。 -#### `transform_keys` and `transform_keys!` + -The method `transform_keys` accepts a block and returns a hash that has applied the block operations to each of the keys in the receiver: +#### `transform_keys` 和 `transform_keys!` + +`transform_keys` 方法接受一个块,使用块中的代码处理接收者的键: ```ruby {nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase } # => {"" => nil, "A" => :a, "1" => 1} ``` -In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: +遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。 ```ruby {"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase } -# The result could either be +# 结果可能是 # => {"A"=>2} -# or +# 也可能是 # => {"A"=>1} ``` -This method may be useful for example to build specialized conversions. For instance `stringify_keys` and `symbolize_keys` use `transform_keys` to perform their key conversions: +这个方法可以用于构建特殊的转换方式。例如,`stringify_keys` 和 `symbolize_keys` 使用 `transform_keys` 转换键: ```ruby def stringify_keys @@ -2746,37 +2860,39 @@ def symbolize_keys end ``` -There's also the bang variant `transform_keys!` that applies the block operations to keys in the very receiver. +还有爆炸版本,`transform_keys!`,就地使用块中的代码处理接收者的键。 -Besides that, one can use `deep_transform_keys` and `deep_transform_keys!` to perform the block operation on all the keys in the given hash and all the hashes nested into it. An example of the result is: +此外,可以使用 `deep_transform_keys` 和 `deep_transform_keys!` 把块应用到指定散列及其嵌套的散列的所有键上。例如: ```ruby {nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_transform_keys { |key| key.to_s.upcase } # => {""=>nil, "1"=>1, "NESTED"=>{"A"=>3, "5"=>5}} ``` -NOTE: Defined in `active_support/core_ext/hash/keys.rb`. +NOTE: 在 `active_support/core_ext/hash/keys.rb` 文件中定义。 + + -#### `stringify_keys` and `stringify_keys!` +#### `stringify_keys` 和 `stringify_keys!` -The method `stringify_keys` returns a hash that has a stringified version of the keys in the receiver. It does so by sending `to_s` to them: +`stringify_keys` 把接收者中的键都变成字符串,然后返回一个散列。为此,它在键上调用 `to_s`。 ```ruby {nil => nil, 1 => 1, a: :a}.stringify_keys -# => {"" => nil, "a" => :a, "1" => 1} +# => {"" => nil, "1" => 1, "a" => :a} ``` -In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: +遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。 ```ruby {"a" => 1, a: 2}.stringify_keys -# The result could either be +# 结果可能是 # => {"a"=>2} -# or +# 也可能是 # => {"a"=>1} ``` -This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionView::Helpers::FormHelper` defines: +使用这个方法,选项既可以是符号,也可以是字符串。例如 `ActionView::Helpers::FormHelper` 定义的这个方法: ```ruby def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") @@ -2786,41 +2902,43 @@ def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") end ``` -The second line can safely access the "type" key, and let the user to pass either `:type` or "type". +因为有第二行,所以用户可以传入 `:type` 或 `"type"`。 -There's also the bang variant `stringify_keys!` that stringifies keys in the very receiver. +也有爆炸版本,`stringify_keys!`,直接把接收者的键变成字符串。 -Besides that, one can use `deep_stringify_keys` and `deep_stringify_keys!` to stringify all the keys in the given hash and all the hashes nested into it. An example of the result is: +此外,可以使用 `deep_stringify_keys` 和 `deep_stringify_keys!` 把指定散列及其中嵌套的散列的键全都转换成字符串。例如: ```ruby {nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_stringify_keys # => {""=>nil, "1"=>1, "nested"=>{"a"=>3, "5"=>5}} ``` -NOTE: Defined in `active_support/core_ext/hash/keys.rb`. +NOTE: 在 `active_support/core_ext/hash/keys.rb` 文件中定义。 + + -#### `symbolize_keys` and `symbolize_keys!` +#### `symbolize_keys` 和 `symbolize_keys!` -The method `symbolize_keys` returns a hash that has a symbolized version of the keys in the receiver, where possible. It does so by sending `to_sym` to them: +`symbolize_keys` 方法把接收者中的键尽量变成符号。为此,它在键上调用 `to_sym`。 ```ruby {nil => nil, 1 => 1, "a" => "a"}.symbolize_keys -# => {1=>1, nil=>nil, :a=>"a"} +# => {nil=>nil, 1=>1, :a=>"a"} ``` -WARNING. Note in the previous example only one key was symbolized. +WARNING: 注意,在上例中,只有键变成了符号。 -In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash: +遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。 ```ruby {"a" => 1, a: 2}.symbolize_keys -# The result could either be +# 结果可能是 # => {:a=>2} -# or +# 也可能是 # => {:a=>1} ``` -This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionController::UrlRewriter` defines +使用这个方法,选项既可以是符号,也可以是字符串。例如 `ActionController::UrlRewriter` 定义的这个方法: ```ruby def rewrite_path(options) @@ -2830,60 +2948,85 @@ def rewrite_path(options) end ``` -The second line can safely access the `:params` key, and let the user to pass either `:params` or "params". +因为有第二行,所以用户可以传入 `:params` 或 `"params"`。 -There's also the bang variant `symbolize_keys!` that symbolizes keys in the very receiver. +也有爆炸版本,`symbolize_keys!`,直接把接收者的键变成符号。 -Besides that, one can use `deep_symbolize_keys` and `deep_symbolize_keys!` to symbolize all the keys in the given hash and all the hashes nested into it. An example of the result is: +此外,可以使用 `deep_symbolize_keys` 和 `deep_symbolize_keys!` 把指定散列及其中嵌套的散列的键全都转换成符号。例如: ```ruby {nil => nil, 1 => 1, "nested" => {"a" => 3, 5 => 5}}.deep_symbolize_keys # => {nil=>nil, 1=>1, nested:{a:3, 5=>5}} ``` -NOTE: Defined in `active_support/core_ext/hash/keys.rb`. +NOTE: 在 `active_support/core_ext/hash/keys.rb` 文件中定义。 -#### `to_options` and `to_options!` + -The methods `to_options` and `to_options!` are respectively aliases of `symbolize_keys` and `symbolize_keys!`. +#### `to_options` 和 `to_options!` -NOTE: Defined in `active_support/core_ext/hash/keys.rb`. +`to_options` 和 `to_options!` 分别是 `symbolize_keys` and `symbolize_keys!` 的别名。 + +NOTE: 在 `active_support/core_ext/hash/keys.rb` 文件中定义。 + + #### `assert_valid_keys` -The method `assert_valid_keys` receives an arbitrary number of arguments, and checks whether the receiver has any key outside that white list. If it does `ArgumentError` is raised. +`assert_valid_keys` 方法的参数数量不定,检查接收者的键是否在白名单之外。如果是,抛出 `ArgumentError` 异常。 ```ruby {a: 1}.assert_valid_keys(:a) # passes {a: 1}.assert_valid_keys("a") # ArgumentError ``` -Active Record does not accept unknown options when building associations, for example. It implements that control via `assert_valid_keys`. +例如,Active Record 构建关联时不接受未知的选项。这个功能就是通过 `assert_valid_keys` 实现的。 + +NOTE: 在 `active_support/core_ext/hash/keys.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/hash/keys.rb`. + -### Slicing +### 处理值 -Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes: + + +#### `transform_values` 和 `transform_values!` + +`transform_values` 的参数是一个块,使用块中的代码处理接收者中的各个值。 + +```ruby +{ nil => nil, 1 => 1, :x => :a }.transform_values { |value| value.to_s.upcase } +# => {nil=>"", 1=>"1", :x=>"A"} +``` + +也有爆炸版本,`transform_values!`,就地处理接收者的值。 + +NOTE: 在 `active_support/core_ext/hash/transform_values.rb` 文件中定义。 + + + +### 切片 + +Ruby 原生支持从字符串和数组中提取切片。Active Support 为散列增加了这个功能: ```ruby {a: 1, b: 2, c: 3}.slice(:a, :c) -# => {:c=>3, :a=>1} +# => {:a=>1, :c=>3} {a: 1, b: 2, c: 3}.slice(:b, :X) -# => {:b=>2} # non-existing keys are ignored +# => {:b=>2} # 不存在的键会被忽略 ``` -If the receiver responds to `convert_key` keys are normalized: +如果接收者响应 `convert_key`,会使用它对键做整形: ```ruby {a: 1, b: 2}.with_indifferent_access.slice("a") # => {:a=>1} ``` -NOTE. Slicing may come in handy for sanitizing option hashes with a white list of keys. +NOTE: 可以通过切片使用键白名单净化选项散列。 -There's also `slice!` which in addition to perform a slice in place returns what's removed: +也有 `slice!`,它就地执行切片,返回被删除的键值对: ```ruby hash = {a: 1, b: 2} @@ -2891,11 +3034,13 @@ rest = hash.slice!(:a) # => {:b=>2} hash # => {:a=>1} ``` -NOTE: Defined in `active_support/core_ext/hash/slice.rb`. +NOTE: 在 `active_support/core_ext/hash/slice.rb` 文件中定义。 + + -### Extracting +### 提取 -The method `extract!` removes and returns the key/value pairs matching the given keys. +`extract!` 方法删除并返回匹配指定键的键值对。 ```ruby hash = {a: 1, b: 2} @@ -2903,7 +3048,7 @@ rest = hash.extract!(:a) # => {:a=>1} hash # => {:b=>2} ``` -The method `extract!` returns the same subclass of Hash, that the receiver is. +`extract!` 方法的返回值类型与接收者一样,是 `Hash` 或其子类。 ```ruby hash = {a: 1, b: 2}.with_indifferent_access @@ -2911,34 +3056,41 @@ rest = hash.extract!(:a).class # => ActiveSupport::HashWithIndifferentAccess ``` -NOTE: Defined in `active_support/core_ext/hash/slice.rb`. +NOTE: 在 `active_support/core_ext/hash/slice.rb` 文件中定义。 + + -### Indifferent Access +### 无差别访问 -The method `with_indifferent_access` returns an `ActiveSupport::HashWithIndifferentAccess` out of its receiver: +`with_indifferent_access` 方法把接收者转换成 `ActiveSupport::HashWithIndifferentAccess` 实例: ```ruby {a: 1}.with_indifferent_access["a"] # => 1 ``` -NOTE: Defined in `active_support/core_ext/hash/indifferent_access.rb`. +NOTE: 在 `active_support/core_ext/hash/indifferent_access.rb` 文件中定义。 -### Compacting + -The methods `compact` and `compact!` return a Hash without items with `nil` value. +### 压缩 + +`compact` 和 `compact!` 方法返回没有 `nil` 值的散列: ```ruby {a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2} ``` -NOTE: Defined in `active_support/core_ext/hash/compact.rb`. +NOTE: 在 `active_support/core_ext/hash/compact.rb` 文件中定义。 + + + +## `Regexp` 的扩展 -Extensions to `Regexp` ----------------------- + ### `multiline?` -The method `multiline?` says whether a regexp has the `/m` flag set, that is, whether the dot matches newlines. +`multiline?` 方法判断正则表达式有没有设定 `/m` 旗标,即点号是否匹配换行符。 ```ruby %r{.}.multiline? # => false @@ -2948,7 +3100,7 @@ Regexp.new('.').multiline? # => false Regexp.new('.', Regexp::MULTILINE).multiline? # => true ``` -Rails uses this method in a single place, also in the routing code. Multiline regexps are disallowed for route requirements and this flag eases enforcing that constraint. +Rails 只在一处用到了这个方法,也在路由代码中。路由的条件不允许使用多行正则表达式,这个方法简化了这一约束的实施。 ```ruby def assign_route_options(segments, defaults, requirements) @@ -2960,14 +3112,33 @@ def assign_route_options(segments, defaults, requirements) end ``` -NOTE: Defined in `active_support/core_ext/regexp.rb`. +NOTE: 在 `active_support/core_ext/regexp.rb` 文件中定义。 + + + +### `match?` + +Rails 实现了 `Regexp#match?` 方法,供 Ruby 2.4 之前的版本使用: + +```ruby +/oo/.match?('foo') # => true +/oo/.match?('bar') # => false +/oo/.match?('foo', 1) # => true +``` + +这个向后移植的版本与原生的 `match?` 方法具有相同的接口,但是调用方没有未设定 `$1` 等副作用,不过速度没什么优势。定义这个方法的目的是编写与 2.4 兼容的代码。Rails 内部有用到这个判断方法。 + +只有 Ruby 未定义 `Regexp#match?` 方法时,Rails 才会定义,因此在 Ruby 2.4 或以上版本中运行的代码使用的是原生版本,性能有保障。 -Extensions to `Range` ---------------------- + + +## `Range` 的扩展 + + ### `to_s` -Active Support extends the method `Range#to_s` so that it understands an optional format argument. As of this writing the only supported non-default format is `:db`: +Active Support 扩展了 `Range#to_s` 方法,让它接受一个可选的格式参数。目前,唯一支持的非默认格式是 `:db`: ```ruby (Date.today..Date.tomorrow).to_s @@ -2977,19 +3148,21 @@ Active Support extends the method `Range#to_s` so that it understands an optiona # => "BETWEEN '2009-10-25' AND '2009-10-26'" ``` -As the example depicts, the `:db` format generates a `BETWEEN` SQL clause. That is used by Active Record in its support for range values in conditions. +如上例所示,`:db` 格式生成一个 `BETWEEN` SQL 子句。Active Record 使用它支持范围值条件。 + +NOTE: 在 `active_support/core_ext/range/conversions.rb` 文件中定义。 -NOTE: Defined in `active_support/core_ext/range/conversions.rb`. + ### `include?` -The methods `Range#include?` and `Range#===` say whether some value falls between the ends of a given instance: +`Range#include?` 和 `Range#===` 方法判断值是否在值域的范围内: ```ruby (2..3).include?(Math::E) # => true ``` -Active Support extends these methods so that the argument may be another range in turn. In that case we test whether the ends of the argument range belong to the receiver themselves: +Active Support 扩展了这两个方法,允许参数为另一个值域。此时,测试参数指定的值域是否在接收者的范围内: ```ruby (1..10).include?(3..7) # => true @@ -3003,11 +3176,13 @@ Active Support extends these methods so that the argument may be another range i (1...9) === (3..9) # => false ``` -NOTE: Defined in `active_support/core_ext/range/include_range.rb`. +NOTE: 在 `active_support/core_ext/range/include_range.rb` 文件中定义。 + + ### `overlaps?` -The method `Range#overlaps?` says whether any two given ranges have non-void intersection: +`Range#overlaps?` 方法测试两个值域是否有交集: ```ruby (1..10).overlaps?(7..11) # => true @@ -3015,75 +3190,37 @@ The method `Range#overlaps?` says whether any two given ranges have non-void int (1..10).overlaps?(11..27) # => false ``` -NOTE: Defined in `active_support/core_ext/range/overlaps.rb`. - -Extensions to `Proc` --------------------- - -### `bind` - -As you surely know Ruby has an `UnboundMethod` class whose instances are methods that belong to the limbo of methods without a self. The method `Module#instance_method` returns an unbound method for example: - -```ruby -Hash.instance_method(:delete) # => # -``` - -An unbound method is not callable as is, you need to bind it first to an object with `bind`: - -```ruby -clear = Hash.instance_method(:clear) -clear.bind({a: 1}).call # => {} -``` - -Active Support defines `Proc#bind` with an analogous purpose: - -```ruby -Proc.new { size }.bind([]).call # => 0 -``` - -As you see that's callable and bound to the argument, the return value is indeed a `Method`. +NOTE: 在 `active_support/core_ext/range/overlaps.rb` 文件中定义。 -NOTE: To do so `Proc#bind` actually creates a method under the hood. If you ever see a method with a weird name like `__bind_1256598120_237302` in a stack trace you know now where it comes from. + -Action Pack uses this trick in `rescue_from` for example, which accepts the name of a method and also a proc as callbacks for a given rescued exception. It has to call them in either case, so a bound method is returned by `handler_for_rescue`, thus simplifying the code in the caller: +## `Date` 的扩展 -```ruby -def handler_for_rescue(exception) - _, rescuer = Array(rescue_handlers).reverse.detect do |klass_name, handler| - ... - end + - case rescuer - when Symbol - method(rescuer) - when Proc - rescuer.bind(self) - end -end -``` +### 计算 -NOTE: Defined in `active_support/core_ext/proc.rb`. +NOTE: 这一节的方法都在 `active_support/core_ext/date/calculations.rb` 文件中定义。 -Extensions to `Date` --------------------- +TIP: 下述计算方法在 1582 年 10 月有边缘情况,因为 5..14 日不存在。简单起见,本文没有说明这些日子的行为,不过可以说,其行为与预期是相符的。即,`Date.new(1582, 10, 4).tomorrow` 返回 `Date.new(1582, 10, 15)`,等等。预期的行为参见 `test/core_ext/date_ext_test.rb` 中的 Active Support 测试组件。 -### Calculations + -NOTE: All the following methods are defined in `active_support/core_ext/date/calculations.rb`. +#### `Date.current` -INFO: The following calculation methods have edge cases in October 1582, since days 5..14 just do not exist. This guide does not document their behavior around those days for brevity, but it is enough to say that they do what you would expect. That is, `Date.new(1582, 10, 4).tomorrow` returns `Date.new(1582, 10, 15)` and so on. Please check `test/core_ext/date_ext_test.rb` in the Active Support test suite for expected behavior. +Active Support 定义的 `Date.current` 方法表示当前时区中的今天。其作用类似于 `Date.today`,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了 `Date.yesterday` 和 `Date.tomorrow`,以及实例判断方法 `past?`、`today?`、`future?`、`on_weekday?` 和 `on_weekend?`,这些方法都与 `Date.current` 相关。 -#### `Date.current` +比较日期时,如果要考虑用户设定的时区,应该使用 `Date.current`,而不是 `Date.today`。与系统的时区(`Date.today` 默认采用)相比,用户设定的时区可能超前,这意味着,`Date.today` 可能等于 `Date.yesterday`。 -Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Date.current`. + -When making Date comparisons using methods which honor the user time zone, make sure to use `Date.current` and not `Date.today`. There are cases where the user time zone might be in the future compared to the system time zone, which `Date.today` uses by default. This means `Date.today` may equal `Date.yesterday`. +#### 具名日期 -#### Named dates + -##### `prev_year`, `next_year` +##### `prev_year`、`next_year` -In Ruby 1.9 `prev_year` and `next_year` return a date with the same day/month in the last or next year: +在 Ruby 1.9 中,`prev_year` 和 `next_year` 方法返回前一年和下一年中的相同月和日: ```ruby d = Date.new(2010, 5, 8) # => Sat, 08 May 2010 @@ -3091,7 +3228,7 @@ d.prev_year # => Fri, 08 May 2009 d.next_year # => Sun, 08 May 2011 ``` -If date is the 29th of February of a leap year, you obtain the 28th: +如果是润年的 2 月 29 日,得到的是 28 日: ```ruby d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000 @@ -3099,11 +3236,13 @@ d.prev_year # => Sun, 28 Feb 1999 d.next_year # => Wed, 28 Feb 2001 ``` -`prev_year` is aliased to `last_year`. +`last_year` 是 `prev_year` 的别名。 + + -##### `prev_month`, `next_month` +##### `prev_month`、`next_month` -In Ruby 1.9 `prev_month` and `next_month` return the date with the same day in the last or next month: +在 Ruby 1.9 中,`prev_month` 和 `next_month` 方法分别返回前一个月和后一个月中的相同日: ```ruby d = Date.new(2010, 5, 8) # => Sat, 08 May 2010 @@ -3111,7 +3250,7 @@ d.prev_month # => Thu, 08 Apr 2010 d.next_month # => Tue, 08 Jun 2010 ``` -If such a day does not exist, the last day of the corresponding month is returned: +如果日不存在,返回前一月中的最后一天: ```ruby Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000 @@ -3120,11 +3259,13 @@ Date.new(2000, 5, 31).next_month # => Fri, 30 Jun 2000 Date.new(2000, 1, 31).next_month # => Tue, 29 Feb 2000 ``` -`prev_month` is aliased to `last_month`. +`last_month` 是 `prev_month` 的别名。 -##### `prev_quarter`, `next_quarter` + -Same as `prev_month` and `next_month`. It returns the date with the same day in the previous or next quarter: +##### `prev_quarter`、`next_quarter` + +类似于 `prev_month` 和 `next_month`,返回前一季度和下一季度中的相同日: ```ruby t = Time.local(2010, 5, 8) # => Sat, 08 May 2010 @@ -3132,7 +3273,7 @@ t.prev_quarter # => Mon, 08 Feb 2010 t.next_quarter # => Sun, 08 Aug 2010 ``` -If such a day does not exist, the last day of the corresponding month is returned: +如果日不存在,返回前一月中的最后一天: ```ruby Time.local(2000, 7, 31).prev_quarter # => Sun, 30 Apr 2000 @@ -3141,14 +3282,13 @@ Time.local(2000, 10, 31).prev_quarter # => Mon, 30 Oct 2000 Time.local(2000, 11, 31).next_quarter # => Wed, 28 Feb 2001 ``` -`prev_quarter` is aliased to `last_quarter`. +`last_quarter` 是 `prev_quarter` 的别名。 + + -##### `beginning_of_week`, `end_of_week` +##### `beginning_of_week`、`end_of_week` -The methods `beginning_of_week` and `end_of_week` return the dates for the -beginning and end of the week, respectively. Weeks are assumed to start on -Monday, but that can be changed passing an argument, setting thread local -`Date.beginning_of_week` or `config.beginning_of_week`. +`beginning_of_week` 和 `end_of_week` 方法分别返回某一周的第一天和最后一天的日期。一周假定从周一开始,不过这是可以修改的,方法是在线程中设定 `Date.beginning_of_week` 或 `config.beginning_of_week`。 ```ruby d = Date.new(2010, 5, 8) # => Sat, 08 May 2010 @@ -3158,12 +3298,13 @@ d.end_of_week # => Sun, 09 May 2010 d.end_of_week(:sunday) # => Sat, 08 May 2010 ``` -`beginning_of_week` is aliased to `at_beginning_of_week` and `end_of_week` is aliased to `at_end_of_week`. +`at_beginning_of_week` 是 `beginning_of_week` 的别名,`at_end_of_week` 是 `end_of_week` 的别名。 + + -##### `monday`, `sunday` +##### `monday`、`sunday` -The methods `monday` and `sunday` return the dates for the previous Monday and -next Sunday, respectively. +`monday` 和 `sunday` 方法分别返回前一个周一和下一个周日的日期: ```ruby d = Date.new(2010, 5, 8) # => Sat, 08 May 2010 @@ -3177,9 +3318,11 @@ d = Date.new(2012, 9, 16) # => Sun, 16 Sep 2012 d.sunday # => Sun, 16 Sep 2012 ``` -##### `prev_week`, `next_week` + -The method `next_week` receives a symbol with a day name in English (default is the thread local `Date.beginning_of_week`, or `config.beginning_of_week`, or `:monday`) and it returns the date corresponding to that day. +##### `prev_week`、`next_week` + +`next_week` 的参数是一个符号,指定周几的英文名称(默认为线程中的 `Date.beginning_of_week` 或 `config.beginning_of_week`,或者 `:monday`),返回那一天的日期。 ```ruby d = Date.new(2010, 5, 9) # => Sun, 09 May 2010 @@ -3187,7 +3330,7 @@ d.next_week # => Mon, 10 May 2010 d.next_week(:saturday) # => Sat, 15 May 2010 ``` -The method `prev_week` is analogous: +`prev_week` 的作用类似: ```ruby d.prev_week # => Mon, 26 Apr 2010 @@ -3195,13 +3338,15 @@ d.prev_week(:saturday) # => Sat, 01 May 2010 d.prev_week(:friday) # => Fri, 30 Apr 2010 ``` -`prev_week` is aliased to `last_week`. +`last_week` 是 `prev_week` 的别名。 + +设定 `Date.beginning_of_week` 或 `config.beginning_of_week` 之后,`next_week` 和 `prev_week` 能按预期工作。 -Both `next_week` and `prev_week` work as expected when `Date.beginning_of_week` or `config.beginning_of_week` are set. + -##### `beginning_of_month`, `end_of_month` +##### `beginning_of_month`、`end_of_month` -The methods `beginning_of_month` and `end_of_month` return the dates for the beginning and end of the month: +`beginning_of_month` 和 `end_of_month` 方法分别返回某个月的第一天和最后一天的日期: ```ruby d = Date.new(2010, 5, 9) # => Sun, 09 May 2010 @@ -3209,11 +3354,13 @@ d.beginning_of_month # => Sat, 01 May 2010 d.end_of_month # => Mon, 31 May 2010 ``` -`beginning_of_month` is aliased to `at_beginning_of_month`, and `end_of_month` is aliased to `at_end_of_month`. +`at_beginning_of_month` 是 `beginning_of_month` 的别名,`at_end_of_month` 是 `end_of_month` 的别名。 + + -##### `beginning_of_quarter`, `end_of_quarter` +##### `beginning_of_quarter`、`end_of_quarter` -The methods `beginning_of_quarter` and `end_of_quarter` return the dates for the beginning and end of the quarter of the receiver's calendar year: +`beginning_of_quarter` 和 `end_of_quarter` 分别返回接收者日历年的季度第一天和最后一天的日期: ```ruby d = Date.new(2010, 5, 9) # => Sun, 09 May 2010 @@ -3221,11 +3368,13 @@ d.beginning_of_quarter # => Thu, 01 Apr 2010 d.end_of_quarter # => Wed, 30 Jun 2010 ``` -`beginning_of_quarter` is aliased to `at_beginning_of_quarter`, and `end_of_quarter` is aliased to `at_end_of_quarter`. +`at_beginning_of_quarter` 是 `beginning_of_quarter` 的别名,`at_end_of_quarter` 是 `end_of_quarter` 的别名。 -##### `beginning_of_year`, `end_of_year` + -The methods `beginning_of_year` and `end_of_year` return the dates for the beginning and end of the year: +##### `beginning_of_year`、`end_of_year` + +`beginning_of_year` 和 `end_of_year` 方法分别返回一年的第一天和最后一天的日期: ```ruby d = Date.new(2010, 5, 9) # => Sun, 09 May 2010 @@ -3233,61 +3382,71 @@ d.beginning_of_year # => Fri, 01 Jan 2010 d.end_of_year # => Fri, 31 Dec 2010 ``` -`beginning_of_year` is aliased to `at_beginning_of_year`, and `end_of_year` is aliased to `at_end_of_year`. +`at_beginning_of_year` 是 `beginning_of_year` 的别名,`at_end_of_year` 是 `end_of_year` 的别名。 + + + +#### 其他日期计算方法 -#### Other Date Computations + -##### `years_ago`, `years_since` +##### `years_ago`、`years_since` -The method `years_ago` receives a number of years and returns the same date those many years ago: +`years_ago` 方法的参数是一个数字,返回那么多年以前同一天的日期: ```ruby date = Date.new(2010, 6, 7) date.years_ago(10) # => Wed, 07 Jun 2000 ``` -`years_since` moves forward in time: +`years_since` 方法向前移动时间: ```ruby date = Date.new(2010, 6, 7) date.years_since(10) # => Sun, 07 Jun 2020 ``` -If such a day does not exist, the last day of the corresponding month is returned: +如果那一天不存在,返回前一个月的最后一天: ```ruby Date.new(2012, 2, 29).years_ago(3) # => Sat, 28 Feb 2009 Date.new(2012, 2, 29).years_since(3) # => Sat, 28 Feb 2015 ``` -##### `months_ago`, `months_since` + -The methods `months_ago` and `months_since` work analogously for months: +##### `months_ago`、`months_since` + +`months_ago` 和 `months_since` 方法的作用类似,不过是针对月的: ```ruby Date.new(2010, 4, 30).months_ago(2) # => Sun, 28 Feb 2010 Date.new(2010, 4, 30).months_since(2) # => Wed, 30 Jun 2010 ``` -If such a day does not exist, the last day of the corresponding month is returned: +如果那一天不存在,返回前一个月的最后一天: ```ruby Date.new(2010, 4, 30).months_ago(2) # => Sun, 28 Feb 2010 Date.new(2009, 12, 31).months_since(2) # => Sun, 28 Feb 2010 ``` + + ##### `weeks_ago` -The method `weeks_ago` works analogously for weeks: +`weeks_ago` 方法的作用类似,不过是针对周的: ```ruby Date.new(2010, 5, 24).weeks_ago(1) # => Mon, 17 May 2010 Date.new(2010, 5, 24).weeks_ago(2) # => Mon, 10 May 2010 ``` + + ##### `advance` -The most generic way to jump to other days is `advance`. This method receives a hash with keys `:years`, `:months`, `:weeks`, `:days`, and returns a date advanced as much as the present keys indicate: +跳到另一天最普适的方法是 `advance`。这个方法的参数是一个散列,包含 `:years`、`:months`、`:weeks`、`:days` 键,返回移动相应量之后的日期。 ```ruby date = Date.new(2010, 6, 6) @@ -3295,43 +3454,47 @@ date.advance(years: 1, weeks: 2) # => Mon, 20 Jun 2011 date.advance(months: 2, days: -2) # => Wed, 04 Aug 2010 ``` -Note in the previous example that increments may be negative. +如上例所示,增量可以是负数。 -To perform the computation the method first increments years, then months, then weeks, and finally days. This order is important towards the end of months. Say for example we are at the end of February of 2010, and we want to move one month and one day forward. +这个方法做计算时,先增加年,然后是月和周,最后是日。这个顺序是重要的,向一个月的末尾流动。假如我们在 2010 年 2 月的最后一天,我们想向前移动一个月和一天。 -The method `advance` advances first one month, and then one day, the result is: +此时,`advance` 先向前移动一个月,然后移动一天,结果是: ```ruby Date.new(2010, 2, 28).advance(months: 1, days: 1) # => Sun, 29 Mar 2010 ``` -While if it did it the other way around the result would be different: +如果以其他方式移动,得到的结果就不同了: ```ruby Date.new(2010, 2, 28).advance(days: 1).advance(months: 1) # => Thu, 01 Apr 2010 ``` -#### Changing Components + -The method `change` allows you to get a new date which is the same as the receiver except for the given year, month, or day: +#### 修改日期组成部分 + +`change` 方法在接收者的基础上修改日期,修改的值由参数指定: ```ruby Date.new(2010, 12, 23).change(year: 2011, month: 11) # => Wed, 23 Nov 2011 ``` -This method is not tolerant to non-existing dates, if the change is invalid `ArgumentError` is raised: +这个方法无法容错不存在的日期,如果修改无效,抛出 `ArgumentError` 异常: ```ruby Date.new(2010, 1, 31).change(month: 2) # => ArgumentError: invalid date ``` -#### Durations + + +#### 时间跨度 -Durations can be added to and subtracted from dates: +可以为日期增加或减去时间跨度: ```ruby d = Date.current @@ -3342,105 +3505,114 @@ d - 3.hours # => Sun, 08 Aug 2010 21:00:00 UTC +00:00 ``` -They translate to calls to `since` or `advance`. For example here we get the correct jump in the calendar reform: +增加跨度会调用 `since` 或 `advance`。例如,跳跃时能正确考虑历法改革: ```ruby Date.new(1582, 10, 4) + 1.day # => Fri, 15 Oct 1582 ``` -#### Timestamps + + +#### 时间戳 -INFO: The following methods return a `Time` object if possible, otherwise a `DateTime`. If set, they honor the user time zone. +TIP: 如果可能,下述方法返回 `Time` 对象,否则返回 `DateTime` 对象。如果用户设定了时区,会将其考虑在内。 -##### `beginning_of_day`, `end_of_day` + -The method `beginning_of_day` returns a timestamp at the beginning of the day (00:00:00): +##### `beginning_of_day`、`end_of_day` + +`beginning_of_day` 方法返回一天的起始时间戳(00:00:00): ```ruby date = Date.new(2010, 6, 7) date.beginning_of_day # => Mon Jun 07 00:00:00 +0200 2010 ``` -The method `end_of_day` returns a timestamp at the end of the day (23:59:59): +`end_of_day` 方法返回一天的结束时间戳(23:59:59): ```ruby date = Date.new(2010, 6, 7) date.end_of_day # => Mon Jun 07 23:59:59 +0200 2010 ``` -`beginning_of_day` is aliased to `at_beginning_of_day`, `midnight`, `at_midnight`. +`at_beginning_of_day`、`midnight` 和 `at_midnight` 是 `beginning_of_day` 的别名, + + -##### `beginning_of_hour`, `end_of_hour` +##### `beginning_of_hour`、`end_of_hour` -The method `beginning_of_hour` returns a timestamp at the beginning of the hour (hh:00:00): +`beginning_of_hour` 返回一小时的起始时间戳(hh:00:00): ```ruby date = DateTime.new(2010, 6, 7, 19, 55, 25) date.beginning_of_hour # => Mon Jun 07 19:00:00 +0200 2010 ``` -The method `end_of_hour` returns a timestamp at the end of the hour (hh:59:59): +`end_of_hour` 方法返回一小时的结束时间戳(hh:59:59): ```ruby date = DateTime.new(2010, 6, 7, 19, 55, 25) date.end_of_hour # => Mon Jun 07 19:59:59 +0200 2010 ``` -`beginning_of_hour` is aliased to `at_beginning_of_hour`. +`at_beginning_of_hour` 是 `beginning_of_hour` 的别名。 + + -##### `beginning_of_minute`, `end_of_minute` +##### `beginning_of_minute`、`end_of_minute` -The method `beginning_of_minute` returns a timestamp at the beginning of the minute (hh:mm:00): +`beginning_of_minute` 方法返回一分钟的起始时间戳(hh:mm:00): ```ruby date = DateTime.new(2010, 6, 7, 19, 55, 25) date.beginning_of_minute # => Mon Jun 07 19:55:00 +0200 2010 ``` -The method `end_of_minute` returns a timestamp at the end of the minute (hh:mm:59): +`end_of_minute` 方法返回一分钟的结束时间戳(hh:mm:59): ```ruby date = DateTime.new(2010, 6, 7, 19, 55, 25) date.end_of_minute # => Mon Jun 07 19:55:59 +0200 2010 ``` -`beginning_of_minute` is aliased to `at_beginning_of_minute`. +`at_beginning_of_minute` 是 `beginning_of_minute` 的别名。 -INFO: `beginning_of_hour`, `end_of_hour`, `beginning_of_minute` and `end_of_minute` are implemented for `Time` and `DateTime` but **not** `Date` as it does not make sense to request the beginning or end of an hour or minute on a `Date` instance. +TIP: `Time` 和 `DateTime` 实现了 `beginning_of_hour`、`end_of_hour`、`beginning_of_minute` 和 `end_of_minute` 方法,但是 `Date` 没有实现,因为在 `Date` 实例上请求小时和分钟的起始和结束时间戳没有意义。 -##### `ago`, `since` + -The method `ago` receives a number of seconds as argument and returns a timestamp those many seconds ago from midnight: +##### `ago`、`since` + +`ago` 的参数是秒数,返回自午夜起那么多秒之后的时间戳: ```ruby date = Date.current # => Fri, 11 Jun 2010 date.ago(1) # => Thu, 10 Jun 2010 23:59:59 EDT -04:00 ``` -Similarly, `since` moves forward: +类似的,`since` 向前移动: ```ruby date = Date.current # => Fri, 11 Jun 2010 date.since(1) # => Fri, 11 Jun 2010 00:00:01 EDT -04:00 ``` -#### Other Time Computations + -### Conversions +## `DateTime` 的扩展 -Extensions to `DateTime` ------------------------- +WARNING: `DateTime` 不理解夏令时规则,因此如果正处于夏令时,这些方法可能有边缘情况。例如,在夏令时中,`seconds_since_midnight` 可能无法返回真实的量。 -WARNING: `DateTime` is not aware of DST rules and so some of these methods have edge cases when a DST change is going on. For example `seconds_since_midnight` might not return the real amount in such a day. + -### Calculations +### 计算 -NOTE: All the following methods are defined in `active_support/core_ext/date_time/calculations.rb`. +NOTE: 本节的方法都在 `active_support/core_ext/date_time/calculations.rb` 文件中定义。 -The class `DateTime` is a subclass of `Date` so by loading `active_support/core_ext/date/calculations.rb` you inherit these methods and their aliases, except that they will always return datetimes: +`DateTime` 类是 `Date` 的子类,因此加载 `active_support/core_ext/date/calculations.rb` 时也就继承了下述方法及其别名,只不过,此时都返回 `DateTime` 对象: -```ruby +```text yesterday tomorrow beginning_of_week (at_beginning_of_week) @@ -3464,57 +3636,71 @@ years_ago years_since prev_year (last_year) next_year +on_weekday? +on_weekend? ``` -The following methods are reimplemented so you do **not** need to load `active_support/core_ext/date/calculations.rb` for these ones: +下述方法重新实现了,因此使用它们时无需加载 `active_support/core_ext/date/calculations.rb`: -```ruby +```text beginning_of_day (midnight, at_midnight, at_beginning_of_day) end_of_day ago since (in) ``` -On the other hand, `advance` and `change` are also defined and support more options, they are documented below. +此外,还定义了 `advance` 和 `change` 方法,而且支持更多选项。参见下文。 -The following methods are only implemented in `active_support/core_ext/date_time/calculations.rb` as they only make sense when used with a `DateTime` instance: +下述方法只在 `active_support/core_ext/date_time/calculations.rb` 中实现,因为它们只对 `DateTime` 实例有意义: ```ruby beginning_of_hour (at_beginning_of_hour) end_of_hour ``` -#### Named Datetimes + + +#### 具名日期时间 + + ##### `DateTime.current` -Active Support defines `DateTime.current` to be like `Time.now.to_datetime`, except that it honors the user time zone, if defined. It also defines `DateTime.yesterday` and `DateTime.tomorrow`, and the instance predicates `past?`, and `future?` relative to `DateTime.current`. +Active Support 定义的 `DateTime.current` 方法类似于 `Time.now.to_datetime`,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了 `DateTime.yesterday` 和 `DateTime.tomorrow`,以及与 `DateTime.current` 相关的判断方法 `past?` 和 `future?`。 -#### Other Extensions + + +#### 其他扩展 + + ##### `seconds_since_midnight` -The method `seconds_since_midnight` returns the number of seconds since midnight: +`seconds_since_midnight` 方法返回自午夜起的秒数: ```ruby now = DateTime.current # => Mon, 07 Jun 2010 20:26:36 +0000 now.seconds_since_midnight # => 73596 ``` + + ##### `utc` -The method `utc` gives you the same datetime in the receiver expressed in UTC. +`utc` 返回的日期时间与接收者一样,不过使用 UTC 表示。 ```ruby now = DateTime.current # => Mon, 07 Jun 2010 19:27:52 -0400 now.utc # => Mon, 07 Jun 2010 23:27:52 +0000 ``` -This method is also aliased as `getutc`. +这个方法有个别名,`getutc`。 + + ##### `utc?` -The predicate `utc?` says whether the receiver has UTC as its time zone: +`utc?` 判断接收者的时区是不是 UTC: ```ruby now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400 @@ -3522,9 +3708,11 @@ now.utc? # => false now.utc.utc? # => true ``` + + ##### `advance` -The most generic way to jump to another datetime is `advance`. This method receives a hash with keys `:years`, `:months`, `:weeks`, `:days`, `:hours`, `:minutes`, and `:seconds`, and returns a datetime advanced as much as the present keys indicate. +跳到其他日期时间最普适的方法是 `advance`。这个方法的参数是一个散列,包含 `:years`、`:months`、`:weeks`、`:days`、`:hours`、`:minutes` 和 `:seconds` 等键,返回移动相应量之后的日期时间。 ```ruby d = DateTime.current @@ -3533,9 +3721,9 @@ d.advance(years: 1, months: 1, days: 1, hours: 1, minutes: 1, seconds: 1) # => Tue, 06 Sep 2011 12:34:32 +0000 ``` -This method first computes the destination date passing `:years`, `:months`, `:weeks`, and `:days` to `Date#advance` documented above. After that, it adjusts the time calling `since` with the number of seconds to advance. This order is relevant, a different ordering would give different datetimes in some edge-cases. The example in `Date#advance` applies, and we can extend it to show order relevance related to the time bits. +这个方法计算目标日期时,把 `:years`、`:months`、`:weeks` 和 `:days` 传给 `Date#advance`,然后调用 `since` 处理时间,前进相应的秒数。这个顺序是重要的,如若不然,在某些边缘情况下可能得到不同的日期时间。讲解 `Date#advance` 时所举的例子在这里也适用,我们可以扩展一下,显示处理时间的顺序。 -If we first move the date bits (that have also a relative order of processing, as documented before), and then the time bits we get for example the following computation: +如果先移动日期部分(如前文所述,处理日期的顺序也很重要),然后再计算时间,得到的结果如下: ```ruby d = DateTime.new(2010, 2, 28, 23, 59, 59) @@ -3544,18 +3732,20 @@ d.advance(months: 1, seconds: 1) # => Mon, 29 Mar 2010 00:00:00 +0000 ``` -but if we computed them the other way around, the result would be different: +但是如果以其他方式计算,结果就不同了: ```ruby d.advance(seconds: 1).advance(months: 1) # => Thu, 01 Apr 2010 00:00:00 +0000 ``` -WARNING: Since `DateTime` is not DST-aware you can end up in a non-existing point in time with no warning or error telling you so. +WARNING: 因为 `DateTime` 不支持夏令时,所以可能得到不存在的时间点,而且没有提醒或报错。 + + -#### Changing Components +#### 修改日期时间组成部分 -The method `change` allows you to get a new datetime which is the same as the receiver except for the given options, which may include `:year`, `:month`, `:day`, `:hour`, `:min`, `:sec`, `:offset`, `:start`: +`change` 方法在接收者的基础上修改日期时间,修改的值由选项指定,可以包括 `:year`、`:month`、`:day`、`:hour`、`:min`、`:sec`、`:offset` 和 `:start`: ```ruby now = DateTime.current @@ -3564,30 +3754,32 @@ now.change(year: 2011, offset: Rational(-6, 24)) # => Wed, 08 Jun 2011 01:56:22 -0600 ``` -If hours are zeroed, then minutes and seconds are too (unless they have given values): +如果小时归零了,分钟和秒也归零(除非指定了值): ```ruby now.change(hour: 0) # => Tue, 08 Jun 2010 00:00:00 +0000 ``` -Similarly, if minutes are zeroed, then seconds are too (unless it has given a value): +类似地,如果分钟归零了,秒也归零(除非指定了值): ```ruby now.change(min: 0) # => Tue, 08 Jun 2010 01:00:00 +0000 ``` -This method is not tolerant to non-existing dates, if the change is invalid `ArgumentError` is raised: +这个方法无法容错不存在的日期,如果修改无效,抛出 `ArgumentError` 异常: ```ruby DateTime.current.change(month: 2, day: 30) # => ArgumentError: invalid date ``` -#### Durations + -Durations can be added to and subtracted from datetimes: +#### 时间跨度 + +可以为日期时间增加或减去时间跨度: ```ruby now = DateTime.current @@ -3598,23 +3790,26 @@ now - 1.week # => Mon, 02 Aug 2010 23:15:17 +0000 ``` -They translate to calls to `since` or `advance`. For example here we get the correct jump in the calendar reform: +增加跨度会调用 `since` 或 `advance`。例如,跳跃时能正确考虑历法改革: ```ruby DateTime.new(1582, 10, 4, 23) + 1.hour # => Fri, 15 Oct 1582 00:00:00 +0000 ``` -Extensions to `Time` --------------------- + -### Calculations +## `Time` 的扩展 -NOTE: All the following methods are defined in `active_support/core_ext/time/calculations.rb`. + -Active Support adds to `Time` many of the methods available for `DateTime`: +### 计算 -```ruby +NOTE: 本节的方法都在 `active_support/core_ext/time/calculations.rb` 文件中定义。 + +Active Support 为 `Time` 添加了 `DateTime` 的很多方法: + +```text past? today? future? @@ -3650,35 +3845,42 @@ years_ago years_since prev_year (last_year) next_year +on_weekday? +on_weekend? ``` -They are analogous. Please refer to their documentation above and take into account the following differences: +它们的作用与之前类似。详情参见前文,不过要知道下述区别: -* `change` accepts an additional `:usec` option. -* `Time` understands DST, so you get correct DST calculations as in +* `change` 额外接受 `:usec` 选项。 +* `Time` 支持夏令时,因此能正确计算夏令时。 -```ruby -Time.zone_default -# => # + ```ruby + Time.zone_default + # => # + + # 因为采用夏令时,在巴塞罗那,2010/03/28 02:00 +0100 变成 2010/03/28 03:00 +0200 + t = Time.local(2010, 3, 28, 1, 59, 59) + # => Sun Mar 28 01:59:59 +0100 2010 + t.advance(seconds: 1) + # => Sun Mar 28 03:00:00 +0200 2010 + ``` -# In Barcelona, 2010/03/28 02:00 +0100 becomes 2010/03/28 03:00 +0200 due to DST. -t = Time.local(2010, 3, 28, 1, 59, 59) -# => Sun Mar 28 01:59:59 +0100 2010 -t.advance(seconds: 1) -# => Sun Mar 28 03:00:00 +0200 2010 -``` -* If `since` or `ago` jump to a time that can't be expressed with `Time` a `DateTime` object is returned instead. +* 如果 `since` 或 `ago` 的目标时间无法使用 `Time` 对象表示,返回一个 `DateTime` 对象。 + + #### `Time.current` -Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines `Time.yesterday` and `Time.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`. +Active Support 定义的 `Time.current` 方法表示当前时区中的今天。其作用类似于 `Time.now`,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了与 `Time.current` 有关的实例判断方法 `past?`、`today?` 和 `future?`。 -When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` and not `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.today` uses by default. This means `Time.now` may equal `Time.yesterday`. +比较时间时,如果要考虑用户设定的时区,应该使用 `Time.current`,而不是 `Time.now`。与系统的时区(`Time.now` 默认采用)相比,用户设定的时区可能超前,这意味着,`Time.now.to_date` 可能等于 `Date.yesterday`。 -#### `all_day`, `all_week`, `all_month`, `all_quarter` and `all_year` + -The method `all_day` returns a range representing the whole day of the current time. +#### `all_day`、`all_week`、`all_month`、`all_quarter` 和 `all_year` + +`all_day` 方法返回一个值域,表示当前时间的一整天。 ```ruby now = Time.current @@ -3687,7 +3889,7 @@ now.all_day # => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00 ``` -Analogously, `all_week`, `all_month`, `all_quarter` and `all_year` all serve the purpose of generating time ranges. +类似地,`all_week`、`all_month`、`all_quarter` 和 `all_year` 分别生成相应的时间值域。 ```ruby now = Time.current @@ -3704,9 +3906,11 @@ now.all_year # => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00 ``` -### Time Constructors + + +### 时间构造方法 -Active Support defines `Time.current` to be `Time.zone.now` if there's a user time zone defined, with fallback to `Time.now`: +Active Support 定义的 `Time.current` 方法,在用户设定了时区时,等价于 `Time.zone.now`,否则回落到 `Time.now`: ```ruby Time.zone_default @@ -3715,13 +3919,15 @@ Time.current # => Fri, 06 Aug 2010 17:11:58 CEST +02:00 ``` -Analogously to `DateTime`, the predicates `past?`, and `future?` are relative to `Time.current`. +与 `DateTime` 一样,判断方法 `past?` 和 `future?` 与 `Time.current` 相关。 + +如果要构造的时间超出了运行时平台对 `Time` 的支持范围,微秒会被丢掉,然后返回 `DateTime` 对象。 -If the time to be constructed lies beyond the range supported by `Time` in the runtime platform, usecs are discarded and a `DateTime` object is returned instead. + -#### Durations +#### 时间跨度 -Durations can be added to and subtracted from time objects: +可以为时间增加或减去时间跨度: ```ruby now = Time.current @@ -3732,23 +3938,26 @@ now - 1.week # => Mon, 02 Aug 2010 23:21:11 UTC +00:00 ``` -They translate to calls to `since` or `advance`. For example here we get the correct jump in the calendar reform: +增加跨度会调用 `since` 或 `advance`。例如,跳跃时能正确考虑历法改革: ```ruby Time.utc(1582, 10, 3) + 5.days # => Mon Oct 18 00:00:00 UTC 1582 ``` -Extensions to `File` --------------------- + + +## `File` 的扩展 + + ### `atomic_write` -With the class method `File.atomic_write` you can write to a file in a way that will prevent any reader from seeing half-written content. +使用类方法 `File.atomic_write` 写文件时,可以避免在写到一半时读取内容。 -The name of the file is passed as an argument, and the method yields a file handle opened for writing. Once the block is done `atomic_write` closes the file handle and completes its job. +这个方法的参数是文件名,它会产出一个文件句柄,把文件打开供写入。块执行完毕后,`atomic_write` 会关闭文件句柄,完成工作。 -For example, Action Pack uses this method to write asset cache files like `all.css`: +例如,Action Pack 使用这个方法写静态资源缓存文件,如 `all.css`: ```ruby File.atomic_write(joined_asset_path) do |cache| @@ -3756,123 +3965,84 @@ File.atomic_write(joined_asset_path) do |cache| end ``` -To accomplish this `atomic_write` creates a temporary file. That's the file the code in the block actually writes to. On completion, the temporary file is renamed, which is an atomic operation on POSIX systems. If the target file exists `atomic_write` overwrites it and keeps owners and permissions. However there are a few cases where `atomic_write` cannot change the file ownership or permissions, this error is caught and skipped over trusting in the user/filesystem to ensure the file is accessible to the processes that need it. +为此,`atomic_write` 会创建一个临时文件。块中的代码其实是向这个临时文件写入。写完之后,重命名临时文件,这在 POSIX 系统中是原子操作。如果目标文件存在,`atomic_write` 将其覆盖,并且保留属主和权限。不过,有时 `atomic_write` 无法修改文件的归属或权限。这个错误会被捕获并跳过,从而确保需要它的进程能访问它。 -NOTE. Due to the chmod operation `atomic_write` performs, if the target file has an ACL set on it this ACL will be recalculated/modified. +NOTE: `atomic_write` 会执行 chmod 操作,因此如果目标文件设定了 ACL,`atomic_write` 会重新计算或修改 ACL。 -WARNING. Note you can't append with `atomic_write`. +WARNING: 注意,不能使用 `atomic_write` 追加内容。 -The auxiliary file is written in a standard directory for temporary files, but you can pass a directory of your choice as second argument. +临时文件在存储临时文件的标准目录中,但是可以传入第二个参数指定一个目录。 -NOTE: Defined in `active_support/core_ext/file/atomic.rb`. +NOTE: 在 `active_support/core_ext/file/atomic.rb` 文件中定义。 -Extensions to `Marshal` ------------------------ + -### `load` +## `Marshal` 的扩展 -Active Support adds constant autoloading support to `load`. + -For example, the file cache store deserializes this way: - -```ruby -File.open(file_name) { |f| Marshal.load(f) } -``` - -If the cached data refers to a constant that is unknown at that point, the autoloading mechanism is triggered and if it succeeds the deserialization is retried transparently. - -WARNING. If the argument is an `IO` it needs to respond to `rewind` to be able to retry. Regular files respond to `rewind`. - -NOTE: Defined in `active_support/core_ext/marshal.rb`. - -Extensions to `Logger` ----------------------- - -### `around_[level]` - -Takes two arguments, a `before_message` and `after_message` and calls the current level method on the `Logger` instance, passing in the `before_message`, then the specified message, then the `after_message`: - -```ruby -logger = Logger.new("log/development.log") -logger.around_info("before", "after") { |logger| logger.info("during") } -``` +### `load` -### `silence` +Active Support 为 `load` 增加了常量自动加载功能。 -Silences every log level lesser to the specified one for the duration of the given block. Log level orders are: debug, info, error and fatal. +例如,文件缓存存储像这样反序列化: ```ruby -logger = Logger.new("log/development.log") -logger.silence(Logger::INFO) do - logger.debug("In space, no one can hear you scream.") - logger.info("Scream all you want, small mailman!") -end +File.open(file_name) { |f| Marshal.load(f) } ``` -### `datetime_format=` +如果缓存的数据指代那一刻未知的常量,自动加载机制会被触发,如果成功加载,会再次尝试反序列化。 -Modifies the datetime format output by the formatter class associated with this logger. If the formatter class does not have a `datetime_format` method then this is ignored. +WARNING: 如果参数是 `IO` 对象,要能响应 `rewind` 方法才会重试。常规的文件响应 `rewind` 方法。 -```ruby -class Logger::FormatWithTime < Logger::Formatter - cattr_accessor(:datetime_format) { "%Y%m%d%H%m%S" } +NOTE: 在 `active_support/core_ext/marshal.rb` 文件中定义。 - def self.call(severity, timestamp, progname, msg) - "#{timestamp.strftime(datetime_format)} -- #{String === msg ? msg : msg.inspect}\n" - end -end - -logger = Logger.new("log/development.log") -logger.formatter = Logger::FormatWithTime -logger.info("<- is the current time") -``` + -NOTE: Defined in `active_support/core_ext/logger.rb`. +## `NameError` 的扩展 -Extensions to `NameError` -------------------------- +Active Support 为 `NameError` 增加了 `missing_name?` 方法,测试异常是不是由于参数的名称引起的。 -Active Support adds `missing_name?` to `NameError`, which tests whether the exception was raised because of the name passed as argument. +参数的名称可以使用符号或字符串指定。指定符号时,使用裸常量名测试;指定字符串时,使用完全限定常量名测试。 -The name may be given as a symbol or string. A symbol is tested against the bare constant name, a string is against the fully-qualified constant name. +TIP: 符号可以表示完全限定常量名,例如 `:"ActiveRecord::Base"`,因此这里符号的行为是为了便利而特别定义的,不是说在技术上只能如此。 -TIP: A symbol can represent a fully-qualified constant name as in `:"ActiveRecord::Base"`, so the behavior for symbols is defined for convenience, not because it has to be that way technically. - -For example, when an action of `ArticlesController` is called Rails tries optimistically to use `ArticlesHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `articles_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases: +例如,调用 `ArticlesController` 的动作时,Rails 会乐观地使用 `ArticlesHelper`。如果那个模块不存在也没关系,因此,由那个常量名引起的异常要静默。不过,可能是由于确实是未知的常量名而由 `articles_helper.rb` 抛出的 `NameError` 异常。此时,异常应该抛出。`missing_name?` 方法能区分这两种情况: ```ruby def default_helper_module! module_name = name.sub(/Controller$/, '') module_path = module_name.underscore helper module_path -rescue MissingSourceFile => e +rescue LoadError => e raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" end ``` -NOTE: Defined in `active_support/core_ext/name_error.rb`. +NOTE: 在 `active_support/core_ext/name_error.rb` 文件中定义。 + + -Extensions to `LoadError` -------------------------- +## `LoadError` 的扩展 -Active Support adds `is_missing?` to `LoadError`, and also assigns that class to the constant `MissingSourceFile` for backwards compatibility. +Active Support 为 `LoadError` 增加了 `is_missing?` 方法。 -Given a path name `is_missing?` tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension). +`is_missing?` 方法判断异常是不是由指定路径名(不含“.rb”扩展名)引起的。 -For example, when an action of `ArticlesController` is called Rails tries to load `articles_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases: +例如,调用 `ArticlesController` 的动作时,Rails 会尝试加载 `articles_helper.rb`,但是那个文件可能不存在。这没关系,辅助模块不是必须的,因此 Rails 会静默加载错误。但是,有可能是辅助模块存在,而它引用的其他库不存在。此时,Rails 必须抛出异常。`is_missing?` 方法能区分这两种情况: ```ruby def default_helper_module! module_name = name.sub(/Controller$/, '') module_path = module_name.underscore helper module_path -rescue MissingSourceFile => e +rescue LoadError => e raise e unless e.is_missing? "helpers/#{module_path}_helper" rescue NameError => e raise e unless e.missing_name? "#{module_name}Helper" end ``` -NOTE: Defined in `active_support/core_ext/load_error.rb`. +NOTE: 在 `active_support/core_ext/load_error.rb` 文件中定义。 diff --git a/source/zh-CN/active_support_instrumentation.md b/source/zh-CN/active_support_instrumentation.md index 7033947..eae5df0 100644 --- a/source/zh-CN/active_support_instrumentation.md +++ b/source/zh-CN/active_support_instrumentation.md @@ -1,41 +1,47 @@ -Active Support Instrumentation -============================== +# Active Support 监测程序 -Active Support is a part of core Rails that provides Ruby language extensions, utilities and other things. One of the things it includes is an instrumentation API that can be used inside an application to measure certain actions that occur within Ruby code, such as that inside a Rails application or the framework itself. It is not limited to Rails, however. It can be used independently in other Ruby scripts if it is so desired. +Active Support 是 Rails 核心的一部分,提供 Ruby 语言扩展、实用方法等。其中包括一份监测 API,在应用中可以用它测度 Ruby 代码(如 Rails 应用或框架自身)中的特定操作。不过,这个 API 不限于只能在 Rails 中使用,如果愿意,也可以在其他 Ruby 脚本中使用。 -In this guide, you will learn how to use the instrumentation API inside of Active Support to measure events inside of Rails and other Ruby code. +本文教你如何使用 Active Support 中的监测 API 测度 Rails 和其他 Ruby 代码中的事件。 -After reading this guide, you will know: +读完本文后,您将学到: -* What instrumentation can provide. -* The hooks inside the Rails framework for instrumentation. -* Adding a subscriber to a hook. -* Building a custom instrumentation implementation. +* 使用监测程序能做什么; +* Rails 框架为监测提供的钩子; +* 订阅钩子; +* 自定义监测点。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -Introduction to instrumentation -------------------------------- +NOTE: 本文原文尚未完工! -The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code. + -For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken. +## 监测程序简介 -You are even able to create your own events inside your application which you can later subscribe to. +Active Support 提供的监测 API 允许开发者提供钩子,供其他开发者订阅。在 Rails 框架中,有很多。通过这个 API,开发者可以选择在应用或其他 Ruby 代码中发生特定事件时接收通知。 -Rails framework hooks ---------------------- +例如,Active Record 中有一个钩子,在每次使用 SQL 查询数据库时调用。开发者可以订阅这个钩子,记录特定操作执行的查询次数。还有一个钩子在控制器的动作执行前后调用,记录动作的执行时间。 -Within the Ruby on Rails framework, there are a number of hooks provided for common events. These are detailed below. +在应用中甚至还可以自己创建事件,然后订阅。 -Action Controller ------------------ + + +## Rails 框架中的钩子 + +Ruby on Rails 框架为很多常见的事件提供了钩子。下面详述。 + + + +## Action Controller + + ### write_fragment.action_controller -| Key | Value | -| ------ | ---------------- | -| `:key` | The complete key | +| 键 | 值 | +|---|---| +| `:key` | 完整的键 | ```ruby { @@ -43,11 +49,13 @@ Action Controller } ``` + + ### read_fragment.action_controller -| Key | Value | -| ------ | ---------------- | -| `:key` | The complete key | +| 键 | 值 | +|---|---| +| `:key` | 完整的键 | ```ruby { @@ -55,11 +63,13 @@ Action Controller } ``` + + ### expire_fragment.action_controller -| Key | Value | -| ------ | ---------------- | -| `:key` | The complete key | +| 键 | 值 | +|---|---| +| `:key` | 完整的键 | ```ruby { @@ -67,11 +77,13 @@ Action Controller } ``` + + ### exist_fragment?.action_controller -| Key | Value | -| ------ | ---------------- | -| `:key` | The complete key | +| 键 | 值 | +|---|---| +| `:key` | 完整的键 | ```ruby { @@ -79,11 +91,13 @@ Action Controller } ``` + + ### write_page.action_controller -| Key | Value | -| ------- | ----------------- | -| `:path` | The complete path | +| 键 | 值 | +|---|---| +| `:path` | 完整的路径 | ```ruby { @@ -91,11 +105,13 @@ Action Controller } ``` + + ### expire_page.action_controller -| Key | Value | -| ------- | ----------------- | -| `:path` | The complete path | +| 键 | 值 | +|---|---| +| `:path` | 完整的路径 | ```ruby { @@ -103,45 +119,55 @@ Action Controller } ``` + + ### start_processing.action_controller -| Key | Value | -| ------------- | --------------------------------------------------------- | -| `:controller` | The controller name | -| `:action` | The action | -| `:params` | Hash of request parameters without any filtered parameter | -| `:format` | html/js/json/xml etc | -| `:method` | HTTP request verb | -| `:path` | Request path | +| 键 | 值 | +|---|---| +| `:controller` | 控制器名 | +| `:action` | 动作名 | +| `:params` | 请求参数散列,不过滤 | +| `:headers` | 请求首部 | +| `:format` | html、js、json、xml 等 | +| `:method` | HTTP 请求方法 | +| `:path` | 请求路径 | ```ruby { controller: "PostsController", action: "new", params: { "action" => "new", "controller" => "posts" }, + headers: #, format: :html, method: "GET", path: "/posts/new" } ``` + + ### process_action.action_controller -| Key | Value | -| --------------- | --------------------------------------------------------- | -| `:controller` | The controller name | -| `:action` | The action | -| `:params` | Hash of request parameters without any filtered parameter | -| `:format` | html/js/json/xml etc | -| `:method` | HTTP request verb | -| `:path` | Request path | -| `:view_runtime` | Amount spent in view in ms | +| 键 | 值 | +|---|---| +| `:controller` | 控制器名 | +| `:action` | 动作名 | +| `:params` | 请求参数散列,不过滤 | +| `:headers` | 请求首部 | +| `:format` | html、js、json、xml 等 | +| `:method` | HTTP 请求方法 | +| `:path` | 请求路径 | +| `:status` | HTTP 状态码 | +| `:view_runtime` | 花在视图上的时间量(ms) | +| `:db_runtime` | 执行数据库查询的时间量(ms) | ```ruby { controller: "PostsController", action: "index", params: {"action" => "index", "controller" => "posts"}, + headers: #, format: :html, method: "GET", path: "/posts", @@ -151,24 +177,31 @@ Action Controller } ``` + + ### send_file.action_controller -| Key | Value | -| ------- | ------------------------- | -| `:path` | Complete path to the file | +| 键 | 值 | +|---|---| +| `:path` | 文件的完整路径 | + +TIP: 调用方可以添加额外的键。 -INFO. Additional keys may be added by the caller. + + ### send_data.action_controller -`ActionController` does not had any specific information to the payload. All options are passed through to the payload. +`ActionController` 在载荷(payload)中没有任何特定的信息。所有选项都传到载荷中。 + + ### redirect_to.action_controller -| Key | Value | -| ----------- | ------------------ | -| `:status` | HTTP response code | -| `:location` | URL to redirect to | +| 键 | 值 | +|---|---| +| `:status` | HTTP 响应码 | +| `:location` | 重定向的 URL | ```ruby { @@ -177,11 +210,13 @@ INFO. Additional keys may be added by the caller. } ``` + + ### halted_callback.action_controller -| Key | Value | -| --------- | ----------------------------- | -| `:filter` | Filter that halted the action | +| 键 | 值 | +|---|---| +| `:filter` | 过滤暂停的动作 | ```ruby { @@ -189,15 +224,18 @@ INFO. Additional keys may be added by the caller. } ``` -Action View ------------ + + +## Action View + + ### render_template.action_view -| Key | Value | -| ------------- | --------------------- | -| `:identifier` | Full path to template | -| `:layout` | Applicable layout | +| 键 | 值 | +|---|---| +| `:identifier` | 模板的完整路径 | +| `:layout` | 使用的布局 | ```ruby { @@ -206,30 +244,58 @@ Action View } ``` -### render_partial.action_view + + +### render-partial-action-view -| Key | Value | -| ------------- | --------------------- | -| `:identifier` | Full path to template | +| 键 | 值 | +|---|---| +| `:identifier` | 模板的完整路径 | ```ruby { - identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb", + identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb" } ``` -Active Record ------------- + + +### render_collection.action_view + +| 键 | 值 | +|---|---| +| `:identifier` | 模板的完整路径 | +| `:count` | 集合的大小 | +| `:cache_hits` | 从缓存中获取的局部视图数量 | + +仅当渲染集合时设定了 `cached: true` 选项,才有 `:cache_hits` 键。 + +```ruby +{ + identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb", + count: 3, + cache_hits: 0 +} +``` + + + +## Active Record + + ### sql.active_record -| Key | Value | -| ------------ | --------------------- | -| `:sql` | SQL statement | -| `:name` | Name of the operation | -| `:object_id` | `self.object_id` | +| 键 | 值 | +|---|---| +| `:sql` | SQL 语句 | +| `:name` | 操作的名称 | +| `:connection_id` | `self.object_id` | +| `:binds` | 绑定的参数 | +| `:cached` | 使用缓存的查询时为 `true` | + +TIP: 适配器也会添加数据。 -INFO. The adapters will add their own data as well. ```ruby { @@ -240,30 +306,41 @@ INFO. The adapters will add their own data as well. } ``` -### identity.active_record + -| Key | Value | -| ---------------- | ----------------------------------------- | -| `:line` | Primary Key of object in the identity map | -| `:name` | Record's class | -| `:connection_id` | `self.object_id` | +### instantiation.active_record -Action Mailer -------------- +| 键 | 值 | +|---|---| +| `:record_count` | 实例化记录的数量 | +| `:class_name` | 记录所属的类 | + +```ruby +{ + record_count: 1, + class_name: "User" +} +``` + + + +## Action Mailer + + ### receive.action_mailer -| Key | Value | -| ------------- | -------------------------------------------- | -| `:mailer` | Name of the mailer class | -| `:message_id` | ID of the message, generated by the Mail gem | -| `:subject` | Subject of the mail | -| `:to` | To address(es) of the mail | -| `:from` | From address of the mail | -| `:bcc` | BCC addresses of the mail | -| `:cc` | CC addresses of the mail | -| `:date` | Date of the mail | -| `:mail` | The encoded form of the mail | +| 键 | 值 | +|---|---| +| `:mailer` | 邮件程序类的名称 | +| `:message_id` | 邮件的 ID,由 Mail gem 生成 | +| `:subject` | 邮件的主题 | +| `:to` | 邮件的收件地址 | +| `:from` | 邮件的发件地址 | +| `:bcc` | 邮件的密送地址 | +| `:cc` | 邮件的抄送地址 | +| `:date` | 发送邮件的日期 | +| `:mail` | 邮件的编码形式 | ```ruby { @@ -273,23 +350,25 @@ Action Mailer to: ["users@rails.com", "ddh@rails.com"], from: ["me@rails.com"], date: Sat, 10 Mar 2012 14:18:09 +0100, - mail: "..." # omitted for brevity + mail: "..." # 为了节省空间,省略 } ``` + + ### deliver.action_mailer -| Key | Value | -| ------------- | -------------------------------------------- | -| `:mailer` | Name of the mailer class | -| `:message_id` | ID of the message, generated by the Mail gem | -| `:subject` | Subject of the mail | -| `:to` | To address(es) of the mail | -| `:from` | From address of the mail | -| `:bcc` | BCC addresses of the mail | -| `:cc` | CC addresses of the mail | -| `:date` | Date of the mail | -| `:mail` | The encoded form of the mail | +| 键 | 值 | +|---|---| +| `:mailer` | 邮件程序类的名称 | +| `:message_id` | 邮件的 ID,由 Mail gem 生成 | +| `:subject` | 邮件的主题 | +| `:to` | 邮件的收件地址 | +| `:from` | 邮件的发件地址 | +| `:bcc` | 邮件的密送地址 | +| `:cc` | 邮件的抄送地址 | +| `:date` | 发送邮件的日期 | +| `:mail` | 邮件的编码形式 | ```ruby { @@ -299,41 +378,36 @@ Action Mailer to: ["users@rails.com", "ddh@rails.com"], from: ["me@rails.com"], date: Sat, 10 Mar 2012 14:18:09 +0100, - mail: "..." # omitted for brevity + mail: "..." # 为了节省空间,省略 } ``` -ActiveResource --------------- - -### request.active_resource + -| Key | Value | -| -------------- | -------------------- | -| `:method` | HTTP method | -| `:request_uri` | Complete URI | -| `:result` | HTTP response object | +## Active Support -Active Support --------------- + ### cache_read.active_support -| Key | Value | -| ------------------ | ------------------------------------------------- | -| `:key` | Key used in the store | -| `:hit` | If this read is a hit | -| `:super_operation` | :fetch is added when a read is used with `#fetch` | +| 键 | 值 | +|---|---| +| `:key` | 存储器中使用的键 | +| `:hit` | 是否读取了缓存 | +| `:super_operation` | 如果使用 `#fetch` 读取了,添加 `:fetch` | + + ### cache_generate.active_support -This event is only used when `#fetch` is called with a block. +仅当使用块调用 `#fetch` 时使用这个事件。 + +| 键 | 值 | +|---|---| +| `:key` | 存储器中使用的键 | -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | +TIP: 写入存储器时,传给 `fetch` 的选项会合并到载荷中。 -INFO. Options passed to fetch will be merged with the payload when writing to the store ```ruby { @@ -341,16 +415,18 @@ INFO. Options passed to fetch will be merged with the payload when writing to th } ``` + ### cache_fetch_hit.active_support -This event is only used when `#fetch` is called with a block. +仅当使用块调用 `#fetch` 时使用这个事件。 -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | +| 键 | 值 | +|---|---| +| `:key` | 存储器中使用的键 | + +TIP: 传给 `fetch` 的选项会合并到载荷中。 -INFO. Options passed to fetch will be merged with the payload. ```ruby { @@ -358,13 +434,16 @@ INFO. Options passed to fetch will be merged with the payload. } ``` + + ### cache_write.active_support -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | +| 键 | 值 | +|---|---| +| `:key` | 存储器中使用的键 | + +TIP: 缓存存储器可能会添加其他键。 -INFO. Cache stores may add their own keys ```ruby { @@ -372,11 +451,13 @@ INFO. Cache stores may add their own keys } ``` + + ### cache_delete.active_support -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | +| 键 | 值 | +|---|---| +| `:key` | 存储器中使用的键 | ```ruby { @@ -384,11 +465,13 @@ INFO. Cache stores may add their own keys } ``` + + ### cache_exist?.active_support -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | +| 键 | 值 | +|---|---| +| `:key` | 存储器中使用的键 | ```ruby { @@ -396,48 +479,93 @@ INFO. Cache stores may add their own keys } ``` -Railties --------- + + +## Active Job + + + +### enqueue_at.active_job + +| 键 | 值 | +|---|---| +| `:adapter` | 处理作业的 `QueueAdapter` 对象 | +| `:job` | 作业对象 | + + + +### enqueue.active_job + +| 键 | 值 | +|---|---| +| `:adapter` | 处理作业的 `QueueAdapter` 对象 | +| `:job` | 作业对象 | + + + +### perform_start.active_job + +| 键 | 值 | +|---|---| +| `:adapter` | 处理作业的 `QueueAdapter` 对象 | +| `:job` | 作业对象 | + + + +### perform.active_job + +| 键 | 值 | +|---|---| +| `:adapter` | 处理作业的 `QueueAdapter` 对象 | +| `:job` | 作业对象 | + + + +## Railties + + ### load_config_initializer.railties -| Key | Value | -| -------------- | ----------------------------------------------------- | -| `:initializer` | Path to loaded initializer from `config/initializers` | +| 键 | 值 | +|---|---| +| `:initializer` | 从 `config/initializers` 中加载的初始化脚本的路径 | -Rails ------ + + +## Rails + + ### deprecation.rails -| Key | Value | -| ------------ | ------------------------------- | -| `:message` | The deprecation warning | -| `:callstack` | Where the deprecation came from | +| 键 | 值 | +|---|---| +| `:message` | 弃用提醒 | +| `:callstack` | 弃用的位置 | -Subscribing to an event ------------------------ + -Subscribing to an event is easy. Use `ActiveSupport::Notifications.subscribe` with a block to -listen to any notification. +## 订阅事件 -The block receives the following arguments: +订阅事件是件简单的事,在 `ActiveSupport::Notifications.subscribe` 的块中监听通知即可。 -* The name of the event -* Time when it started -* Time when it finished -* An unique ID for this event -* The payload (described in previous sections) +这个块接收下述参数: + +* 事件的名称 +* 开始时间 +* 结束时间 +* 事件的唯一 ID +* 载荷(参见前述各节) ```ruby ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, data| - # your own custom stuff + # 自己编写的其他代码 Rails.logger.info "#{name} Received!" end ``` -Defining all those block arguments each time can be tedious. You can easily create an `ActiveSupport::Notifications::Event` -from block arguments like this: +每次都定义这些块参数很麻烦,我们可以使用 `ActiveSupport::Notifications::Event` 创建块参数,如下: ```ruby ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args| @@ -451,7 +579,7 @@ ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*a end ``` -Most times you only care about the data itself. Here is a shortcut to just get the data. +多数时候,我们只关注数据本身。下面是只获取数据的简洁方式: ```ruby ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args| @@ -460,32 +588,29 @@ ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*a end ``` -You may also subscribe to events matching a regular expression. This enables you to subscribe to -multiple events at once. Here's you could subscribe to everything from `ActionController`. +此外,还可以订阅匹配正则表达式的事件。这样可以一次订阅多个事件。下面是订阅 `ActionController` 中所有事件的方式: ```ruby ActiveSupport::Notifications.subscribe /action_controller/ do |*args| - # inspect all ActionController events + # 审查所有 ActionController 事件 end ``` -Creating custom events ----------------------- + + +## 自定义事件 -Adding your own events is easy as well. `ActiveSupport::Notifications` will take care of -all the heavy lifting for you. Simply call `instrument` with a `name`, `payload` and a block. -The notification will be sent after the block returns. `ActiveSupport` will generate the start and end times -as well as the unique ID. All data passed into the `instrument` call will make it into the payload. +自己添加事件也很简单,繁重的工作都由 `ActiveSupport::Notifications` 代劳,我们只需调用 `instrument`,并传入 `name`、`payload` 和一个块。通知在块返回后发送。`ActiveSupport` 会生成起始时间和唯一的 ID。传给 `instrument` 调用的所有数据都会放入载荷中。 -Here's an example: +下面举个例子: ```ruby ActiveSupport::Notifications.instrument "my.custom.event", this: :data do - # do your custom stuff here + # 自己编写的其他代码 end ``` -Now you can listen to this event with: +然后可以使用下述代码监听这个事件: ```ruby ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data| @@ -493,5 +618,4 @@ ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, fini end ``` -You should follow Rails conventions when defining your own events. The format is: `event.library`. -If you application is sending Tweets, you should create an event named `tweet.twitter`. +自己定义事件时,应该遵守 Rails 的约定。事件名称的格式是 `event.library`。如果应用发送推文,应该把事件命名为 `tweet.twitter`。 diff --git a/source/zh-CN/api_app.md b/source/zh-CN/api_app.md new file mode 100644 index 0000000..e57de05 --- /dev/null +++ b/source/zh-CN/api_app.md @@ -0,0 +1,335 @@ +# 使用 Rails 开发只提供 API 的应用 + +在本文中您将学到: + +* Rails 对只提供 API 的应用的支持; +* 如何配置 Rails,不使用任何针对浏览器的功能; +* 如何决定使用哪些中间件; +* 如何决定在控制器中使用哪些模块。 + +----------------------------------------------------------------------------- + + + +## 什么是 API 应用? + +人们说把 Rails 用作“API”,通常指的是在 Web 应用之外提供一份可通过编程方式访问的 API。例如,GitHub 提供了 [API](http://developer.github.com/),供你在自己的客户端中使用。 + +随着客户端框架的出现,越来越多的开发者使用 Rails 构建后端,在 Web 应用和其他原生应用之间共享。 + +例如,Twitter 使用自己的[公开 API](https://dev.twitter.com/) 构建 Web 应用,而文档网站是一个静态网站,消费 JSON 资源。 + +很多人不再使用 Rails 生成 HTML,通过表单和链接与服务器通信,而是把 Web 应用当做 API 客户端,分发包含 JavaScript 的 HTML,消费 JSON API。 + +本文说明如何构建伺服 JSON 资源的 Rails 应用,供 API 客户端(包括客户端框架)使用。 + + + +## 为什么使用 Rails 构建 JSON API? + +提到使用 Rails 构建 JSON API,多数人想到的第一个问题是:“使用 Rails 生成 JSON 是不是有点大材小用了?使用 Sinatra 这样的框架是不是更好?” + +对特别简单的 API 来说,确实如此。然而,对大量使用 HTML 的应用来说,应用的逻辑大都在视图层之外。 + +多数人使用 Rails 的原因是,Rails 提供了一系列默认值,开发者能快速上手,而不用做些琐碎的决定。 + +下面是 Rails 提供的一些开箱即用的功能,这些功能在 API 应用中也适用。 + +在中间件层处理的功能: + +* 重新加载:Rails 应用支持简单明了的重新加载机制。即使应用变大,每次请求都重启服务器变得不切实际,这一机制依然适用。 +* 开发模式:Rails 应用自带智能的开发默认值,使得开发过程很愉快,而且不会破坏生产环境的效率。 +* 测试模式:同开发模式。 +* 日志:Rails 应用会在日志中记录每次请求,而且为不同环境设定了合适的详细等级。在开发环境中,Rails 记录的信息包括请求环境、数据库查询和基本的性能信息。 +* 安全性:Rails 能检测并防范 [IP 欺骗攻击](https://en.wikipedia.org/wiki/IP_address_spoofing),还能处理[时序攻击](http://en.wikipedia.org/wiki/Timing_attack)中的加密签名。不知道 IP 欺骗攻击和时序攻击是什么?这就对了。 +* 参数解析:想以 JSON 的形式指定参数,而不是 URL 编码字符串形式?没问题。Rails 会代为解码 JSON,存入 `params` 中。想使用嵌套的 URL 编码参数?也没问题。 +* 条件 GET 请求:Rails 能处理条件 `GET` 请求相关的首部(`ETag` 和 `Last-Modified`),然后返回正确的响应首部和状态码。你只需在控制器中使用 [`stale?`](http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F) 做检查,剩下的 HTTP 细节都由 Rails 处理。 +* HEAD 请求:Rails 会把 `HEAD` 请求转换成 `GET` 请求,只返回首部。这样 `HEAD` 请求在所有 Rails API 中都可靠。 + +虽然这些功能可以使用 Rack 中间件实现,但是上述列表的目的是说明 Rails 默认提供的中间件栈提供了大量有价值的功能,即便“只是生成 JSON”也用得到。 + +在 Action Pack 层处理的功能: + +* 资源式路由:如果构建的是 REST 式 JSON API,你会想用 Rails 路由器的。按照约定以简明的方式把 HTTP 映射到控制器上能节省很多时间,不用再从 HTTP 方面思考如何建模 API。 +* URL 生成:路由的另一面是 URL 生成。基于 HTTP 的优秀 API 包含 URL(比如 [GitHub Gist API](http://developer.github.com/v3/gists/))。 +* 首部和重定向响应:`head :no_content` 和 `redirect_to user_url(https://github.com/current_user)` 用着很方便。当然,你可以自己动手添加相应的响应首部,但是为什么要费这事呢? +* 缓存:Rails 提供了页面缓存、动作缓存和片段缓存。构建嵌套的 JSON 对象时,片段缓存特别有用。 +* 基本身份验证、摘要身份验证和令牌身份验证:Rails 默认支持三种 HTTP 身份验证。 +* 监测程序:Rails 提供了监测 API,在众多事件发生时触发注册的处理程序,例如处理动作、发送文件或数据、重定向和数据库查询。各个事件的载荷中包含相关的信息(对动作处理事件来说,载荷中包括控制器、动作、参数、请求格式、请求方法和完整的请求路径)。 +* 生成器:通常生成一个资源就能把模型、控制器、测试桩件和路由在一个命令中通通创建出来,然后再做调整。迁移等也有生成器。 +* 插件:有很多第三方库支持 Rails,这样不必或很少需要花时间设置及把库与 Web 框架连接起来。插件可以重写默认的生成器、添加 Rake 任务,而且继续使用 Rails 选择的处理方式(如日志记录器和缓存后端)。 + +当然,Rails 启动过程还是要把各个注册的组件连接起来。例如,Rails 启动时会使用 `config/database.yml` 文件配置 Active Record。 + +简单来说,你可能没有想过去掉视图层之后要把 Rails 的哪些部分保留下来,不过答案是,多数都要保留。 + + + +## 基本配置 + +如果你构建的 Rails 应用主要用作 API,可以从较小的 Rails 子集开始,然后再根据需要添加功能。 + + + +### 新建应用 + +生成 Rails API 应用使用下述命令: + +```sh +$ rails new my_api --api +``` + +这个命令主要做三件事: + +* 配置应用,使用有限的中间件(比常规应用少)。具体而言,不含默认主要针对浏览器应用的中间件(如提供 cookie 支持的中间件)。 +* 让 `ApplicationController` 继承 `ActionController::API`,而不继承 `ActionController::Base`。与中间件一样,这样做是为了去除主要针对浏览器应用的 Action Controller 模块。 +* 配置生成器,生成资源时不生成视图、辅助方法和静态资源。 + + + +### 修改现有应用 + +如果你想把现有的应用改成 API 应用,请阅读下述步骤。 + +在 `config/application.rb` 文件中,把下面这行代码添加到 `Application` 类定义的顶部: + +```ruby +config.api_only = true +``` + +在 `config/environments/development.rb` 文件中,设定 `config.debug_exception_response_format` 选项,配置在开发环境中出现错误时响应使用的格式。 + +如果想使用 HTML 页面渲染调试信息,把值设为 `:default`: + +```ruby +config.debug_exception_response_format = :default +``` + +如果想使用响应所用的格式渲染调试信息,把值设为 `:api`: + +```ruby +config.debug_exception_response_format = :api +``` + +默认情况下,`config.api_only` 的值为 `true` 时,`config.debug_exception_response_format` 的值是 `:api`。 + +最后,在 `app/controllers/application_controller.rb` 文件中,把下述代码 + +```ruby +class ApplicationController < ActionController::Base +end +``` + +改为 + +```ruby +class ApplicationController < ActionController::API +end +``` + + + +## 选择中间件 + +API 应用默认包含下述中间件: + +* `Rack::Sendfile` +* `ActionDispatch::Static` +* `ActionDispatch::Executor` +* `ActiveSupport::Cache::Strategy::LocalCache::Middleware` +* `Rack::Runtime` +* `ActionDispatch::RequestId` +* `ActionDispatch::RemoteIp` +* `Rails::Rack::Logger` +* `ActionDispatch::ShowExceptions` +* `ActionDispatch::DebugExceptions` +* `ActionDispatch::Reloader` +* `ActionDispatch::Callbacks` +* `ActiveRecord::Migration::CheckPending` +* `Rack::Head` +* `Rack::ConditionalGet` +* `Rack::ETag` +* `MyApi::Application::Routes` + +各个中间件的作用参见 [内部中间件栈](rails_on_rack.html#internal-middleware-stack)。 + +其他插件,包括 Active Record,可能会添加额外的中间件。一般来说,这些中间件对要构建的应用类型一无所知,可以在只提供 API 的 Rails 应用中使用。 + +可以通过下述命令列出应用中的所有中间件: + +```sh +$ rails middleware +``` + + + +### 使用缓存中间件 + +默认情况下,Rails 会根据应用的配置提供一个缓存存储器(默认为 memcache)。因此,内置的 HTTP 缓存依靠这个中间件。 + +例如,使用 `stale?` 方法: + +```ruby +def show + @post = Post.find(params[:id]) + + if stale?(last_modified: @post.updated_at) + render json: @post + end +end +``` + +上述 `stale?` 调用比较请求中的 `If-Modified-Since` 首部和 `@post.updated_at`。如果首部的值比最后修改时间晚,这个动作返回“304 未修改”响应;否则,渲染响应,并且设定 `Last-Modified` 首部。 + +通常,这个机制会区分客户端。缓存中间件支持跨客户端共享这种缓存机制。跨客户端缓存可以在调用 `stale?` 时启用: + +```ruby +def show + @post = Post.find(params[:id]) + + if stale?(last_modified: @post.updated_at, public: true) + render json: @post + end +end +``` + +这表明,缓存中间件会在 Rails 缓存中存储 URL 的 `Last-Modified` 值,而且为后续对同一个 URL 的入站请求添加 `If-Modified-Since` 首部。 + +可以把这种机制理解为使用 HTTP 语义的页面缓存。 + + + +### 使用 Rack::Sendfile + +在 Rails 控制器中使用 `send_file` 方法时,它会设定 `X-Sendfile` 首部。`Rack::Sendfile` 负责发送文件。 + +如果前端服务器支持加速发送文件,`Rack::Sendfile` 会把文件交给前端服务器发送。 + +此时,可以在环境的配置文件中设定 `config.action_dispatch.x_sendfile_header` 选项,为前端服务器指定首部的名称。 + +关于如何在流行的前端服务器中使用 `Rack::Sendfile`,参见 [`Rack::Sendfile` 的文档](http://rubydoc.info/github/rack/rack/master/Rack/Sendfile)。 + +下面是两个流行的服务器的配置。这样配置之后,就能支持加速文件发送功能了。 + +```ruby +# Apache 和 lighttpd +config.action_dispatch.x_sendfile_header = "X-Sendfile" + +# Nginx +config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" +``` + +请按照 `Rack::Sendfile` 文档中的说明配置你的服务器。 + + + +### 使用 ActionDispatch::Request + +`ActionDispatch::Request#params` 获取客户端发来的 JSON 格式参数,将其存入 `params`,可在控制器中访问。 + +为此,客户端要发送 JSON 编码的参数,并把 `Content-Type` 设为 `application/json`。 + +下面以 jQuery 为例: + +```js +jQuery.ajax({ + type: 'POST', + url: '/people', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({ person: { firstName: "Yehuda", lastName: "Katz" } }), + success: function(json) { } +}); +``` + +`ActionDispatch::Request` 检查 `Content-Type` 后,把参数转换成: + +```ruby +{ :person => { :firstName => "Yehuda", :lastName => "Katz" } } +``` + + + +### 其他中间件 + +Rails 自带的其他中间件在 API 应用中可能也会用到,尤其是 API 客户端包含浏览器时: + +* `Rack::MethodOverride` +* `ActionDispatch::Cookies` +* `ActionDispatch::Flash` +* 管理会话 + + * `ActionDispatch::Session::CacheStore` + * `ActionDispatch::Session::CookieStore` + * `ActionDispatch::Session::MemCacheStore` + + + +这些中间件可通过下述方式添加: + +```ruby +config.middleware.use Rack::MethodOverride +``` + + + +### 删除中间件 + +如果默认的 API 中间件中有不需要使用的,可以通过下述方式将其删除: + +```ruby +config.middleware.delete ::Rack::Sendfile +``` + +注意,删除中间件后 Action Controller 的特定功能就不可用了。 + + + +## 选择控制器模块 + +API 应用(使用 `ActionController::API`)默认有下述控制器模块: + +* `ActionController::UrlFor`:提供 `url_for` 等辅助方法。 +* `ActionController::Redirecting`:提供 `redirect_to`。 +* `AbstractController::Rendering` 和 `ActionController::ApiRendering`:提供基本的渲染支持。 +* `ActionController::Renderers::All`:提供 `render :json` 等。 +* `ActionController::ConditionalGet`:提供 `stale?`。 +* `ActionController::BasicImplicitRender`:如果没有显式响应,确保返回一个空响应。 +* `ActionController::StrongParameters`:结合 Active Model 批量赋值,提供参数白名单过滤功能。 +* `ActionController::ForceSSL`:提供 `force_ssl`。 +* `ActionController::DataStreaming`:提供 `send_file` 和 `send_data`。 +* `AbstractController::Callbacks`:提供 `before_action` 等方法。 +* `ActionController::Rescue`:提供 `rescue_from`。 +* `ActionController::Instrumentation`:提供 Action Controller 定义的监测钩子(详情参见 [Action Controller](active_support_instrumentation.html#action-controller))。 +* `ActionController::ParamsWrapper`:把参数散列放到一个嵌套散列中,这样在发送 POST 请求时无需指定根元素。 +* `ActionController::Head`:返回只有首部没有内容的响应。 + +其他插件可能会添加额外的模块。`ActionController::API` 引入的模块可以在 Rails 控制台中列出: + +```sh +$ bin/rails c +>> ActionController::API.ancestors - ActionController::Metal.ancestors +=> [ActionController::API, + ActiveRecord::Railties::ControllerRuntime, + ActionDispatch::Routing::RouteSet::MountedHelpers, + ActionController::ParamsWrapper, + ... , + AbstractController::Rendering, + ActionView::ViewPaths] +``` + + + +### 添加其他模块 + +所有 Action Controller 模块都知道它们所依赖的模块,因此在控制器中可以放心引入任何模块,所有依赖都会自动引入。 + +可能想添加的常见模块有: + +* `AbstractController::Translation`:提供本地化和翻译方法 `l` 和 `t`。 +* `ActionController::HttpAuthentication::Basic`(或 `Digest` 或 `Token`):提供基本、摘要或令牌 HTTP 身份验证。 +* `ActionView::Layouts`:渲染时支持使用布局。 +* `ActionController::MimeResponds`:提供 `respond_to`。 +* `ActionController::Cookies`:提供 `cookies`,包括签名和加密 cookie。需要 cookies 中间件支持。 + +模块最好添加到 `ApplicationController` 中,不过也可以在各个控制器中添加。 diff --git a/source/zh-CN/api_documentation_guidelines.md b/source/zh-CN/api_documentation_guidelines.md index 7e9b288..54b64e7 100644 --- a/source/zh-CN/api_documentation_guidelines.md +++ b/source/zh-CN/api_documentation_guidelines.md @@ -1,42 +1,39 @@ -API Documentation Guidelines -============================ +# API 文档指导方针 -This guide documents the Ruby on Rails API documentation guidelines. +本文说明 Ruby on Rails 的 API 文档指导方针。 -After reading this guide, you will know: +读完本文后,您将学到: -* How to write effective prose for documentation purposes. -* Style guidelines for documenting different kinds of Ruby code. +* 如何编写有效的文档; +* 为不同 Ruby 代码编写文档的风格指导方针。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -RDoc ----- + -The [Rails API documentation](http://api.rubyonrails.org) is generated with -[RDoc](http://docs.seattlerb.org/rdoc/). +## RDoc -```bash - bundle exec rake rdoc +[Rails API 文档](http://api.rubyonrails.org/)使用 [RDoc](http://docs.seattlerb.org/rdoc/) 生成。如果想生成 API 文档,要在 Rails 根目录中执行 `bundle install`,然后再执行: + +```sh +$ bundle exec rake rdoc ``` -Resulting HTML files can be found in the ./doc/rdoc directory. +得到的 HTML 文件在 `./doc/rdoc` 目录中。 -Please consult the RDoc documentation for help with the -[markup](http://docs.seattlerb.org/rdoc/RDoc/Markup.html), -and also take into account these [additional -directives](http://docs.seattlerb.org/rdoc/RDoc/Parser/Ruby.html). +RDoc 的[标记](http://docs.seattlerb.org/rdoc/RDoc/Markup.html)和[额外的指令](http://docs.seattlerb.org/rdoc/RDoc/Parser/Ruby.html)参见文档。 -Wording -------- + -Write simple, declarative sentences. Brevity is a plus: get to the point. +## 用词 -Write in present tense: "Returns a hash that...", rather than "Returned a hash that..." or "Will return a hash that...". +使用简单的陈述句。简短更好,要说到点子上。 -Start comments in upper case. Follow regular punctuation rules: +使用现在时:“Returns a hash that…​”,而非“Returned a hash that…​”或“Will return a hash that…​”。 -```ruby +注释的第一个字母大写,后续内容遵守常规的标点符号规则: + +```sh # Declares an attribute reader backed by an internally-named # instance variable. def attr_internal_reader(*attrs) @@ -44,51 +41,57 @@ def attr_internal_reader(*attrs) end ``` -Communicate to the reader the current way of doing things, both explicitly and implicitly. Use the idioms recommended in edge. Reorder sections to emphasize favored approaches if needed, etc. The documentation should be a model for best practices and canonical, modern Rails usage. +使用通行的方式与读者交流,可以直言,也可以隐晦。使用当下推荐的习语。如有必要,调整内容的顺序,强调推荐的方式。文档应该说明最佳实践和现代的权威 Rails 用法。 -Documentation has to be concise but comprehensive. Explore and document edge cases. What happens if a module is anonymous? What if a collection is empty? What if an argument is nil? +文档应该简洁全面,要指明边缘情况。如果模块是匿名的呢?如果集合是空的呢?如果参数是 nil 呢? -The proper names of Rails components have a space in between the words, like "Active Support". `ActiveRecord` is a Ruby module, whereas Active Record is an ORM. All Rails documentation should consistently refer to Rails components by their proper name, and if in your next blog post or presentation you remember this tidbit and take it into account that'd be phenomenal. +Rails 组件的名称在单词之间有个空格,如“Active Support”。`ActiveRecord` 是一个 Ruby 模块,而 Active Record 是一个 ORM。所有 Rails 文档都应该始终使用正确的名称引用 Rails 组件。如果你在下一篇博客文章或演示文稿中这么做,人们会觉得你很正规。 -Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERB. When in doubt, please have a look at some authoritative source like their official documentation. +拼写要正确:Arel、Test::Unit、RSpec、HTML、MySQL、JavaScript、ERB。如果不确定,请查看一些权威资料,如各自的官方文档。 -Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database". +“SQL”前面使用不定冠词“an”,如“an SQL statement”和“an SQLite database”。 -Prefer wordings that avoid "you"s and "your"s. For example, instead of +避免使用“you”和“your”。例如,较之 -```markdown +``` If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods. ``` -use this style: +这样写更好: -```markdown +``` If `return` is needed it is recommended to explicitly define a method. ``` -That said, when using pronouns in reference to a hypothetical person, such as "a -user with a session cookie", gender neutral pronouns (they/their/them) should be -used. Instead of: +不过,使用代词指代虚构的人时,例如“有会话 cookie 的用户”,应该使用中性代词(they/their/them)。 + +* 不用 he 或 she,用 they +* 不用 him 或 her,用 them +* 不用 his 或 her,用 their +* 不用 his 或 hers,用 theirs +* 不用 himself 或 herself,用 themselves + + -* he or she... use they. -* him or her... use them. -* his or her... use their. -* his or hers... use theirs. -* himself or herself... use themselves. +## 英语 -English -------- +请使用美式英语(color、center、modularize,等等)。美式英语与英式英语之间的拼写差异参见[这里](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences)。 -Please use American English (color, center, modularize, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). + -Example Code ------------- +## 牛津式逗号 -Choose meaningful examples that depict and cover the basics as well as interesting points or gotchas. +请使用[牛津式逗号](http://en.wikipedia.org/wiki/Serial_comma)(“red, white, and blue”,而非“red, white and blue”)。 -Use two spaces to indent chunks of code--that is, for markup purposes, two spaces with respect to the left margin. The examples themselves should use [Rails coding conventions](contributing_to_ruby_on_rails.html#follow-the-coding-conventions). + -Short docs do not need an explicit "Examples" label to introduce snippets; they just follow paragraphs: +## 示例代码 + +选择有意义的示例,说明基本用法和有趣的点或坑。 + +代码使用两个空格缩进,即根据标记在左外边距的基础上增加两个空格。示例应该遵守 [Rails 编程约定](contributing_to_ruby_on_rails.html#follow-the-coding-conventions)。 + +简短的文档无需明确使用“Examples”标注引入代码片段,直接跟在段后即可: ```ruby # Converts a collection of elements into a formatted string by @@ -97,7 +100,7 @@ Short docs do not need an explicit "Examples" label to introduce snippets; they # Blog.all.to_formatted_s # => "First PostSecond PostThird Post" ``` -On the other hand, big chunks of structured documentation may have a separate "Examples" section: +但是大段文档可以单独有个“Examples”部分: ```ruby # ==== Examples @@ -108,10 +111,10 @@ On the other hand, big chunks of structured documentation may have a separate "E # Person.exists?(['name LIKE ?', "%#{query}%"]) ``` -The results of expressions follow them and are introduced by "# => ", vertically aligned: +表达式的结果在表达式之后,使用 “# => ”给出,而且要纵向对齐: ```ruby -# For checking if a fixnum is even or odd. +# For checking if an integer is even or odd. # # 1.even? # => false # 1.odd? # => true @@ -119,7 +122,7 @@ The results of expressions follow them and are introduced by "# => ", vertically # 2.odd? # => false ``` -If a line is too long, the comment may be placed on the next line: +如果一行太长,结果可以放在下一行: ```ruby # label(:article, :title) @@ -132,39 +135,33 @@ If a line is too long, the comment may be placed on the next line: # # => ``` -Avoid using any printing methods like `puts` or `p` for that purpose. +不要使用打印方法,如 `puts` 或 `p` 给出结果。 -On the other hand, regular comments do not use an arrow: +常规的注释不使用箭头: ```ruby # polymorphic_url(https://github.com/record) # same as comment_url(https://github.com/record) ``` -Booleans --------- + -In predicates and flags prefer documenting boolean semantics over exact values. +## 布尔值 -When "true" or "false" are used as defined in Ruby use regular font. The -singletons `true` and `false` need fixed-width font. Please avoid terms like -"truthy", Ruby defines what is true and false in the language, and thus those -words have a technical meaning and need no substitutes. +在判断方法或旗标中,尽量使用布尔语义,不要用具体的值。 -As a rule of thumb, do not document singletons unless absolutely necessary. That -prevents artificial constructs like `!!` or ternaries, allows refactors, and the -code does not need to rely on the exact values returned by methods being called -in the implementation. +如果所用的“true”或“false”与 Ruby 定义的一样,使用常规字体。`true` 和 `false` 两个单例要使用等宽字体。请不要使用“truthy”,Ruby 语言定义了什么是真什么是假,“true”和“false”就能表达技术意义,无需使用其他词代替。 -For example: +通常,如非绝对必要,不要为单例编写文档。这样能阻止智能的结构,如 `!!` 或三元运算符,便于重构,而且代码不依赖方法返回的具体值。 -```markdown +例如: + +``` `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default ``` -the user does not need to know which is the actual default value of the flag, -and so we only document its boolean semantics. +用户无需知道旗标具体的默认值,因此我们只说明它的布尔语义。 -An example with a predicate: +下面是一个判断方法的文档示例: ```ruby # Returns true if the collection is empty. @@ -184,33 +181,36 @@ def empty? end ``` -The API is careful not to commit to any particular value, the method has -predicate semantics, that's enough. +这个 API 没有提到任何具体的值,知道它具有判断功能就够了。 -File Names ----------- + -As a rule of thumb, use filenames relative to the application root: +## 文件名 -``` +通常,文件名相对于应用的根目录: + +```ruby config/routes.rb # YES routes.rb # NO RAILS_ROOT/config/routes.rb # NO ``` -Fonts ------ + + +## 字体 -### Fixed-width Font + -Use fixed-width fonts for: +### 等宽字体 -* Constants, in particular class and module names. -* Method names. -* Literals like `nil`, `false`, `true`, `self`. -* Symbols. -* Method parameters. -* File names. +使用等宽字体编写: + +* 常量,尤其是类名和模块名 +* 方法名 +* 字面量,如 `nil`、`false`、`true`、`self` +* 符号 +* 方法的参数 +* 文件名 ```ruby class Array @@ -222,21 +222,21 @@ class Array end ``` -WARNING: Using `+...+` for fixed-width font only works with simple content like -ordinary method names, symbols, paths (with forward slashes), etc. Please use -`...` for everything else, notably class or module names with a -namespace as in `ActiveRecord::Base`. +WARNING: 只有简单的内容才能使用 `+...+` 标记使用等宽字体,如常规的方法名、符号、路径(含有正斜线),等等。其他内容应该使用 `…​`,尤其是带有命名空间的类名或模块名,如 `ActiveRecord::Base`。 -You can quickly test the RDoc output with the following command: -``` +可以使用下述命令测试 RDoc 的输出: + +```sh $ echo "+:to_param+" | rdoc --pipe -#=>

:to_param

+# =>

:to_param

``` -### Regular Font + -When "true" and "false" are English words rather than Ruby keywords use a regular font: +### 常规字体 + +“true”和“false”是英语单词而不是 Ruby 关键字时,使用常规字体: ```ruby # Runs all the validations within the specified context. @@ -254,21 +254,23 @@ def valid?(context = nil) end ``` -Description Lists ------------------ + + +## 描述列表 -In lists of options, parameters, etc. use a hyphen between the item and its description (reads better than a colon because normally options are symbols): +在选项、参数等列表中,在项目和描述之间使用一个连字符(而不是一个冒号,因为选项一般是符号): ```ruby # * :allow_nil - Skip validation if attribute is +nil+. ``` -The description starts in upper case and ends with a full stop-it's standard English. +描述开头是大写字母,结尾有一个句号——这是标准的英语。 + + -Dynamically Generated Methods ------------------------------ +## 动态生成的方法 -Methods created with `(module|class)_eval(STRING)` have a comment by their side with an instance of the generated code. That comment is 2 spaces away from the template: +使用 `(module|class)_eval(STRING)` 创建的方法在旁边有个注释,举例说明生成的代码。这种注释与模板之间相距两个空格。 ```ruby for severity in Severity.constants @@ -284,7 +286,7 @@ for severity in Severity.constants end ``` -If the resulting lines are too wide, say 200 columns or more, put the comment above the call: +如果这样得到的行太长,比如说有 200 多列,把注释放在上方: ```ruby # def self.find_by_login_and_activated(*args) @@ -299,18 +301,19 @@ self.class_eval %{ } ``` -Method Visibility ------------------ + -When writing documentation for Rails, it's important to understand the difference between public user-facing API vs internal API. +## 方法可见性 -Rails, like most libraries, uses the private keyword from Ruby for defining internal API. However, public API follows a slightly different convention. Instead of assuming all public methods are designed for user consumption, Rails uses the `:nodoc:` directive to annotate these kinds of methods as internal API. +为 Rails 编写文档时,要区分公开 API 和内部 API。 -This means that there are methods in Rails with `public` visibility that aren't meant for user consumption. +与多数库一样,Rails 使用 Ruby 提供的 `private` 关键字定义内部 API。然而,公开 API 遵照的约定稍有不同。不是所有公开方法都旨在供用户使用,Rails 使用 `:nodoc:` 指令注解内部 API 方法。 -An example of this is `ActiveRecord::Core::ClassMethods#arel_table`: +因此,在 Rails 中有些可见性为 `public` 的方法不是供用户使用的。 -```ruby +`ActiveRecord::Core::ClassMethods#arel_table` 就是一例: + +```sh module ActiveRecord::Core::ClassMethods def arel_table #:nodoc: # do some magic.. @@ -318,44 +321,33 @@ module ActiveRecord::Core::ClassMethods end ``` -If you thought, "this method looks like a public class method for `ActiveRecord::Core`", you were right. But actually the Rails team doesn't want users to rely on this method. So they mark it as `:nodoc:` and it's removed from public documentation. The reasoning behind this is to allow the team to change these methods according to their internal needs across releases as they see fit. The name of this method could change, or the return value, or this entire class may disappear; there's no guarantee and so you shouldn't depend on this API in your plugins or applications. Otherwise, you risk your app or gem breaking when you upgrade to a newer release of Rails. - -As a contributor, it's important to think about whether this API is meant for end-user consumption. The Rails team is committed to not making any breaking changes to public API across releases without going through a full deprecation cycle. It's recommended that you `:nodoc:` any of your internal methods/classes unless they're already private (meaning visibility), in which case it's internal by default. Once the API stabilizes the visibility can change, but changing public API is much harder due to backwards compatibility. +你可能想,“这是 `ActiveRecord::Core` 的一个公开类方法”,没错,但是 Rails 团队不希望用户使用这个方法。因此,他们把它标记为 `:nodoc:`,不包含在公开文档中。这样做,开发团队可以根据内部需要在发布新版本时修改这个方法。方法的名称可能会变,或者返回值有变化,也可能是整个类都不复存在——有太多不确定性,因此不应该在你的插件或应用中使用这个 API。如若不然,升级新版 Rails 时,你的应用或 gem 可能遭到破坏。 -A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly. +为 Rails 做贡献时一定要考虑清楚 API 是否供最终用户使用。未经完整的弃用循环之前,Rails 团队不会轻易对公开 API 做大的改动。如果没有定义为私有的(默认是内部 API),建议你使用 `:nodoc:` 标记所有内部的方法和类。API 稳定之后,可见性可以修改,但是为了向后兼容,公开 API 往往不宜修改。 -If you come across an existing `:nodoc:` you should tread lightly. Consider asking someone from the core team or author of the code before removing it. This should almost always happen through a pull request instead of the docrails project. +使用 `:nodoc:` 标记一个类或模块表示里面的所有方法都是内部 API,不应该直接使用。 -A `:nodoc:` should never be added simply because a method or class is missing documentation. There may be an instance where an internal public method wasn't given a `:nodoc:` by mistake, for example when switching a method from private to public visibility. When this happens it should be discussed over a PR on a case-by-case basis and never committed directly to docrails. +综上,Rails 团队使用 `:nodoc:` 标记供内部使用的可见性为公开的方法和类,对 API 可见性的修改要谨慎,必须先通过一个拉取请求讨论。 -To summarize, the Rails team uses `:nodoc:` to mark publicly visible methods and classes for internal use; changes to the visibility of API should be considered carefully and discussed over a pull request first. + -Regarding the Rails Stack -------------------------- +## 考虑 Rails 栈 -When documenting parts of Rails API, it's important to remember all of the -pieces that go into the Rails stack. +为 Rails API 编写文档时,一定要记住所有内容都身处 Rails 栈中。 -This means that behavior may change depending on the scope or context of the -method or class you're trying to document. +这意味着,方法或类的行为在不同的作用域或上下文中可能有所不同。 -In various places there is different behavior when you take the entire stack -into account, one such example is -`ActionView::Helpers::AssetTagHelper#image_tag`: +把整个栈考虑进来之后,行为在不同的地方可能有变。`ActionView::Helpers::AssetTagHelper#image_tag` 就是一例: ```ruby # image_tag("icon.png") # # => Icon ``` -Although the default behavior for `#image_tag` is to always return -`/images/icon.png`, we take into account the full Rails stack (including the -Asset Pipeline) we may see the result seen above. +虽然 `#image_tag` 的默认行为是返回 `/images/icon.png`,但是把整个 Rails 栈(包括 Asset Pipeline)考虑进来之后,可能会得到上述结果。 -We're only concerned with the behavior experienced when using the full default -Rails stack. +我们只关注考虑整个 Rails 默认栈的行为。 -In this case, we want to document the behavior of the _framework_, and not just -this specific method. +因此,我们要说明的是框架的行为,而不是单个方法。 -If you have a question on how the Rails team handles certain API, don't hesitate to open a ticket or send a patch to the [issue tracker](https://github.com/rails/rails/issues). +如果你对 Rails 团队处理某个 API 的方式有疑问,别迟疑,在[问题追踪系统](https://github.com/rails/rails/issues)中发一个工单,或者提交补丁。 diff --git a/source/zh-CN/asset_pipeline.md b/source/zh-CN/asset_pipeline.md index 1d65cc5..0a15446 100644 --- a/source/zh-CN/asset_pipeline.md +++ b/source/zh-CN/asset_pipeline.md @@ -1,34 +1,30 @@ -Asset Pipeline -============== +# Asset Pipeline 本文介绍 Asset Pipeline。 -读完本文,你将学到: +读完本文后,您将学到: -* Asset Pipeline 是什么以及其作用; -* 如何合理组织程序的静态资源; -* Asset Pipeline 的优势; -* 如何向 Asset Pipeline 中添加预处理器; -* 如何在 gem 中打包静态资源; +* Asset Pipeline 是什么,有什么用处; +* 如何合理组织应用的静态资源文件; +* 使用 Asset Pipeline 的好处; +* 如何为 Asset Pipeline 添加预处理器; +* 如何用 gem 打包静态资源文件。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -Asset Pipeline 是什么? ---------------------- + -Asset Pipeline 提供了一个框架,用于连接、压缩 JavaScript 和 CSS 文件。还允许使用其他语言和预处理器编写 JavaScript 和 CSS,例如 CoffeeScript、Sass 和 ERB。 +## Asset Pipeline 是什么 -严格来说,Asset Pipeline 不是 Rails 4 的核心功能,已经从框架中提取出来,制成了 [sprockets-rails](https://github.com/rails/sprockets-rails) gem。 +Asset Pipeline 提供了用于连接、简化或压缩 JavaScript 和 CSS 静态资源文件的框架。有了 Asset Pipeline,我们还可以使用其他语言和预处理器,例如 CoffeeScript、Sass 和 ERB,编写这些静态资源文件。应用中的静态资源文件还可以自动与其他 gem 中的静态资源文件合并。例如,与 `jquery-rails` gem 中包含的 `jquery.js` 文件合并,从而使 Rails 能够支持 AJAX 特性。 -Asset Pipeline 功能默认是启用的。 +Asset Pipeline 是通过 [sprockets-rails](https://github.com/rails/sprockets-rails) gem 实现的,Rails 默认启用了这个 gem。在新建 Rails 应用时,通过 `--skip-sprockets` 选项可以禁用这个 gem。 -新建程序时如果想禁用 Asset Pipeline,可以在命令行中指定 `--skip-sprockets` 选项。 - -```bash -rails new appname --skip-sprockets +```sh +$ rails new appname --skip-sprockets ``` -Rails 4 会自动把 `sass-rails`、`coffee-rails` 和 `uglifier` 三个 gem 加入 `Gemfile`。Sprockets 使用这三个 gem 压缩静态资源: +在新建 Rails 应用时,Rails 自动在 Gemfile 中添加了 `sass-rails`、`coffee-rails` 和 `uglifier` gem,Sprockets 通过这些 gem 来压缩静态资源文件: ```ruby gem 'sass-rails' @@ -36,97 +32,107 @@ gem 'uglifier' gem 'coffee-rails' ``` -指定 `--skip-sprockets` 命令行选项后,Rails 4 不会把 `sass-rails` 和 `uglifier` 加入 `Gemfile`。如果后续需要使用 Asset Pipeline,需要手动添加这些 gem。而且,指定 `--skip-sprockets` 命令行选项后,生成的 `config/application.rb` 文件也会有点不同,把加载 `sprockets/railtie` 的代码注释掉了。如果后续启用 Asset Pipeline,要把这行前面的注释去掉: +使用 `--skip-sprockets` 选项时,Rails 不会在 Gemfile 中添加这些 gem。因此,之后如果想要启用 Asset Pipeline,就需要手动在 Gemfile 中添加这些 gem。此外,使用 `--skip-sprockets` 选项时生成的 `config/application.rb` 也略有不同,用于加载 `sprockets/railtie` 的代码被注释掉了,因此要启用 Asset Pipeline,还需要取消注释: ```ruby # require "sprockets/railtie" ``` -`production.rb` 文件中有相应的选项设置静态资源的压缩方式:`config.assets.css_compressor` 针对 CSS,`config.assets.js_compressor` 针对 Javascript。 +在 `production.rb` 配置文件中,通过 `config.assets.css_compressor` 和 `config.assets.js_compressor` 选项可以分别为 CSS 和 JavaScript 静态资源文件设置压缩方式: ```ruby config.assets.css_compressor = :yui -config.assets.js_compressor = :uglify +config.assets.js_compressor = :uglifier ``` -NOTE: 如果 `Gemfile` 中有 `sass-rails`,就会自动用来压缩 CSS,无需设置 `config.assets.css_compressor` 选项。 +NOTE: 如果 Gemfile 中包含 `sass-rails` gem,Rails 就会自动使用这个 gem 压缩 CSS 静态资源文件,而无需设置 `config.assets.css_compressor` 选项。 + + -### 主要功能 +### 主要特性 -Asset Pipeline 的第一个功能是连接静态资源,减少渲染页面时浏览器发起的请求数。浏览器对并行的请求数量有限制,所以较少的请求数可以提升程序的加载速度。 +Asset Pipeline 的特性之一是连接静态资源文件,目的是减少渲染网页时浏览器发起的请求次数。Web 浏览器能够同时发起的请求次数是有限的,因此更少的请求次数可能意味着更快的应用加载速度。 -Sprockets 会把所有 JavaScript 文件合并到一个主 `.js` 文件中,把所有 CSS 文件合并到一个主 `.css` 文件中。后文会介绍,合并的方式可按需求随意定制。在生产环境中,Rails 会在文件名后加上 MD5 指纹,以便浏览器缓存,指纹变了缓存就会过期。修改文件的内容后,指纹会自动变化。 +Sprockets 把所有 JavaScript 文件连接为一个主 `.js` 文件,把所有 CSS 文件连接为一个主 `.css` 文件。后文会介绍,我们可以按需定制连接文件的方式。在生产环境中,Rails 会在每个文件名中插入 SHA256 指纹,以便 Web 浏览器缓存文件。当我们修改了文件内容,Rails 会自动修改文件名中的指纹,从而让原有缓存失效。 -Asset Pipeline 的第二个功能是压缩静态资源。对 CSS 文件来说,会删除空白和注释。对 JavaScript 来说,可以做更复杂的处理。处理方式可以从内建的选项中选择,也可使用定制的处理程序。 +Asset Pipeline 的特性之二是简化或压缩静态资源文件。对于 CSS 文件,会删除空格和注释。对于 JavaScript 文件,可以进行更复杂的处理,我们可以从内置选项中选择处理方式,也可以自定义处理方式。 -Asset Pipeline 的第三个功能是允许使用高级语言编写静态资源,再使用预处理器转换成真正的静态资源。默认支持的高级语言有:用来编写 CSS 的 Sass,用来编写 JavaScript 的 CoffeeScript,以及 ERB。 +Asset Pipeline 的特性之三是可以使用更高级的语言编写静态资源文件,再通过预编译转换为实际的静态资源文件。默认支持的高级语言有:用于编写 CSS 的 Sass,用于编写 JavaScript 的 CoffeeScript,以及 ERB。 -### 指纹是什么,我为什么要关心它? + -指纹可以根据文件内容生成文件名。文件内容变化后,文件名也会改变。对于静态内容,或者很少改动的内容,在不同的服务器之间,不同的部署日期之间,使用指纹可以区别文件的两个版本内容是否一样。 +### 指纹识别是什么,为什么要关心指纹? -如果文件名基于内容而定,而且文件名是唯一的,HTTP 报头会建议在所有可能的地方(CDN,ISP,网络设备,网页浏览器)存储一份该文件的副本。修改文件内容后,指纹会发生变化,因此远程客户端会重新请求文件。这种技术叫做“缓存爆裂”(cache busting)。 +指纹是一项根据文件内容修改文件名的技术。一旦文件内容发生变化,文件名就会发生变化。对于静态文件或内容很少发生变化的文件,这项技术提供了确定文件的两个版本是否相同的简单方法,特别是在跨服务器和多次部署的情况下。 -Sprockets 使用指纹的方式是在文件名中加入内容的哈希值,一般加在文件名的末尾。例如,`global.css` 加入指纹后的文件名如下: +当一个文件的文件名能够根据文件内容发生变化,并且能够保证不会出现重名时,就可以通过设置 HTTP 首部来建议所有缓存(CDN、ISP、网络设备或 Web 浏览器的缓存)都保存该文件的副本。一旦文件内容更新,文件名中的指纹就会发生变化,从而使远程客户端发起对文件新副本的请求。这项技术称为“缓存清除”(cache busting)。 + +Sprockets 使用指纹的方式是在文件名中添加文件内容的哈希值,并且通常会添加到文件名末尾。例如,对于 CSS 文件 `global.css`,添加哈希值后文件名可能变为: ``` global-908e25f4bf641868d8683022a5b62f54.css ``` -Asset Pipeline 使用的就是这种指纹实现方式。 +Rails 的 Asset Pipeline 也采取了这种策略。 -以前,Rails 使用内建的帮助方法,在文件名后加上一个基于日期生成的请求字符串,如下所示: +以前 Rails 采用的策略是,通过内置的辅助方法,为每一个指向静态资源文件的链接添加基于日期生成的查询字符串。在网页源代码中,会生成下面这样的链接: ``` /stylesheets/global.css?1309495796 ``` -使用请求字符串有很多缺点: +使用查询字符串的策略有如下缺点: + +**1. 如果一个文件的两个版本只是文件名的查询参数不同,这时不是所有缓存都能可靠地更新该文件的缓存。** -1. **文件名只是请求字符串不同时,缓存并不可靠**
- [Steve Souders 建议](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/):不在要缓存的资源上使用请求字符串。他发现,使用请求字符串的文件不被缓存的可能性有 5-20%。有些 CDN 验证缓存时根本无法识别请求字符串。 +[Steve Souders](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/) 建议,“……避免在可缓存的资源上使用查询字符串”。他发现,在使用查询字符串的情况下,有 5—20% 的请求不会被缓存。对于某些 CDN,通过修改查询字符串根本无法使缓存失效。 -2. **在多服务器环境中,不同节点上的文件名可能不同**
- 在 Rails 2.x 中,默认的请求字符串由文件的修改时间生成。静态资源文件部署到集群后,无法保证时间戳都是一样的,得到的值取决于使用哪台服务器处理请求。 +**2. 在多服务器环境中,不同节点上的文件名有可能发生变化。** -3. **缓存验证失败过多**
- 部署新版代码时,所有静态资源文件的最后修改时间都变了。即便内容没变,客户端也要重新请求这些文件。 +在 Rails 2.x 中,默认基于文件修改时间生成查询字符串。当静态资源文件被部署到某个节点上时,无法保证文件的时间戳保持不变,这样,对于同一个文件的请求,不同服务器可能返回不同的文件名。 -使用指纹就无需再用请求字符串了,而且文件名基于文件内容,始终保持一致。 +**3. 缓存失效的情况过多。** -默认情况下,指纹只在生产环境中启用,其他环境都被禁用。可以设置 `config.assets.digest` 选项启用或禁用。 +每次部署代码的新版本时,静态资源文件都会被重新部署,这些文件的最后修改时间也会发生变化。这样,不管其内容是否发生变化,客户端都不得不重新获取这些文件。 + +使用指纹可以避免使用查询字符串的这些缺点,并且能够确保文件内容相同时文件名也相同。 + +在开发环境和生产环境中,指纹都是默认启用的。通过 `config.assets.digest` 配置选项,可以启用或禁用指纹。 扩展阅读: -* [Optimize caching](http://code.google.com/speed/page-speed/docs/caching.html) -* [Revving Filenames: don't use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/) +* [优化缓存](http://code.google.com/speed/page-speed/docs/caching.html) +* [为文件名添加版本号:请不要使用查询字符串](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/) + + + +## 如何使用 Asset Pipeline -如何使用 Asset Pipeline ----------------------- +在 Rails 的早期版本中,所有静态资源文件都放在 `public` 文件夹的子文件夹中,例如 `images`、`javascripts` 和 `stylesheets` 子文件夹。当 Rails 开始使用 Asset Pipeline 后,就推荐把静态资源文件放在 `app/assets` 文件夹中,并使用 Sprockets 中间件处理这些文件。 -在以前的 Rails 版本中,所有静态资源都放在 `public` 文件夹的子文件夹中,例如 `images`、`javascripts` 和 `stylesheets`。使用 Asset Pipeline 后,建议把静态资源放在 `app/assets` 文件夹中。这个文件夹中的文件会经由 Sprockets 中间件处理。 +当然,静态资源文件仍然可以放在 `public` 文件夹及其子文件夹中。只要把 `config.public_file_server.enabled` 选项设置为 `true`,Rails 应用或 Web 服务器就会处理 `public` 文件夹及其子文件夹中的所有静态资源文件。但对于需要预处理的文件,都应该放在 `app/assets` 文件夹中。 -静态资源仍然可以放在 `public` 文件夹中,其中所有文件都会被程序或网页服务器视为静态文件。如果文件要经过预处理器处理,就得放在 `app/assets` 文件夹中。 +在生产环境中,Rails 默认会对 `public/assets` 文件夹中的文件进行预处理。经过预处理的静态资源文件将由 Web 服务器直接处理。在生产环境中,`app/assets` 文件夹中的文件不会直接交由 Web 服务器处理。 -默认情况下,在生产环境中,Rails 会把预先编译好的文件保存到 `public/assets` 文件夹中,网页服务器会把这些文件视为静态资源。在生产环境中,不会直接伺服 `app/assets` 文件夹中的文件。 + -### 控制器相关的静态资源 +### 针对控制器的静态资源文件 -生成脚手架或控制器时,Rails 会生成一个 JavaScript 文件(如果 `Gemfile` 中有 `coffee-rails`,会生成 CoffeeScript 文件)和 CSS 文件(如果 `Gemfile` 中有 `sass-rails`,会生成 SCSS 文件)。生成脚手架时,Rails 还会生成 `scaffolds.css` 文件(如果 `Gemfile` 中有 `sass-rails`,会生成 `scaffolds.css.scss` 文件)。 +当我们使用生成器生成脚手架或控制器时,Rails 会同时为控制器生成 JavaScript 文件(如果 Gemfile 中包含了 `coffee-rails` gem,那么生成的是 CoffeeScript 文件)和 CSS 文件(如果 Gemfile 中包含了 `sass-rails` gem,那么生成的是 SCSS 文件)。此外,在生成脚手架时,Rails 还会生成 `scaffolds.css` 文件(如果 Gemfile 中包含了 `sass-rails` gem,那么生成的是 `scaffolds.scss` 文件)。 -例如,生成 `ProjectsController` 时,Rails 会新建 `app/assets/javascripts/projects.js.coffee` 和 `app/assets/stylesheets/projects.css.scss` 两个文件。默认情况下,这两个文件立即就可以使用 `require_tree` 引入程序。关于 `require_tree` 的介绍,请阅读“[清单文件和指令](#manifest-files-and-directives)”一节。 +例如,当我们生成 `ProjectsController` 时,Rails 会新建 `app/assets/javascripts/projects.coffee` 文件和 `app/assets/stylesheets/projects.scss` 文件。默认情况下,应用会通过 `require_tree` 指令引入这两个文件。关于 `require_tree` 指令的更多介绍,请参阅 [清单文件和指令](#manifest-files-and-directives)。 -针对控制器的样式表和 JavaScript 文件也可只在相应的控制器中引入: +针对控制器的 JavaScript 文件和 CSS 文件也可以只在相应的控制器中引入: `<%= javascript_include_tag params[:controller] %>` 或 `<%= stylesheet_link_tag params[:controller] %>` -如果需要这么做,切记不要使用 `require_tree`。如果使用了这个指令,会多次引入相同的静态资源。 +此时,千万不要使用 `require_tree` 指令,否则就会重复包含这些静态资源文件。 -WARNING: 预处理静态资源时要确保同时处理控制器相关的静态资源。默认情况下,不会自动编译 `.coffee` 和 `.scss` 文件。在开发环境中没什么问题,因为会自动编译。但在生产环境中会得到 500 错误,因为此时自动编译默认是关闭的。关于预编译的工作机理,请阅读“[事先编译好静态资源](#precompiling-assets)”一节。 +WARNING: 在进行静态资源文件预编译时,请确保针对控制器的静态文件是在按页加载时进行预编译的。默认情况下,Rails 不会自动对 `.coffee` 和 `.scss` 文件进行预编译。关于预编译工作原理的更多介绍,请参阅 [预编译静态资源文件](#precompiling-assets)。 -NOTE: 要想使用 CoffeeScript,必须安装支持 ExecJS 的运行时。如果使用 Mac OS X 和 Windows,系统中已经安装了 JavaScript 运行时。所有支持的 JavaScript 运行时参见 [ExecJS](https://github.com/sstephenson/execjs#readme) 的文档。 +NOTE: 要使用 CoffeeScript,就必须安装支持 ExecJS 的运行时。macOS 和 Windows 已经预装了此类运行时。关于所有可用运行时的更多介绍,请参阅 [ExecJS](https://github.com/rails/execjs#readme) 文档。 -在 `config/application.rb` 文件中加入以下代码可以禁止生成控制器相关的静态资源: +通过在 `config/application.rb` 配置文件中添加下述代码,可以禁止生成针对控制器的静态资源文件: ```ruby config.generators do |g| @@ -134,23 +140,27 @@ config.generators do |g| end ``` -### 静态资源的组织方式 + -Asset Pipeline 的静态文件可以放在三个位置:`app/assets`,`lib/assets` 或 `vendor/assets`。 +### 静态资源文件的组织方式 -* `app/assets`:存放程序的静态资源,例如图片、JavaScript 和样式表; -* `lib/assets`:存放自己的代码库,或者共用代码库的静态资源; -* `vendor/assets`:存放他人的静态资源,例如 JavaScript 插件,或者 CSS 框架; +应用的 Asset Pipeline 静态资源文件可以储存在三个位置:`app/assets`、`lib/assets` 和 `vendor/assets`。 -WARNING: 如果从 Rails 3 升级过来,请注意,`lib/assets` 和 `vendor/assets` 中的静态资源可以引入程序,但不在预编译的范围内。详情参见“[事先编译好静态资源](#precompiling-assets)”一节。 +* `app/assets` 文件夹用于储存应用自有的静态资源文件,例如自定义图像、JavaScript 文件和 CSS 文件。 +* `lib/assets` 文件夹用于储存自有代码库的静态资源文件,这些代码库或者不适合放在当前应用中,或者需要在多个应用间共享。 +* `vendor/assets` 文件夹用于储存第三方代码库的静态资源文件,例如 JavaScript 插件和 CSS 框架。如果第三方代码库中引用了同样由 Asset Pipeline 处理的静态资源文件(图像、CSS 文件等),就必须使用 `asset_path` 这样的辅助方法重新编写相关代码。 + +WARNING: 从 Rails 3 升级而来的用户需要注意,通过设置应用的清单文件, 我们可以包含 `lib/assets` 和 `vendor/assets` 文件夹中的静态资源文件,但是这两个文件夹不再是预编译数组的一部分。更多介绍请参阅 [预编译静态资源文件](#precompiling-assets)。 + + #### 搜索路径 -在清单文件或帮助方法中引用静态资源时,Sprockets 会在默认的三个位置中查找对应的文件。 +当清单文件或辅助方法引用了静态资源文件时,Sprockets 会在静态资源文件的三个默认存储位置中进行查找。 -默认的位置是 `apps/assets` 文件夹中的 `images`、`javascripts` 和 `stylesheets` 三个子文件夹。这三个文件夹没什么特别之处,其实 Sprockets 会搜索 `apps/assets` 文件夹中的所有子文件夹。 +这三个默认存储位置分别是 `app/assets` 文件夹的 `images`、`javascripts` 和 `stylesheets` 子文件夹,实际上这三个文件夹并没有什么特别之处,所有的 `app/assets/*` 文件夹及其子文件夹都会被搜索。 -例如,如下的文件: +例如,下列文件: ``` app/assets/javascripts/home.js @@ -159,157 +169,169 @@ vendor/assets/javascripts/slider.js vendor/assets/somepackage/phonebox.js ``` -在清单文件中可以这么引用: +在清单文件中可以像下面这样进行引用: -```js +```javascript //= require home //= require moovinator //= require slider //= require phonebox ``` -子文件夹中的静态资源也可引用: +这些文件夹的子文件夹中的静态资源文件: ``` app/assets/javascripts/sub/something.js ``` -引用方式如下: +可以像下面这样进行引用: -```js +```javascript //= require sub/something ``` -在 Rails 控制台中执行 `Rails.application.config.assets.paths`,可以查看所有的搜索路径。 +通过在 Rails 控制台中检查 `Rails.application.config.assets.paths` 变量,我们可以查看搜索路径。 -除了标准的 `assets/*` 路径之外,还可以在 `config/application.rb` 文件中向 Asset Pipeline 添加其他路径。例如: +除了标准的 `app/assets/*` 路径,还可以在 `config/application.rb` 配置文件中为 Asset Pipeline 添加其他路径。例如: ```ruby config.assets.paths << Rails.root.join("lib", "videoplayer", "flash") ``` -Sprockets 会按照搜索路径中各路径出现的顺序进行搜索。默认情况下,这意味着 `app/assets` 文件夹中的静态资源优先级较高,会遮盖 `lib` 和 `vendor` 文件夹中的相应文件。 +Rails 会按照路径在搜索路径中出现的先后顺序,对路径进行遍历。因此,在默认情况下,`app/assets` 中的文件优先级最高,将会遮盖 `lib` 和 `vendor` 文件夹中的同名文件。 + +千万注意,在清单文件之外引用的静态资源文件必须添加到预编译数组中,否则无法在生产环境中使用。 -有一点要注意,如果静态资源不会在清单文件中引入,就要添加到预编译的文件列表中,否则在生产环境中就无法访问文件。 + #### 使用索引文件 -在 Sprockets 中,名为 `index` 的文件(扩展名各异)有特殊作用。 +对于 Sprockets,名为 `index`(带有相关扩展名)的文件具有特殊用途。 -例如,程序中使用了 jQuery 代码库和许多模块,都保存在 `lib/assets/javascripts/library_name` 文件夹中,那么 `lib/assets/javascripts/library_name/index.js` 文件的作用就是这个代码库的清单。清单文件中可以按顺序列出所需的文件,或者干脆使用 `require_tree` 指令。 +例如,假设应用中使用的 jQuery 库及多个模块储存在 `lib/assets/javascripts/library_name` 文件夹中,那么 `lib/assets/javascripts/library_name/index.js` 文件将作为这个库的清单文件。在这个库的清单文件中,应该按顺序列出所有需要加载的文件,或者干脆使用 `require_tree` 指令。 -在清单文件中,可以把这个库作为一个整体引入: +在应用的清单文件中,可以把这个库作为一个整体加载: -```js +```javascript //= require library_name ``` -这么做可以减少维护成本,保持代码整洁。 +这样,相关代码总是作为整体在应用中使用,降低了维护成本,并使代码保持简洁。 -### 链接静态资源 + -Sprockets 并没有为获取静态资源添加新的方法,还是使用熟悉的 `javascript_include_tag` 和 `stylesheet_link_tag`: +### 创建指向静态资源文件的链接 + +Sprockets 没有为访问静态资源文件添加任何新方法,而是继续使用我们熟悉的 `javascript_include_tag` 和 `stylesheet_link_tag` 辅助方法: ```erb <%= stylesheet_link_tag "application", media: "all" %> <%= javascript_include_tag "application" %> ``` -如果使用 Turbolinks(Rails 4 默认启用),加上 `data-turbolinks-track` 选项后,Turbolinks 会检查静态资源是否有更新,如果更新了就会将其载入页面: +如果使用了 Rails 默认包含的 `turbolinks` gem,并使用了 `data-turbolinks-track` 选项,Turbolinks 就会检查静态资源文件是否有更新,如果有更新就加载到页面中: ```erb -<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> -<%= javascript_include_tag "application", "data-turbolinks-track" => true %> +<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => "reload" %> +<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %> ``` -在普通的视图中可以像下面这样获取 `public/assets/images` 文件夹中的图片: +在常规视图中,我们可以像下面这样访问 `app/assets/images` 文件夹中的图像: ```erb <%= image_tag "rails.png" %> ``` -如果程序启用了 Asset Pipeline,且在当前环境中没有禁用,那么这个文件会经由 Sprockets 伺服。如果文件的存放位置是 `public/assets/rails.png`,则直接由网页服务器伺服。 +如果在应用中启用了 Asset Pipeline,并且未在当前环境中禁用 Asset Pipeline,那么这个图像文件将由 Sprockets 处理。如果图像的位置是 `public/assets/rails.png`,那么将由 Web 服务器处理。 -如果请求的文件中包含 MD5 哈希,处理的方式还是一样。关于这个哈希是怎么生成的,请阅读“[在生产环境中](#in-production)”一节。 +如果文件请求包含 SHA256 哈希值,例如 `public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png`,处理的方式也是一样的。关于如何生成哈希值的介绍,请参阅 [在生产环境中](#in-production)。 -Sprockets 还会检查 `config.assets.paths` 中指定的路径。`config.assets.paths` 包含标准路径和其他 Rails 引擎添加的路径。 +Sprockets 还会检查 `config.assets.paths` 中指定的路径,其中包括 Rails 应用的标准路径和 Rails 引擎添加的路径。 -图片还可以放入子文件夹中,获取时指定文件夹的名字即可: +也可以把图像放在子文件夹中,访问时只需加上子文件夹的名称即可: ```erb <%= image_tag "icons/rails.png" %> ``` -WARNING: 如果预编译了静态资源(参见“[在生产环境中](#in-production)”一节),链接不存在的资源(也包括链接到空字符串的情况)会在调用页面抛出异常。因此,在处理用户提交的数据时,使用 `image_tag` 等帮助方法要小心一点。 +WARNING: 如果对静态资源文件进行了预编译(请参阅 [在生产环境中](#in-production)),那么在页面中链接到并不存在的静态资源文件或空字符串将导致该页面抛出异常。因此,在使用 `image_tag` 等辅助方法处理用户提供的数据时一定要小心。 + + #### CSS 和 ERB -Asset Pipeline 会自动执行 ERB 代码,所以如果在 CSS 文件名后加上扩展名 `erb`(例如 `application.css.erb`),那么在 CSS 规则中就可使用 `asset_path` 等帮助方法。 +Asset Pipeline 会自动计算 ERB 的值。也就是说,只要给 CSS 文件添加 `.erb` 扩展名(例如 `application.css.erb`),就可以在 CSS 规则中使用 `asset_path` 等辅助方法。 -```css +```erb .class { background-image: url(https://github.com/%3C%=%20asset_path%20'image.png'%20%%3E) } ``` -Asset Pipeline 会计算出静态资源的真实路径。在上面的代码中,指定的图片要出现在加载路径中。如果在 `public/assets` 中有该文件带指纹版本,则会使用这个文件的路径。 +上述代码中的 `asset_path` 辅助方法会返回指向图像真实路径的链接。图像必须位于静态文件加载路径中,例如 `app/assets/images/image.png`,以便在这里引用。如果在 `public/assets` 文件夹中已经存在此图像的带指纹的版本,那么将引用这个带指纹的版本。 -如果想使用 [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme)(直接把图片数据内嵌在 CSS 文件中),可以使用 `asset_data_uri` 帮助方法。 +要想使用 [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme)(用于把图像数据直接嵌入 CSS 文件中),可以使用 `asset_data_uri` 辅助方法: -```css +```erb #logo { background: url(https://github.com/%3C%=%20asset_data_uri%20'logo.png'%20%%3E) } ``` -`asset_data_uri` 会把正确格式化后的 data URI 写入 CSS 文件。 +`asset_data_uri` 辅助方法会把正确格式化后的 data URI 插入 CSS 源代码中。 注意,关闭标签不能使用 `-%>` 形式。 + + #### CSS 和 Sass -使用 Asset Pipeline,静态资源的路径要使用 `sass-rails` 提供的 `-url` 和 `-path` 帮助方法(在 Sass 中使用连字符,在 Ruby 中使用下划线)重写。这两种帮助方法可用于引用图片,字体,视频,音频,JavaScript 和样式表。 +在使用 Asset Pipeline 时,静态资源文件的路径都必须重写,为此 `sass-rails` gem 提供了 `-url` 和 `-path` 系列辅助方法(在 Sass 中使用连字符,在 Ruby 中使用下划线),用于处理图像、字体、视频、音频、JavaScript 和 CSS 等类型的静态资源文件。 + +* `image-url("https://github.com/rails.png")` 会返回 `url(https://github.com/assets/rails.png)` +* `image-path("rails.png")` 会返回 `"/assets/rails.png"` -* `image-url("https://github.com/rails.png")` 编译成 `url(https://github.com/assets/rails.png)` -* `image-path("rails.png")` 编译成 `"/assets/rails.png"`. +或使用更通用的形式: -还可使用通用方法: +* `asset-url("https://github.com/rails.png")` 返回 `url(https://github.com/assets/rails.png)` +* `asset-path("rails.png")` 返回 `"/assets/rails.png"` -* `asset-url("https://github.com/rails.png")` 编译成 `url(https://github.com/assets/rails.png)` -* `asset-path("rails.png")` 编译成 `"/assets/rails.png"` + #### JavaScript/CoffeeScript 和 ERB -如果在 JavaScript 文件后加上扩展名 `erb`,例如 `application.js.erb`,就可以在 JavaScript 代码中使用帮助方法 `asset_path`: +只要给 JavaScript 文件添加 `.erb` 扩展名(例如 `application.js.erb`),就可以在 JavaScript 源代码中使用 `asset_path` 辅助方法: -```js +```erb $('#logo').attr({ src: "<%= asset_path('logo.png') %>" }); ``` -Asset Pipeline 会计算出静态资源的真实路径。 +上述代码中的 `asset_path` 辅助方法会返回指向图像真实路径的链接。 -类似地,如果在 CoffeeScript 文件后加上扩展名 `erb`,例如 `application.js.coffee.erb`,也可在代码中使用帮助方法 `asset_path`: +同样,只要给 CoffeeScript 文件添加 `.erb` 扩展名(例如 `application.coffee.erb`),就可以在 CoffeeScript 源代码中使用 `asset_path` 辅助方法: -```js +```erb $('#logo').attr src: "<%= asset_path('logo.png') %>" ``` + + ### 清单文件和指令 -Sprockets 通过清单文件决定要引入和伺服哪些静态资源。清单文件中包含一些指令,告知 Sprockets 使用哪些文件生成主 CSS 或 JavaScript 文件。Sprockets 会解析这些指令,加载指定的文件,如有需要还会处理文件,然后再把各个文件合并成一个文件,最后再压缩文件(如果 `Rails.application.config.assets.compress` 选项为 `true`)。只伺服一个文件可以大大减少页面加载时间,因为浏览器发起的请求数更少。压缩能减小文件大小,加快浏览器下载速度。 +Sprockets 使用清单文件来确定需要包含和处理哪些静态资源文件。这些清单文件中的指令会告诉 Sprockets,要想创建 CSS 或 JavaScript 文件需要加载哪些文件。通过这些指令,可以让 Sprockets 加载指定文件,对这些文件进行必要的处理,然后把它们连接为单个文件,最后进行压缩(压缩方式取决于 `Rails.application.config.assets.js_compressor` 选项的值)。这样在页面中只需处理一个文件而非多个文件,减少了浏览器的请求次数,大大缩短了页面的加载时间。通过压缩还能使文件变小,使浏览器可以更快地下载。 -例如,新建的 Rails 4 程序中有个 `app/assets/javascripts/application.js` 文件,包含以下内容: +例如,在默认情况下,新建 Rails 应用的 `app/assets/javascripts/application.js` 文件包含下面几行代码: -```js +```javascript // ... //= require jquery //= require jquery_ujs //= require_tree . ``` -在 JavaScript 文件中,Sprockets 的指令以 `//=` 开头。在上面的文件中,用到了 `require` 和 the `require_tree` 指令。`require` 指令告知 Sprockets 要加载的文件。在上面的文件中,加载了 Sprockets 搜索路径中的 `jquery.js` 和 `jquery_ujs.js` 两个文件。文件名后无需加上扩展名,在 `.js` 文件中 Sprockets 默认会加载 `.js` 文件。 +在 JavaScript 文件中,Sprockets 指令以 `//=.` 开头。上述代码中使用了 `require` 和 `require_tree` 指令。`require` 指令用于告知 Sprockets 哪些文件需要加载。这里加载的是 Sprockets 搜索路径中的 `jquery.js` 和 `jquery_ujs.js` 文件。我们不必显式提供文件的扩展名,因为 Sprockets 假定在 `.js` 文件中加载的总是 `.js` 文件。 -`require_tree` 指令告知 Sprockets 递归引入指定文件夹中的所有 JavaScript 文件。文件夹的路径必须相对于清单文件。也可使用 `require_directory` 指令加载指定文件夹中的所有 JavaScript 文件,但不会递归。 +`require_tree` 指令告知 `Sprockets` 以递归方式包含指定文件夹中的所有 JavaScript 文件。在指定文件夹路径时,必须使用相对于清单文件的相对路径。也可以通过 `require_directory` 指令包含指定文件夹中的所有 JavaScript 文件,此时将不会采取递归方式。 -Sprockets 会按照从上至下的顺序处理指令,但 `require_tree` 引入的文件顺序是不可预期的,不要设想能得到一个期望的顺序。如果要确保某些 JavaScript 文件出现在其他文件之前,就要先在清单文件中引入。注意,`require` 等指令不会多次加载同一个文件。 +清单文件中的指令是按照从上到下的顺序处理的,但我们无法确定 `require_tree` 指令包含文件的顺序,因此不应该依赖于这些文件的顺序。如果想要确保连接文件时某些 JavaScript 文件出现在其他 JavaScript 文件之前,可以在清单文件中先行加载这些文件。注意,`require` 系列指令不会重复加载文件。 -Rails 还会生成 `app/assets/stylesheets/application.css` 文件,内容如下: +在默认情况下,新建 Rails 应用的 `app/assets/stylesheets/application.css` 文件包含下面几行代码: ```css /* ... @@ -318,17 +340,19 @@ Rails 还会生成 `app/assets/stylesheets/application.css` 文件,内容如 */ ``` -不管创建新程序时有没有指定 `--skip-sprockets` 选项,Rails 4 都会生成 `app/assets/javascripts/application.js` 和 `app/assets/stylesheets/application.css`。这样如果后续需要使用 Asset Pipelining,操作就方便了。 +无论新建 Rails 应用时是否使用了 `--skip-sprockets` 选项,Rails 都会创建 `app/assets/javascripts/application.js` 和 `app/assets/stylesheets/application.css` 文件。因此,之后想要使用 Asset Pipeline 非常容易。 -样式表中使用的指令和 JavaScript 文件一样,不过加载的是样式表而不是 JavaScript 文件。`require_tree` 指令在 CSS 清单文件中的作用和在 JavaScript 清单文件中一样,从指定的文件夹中递归加载所有样式表。 +我们在 JavaScript 文件中使用的指令同样可以在 CSS 文件中使用,此时加载的是 CSS 文件而不是 JavaScript 文件。在 CSS 清单文件中,`require_tree` 指令的工作原理和在 JavaScript 清单文件中相同,会加载指定文件夹中的所有 CSS 文件。 -上面的代码中还用到了 `require_self`。这么做可以把当前文件中的 CSS 加入调用 `require_self` 的位置。如果多次调用 `require_self`,只有最后一次调用有效。 +上述代码中使用了 `require_self` 指令,用于把当前文件中的 CSS 代码(如果存在)插入调用这个指令的位置。 -NOTE: 如果想使用多个 Sass 文件,应该使用 [Sass 中的 `@import` 规则](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import),不要使用 Sprockets 指令。如果使用 Sprockets 指令,Sass 文件只出现在各自的作用域中,Sass 变量和混入只在定义所在文件中有效。为了达到 `require_tree` 指令的效果,可以使用通配符,例如 `@import "https://github.com/*"` 和 `@import "https://github.com/**/*"`。详情参见 [sass-rails 的文档](https://github.com/rails/sass-rails#features)。 +NOTE: 要想使用多个 Sass 文件,通常应该使用 [Sass @import 规则](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import),而不是 Sprockets 指令。如果使用 Sprockets 指令,这些 Sass 文件将拥有各自的作用域,这样变量和混入只能在定义它们的文件中使用。 -清单文件可以有多个。例如,`admin.css` 和 `admin.js` 这两个清单文件包含程序管理后台所需的 JS 和 CSS 文件。 +和使用 `require_tree` 指令相比,使用 `@import "https://github.com/*"` 和 `@import "https://github.com/**/*"` 的效果完全相同,都能加载指定文件夹中的所有文件。更多介绍和注意事项请参阅 [sass-rails 文档](https://github.com/rails/sass-rails#features)。 -CSS 清单中的指令也适用前面介绍的加载顺序。分别引入各文件,Sprockets 会按照顺序编译。例如,可以按照下面的方式合并三个 CSS 文件: +我们可以根据需要使用多个清单文件。例如,可以用 `admin.js` 和 `admin.css` 清单文件分别包含应用管理后台的 JS 和 CSS 文件。 + +CSS 清单文件中指令的执行顺序类似于前文介绍的 JavaScript 清单文件,尤其是加载的文件都会按照指定顺序依次编译。例如,我们可以像下面这样把 3 个 CSS 文件连接在一起: ```css /* ... @@ -338,30 +362,33 @@ CSS 清单中的指令也适用前面介绍的加载顺序。分别引入各文 */ ``` + + ### 预处理 -静态资源的文件扩展名决定了使用哪个预处理器处理。如果使用默认的 gem,生成控制器或脚手架时,会生成 CoffeeScript 和 SCSS 文件,而不是普通的 JavaScript 和 CSS 文件。前文举过例子,生成 `projects` 控制器时会创建 `app/assets/javascripts/projects.js.coffee` 和 `app/assets/stylesheets/projects.css.scss` 两个文件。 +静态资源文件的扩展名决定了预处理的方式。在使用默认的 Rails gemset 生成控制器或脚手架时,会生成 CoffeeScript 和 SCSS 文件,而不是普通的 JavaScript 和 CSS 文件。在前文的例子中,生成 `projects` 控制器时会生成 `app/assets/javascripts/projects.coffee` 和 `app/assets/stylesheets/projects.scss` 文件。 -在开发环境中,或者禁用 Asset Pipeline 时,这些文件会使用 `coffee-script` 和 `sass` 提供的预处理器处理,然后再发给浏览器。启用 Asset Pipeline 时,这些文件会先使用预处理器处理,然后保存到 `public/assets` 文件夹中,再由 Rails 程序或网页服务器伺服。 +在开发环境中,或 Asset Pipeline 被禁用时,会使用 `coffee-script` 和 `sass` gem 提供的处理器分别处理相应的文件请求,并把生成的 JavaScript 和 CSS 文件发给浏览器。当 Asset Pipeline 可用时,会对这些文件进行预处理,然后储存在 `public/assets` 文件夹中,由 Rails 应用或 Web 服务器处理。 -添加额外的扩展名可以增加预处理次数,预处理程序会按照扩展名从右至左的顺序处理文件内容。所以,扩展名的顺序要和处理的顺序一致。例如,名为 `app/assets/stylesheets/projects.css.scss.erb` 的样式表首先会使用 ERB 处理,然后是 SCSS,最后才以 CSS 格式发送给浏览器。JavaScript 文件类似,`app/assets/javascripts/projects.js.coffee.erb` 文件先由 ERB 处理,然后是 CoffeeScript,最后以 JavaScript 格式发送给浏览器。 +通过添加其他扩展名,可以对文件进行更多预处理。对扩展名的解析顺序是从右到左,相应的预处理顺序也是从右到左。例如,对于 `app/assets/stylesheets/projects.scss.erb` 文件,会先处理 ERB,再处理 SCSS,最后作为 CSS 文件处理。同样,对于 `app/assets/javascripts/projects.coffee.erb` 文件,会先处理 ERB,再处理 CoffeeScript,最后作为 JavaScript 文件处理。 -记住,预处理器的执行顺序很重要。例如,名为 `app/assets/javascripts/projects.js.erb.coffee` 的文件首先由 CoffeeScript 处理,但是 CoffeeScript 预处理器并不懂 ERB 代码,因此会导致错误。 +记住预处理顺序很重要。例如,如果我们把文件名写为 `app/assets/javascripts/projects.erb.coffee`,就会先处理 CoffeeScript,这时一旦遇到 ERB 代码就会出错。 -开发环境 --------- + -在开发环境中,Asset Pipeline 按照清单文件中指定的顺序伺服各静态资源。 +## 在开发环境中 -清单 `app/assets/javascripts/application.js` 的内容如下: +在开发环境中,Asset Pipeline 会按照清单文件中指定的顺序处理静态资源文件。 -```js +对于清单文件 `app/assets/javascripts/application.js`: + +```javascript //= require core //= require projects //= require tickets ``` -生成的 HTML 如下: +会生成下面的 HTML: ```html @@ -369,77 +396,91 @@ CSS 清单中的指令也适用前面介绍的加载顺序。分别引入各文 ``` -Sprockets 要求必须使用 `body` 参数。 +其中 `body` 参数是使用 Sprockets 时必须使用的参数。 + + ### 检查运行时错误 -默认情况下,在生产环境中 Asset Pipeline 会检查潜在的错误。要想禁用这一功能,可以做如下设置: +在生产环境中,Asset Pipeline 默认会在运行时检查潜在错误。要想禁用此行为,可以设置: ```ruby config.assets.raise_runtime_errors = false ``` -`raise_runtime_errors` 设为 `false` 时,Sprockets 不会检查静态资源的依赖关系是否正确。遇到下面这种情况时,必须告知 Asset Pipeline 其中的依赖关系。 +当此选项设置为 `true` 时,Asset Pipeline 会检查应用中加载的所有静态资源文件是否都已包含在 `config.assets.precompile` 列表中。如果此时 `config.assets.digest` 也设置为 `true`,Asset Pipeline 会要求所有对静态资源文件的请求都包含指纹(digest)。 -如果在 `application.css.erb` 中引用了 `logo.png`,如下所示: + -```css -#logo { background: url(https://github.com/%3C%=%20asset_data_uri%20'logo.png'%20%%3E) } +### 找不到静态资源时抛出错误 + +如果使用的 sprockets-rails 是 3.2.0 或以上版本,可以配置找不到静态资源时的行为。如果禁用了“静态资源后备机制”,找不到静态资源时抛出错误。 + +```ruby +config.assets.unknown_asset_fallback = false ``` -就必须声明 `logo.png` 是 `application.css.erb` 的一个依赖件,这样重新编译图片时才会同时重新编译 CSS 文件。依赖关系可以使用 `//= depend_on_asset` 声明: +如果启用了“静态资源后备机制”,找不到静态资源时,输出路径,而不抛出错误。静态资源后备机制默认启用。 -```css -//= depend_on_asset "logo.png" -#logo { background: url(https://github.com/%3C%=%20asset_data_uri%20'logo.png'%20%%3E) } + + +### 关闭指纹 + +通过修改 `config/environments/development.rb` 配置文件,我们可以关闭指纹: + +```ruby +config.assets.digest = false ``` -如果没有这个声明,在生产环境中可能遇到难以查找的奇怪问题。`raise_runtime_errors` 设为 `true` 时,运行时会自动检查依赖关系。 +当此选项设置为 `true` 时,Rails 会为静态资源文件的 URL 生成指纹。 + + -### 关闭调试功能 +### 关闭调试 -在 `config/environments/development.rb` 中添加如下设置可以关闭调试功能: +通过修改 `config/environments/development.rb` 配置文件,我们可以关闭调式模式: ```ruby config.assets.debug = false ``` -关闭调试功能后,Sprockets 会预处理所有文件,然后合并。关闭调试功能后,前文的清单文件生成的 HTML 如下: +当调试模式关闭时,Sprockets 会对所有文件进行必要的预处理,然后把它们连接起来。此时,前文的清单文件会生成下面的 HTML: ```html ``` -服务器启动后,首次请求发出后会编译并缓存静态资源。Sprockets 会把 `Cache-Control` 报头设为 `must-revalidate`。再次请求时,浏览器会得到 304 (Not Modified) 响应。 +当服务器启动后,静态资源文件将在第一次请求时进行编译和缓存。Sprockets 通过设置 `must-revalidate Cache-Control` HTTP 首部,来减少后续请求造成的开销,此时对于后续请求浏览器会得到 304(未修改)响应。 -如果清单中的文件内容发生了变化,服务器会返回重新编译后的文件。 +如果清单文件中的某个文件在两次请求之间发生了变化,服务器会使用新编译的文件作为响应。 -调试功能可以在 Rails 帮助方法中启用: +还可以通过 Rails 辅助方法启用调试模式: ```erb <%= stylesheet_link_tag "application", debug: true %> <%= javascript_include_tag "application", debug: true %> ``` -如果已经启用了调试模式,再使用 `:debug` 选项就有点多余了。 +当然,如果已经启用了调式模式,再使用 `:debug` 选项就完全是多余的了。 -在开发环境中也可启用压缩功能,检查是否能正常运行。需要调试时再禁用压缩即可。 +在开发模式中,我们也可以启用压缩功能以检查其工作是否正常,在需要进行调试时再禁用压缩功能。 -生产环境 -------- + -在生产环境中,Sprockets 使用前文介绍的指纹机制。默认情况下,Rails 认为静态资源已经事先编译好了,直接由网页服务器伺服。 +## 在生产环境中 -在预先编译的过程中,会根据文件的内容生成 MD5,写入硬盘时把 MD5 加到文件名中。Rails 帮助方法会使用加上指纹的文件名代替清单文件中使用的文件名。 +在生产环境中,Sprockets 会使用前文介绍的指纹机制。默认情况下,Rails 假定静态资源文件都经过了预编译,并将由 Web 服务器处理。 -例如: +在预编译阶段,Sprockets 会根据静态资源文件的内容生成 SHA256 哈希值,并在保存文件时把这个哈希值添加到文件名中。Rails 辅助方法会用这些包含指纹的文件名代替清单文件中的文件名。 + +例如,下面的代码: ```erb <%= javascript_include_tag "application" %> <%= stylesheet_link_tag "application" %> ``` -生成的 HTML 如下: +会生成下面的 HTML: ```html @@ -447,242 +488,357 @@ config.assets.debug = false rel="stylesheet" /> ``` -注意,推出 Asset Pipeline 功能后不再使用 `:cache` 和 `:concat` 选项了,请从 `javascript_include_tag` 和 `stylesheet_link_tag` 标签上将其删除。 +NOTE: Rails 开始使用 Asset Pipeline 后,不再使用 `:cache` 和 `:concat` 选项,因此在调用 `javascript_include_tag` 和 `stylesheet_link_tag` 辅助方法时需要删除这些选项。 + +可以通过 `config.assets.digest` 初始化选项(默认为 `true`)启用或禁用指纹功能。 -指纹由 `config.assets.digest` 初始化选项控制(生产环境默认为 `true`,其他环境为 `false`)。 +NOTE: 在正常情况下,请不要修改默认的 `config.assets.digest` 选项(默认为 `true`)。如果文件名中未包含指纹,并且 HTTP 头信息的过期时间设置为很久以后,远程客户端将无法在文件内容发生变化时重新获取文件。 -NOTE: 一般情况下,请勿修改 `config.assets.digest` 的默认值。如果文件名中没有指纹,而且缓存报头的时间设置为很久以后,那么即使文件的内容变了,客户端也不会重新获取文件。 + -### 事先编译好静态资源 +### 预编译静态资源文件 -Rails 提供了一个 rake 任务用来编译清单文件中的静态资源和其他相关文件。 +Rails 提供了一个 Rake 任务,用于编译 Asset Pipeline 清单文件中的静态资源文件和其他相关文件。 -编译后的静态资源保存在 `config.assets.prefix` 选项指定的位置。默认是 `/assets` 文件夹。 +经过编译的静态资源文件将储存在 `config.assets.prefix` 选项指定的路径中,默认为 `/assets` 文件夹。 -部署时可以在服务器上执行这个任务,直接在服务器上编译静态资源。下一节会介绍如何在本地编译。 +部署 Rails 应用时可以在服务器上执行这个 Rake 任务,以便直接在服务器上完成静态资源文件的编译。关于本地编译的介绍,请参阅下一节。 -这个 rake 任务是: +这个 Rake 任务是: -```bash -$ RAILS_ENV=production bundle exec rake assets:precompile +```sh +$ RAILS_ENV=production bin/rails assets:precompile ``` -Capistrano(v2.15.1 及以上版本)提供了一个配方,可在部署时编译静态资源。把下面这行加入 `Capfile` 文件即可: +Capistrano(v2.15.1 及更高版本)提供了对这个 Rake 任务的支持。只需把下面这行代码添加到 `Capfile` 中: ```ruby load 'deploy/assets' ``` -这个配方会把 `config.assets.prefix` 选项指定的文件夹链接到 `shared/assets`。如果 `shared/assets` 已经占用,就要修改部署任务。 +就会把 `config.assets.prefix` 选项指定的文件夹链接到 `shared/assets` 文件夹。当然,如果 `shared/assets` 文件夹已经用于其他用途,我们就得自己编写部署任务了。 -在多次部署之间共用这个文件夹是十分重要的,这样只要缓存的页面可用,其中引用的编译后的静态资源就能正常使用。 +需要注意的是,`shared/assets` 文件夹会在多次部署之间共享,这样引用了这些静态资源文件的远程客户端的缓存页面在其生命周期中就能正常工作。 -默认编译的文件包括 `application.js`、`application.css` 以及 gem 中 `app/assets` 文件夹中的所有非 JS/CSS 文件(会自动加载所有图片): +编译文件时的默认匹配器(matcher)包括 `application.js`、`application.css`,以及 `app/assets` 文件夹和 gem 中的所有非 JS/CSS 文件(会自动包含所有图像): ```ruby -[ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) }, +[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) }, /application.(css|js)$/ ] ``` -NOTE: 这个正则表达式表示最终要编译的文件。也就是说,JS/CSS 文件不包含在内。例如,因为 `.coffee` 和 `.scss` 文件能编译成 JS 和 CSS 文件,所以**不在**自动编译的范围内。 - -如果想编译其他清单,或者单独的样式表和 JavaScript,可以添加到 `config/application.rb` 文件中的 `precompile` 选项: - -```ruby -config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js'] -``` +NOTE: 这个匹配器(及预编译数组的其他成员;见后文)会匹配编译后的文件名,这意味着无论是 JS/CSS 文件,还是能够编译为 JS/CSS 的文件,都将被排除在外。例如,`.coffee` 和 `.scss` 文件能够编译为 JS/CSS,因此被排除在默认的编译范围之外。 -或者可以按照下面的方式,设置编译所有静态资源: +要想包含其他清单文件,或单独的 JavaScript 和 CSS 文件,可以把它们添加到 `config/initializers/assets.rb` 配置文件的 `precompile` 数组中: ```ruby -# config/application.rb -config.assets.precompile << Proc.new do |path| - if path =~ /\.(css|js)\z/ - full_path = Rails.application.assets.resolve(path).to_path - app_assets_path = Rails.root.join('app', 'assets').to_path - if full_path.starts_with? app_assets_path - puts "including asset: " + full_path - true - else - puts "excluding asset: " + full_path - false - end - else - false - end -end +Rails.application.config.assets.precompile += %w( admin.js admin.css ) ``` -NOTE: 即便想添加 Sass 或 CoffeeScript 文件,也要把希望编译的文件名设为 .js 或 .css。 +NOTE: 添加到 `precompile` 数组的文件名应该以 `.js` 或 `.css` 结尾,即便实际添加的是 CoffeeScript 或 Sass 文件也是如此。 -这个 rake 任务还会生成一个名为 `manifest-md5hash.json` 的文件,列出所有静态资源和对应的指纹。这样 Rails 帮助方法就不用再通过 Sprockets 获取指纹了。下面是一个 `manifest-md5hash.json` 文件内容示例: +`assets:precompile` 这个 Rake 任务还会成生 `.sprockets-manifest-md5hash.json` 文件(其中 `md5hash` 是一个 MD5 哈希值),其内容是所有静态资源文件及其指纹的列表。有了这个文件,Rails 辅助方法不需要 Sprockets 就能获得静态资源文件对应的指纹。下面是一个典型的 `.sprockets-manifest-md5hash.json` 文件的例子: -```ruby -{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506, -"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560, -"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591, -"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406, -"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646, -"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets"{"application.js": -"application-723d1be6cc741a3aabb1cec24276d681.js","application.css": -"application-1c5752789588ac18d7e1a50b1f0fd4c2.css", -"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png": -"my_image-231a680f23887d9dd70710ea5efd3c62.png"}} +```json +{"files":{"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383, +"digest":"aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b","integrity":"sha256-ruS+cfEogDeueLmX3ziDMu39JGRxtTPc7aqPn+FWRCs="}, +"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css":{"logical_path":"application.css","mtime":"2016-12-23T19:12:20-05:00","size":2994, +"digest":"86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18","integrity":"sha256-hqKStQcHk8N+LA5fOfc7s4dkTq6tp/lub8BAoCixbBg="}, +"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico":{"logical_path":"favicon.ico","mtime":"2016-12-23T20:11:00-05:00","size":8629, +"digest":"8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda","integrity":"sha256-jSOHuNTTLOzZP6OQDfDp/4nQGqzYT1DngMF8n2s9Dto="}, +"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png":{"logical_path":"my_image.png","mtime":"2016-12-23T20:10:54-05:00","size":23414, +"digest":"f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493","integrity":"sha256-9AKBVv1+ygNYTV8vwEcN8eDbxzaequY4sv8DP5iOxJM="}}, +"assets":{"application.js":"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js", +"application.css":"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css", +"favicon.ico":"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico", +"my_image.png":"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png"}} ``` -`manifest-md5hash.json` 文件的存放位置是 `config.assets.prefix` 选项指定位置(默认为 `/assets`)的根目录。 +`.sprockets-manifest-md5hash.json` 文件默认位于 `config.assets.prefix` 选项所指定的位置的根目录(默认为 `/assets` 文件夹)。 -NOTE: 在生产环境中,如果找不到编译好的文件,会抛出 `Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError` 异常,并提示找不到哪个文件。 +NOTE: 在生产环境中,如果有些预编译后的文件丢失了,Rails 就会抛出 `Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError` 异常,提示所丢失文件的文件名。 -#### 把 Expires 报头设置为很久以后 + -编译好的静态资源存放在服务器的文件系统中,直接由网页服务器伺服。默认情况下,没有为这些文件设置一个很长的过期时间。为了能充分发挥指纹的作用,需要修改服务器的设置,添加相关的报头。 +#### 在 HTTP 首部中设置为很久以后才过期 -针对 Apache 的设置: +预编译后的静态资源文件储存在文件系统中,并由 Web 服务器直接处理。默认情况下,这些文件的 HTTP 首部并不会在很久以后才过期,为了充分发挥指纹的作用,我们需要修改服务器配置中的请求头过期时间。 -```conf -# The Expires* directives requires the Apache module -# `mod_expires` to be enabled. +对于 Apache: + +```apache +# 在启用 Apache 模块 `mod_expires` 的情况下,才能使用 +# Expires* 系列指令。 - # Use of ETag is discouraged when Last-Modified is present - Header unset ETag FileETag None - # RFC says only cache for 1 year - ExpiresActive On ExpiresDefault "access plus 1 year" + # 在使用 Last-Modified 的情况下,不推荐使用 ETag + Header unset ETag + FileETag None + # RFC 规定缓存时间为 1 年 + ExpiresActive On + ExpiresDefault "access plus 1 year" ``` -针对 Nginx 的设置: +对于 Nginx: -```conf +```nginx location ~ ^/assets/ { expires 1y; add_header Cache-Control public; add_header ETag ""; - break; } ``` -#### GZip 压缩 + -Sprockets 预编译文件时还会创建静态资源的 [gzip](http://en.wikipedia.org/wiki/Gzip) 版本(.gz)。网页服务器一般使用中等压缩比例,不过因为预编译只发生一次,所以 Sprockets 会使用最大的压缩比例,尽量减少传输的数据大小。网页服务器可以设置成直接从硬盘伺服压缩版文件,无需直接压缩文件本身。 +### 本地预编译 -在 Nginx 中启动 `gzip_static` 模块后就能自动实现这一功能: +在本地预编译静态资源文件的理由如下: -```nginx -location ~ ^/(assets)/ { - root /path/to/public; - gzip_static on; # to serve pre-gzipped version - expires max; - add_header Cache-Control public; -} -``` +* 可能没有生产环境服务器文件系统的写入权限; +* 可能需要部署到多台服务器,不想重复编译; +* 部署可能很频繁,但静态资源文件很少变化。 -如果编译 Nginx 时加入了 `gzip_static` 模块,就能使用这个指令。Nginx 针对 Ubuntu/Debian 的安装包,以及 `nginx-light` 都会编译这个模块。否则就要手动编译: +本地编译允许我们把编译后的静态资源文件纳入源代码版本控制,并按常规方式部署。 -```bash -./configure --with-http_gzip_static_module +有三个注意事项: + +* 不要运行用于预编译静态资源文件的 Capistrano 部署任务; +* 开发环境中必须安装压缩或简化静态资源文件所需的工具; +* 必须修改下面这个设置: + +在 `config/environments/development.rb` 配置文件中添加下面这行代码: + +```ruby +config.assets.prefix = "/dev-assets" ``` -如果编译支持 Phusion Passenger 的 Nginx,就必须加入这个命令行选项。 +在开发环境中,通过修改 `prefix`,可以让 Sprockets 使用不同的 URL 处理静态资源文件,并把所有请求都交给 Sprockets 处理。在生产环境中,`prefix` 仍然应该设置为 `/assets`。在开发环境中,如果不修改 `prefix`,应用就会优先读取 `/assets` 文件夹中预编译后的静态资源文件,这样对静态资源文件进行修改后,除非重新编译,否则看不到任何效果。 -针对 Apache 的设置很复杂,请自行 Google。 +实际上,通过修改 `prefix`,我们可以在本地预编译静态资源文件,并把这些文件储存在工作目录中,同时可以根据需要随时将其纳入源代码版本控制。开发模式将按我们的预期正常工作。 -### 在本地预编译 + -为什么要在本地预编译静态文件呢?原因如下: +### 实时编译 -* 可能无权限访问生产环境服务器的文件系统; -* 可能要部署到多个服务器,避免重复编译; -* 可能会经常部署,但静态资源很少改动; +在某些情况下,我们需要使用实时编译。在实时编译模式下,Asset Pipeline 中的所有静态资源文件都由 Sprockets 直接处理。 -在本地预编译后,可以把编译好的文件纳入版本控制系统,再按照常规的方式部署。 +通过如下设置可以启用实时编译: -不过有两点要注意: +```ruby +config.assets.compile = true +``` -* 一定不能运行 Capistrano 部署任务来预编译静态资源; -* 必须修改下面这个设置; +如前文所述,静态资源文件会在首次请求时被编译和缓存,辅助方法会把清单文件中的文件名转换为带 SHA256 哈希值的版本。 -在 `config/environments/development.rb` 中加入下面这行代码: +Sprockets 还会把 `Cache-Control` HTTP 首部设置为 `max-age=31536000`,意思是服务器和客户端浏览器的所有缓存的过期时间是 1 年。这样在本地浏览器缓存或中间缓存中找到所需静态资源文件的可能性会大大增加,从而减少从服务器上获取静态资源文件的请求次数。 + +但是实时编译模式会使用更多内存,性能也比默认设置更差,因此并不推荐使用。 + +如果部署应用的生产服务器没有预装 JavaScript 运行时,可以在 Gemfile 中添加一个: ```ruby -config.assets.prefix = "/dev-assets" +group :production do + gem 'therubyracer' +end ``` -修改 `prefix` 后,在开发环境中 Sprockets 会使用其他的 URL 伺服静态资源,把请求都交给 Sprockets 处理。但在生产环境中 `prefix` 仍是 `/assets`。如果没作上述修改,在生产环境中会从 `/assets` 伺服静态资源,除非再次编译,否则看不到文件的变化。 + -同时还要确保所需的压缩程序在生产环境中可用。 +### CDN -在本地预编译静态资源,这些文件就会出现在工作目录中,而且可以根据需要纳入版本控制系统。开发环境仍能按照预期正常运行。 +CDN 的意思是[内容分发网络](http://en.wikipedia.org/wiki/Content_delivery_network),主要用于缓存全世界的静态资源文件。当 Web 浏览器请求静态资源文件时,CDN 会从地理位置最近的 CDN 服务器上发送缓存的文件副本。如果我们在生产环境中让 Rails 直接处理静态资源文件,那么在应用前端使用 CDN 将是最好的选择。 -### 实时编译 +使用 CDN 的常见模式是把生产环境中的应用设置为“源”服务器,也就是说,当浏览器从 CDN 请求静态资源文件但缓存未命中时,CDN 将立即从“源”服务器中抓取该文件,并对其进行缓存。例如,假设我们在 `example.com` 上运行 Rails 应用,并在 `mycdnsubdomain.fictional-cdn.com` 上配置了 CDN,在处理对 `mycdnsubdomain.fictional-cdn.com/assets/smile.png` 的首次请求时,CDN 会抓取 `example.com/assets/smile.png` 并进行缓存。之后再请求 `mycdnsubdomain.fictional-cdn.com/assets/smile.png` 时,CDN 会直接提供缓存中的文件副本。对于任何请求,只要 CDN 能够直接处理,就不会访问 Rails 服务器。由于 CDN 提供的静态资源文件由地理位置最近的 CDN 服务器提供,因此对请求的响应更快,同时 Rails 服务器不再需要花费大量时间处理静态资源文件,因此可以专注于更快地处理应用代码。 -某些情况下可能需要实时编译,此时静态资源直接由 Sprockets 处理。 + -要想使用实时编译,要做如下设置: +#### 设置用于处理静态资源文件的 CDN + +要设置 CDN,首先必须在公开的互联网 URL 地址上(例如 `example.com`)以生产环境运行 Rails 应用。下一步,注册云服务提供商的 CDN 服务。然后配置 CDN 的“源”服务器,把它指向我们的网站 `example.com`,具体配置方法请参考云服务提供商的文档。 + +CDN 提供商会为我们的应用提供一个自定义子域名,例如 `mycdnsubdomain.fictional-cdn.com`(注意 `fictional-cdn.com` 只是撰写本文时杜撰的一个 CDN 提供商)。完成 CDN 服务器配置后,还需要告诉浏览器从 CDN 抓取静态资源文件,而不是直接从 Rails 服务器抓取。为此,需要在 Rails 配置中,用静态资源文件的主机代替相对路径。通过 `config/environments/production.rb` 配置文件的 `config.action_controller.asset_host` 选项,我们可以设置静态资源文件的主机: ```ruby -config.assets.compile = true +config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com' +``` + +NOTE: 这里只需提供“主机”,即前文提到的子域名,而不需要指定 HTTP 协议,例如 `http://` 或 `https://`。默认情况下,Rails 会使用网页请求的 HTTP 协议作为指向静态资源文件链接的协议。 + +还可以通过[环境变量](http://en.wikipedia.org/wiki/Environment_variable)设置静态资源文件的主机,这样可以方便地在不同的运行环境中使用不同的静态资源文件: + +```ruby +config.action_controller.asset_host = ENV['CDN_HOST'] +``` + +NOTE: 这里还需要把服务器上的 `CDN_HOST` 环境变量设置为 `mycdnsubdomain.fictional-cdn.com`。 + +服务器和 CDN 配置好后,就可以像下面这样引用静态资源文件: + +```erb +<%= asset_path('smile.png') %> +``` + +这时返回的不再是相对路径 `/assets/smile.png`(出于可读性考虑省略了文件名中的指纹),而是指向 CDN 的完整路径: + +``` +http://mycdnsubdomain.fictional-cdn.com/assets/smile.png +``` + +如果 CDN 上有 `smile.png` 文件的副本,就会直接返回给浏览器,而 Rails 服务器甚至不知道有浏览器请求了 `smile.png` 文件。如果 CDN 上没有 `smile.png` 文件的副本,就会先从“源”服务器上抓取 `example.com/assets/smile.png` 文件,再返回给浏览器,同时保存文件的副本以备将来使用。 + +如果只想让 CDN 处理部分静态资源文件,可以在调用静态资源文件辅助方法时使用 `:host` 选项,以覆盖 `config.action_controller.asset_host` 选项中设置的值: + +```erb +<%= asset_path 'image.png', host: 'mycdnsubdomain.fictional-cdn.com' %> +``` + + + +#### 自定义 CDN 缓存行为 + +CDN 的作用是为内容提供缓存。如果 CDN 上有过期或不良内容,那么不仅不能对应用有所助益,反而会造成负面影响。本小节将介绍大多数 CDN 的一般缓存行为,而我们使用的 CDN 在特性上可能会略有不同。 + + + +##### CDN 请求缓存 + +我们常说 CDN 对于缓存静态资源文件非常有用,但实际上 CDN 缓存的是整个请求。其中既包括了静态资源文件的请求体,也包括了其首部。其中,`Cache-Control` 首部是最重要的,用于告知 CDN(和 Web 浏览器)如何缓存文件内容。假设用户请求了 `/assets/i-dont-exist.png` 这个并不存在的静态资源文件,并且 Rails 应用返回的是 404,那么只要设置了合法的 `Cache-Control` 首部,CDN 就会缓存 404 页面。 + + + +##### 调试 CDN 首部 + +检查 CDN 是否正确缓存了首部的方法之一是使用 [curl](http://explainshell.com/explain?cmd=curl+-I+http%3A%2F%2Fwww.example.com)。我们可以分别从 Rails 服务器和 CDN 获取首部,然后确认二者是否相同: + +```sh +$ curl -I http://www.example/assets/application- +d0e099e021c95eb0de3615fd1d8c4d83.css +HTTP/1.1 200 OK +Server: Cowboy +Date: Sun, 24 Aug 2014 20:27:50 GMT +Connection: keep-alive +Last-Modified: Thu, 08 May 2014 01:24:14 GMT +Content-Type: text/css +Cache-Control: public, max-age=2592000 +Content-Length: 126560 +Via: 1.1 vegur +``` + +CDN 中副本的首部: + +```sh +$ curl -I http://mycdnsubdomain.fictional-cdn.com/application- +d0e099e021c95eb0de3615fd1d8c4d83.css +HTTP/1.1 200 OK Server: Cowboy Last- +Modified: Thu, 08 May 2014 01:24:14 GMT Content-Type: text/css +Cache-Control: +public, max-age=2592000 +Via: 1.1 vegur +Content-Length: 126560 +Accept-Ranges: +bytes +Date: Sun, 24 Aug 2014 20:28:45 GMT +Via: 1.1 varnish +Age: 885814 +Connection: keep-alive +X-Served-By: cache-dfw1828-DFW +X-Cache: HIT +X-Cache-Hits: +68 +X-Timer: S1408912125.211638212,VS0,VE0 ``` -初次请求时,Asset Pipeline 会编译静态资源,并缓存,这一过程前文已经提过了。引用文件时,会使用加上 MD5 哈希的文件名代替清单文件中的名字。 +在 CDN 文档中可以查询 CDN 提供的额外首部,例如 `X-Cache`。 -Sprockets 还会把 `Cache-Control` 报头设为 `max-age=31536000`。这个报头的意思是,服务器和客户端浏览器之间的缓存可以存储一年,以减少从服务器上获取静态资源的请求数量。静态资源的内容可能存在本地浏览器的缓存或者其他中间缓存中。 + -实时编译消耗的内存更多,比默认的编译方式性能更低,因此不推荐使用。 +##### CDN 和 `Cache-Control` 首部 -如果要把程序部署到没有安装 JavaScript 运行时的服务器,可以在 `Gemfile` 中加入: +[Cache-Control 首部](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9)是一个 W3C 规范,用于描述如何缓存请求。当未使用 CDN 时,浏览器会根据 `Cache-Control` 首部来缓存文件内容。在静态资源文件未修改的情况下,浏览器就不必重新下载 CSS 或 JavaScript 等文件了。通常,Rails 服务器需要告诉 CDN(和浏览器)这些静态资源文件是“公共的”,这样任何缓存都可以保存这些文件的副本。此外,通常还会通过 `max-age` 字段来设置缓存失效前储存对象的时间。`max-age` 字段的单位是秒,最大设置为 31536000,即一年。在 Rails 应用中设置 `Cache-Control` 首部的方法如下: ```ruby -group :production do - gem 'therubyracer' -end +config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=31536000' +} ``` -### CDN +现在,在生产环境中,Rails 应用的静态资源文件在 CDN 上会被缓存长达 1 年之久。由于大多数 CDN 会缓存首部,静态资源文件的 `Cache-Control` 首部会被传递给请求该静态资源文件的所有浏览器,这样浏览器就会长期缓存该静态资源文件,直到缓存过期后才会重新请求该文件。 + + + +##### CDN 和基于 URL 地址的缓存失效 + +大多数 CDN 会根据完整的 URL 地址来缓存静态资源文件的内容。因此,缓存 + +``` +http://mycdnsubdomain.fictional-cdn.com/assets/smile-123.png +``` + +和缓存 -如果用 CDN 分发静态资源,要确保文件不会被缓存,因为缓存会导致问题。如果设置了 `config.action_controller.perform_caching = true`,`Rack::Cache` 会使用 `Rails.cache` 存储静态文件,很快缓存空间就会用完。 +``` +http://mycdnsubdomain.fictional-cdn.com/assets/smile.png +``` -每种缓存的工作方式都不一样,所以要了解你所用 CDN 是如何处理缓存的,确保能和 Asset Pipeline 和谐相处。有时你会发现某些设置能导致诡异的表现,而有时又不会。例如,作为 HTTP 缓存使用时,Nginx 的默认设置就不会出现什么问题。 +被认为是两个完全不同的静态资源文件的缓存。 -定制 Asset Pipeline -------------------- +如果我们把 `Cache-Control` HTTP 首部的 `max-age` 值设得很大,那么当静态资源文件的内容发生变化时,应同时使原有缓存失效。例如,当我们把黄色笑脸图像更换为蓝色笑脸图像时,我们希望网站的所有访客看到的都是新的蓝色笑脸图像。如果我们使用了 CDN,并使用了 Rails Asset Pipeline `config.assets.digest` 选项的默认值 `true`,一旦静态资源文件的内容发生变化,其文件名就会发生变化。这样,我们就不需要每次手动使某个静态资源文件的缓存失效。通过使用唯一的新文件名,我们就能确保用户访问的总是静态资源文件的最新版本。 + + + +## 自定义 Asset Pipeline + + ### 压缩 CSS -压缩 CSS 的方式之一是使用 YUI。[YUI CSS compressor](http://yui.github.io/yuicompressor/css.html) 提供了压缩功能。 +压缩 CSS 的可选方式之一是使用 YUI。通过 [YUI CSS 压缩器](http://yui.github.io/yuicompressor/css.html)可以缩小 CSS 文件的大小。 -下面这行设置会启用 YUI 压缩,在此之前要先安装 `yui-compressor` gem: +在 Gemfile 中添加 `yui-compressor` gem 后,通过下面的设置可以启用 YUI 压缩: ```ruby config.assets.css_compressor = :yui ``` -如果安装了 `sass-rails` gem,还可以使用其他的方式压缩 CSS: +如果我们在 Gemfile 中添加了 `sass-rails` gem,那么也可以使用 Sass 压缩: ```ruby config.assets.css_compressor = :sass ``` + + ### 压缩 JavaScript -压缩 JavaScript 的方式有:`:closure`,`:uglifier` 和 `:yui`。这三种方式分别需要安装 `closure-compiler`、`uglifier` 和 `yui-compressor`。 +压缩 JavaScript 的可选方式有 `:closure`、`:uglifier` 和 `:yui`,分别要求在 Gemfile 中添加 `closure-compiler`、`uglifier` 和 `yui-compressor` gem。 -默认的 `Gemfile` 中使用的是 [uglifier](https://github.com/lautis/uglifier)。这个 gem 使用 Ruby 包装了 [UglifyJS](https://github.com/mishoo/UglifyJS)(为 NodeJS 开发)。uglifier 可以删除空白和注释,缩短本地变量名,还会做些微小的优化,例如把 `if...else` 语句改写成三元操作符形式。 +默认情况下,Gemfile 中包含了 [uglifier](https://github.com/lautis/uglifier) gem,这个 gem 使用 Ruby 包装 [UglifyJS](https://github.com/mishoo/UglifyJS)(使用 NodeJS 开发),作用是通过删除空白和注释、缩短局部变量名及其他微小优化(例如在可能的情况下把 `if…​else` 语句修改为三元运算符)压缩 JavaScript 代码。 -下面这行设置使用 `uglifier` 压缩 JavaScript: +使用 `uglifier` 压缩 JavaScript 需进行如下设置: ```ruby config.assets.js_compressor = :uglifier ``` -NOTE: 系统中要安装支持 [ExecJS](https://github.com/sstephenson/execjs#readme) 的运行时才能使用 `uglifier`。Mac OS X 和 Windows 系统中已经安装了 JavaScript 运行时。 -I> -NOTE: `config.assets.compress` 初始化选项在 Rails 4 中不可用,即便设置了也没有效果。请分别使用 `config.assets.css_compressor` 和 `config.assets.js_compressor` 这两个选项设置 CSS 和 JavaScript 的压缩方式。 +NOTE: 要使用 `uglifier` 压缩 JavaScript,就必须安装支持 [ExecJS](https://github.com/rails/execjs#readme) 的运行时。macOS 和 Windows 已经预装了此类运行时。 -### 使用自己的压缩程序 + -设置压缩 CSS 和 JavaScript 所用压缩程序的选项还可接受对象,这个对象必须能响应 `compress` 方法。`compress` 方法只接受一个字符串参数,返回值也必须是字符串。 +### 用 GZip 压缩静态资源文件 + +默认情况下,Sprockets 会用 GZip 压缩编译后的静态资源文件,同时也会保留未压缩的版本。通过 GZip 压缩可以减少对带宽的占用。设置 GZip 压缩的方式如下: + +```ruby +config.assets.gzip = false # 禁止用 GZip 压缩静态资源文件 +``` + + + +### 自定义压缩工具 + +在设置 CSS 和 JavaScript 压缩工具时还可以使用对象。这个对象要能响应 `compress` 方法,这个方法接受一个字符串作为唯一参数,并返回一个字符串。 ```ruby class Transformer @@ -692,134 +848,139 @@ class Transformer end ``` -要想使用这个压缩程序,请在 `application.rb` 中做如下设置: +要使用这个压缩工具,需在 `application.rb` 配置文件中做如下设置: ```ruby config.assets.css_compressor = Transformer.new ``` -### 修改 `assets` 的路径 + -Sprockets 默认使用的公开路径是 `/assets`。 +### 修改静态资源文件的路径 -这个路径可以修改成其他值: +默认情况下,Sprockets 使用 `/assets` 作为静态资源文件的公开路径。 + +我们可以修改这个路径: ```ruby config.assets.prefix = "/some_other_path" ``` -升级没使用 Asset Pipeline 的旧项目时,或者默认路径已有其他用途,或者希望指定一个新资源路径时,可以设置这个选项。 +通过这种方式,在升级未使用 Asset Pipeline 但使用了 `/assets` 路径的老项目时,我们就可以轻松为新的静态资源文件设置另一个公开路径。 + + -### X-Sendfile 报头 +### `X-Sendfile` 首部 -X-Sendfile 报头的作用是让服务器忽略程序的响应,直接从硬盘上伺服指定的文件。默认情况下服务器不会发送这个报头,但在支持该报头的服务器上可以启用。启用后,会跳过响应直接由服务器伺服文件,速度更快。X-Sendfile 报头的用法参见 [API 文档](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file)。 +`X-Sendfile` 首部的作用是让 Web 服务器忽略应用对请求的响应,直接返回磁盘中的指定文件。默认情况下 Rails 不会发送这个首部,但在支持这个首部的服务器上可以启用这一特性,以提供更快的响应速度。关于这一特性的更多介绍,请参阅 [`send_file` 方法的文档](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file)。 -Apache 和 Nginx 都支持这个报头,可以在 `config/environments/production.rb` 中启用: +Apache 和 NGINX 支持 `X-Sendfile` 首部,启用方法是在 `config/environments/production.rb` 配置文件中进行设置: ```ruby -# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache -# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx +# config.action_dispatch.x_sendfile_header = "X-Sendfile" # 用于 Apache +# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # 用于 NGINX ``` -WARNING: 如果升级现有程序,请把这两个设置写入 `production.rb`,以及其他类似生产环境的设置文件中。不能写入 `application.rb`。 +WARNING: 要想在升级现有应用时使用上述选项,可以把这两行代码粘贴到 `production.rb` 配置文件中,或其他类似的生产环境配置文件中。 -TIP: 详情参见生产环境所用服务器的文档: -T> -TIP: - [Apache](https://tn123.org/mod_xsendfile/) -TIP: - [Nginx](http://wiki.nginx.org/XSendfile) +TIP: 更多介绍请参阅生产服务器的相关文档:[Apache](https://tn123.org/mod_xsendfile/)、[NGINX](http://wiki.nginx.org/XSendfile)。 -静态资源缓存的存储方式 -------------------- + -在开发环境和生产环境中,Sprockets 使用 Rails 默认的存储方式缓存静态资源。可以使用 `config.assets.cache_store` 设置使用其他存储方式: +## 静态资源文件缓存的存储方式 + +在开发环境和生产环境中,Sprockets 默认在 `tmp/cache/assets` 文件夹中缓存静态资源文件。修改这一设置的方式如下: ```ruby -config.assets.cache_store = :memory_store +config.assets.configure do |env| + env.cache = ActiveSupport::Cache.lookup_store(:memory_store, + { size: 32.megabytes }) +end ``` -静态资源缓存可用的存储方式和程序的缓存存储一样。 +禁用静态资源文件缓存的方式如下: ```ruby -config.assets.cache_store = :memory_store, { size: 32.megabytes } +config.assets.configure do |env| + env.cache = ActiveSupport::Cache.lookup_store(:null_store) +end ``` -在 gem 中使用静态资源 -------------------- + -静态资源也可由 gem 提供。 +## 通过 gem 添加静态资源文件 -为 Rails 提供标准 JavaScript 代码库的 `jquery-rails` gem 是个很好的例子。这个 gem 中有个引擎类,继承自 `Rails::Engine`。添加这层继承关系后,Rails 就知道这个 gem 中可能包含静态资源文件,会把这个引擎中的 `app/assets`、`lib/assets` 和 `vendor/assets` 三个文件夹加入 Sprockets 的搜索路径中。 +我们还可以通过 gem 添加静态资源文件。 -把代码库或者 gem 变成预处理器 --------------------------- +为 Rails 提供标准 JavaScript 库的 `jquery-rails` gem 就是很好的例子。这个 gem 中包含了继承自 `Rails::Engine` 类的引擎类,这样 Rails 就知道这个 gem 中可能包含静态资源文件,于是会把其中的 `app/assets`、`lib/assets` 和 `vendor/assets` 文件夹添加到 Sprockets 的搜索路径中。 -Sprockets 使用 [Tilt](https://github.com/rtomayko/tilt) 作为不同模板引擎的通用接口。在你自己的 gem 中也可实现 Tilt 的模板协议。一般情况下,需要继承 `Tilt::Template` 类,然后重新定义 `prepare` 方法(初始化模板),以及 `evaluate` 方法(返回处理后的内容)。原始数据存储在 `data` 中。详情参见 [`Tilt::Template`](https://github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb) 类的源码。 + -```ruby -module BangBang - class Template < ::Tilt::Template - def prepare - # Do any initialization here - end +## 使用代码库或 gem 作为预处理器 - # Adds a "!" to original template. - def evaluate(scope, locals, &block) - "#{data}!" - end +Sprockets 使用 Processors、Transformers、Compressors 和 Exporters 扩展功能。详情参阅“[Extending Sprockets](https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md)”一文。下述示例注册一个预处理器,在 text/css 文件(.css)默认添加一个注释。 + +```ruby +module AddComment + def self.call(input) + { data: input[:data] + "/* Hello From my sprockets extension */" } end end ``` -上述代码定义了 `Template` 类,然后还需要关联模板文件的扩展名: +有了修改输入数据的模块后,还要把它注册为指定 MIME 类型的预处理器: ```ruby -Sprockets.register_engine '.bang', BangBang::Template +Sprockets.register_preprocessor 'text/css', AddComment ``` -升级旧版本 Rails ---------------- + -从 Rails 3.0 或 Rails 2.x 升级,有一些问题要解决。首先,要把 `public/` 文件夹中的文件移到新位置。不同类型文件的存放位置参见“[静态资源的组织方式](#asset-organization)”一节。 +## 从旧版本的 Rails 升级 -其次,避免 JavaScript 文件重复出现。因为从 Rails 3.1 开始,jQuery 是默认的 JavaScript 库,因此不用把 `jquery.js` 复制到 `app/assets` 文件夹中。Rails 会自动加载 jQuery。 +从 Rails 3.0 或 Rails 2.x 升级时有一些问题需要解决。首先,要把 `public/` 文件夹中的文件移动到新位置。关于不同类型文件储存位置的介绍,请参阅 [静态资源文件的组织方式](#asset-organization)。 -然后,更新各环境的设置文件,添加默认设置。 +其次,要避免出现重复的 JavaScript 文件。从 Rails 3.1 开始,jQuery 成为默认的 JavaScript 库,Rails 会自动加载 `jquery.js`,不再需要手动把 `jquery.js` 复制到 `app/assets` 文件夹中。 -在 `application.rb` 中加入: +再次,要使用正确的默认选项更新各种环境配置文件。 + +在 `application.rb` 配置文件中: ```ruby -# Version of your assets, change this if you want to expire all your assets +# 静态资源文件的版本,通过修改这个选项可以使原有的静态资源文件缓存全部过期 config.assets.version = '1.0' -# Change the path that assets are served from config.assets.prefix = "/assets" +# 通过 onfig.assets.prefix = "/assets" 修改静态资源文件的路径 ``` -在 `development.rb` 中加入: +在 `development.rb` 配置文件中: ```ruby -# Expands the lines which load the assets +# 展开用于加载静态资源文件的代码 config.assets.debug = true ``` -在 `production.rb` 中加入: +在 `production.rb` 配置文件中: ```ruby -# Choose the compressors to use (if any) config.assets.js_compressor = -# :uglifier config.assets.css_compressor = :yui +# 选择(可用的)压缩工具 +config.assets.js_compressor = :uglifier +# config.assets.css_compressor = :yui -# Don't fallback to assets pipeline if a precompiled asset is missed +# 在找不到已编译的静态资源文件的情况下,不退回到 Asset Pipeline config.assets.compile = false -# Generate digests for assets URLs. This is planned for deprecation. +# 为静态资源文件的 URL 地址生成指纹 config.assets.digest = true -# Precompile additional assets (application.js, application.css, and all -# non-JS/CSS are already added) config.assets.precompile += %w( search.js ) +# 预编译附加的静态资源文件(application.js、application.css 和所有 +# 已添加的非 JS/CSS 文件) +# config.assets.precompile += %w( admin.js admin.css ) ``` -Rails 4 不会在 `test.rb` 中添加 Sprockets 的默认设置,所以要手动添加。测试环境中以前的默认设置是:`config.assets.compile = true`,`config.assets.compress = false`,`config.assets.debug = false` 和 `config.assets.digest = false`。 +Rails 4 及更高版本不会再在 `test.rb` 配置文件中添加 Sprockets 的默认设置,因此需要手动完成。需要添加的默认设置包括 `config.assets.compile = true`、`config.assets.compress = false`、`config.assets.debug = false` 和 `config.assets.digest = false`。 -最后,还要在 `Gemfile` 中加入以下 gem: +最后,还要在 Gemfile 中加入下列 gem: ```ruby gem 'sass-rails', "~> 3.2.3" diff --git a/source/zh-CN/association_basics.md b/source/zh-CN/association_basics.md index 057f4be..c7316a9 100644 --- a/source/zh-CN/association_basics.md +++ b/source/zh-CN/association_basics.md @@ -1,134 +1,142 @@ -Active Record 关联 -================== +# Active Record 关联 -本文介绍 Active Record 中的关联功能。 +本文介绍 Active Record 的关联功能。 -读完本文,你将学到: +读完本文后,您将学到: -* 如何声明 Active Record 模型间的关联; -* 怎么理解不同的 Active Record 关联类型; -* 如何使用关联添加的方法; +* 如何声明 Active Record 模型间的关联; +* 怎么理解不同的 Active Record 关联类型; +* 如何使用关联为模型添加的方法。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -为什么要使用关联 --------------- + -模型之间为什么要有关联?因为关联让常规操作更简单。例如,在一个简单的 Rails 程序中,有一个顾客模型和一个订单模型。每个顾客可以下多个订单。没用关联的模型定义如下: +## 为什么使用关联 + +在 Rails 中,关联在两个 Active Record 模型之间建立联系。模型之间为什么要有关联?因为关联能让常规操作变得更简单。例如,在一个简单的 Rails 应用中,有一个作者模型和一个图书模型。每位作者可以著有多本图书。不用关联的话,模型可以像下面这样定义: ```ruby -class Customer < ActiveRecord::Base +class Author < ApplicationRecord end -class Order < ActiveRecord::Base +class Book < ApplicationRecord end ``` -假如我们要为一个顾客添加一个订单,得这么做: +现在,假如我们想为一位现有作者添加一本书,得这么做: ```ruby -@order = Order.create(order_date: Time.now, customer_id: @customer.id) +@book = Book.create(published_at: Time.now, author_id: @author.id) ``` -或者说要删除一个顾客,确保他的所有订单都会被删除,得这么做: +假如要删除一位作者的话,也要把属于他的书都删除: ```ruby -@orders = Order.where(customer_id: @customer.id) -@orders.each do |order| - order.destroy +@books = Book.where(author_id: @author.id) +@books.each do |book| + book.destroy end -@customer.destroy +@author.destroy ``` -使用 Active Record 关联,告诉 Rails 这两个模型是有一定联系的,就可以把这些操作连在一起。下面使用关联重新定义顾客和订单模型: +使用 Active Record 关联,Rails 知道两个模型之间有联系,上述操作(以及其他操作)可以得到简化。下面使用关联重新定义作者和图书模型: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, dependent: :destroy +class Author < ApplicationRecord + has_many :books, dependent: :destroy end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -这么修改之后,为某个顾客添加新订单就变得简单了: +这么修改之后,为某位作者添加新书就简单了: ```ruby -@order = @customer.orders.create(order_date: Time.now) +@book = @author.books.create(published_at: Time.now) ``` -删除顾客及其所有订单更容易: +删除作者及其所有图书也更容易: ```ruby -@customer.destroy +@author.destroy ``` -学习更多关联类型,请阅读下一节。下一节介绍了一些使用关联时的小技巧,然后列出了关联添加的所有方法和选项。 +请阅读下一节,进一步学习不同的关联类型。后面还会介绍一些使用关联时的小技巧,然后列出关联添加的所有方法和选项。 + + + +## 关联的类型 -关联的类型 ---------- +Rails 支持六种关联: -在 Rails 中,关联是两个 Active Record 模型之间的关系。关联使用宏的方式实现,用声明的形式为模型添加功能。例如,声明一个模型属于(`belongs_to`)另一个模型后,Rails 会维护两个模型之间的“主键-外键”关系,而且还向模型中添加了很多实用的方法。Rails 支持六种关联: +* `belongs_to` +* `has_one` +* `has_many` +* `has_many :through` +* `has_one :through` +* `has_and_belongs_to_many` -* `belongs_to` -* `has_one` -* `has_many` -* `has_many :through` -* `has_one :through` -* `has_and_belongs_to_many` +关联使用宏式调用实现,用声明的形式为模型添加功能。例如,声明一个模型属于(`belongs_to`)另一个模型后,Rails 会维护两个模型之间的“[主键](https://en.wikipedia.org/wiki/Unique_key)-[外键](https://en.wikipedia.org/wiki/Foreign_key)”关系,而且还会向模型中添加很多实用的方法。 -在后面的几节中,你会学到如何声明并使用这些关联。首先来看一下各种关联适用的场景。 +在下面几小节中,你会学到如何声明并使用这些关联。首先来看一下各种关联适用的场景。 + + ### `belongs_to` 关联 -`belongs_to` 关联创建两个模型之间一对一的关系,声明所在的模型实例属于另一个模型的实例。例如,如果程序中有顾客和订单两个模型,每个订单只能指定给一个顾客,就要这么声明订单模型: +`belongs_to` 关联创建两个模型之间一对一的关系,声明所在的模型实例属于另一个模型的实例。例如,如果应用中有作者和图书两个模型,而且每本书只能指定给一位作者,就要这么声明图书模型: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -![belongs_to 关联](images/belongs_to.png) +![belongs to](images/belongs_to.png) + +NOTE: 在 `belongs_to` 关联声明中必须使用单数形式。如果在上面的代码中使用复数形式定义 `author` 关联,应用会报错,提示“uninitialized constant Book::Authors”。这是因为 Rails 自动使用关联名推导类名。如果关联名错误地使用复数,推导出的类名也就变成了复数。 -NOTE: 在 `belongs_to` 关联声明中必须使用单数形式。如果在上面的代码中使用复数形式,程序会报错,提示未初始化常量 `Order::Customers`。因为 Rails 自动使用关联中的名字引用类名。如果关联中的名字错误的使用复数,引用的类也就变成了复数。 相应的迁移如下: ```ruby -class CreateOrders < ActiveRecord::Migration +class CreateBooks < ActiveRecord::Migration[5.0] def change - create_table :customers do |t| + create_table :authors do |t| t.string :name t.timestamps end - create_table :orders do |t| - t.belongs_to :customer - t.datetime :order_date + create_table :books do |t| + t.belongs_to :author, index: true + t.datetime :published_at t.timestamps end end end ``` + + ### `has_one` 关联 -`has_one` 关联也会建立两个模型之间的一对一关系,但语义和结果有点不一样。这种关联表示模型的实例包含或拥有另一个模型的实例。例如,在程序中,每个供应商只有一个账户,可以这么定义供应商模型: +`has_one` 关联也建立两个模型之间的一对一关系,但语义和结果有点不一样。这种关联表示模型的实例包含或拥有另一个模型的实例。例如,应用中每个供应商只有一个账户,可以这么定义供应商模型: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end ``` -![has_one 关联](images/has_one.png) +![has one](images/has_one.png) 相应的迁移如下: ```ruby -class CreateSuppliers < ActiveRecord::Migration +class CreateSuppliers < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| t.string :name @@ -136,7 +144,7 @@ class CreateSuppliers < ActiveRecord::Migration end create_table :accounts do |t| - t.belongs_to :supplier + t.belongs_to :supplier, index: true t.string :account_number t.timestamps end @@ -144,66 +152,80 @@ class CreateSuppliers < ActiveRecord::Migration end ``` +根据使用需要,可能还要为 accounts 表中的 supplier 列创建唯一性索引和(或)外键约束。这里,我们像下面这样定义这一列: + +```ruby +create_table :accounts do |t| + t.belongs_to :supplier, index: { unique: true }, foreign_key: true + # ... +end +``` + + + ### `has_many` 关联 -`has_many` 关联建立两个模型之间的一对多关系。在 `belongs_to` 关联的另一端经常会使用这个关联。`has_many` 关联表示模型的实例有零个或多个另一个模型的实例。例如,在程序中有顾客和订单两个模型,顾客模型可以这么定义: +`has_many` 关联建立两个模型之间的一对多关系。在 `belongs_to` 关联的另一端经常会使用这个关联。`has_many` 关联表示模型的实例有零个或多个另一模型的实例。例如,对应用中的作者和图书模型来说,作者模型可以这样声明: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` NOTE: 声明 `has_many` 关联时,另一个模型使用复数形式。 -![has_many 关联](images/has_many.png) + +![has many](images/has_many.png) 相应的迁移如下: ```ruby -class CreateCustomers < ActiveRecord::Migration +class CreateAuthors < ActiveRecord::Migration[5.0] def change - create_table :customers do |t| + create_table :authors do |t| t.string :name t.timestamps end - create_table :orders do |t| - t.belongs_to :customer - t.datetime :order_date + create_table :books do |t| + t.belongs_to :author, index: true + t.datetime :published_at t.timestamps end end end ``` + + ### `has_many :through` 关联 -`has_many :through` 关联经常用来建立两个模型之间的多对多关联。这种关联表示一个模型的实例可以借由第三个模型,拥有零个和多个另一个模型的实例。例如,在医疗锻炼中,病人要和医生约定练习时间。这中间的关联声明如下: +`has_many :through` 关联经常用于建立两个模型之间的多对多关联。这种关联表示一个模型的实例可以借由第三个模型,拥有零个和多个另一模型的实例。例如,在医疗锻炼中,病人要和医生约定练习时间。这中间的关联声明如下: ```ruby -class Physician < ActiveRecord::Base +class Physician < ApplicationRecord has_many :appointments has_many :patients, through: :appointments end -class Appointment < ActiveRecord::Base +class Appointment < ApplicationRecord belongs_to :physician belongs_to :patient end -class Patient < ActiveRecord::Base +class Patient < ApplicationRecord has_many :appointments has_many :physicians, through: :appointments end ``` -![has_many :through 关联](images/has_many_through.png) +![has many through](images/has_many_through.png) 相应的迁移如下: ```ruby -class CreateAppointments < ActiveRecord::Migration +class CreateAppointments < ActiveRecord::Migration[5.0] def change create_table :physicians do |t| t.string :name @@ -216,8 +238,8 @@ class CreateAppointments < ActiveRecord::Migration end create_table :appointments do |t| - t.belongs_to :physician - t.belongs_to :patient + t.belongs_to :physician, index: true + t.belongs_to :patient, index: true t.datetime :appointment_date t.timestamps end @@ -225,31 +247,31 @@ class CreateAppointments < ActiveRecord::Migration end ``` -连接模型中的集合可以使用 API 关联。例如: +联结模型可以使用 [`has_many` 关联方法](#has-many-association-reference)管理。例如: ```ruby physician.patients = patients ``` -会为新建立的关联对象创建连接模型实例,如果其中一个对象删除了,相应的记录也会删除。 +会为新建立的关联对象创建联结模型实例。如果其中一个对象删除了,相应的联结记录也会删除。 +WARNING: 自动删除联结模型的操作直接执行,不会触发 `*_destroy` 回调。 -WARNING: 自动删除连接模型的操作直接执行,不会触发 `*_destroy` 回调。 -`has_many :through` 还可用来简化嵌套的 `has_many` 关联。例如,一个文档分为多个部分,每一部分又有多个段落,如果想使用简单的方式获取文档中的所有段落,可以这么做: +`has_many :through` 还能简化嵌套的 `has_many` 关联。例如,一个文档分为多个部分,每一部分又有多个段落,如果想使用简单的方式获取文档中的所有段落,可以这么做: ```ruby -class Document < ActiveRecord::Base +class Document < ApplicationRecord has_many :sections has_many :paragraphs, through: :sections end -class Section < ActiveRecord::Base +class Section < ApplicationRecord belongs_to :document has_many :paragraphs end -class Paragraph < ActiveRecord::Base +class Paragraph < ApplicationRecord belongs_to :section end ``` @@ -260,32 +282,32 @@ end @document.paragraphs ``` + + ### `has_one :through` 关联 -`has_one :through` 关联建立两个模型之间的一对一关系。这种关联表示一个模型通过第三个模型拥有另一个模型的实例。例如,每个供应商只有一个账户,而且每个账户都有一个历史账户,那么可以这么定义模型: +`has_one :through` 关联建立两个模型之间的一对一关系。这种关联表示一个模型通过第三个模型拥有另一模型的实例。例如,每个供应商只有一个账户,而且每个账户都有一个账户历史,那么可以这么定义模型: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account has_one :account_history, through: :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier has_one :account_history end -class AccountHistory < ActiveRecord::Base +class AccountHistory < ApplicationRecord belongs_to :account end ``` -![has_one :through 关联](images/has_one_through.png) - 相应的迁移如下: ```ruby -class CreateAccountHistories < ActiveRecord::Migration +class CreateAccountHistories < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| t.string :name @@ -293,13 +315,13 @@ class CreateAccountHistories < ActiveRecord::Migration end create_table :accounts do |t| - t.belongs_to :supplier + t.belongs_to :supplier, index: true t.string :account_number t.timestamps end create_table :account_histories do |t| - t.belongs_to :account + t.belongs_to :account, index: true t.integer :credit_rating t.timestamps end @@ -307,26 +329,30 @@ class CreateAccountHistories < ActiveRecord::Migration end ``` +![has one through](images/has_one_through.png) + + + ### `has_and_belongs_to_many` 关联 -`has_and_belongs_to_many` 关联之间建立两个模型之间的多对多关系,不借由第三个模型。例如,程序中有装配体和零件两个模型,每个装配体中有多个零件,每个零件又可用于多个装配体,这时可以按照下面的方式定义模型: +`has_and_belongs_to_many` 关联直接建立两个模型之间的多对多关系,不借由第三个模型。例如,应用中有装配体和零件两个模型,每个装配体有多个零件,每个零件又可用于多个装配体,这时可以按照下面的方式定义模型: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` -![has_and_belongs_to_many 关联](images/habtm.png) +![habtm](images/habtm.png) 相应的迁移如下: ```ruby -class CreateAssembliesAndParts < ActiveRecord::Migration +class CreateAssembliesAndParts < ActiveRecord::Migration[5.0] def change create_table :assemblies do |t| t.string :name @@ -339,25 +365,27 @@ class CreateAssembliesAndParts < ActiveRecord::Migration end create_table :assemblies_parts, id: false do |t| - t.belongs_to :assembly - t.belongs_to :part + t.belongs_to :assembly, index: true + t.belongs_to :part, index: true end end end ``` -### 使用 `belongs_to` 还是 `has_one` + -如果想建立两个模型之间的一对一关系,可以在一个模型中声明 `belongs_to`,然后在另一模型中声明 `has_one`。但是怎么知道在哪个模型中声明哪种关联? +### 在 `belongs_to` 和 `has_one` 之间选择 -不同的声明方式带来的区别是外键放在哪个模型对应的数据表中(外键在声明 `belongs_to` 关联所在模型对应的数据表中)。不过声明时要考虑一下语义,`has_one` 的意思是某样东西属于我。例如,说供应商有一个账户,比账户拥有供应商更合理,所以正确的关联应该这么声明: +如果想建立两个模型之间的一对一关系,要在一个模型中添加 `belongs_to`,在另一模型中添加 `has_one`。但是怎么知道在哪个模型中添加哪个呢? + +二者之间的区别是在哪里放置外键(外键在 `belongs_to` 关联所在模型对应的表中),不过也要考虑数据的语义。`has_one` 的意思是某样东西属于我,即哪个东西指向你。例如,说供应商有一个账户,比账户拥有供应商更合理,所以正确的关联应该这么声明: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier end ``` @@ -365,10 +393,10 @@ end 相应的迁移如下: ```ruby -class CreateSuppliers < ActiveRecord::Migration +class CreateSuppliers < ActiveRecord::Migration[5.0] def change create_table :suppliers do |t| - t.string :name + t.string :name t.timestamps end @@ -377,73 +405,82 @@ class CreateSuppliers < ActiveRecord::Migration t.string :account_number t.timestamps end + + add_index :accounts, :supplier_id end end ``` -NOTE: `t.integer :supplier_id` 更明确的表明了外键的名字。在目前的 Rails 版本中,可以抽象实现的细节,使用 `t.references :supplier` 代替。 +NOTE: `t.integer :supplier_id` 更明确地表明了外键的名称。在目前的 Rails 版本中,可以抽象实现的细节,使用 `t.references :supplier` 代替。 + -### 使用 `has_many :through` 还是 `has_and_belongs_to_many` + -Rails 提供了两种建立模型之间多对多关系的方法。其中比较简单的是 `has_and_belongs_to_many`,可以直接建立关联: +### 在 `has_many :through` 和 `has_and_belongs_to_many` 之间选择 + +Rails 提供了两种建立模型之间多对多关系的方式。其中比较简单的是 `has_and_belongs_to_many`,可以直接建立关联: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` -第二种方法是使用 `has_many :through`,但无法直接建立关联,要通过第三个模型: +第二种方式是使用 `has_many :through`,通过联结模型间接建立关联: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_many :manifests has_many :parts, through: :manifests end -class Manifest < ActiveRecord::Base +class Manifest < ApplicationRecord belongs_to :assembly belongs_to :part end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_many :manifests has_many :assemblies, through: :manifests end ``` -根据经验,如果关联的第三个模型要作为独立实体使用,要用 `has_many :through` 关联;如果不需要使用第三个模型,用简单的 `has_and_belongs_to_many` 关联即可(不过要记得在数据库中创建连接数据表)。 +根据经验,如果想把关联模型当做独立实体使用,要用 `has_many :through` 关联;如果不需要使用关联模型,建立 `has_and_belongs_to_many` 关联更简单(不过要记得在数据库中创建联结表)。 + +如果要对联结模型做数据验证、调用回调,或者使用其他属性,要使用 `has_many :through` 关联。 -如果需要做数据验证、回调,或者连接模型上要用到其他属性,此时就要使用 `has_many :through` 关联。 + ### 多态关联 -关联还有一种高级用法,“多态关联”。在多态关联中,在同一个关联中,模型可以属于其他多个模型。例如,图片模型可以属于雇员模型或者产品模型,模型的定义如下: +关联还有一种高级形式——多态关联(polymorphic association)。在多态关联中,在同一个关联中,一个模型可以属于多个模型。例如,图片模型可以属于雇员模型或者产品模型,模型的定义如下: ```ruby -class Picture < ActiveRecord::Base +class Picture < ApplicationRecord belongs_to :imageable, polymorphic: true end -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord has_many :pictures, as: :imageable end -class Product < ActiveRecord::Base +class Product < ApplicationRecord has_many :pictures, as: :imageable end ``` -在 `belongs_to` 中指定使用多态,可以理解成创建了一个接口,可供任何一个模型使用。在 `Employee` 模型实例上,可以使用 `@employee.pictures` 获取图片集合。类似地,可使用 `@product.pictures` 获取产品的图片。 +在 `belongs_to` 中指定使用多态,可以理解成创建了一个接口,可供任何一个模型使用。在 `Employee` 模型实例上,可以使用 `@employee.pictures` 获取图片集合。 + +类似地,可使用 `@product.pictures` 获取产品的图片。 在 `Picture` 模型的实例上,可以使用 `@picture.imageable` 获取父对象。不过事先要在声明多态接口的模型中创建外键字段和类型字段: ```ruby -class CreatePictures < ActiveRecord::Migration +class CreatePictures < ActiveRecord::Migration[5.0] def change create_table :pictures do |t| t.string :name @@ -451,6 +488,8 @@ class CreatePictures < ActiveRecord::Migration t.string :imageable_type t.timestamps end + + add_index :pictures, [:imageable_type, :imageable_id] end end ``` @@ -458,25 +497,27 @@ end 上面的迁移可以使用 `t.references` 简化: ```ruby -class CreatePictures < ActiveRecord::Migration +class CreatePictures < ActiveRecord::Migration[5.0] def change create_table :pictures do |t| t.string :name - t.references :imageable, polymorphic: true + t.references :imageable, polymorphic: true, index: true t.timestamps end end end ``` -![多态关联](images/polymorphic.png) +![polymorphic](images/polymorphic.png) -### 自连接 + -设计数据模型时会发现,有时模型要和自己建立关联。例如,在一个数据表中保存所有雇员的信息,但要建立经理和下属之间的关系。这种情况可以使用自连接关联解决: +### 自联结 + +设计数据模型时,模型有时要和自己建立关系。例如,在一个数据库表中保存所有雇员的信息,但要建立经理和下属之间的关系。这种情况可以使用自联结关联解决: ```ruby -class Employee < ActiveRecord::Base +class Employee < ApplicationRecord has_many :subordinates, class_name: "Employee", foreign_key: "manager_id" @@ -484,117 +525,166 @@ class Employee < ActiveRecord::Base end ``` -这样定义模型后,就可以使用 `@employee.subordinates` 和 `@employee.manager` 了。 +这样定义模型后,可以使用 `@employee.subordinates` 和 `@employee.manager` 检索了。 -在迁移中,要添加一个引用字段,指向模型自身: +在迁移(模式)中,要添加一个引用字段,指向模型自身: ```ruby -class CreateEmployees < ActiveRecord::Migration +class CreateEmployees < ActiveRecord::Migration[5.0] def change create_table :employees do |t| - t.references :manager + t.references :manager, index: true t.timestamps end end end ``` -小技巧和注意事项 --------------- + + +## 小技巧和注意事项 + +为了在 Rails 应用中有效使用 Active Record 关联,要了解以下几点: -在 Rails 程序中高效地使用 Active Record 关联,要了解以下几个知识: +* 控制缓存 +* 避免命名冲突 +* 更新模式 +* 控制关联的作用域 +* 双向关联 -* 缓存控制 -* 避免命名冲突 -* 更新模式 -* 控制关联的作用域 -* Bi-directional associations + -### 缓存控制 +### 控制缓存 -关联添加的方法都会使用缓存,记录最近一次查询结果,以备后用。缓存还会在方法之间共享。例如: +关联添加的方法都会使用缓存,记录最近一次查询的结果,以备后用。缓存还会在方法之间共享。例如: ```ruby -customer.orders # retrieves orders from the database -customer.orders.size # uses the cached copy of orders -customer.orders.empty? # uses the cached copy of orders +author.books # 从数据库中检索图书 +author.books.size # 使用缓存的图书副本 +author.books.empty? # 使用缓存的图书副本 ``` -程序的其他部分会修改数据,那么应该怎么重载缓存呢?调用关联方法时传入 `true` 参数即可: +应用的其他部分可能会修改数据,那么应该怎么重载缓存呢?在关联上调用 `reload` 即可: ```ruby -customer.orders # retrieves orders from the database -customer.orders.size # uses the cached copy of orders -customer.orders(true).empty? # discards the cached copy of orders - # and goes back to the database +author.books # 从数据库中检索图书 +author.books.size # 使用缓存的图书副本 +author.books.reload.empty? # 丢掉缓存的图书副本 + # 重新从数据库中检索 ``` + + ### 避免命名冲突 -关联的名字并不能随意使用。因为创建关联时,会向模型添加同名方法,所以关联的名字不能和 `ActiveRecord::Base` 中的实例方法同名。如果同名,关联方法会覆盖 `ActiveRecord::Base` 中的实例方法,导致错误。例如,关联的名字不能为 `attributes` 或 `connection`。 +关联的名称并不能随意使用。因为创建关联时,会向模型添加同名方法,所以关联的名字不能和 `ActiveRecord::Base` 中的实例方法同名。如果同名,关联方法会覆盖 `ActiveRecord::Base` 中的实例方法,导致错误。例如,关联的名字不能为 `attributes` 或 `connection`。 + + ### 更新模式 -关联非常有用,但没什么魔法。关联对应的数据库模式需要你自己编写。不同的关联类型,要做的事也不同。对 `belongs_to` 关联来说,要创建外键;对 `has_and_belongs_to_many` 来说,要创建相应的连接数据表。 +关联非常有用,但没什么魔法。关联对应的数据库模式需要你自己编写。不同的关联类型,要做的事也不同。对 `belongs_to` 关联来说,要创建外键;对 `has_and_belongs_to_many` 关联来说,要创建相应的联结表。 + + #### 创建 `belongs_to` 关联所需的外键 声明 `belongs_to` 关联后,要创建相应的外键。例如,有下面这个模型: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -这种关联需要在数据表中创建合适的外键: +上述关联需要在 books 表中创建相应的外键: ```ruby -class CreateOrders < ActiveRecord::Migration +class CreateBooks < ActiveRecord::Migration[5.0] def change - create_table :orders do |t| - t.datetime :order_date - t.string :order_number - t.integer :customer_id + create_table :books do |t| + t.datetime :published_at + t.string :book_number + t.integer :author_id end + + add_index :books, :author_id end end ``` 如果声明关联之前已经定义了模型,则要在迁移中使用 `add_column` 创建外键。 -#### 创建 `has_and_belongs_to_many` 关联所需的连接数据表 +为了提升查询性能,最好为外键添加索引;为了保证参照完整性,最好为外键添加约束: + +```ruby +class CreateBooks < ActiveRecord::Migration[5.0] + def change + create_table :books do |t| + t.datetime :published_at + t.string :book_number + t.integer :author_id + end + + add_index :books, :author_id + add_foreign_key :books, :authors + end +end +``` + + + +#### 创建 `has_and_belongs_to_many` 关联所需的联结表 + +创建 `has_and_belongs_to_many` 关联后,必须手动创建联结表。除非使用 `:join_table` 选项指定了联结表的名称,否则 Active Record 会按照类名出现在字典中的顺序为表起名。因此,作者和图书模型使用的联结表默认名为“authors_books”,因为在字典中,“a”在“b”前面。 -声明 `has_and_belongs_to_many` 关联后,必须手动创建连接数据表。除非在 `:join_table` 选项中指定了连接数据表的名字,否则 Active Record 会按照类名出现在字典中的顺序为数据表起名字。那么,顾客和订单模型使用的连接数据表默认名为“customers_orders”,因为在字典中,“c”在“o”前面。 +WARNING: 模型名的顺序使用字符串的 `<=>` 运算符确定。所以,如果两个字符串的长度不同,比较最短长度时,两个字符串是相等的,那么长字符串的排序比短字符串靠前。例如,你可能以为“paper_boxes”和“papers”这两个表生成的联结表名为“papers_paper_boxes”,因为“paper_boxes”比“papers”长,但其实生成的联结表名为“paper_boxes_papers”,因为在一般的编码方式中,“_”比“s”靠前。 -WARNING: 模型名的顺序使用字符串的 `<` 操作符确定。所以,如果两个字符串的长度不同,比较最短长度时,两个字符串是相等的,但长字符串的排序比短字符串靠前。例如,你可能以为“"paper\_boxes”和“papers”这两个表生成的连接表名为“papers\_paper\_boxes”,因为“paper\_boxes”比“papers”长。其实生成的连接表名为“paper\_boxes\_papers”,因为在一般的编码方式中,“\_”比“s”靠前。 -不管名字是什么,你都要在迁移中手动创建连接数据表。例如下面的关联声明: +不管名称是什么,你都要在迁移中手动创建联结表。例如下面的关联: ```ruby -class Assembly < ActiveRecord::Base +class Assembly < ApplicationRecord has_and_belongs_to_many :parts end -class Part < ActiveRecord::Base +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` -需要在迁移中创建 `assemblies_parts` 数据表,而且该表无主键: +上述关联需要在迁移中创建 `assemblies_parts` 表,而且该表无主键: ```ruby -class CreateAssembliesPartsJoinTable < ActiveRecord::Migration +class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0] def change create_table :assemblies_parts, id: false do |t| t.integer :assembly_id t.integer :part_id end + + add_index :assemblies_parts, :assembly_id + add_index :assemblies_parts, :part_id + end +end +``` + +我们把 `id: false` 选项传给 `create_table` 方法,因为这个表不对应模型。只有这样,关联才能正常建立。如果在使用 `has_and_belongs_to_many` 关联时遇到奇怪的行为,例如提示模型 ID 损坏,或 ID 冲突,有可能就是因为创建了主键。 + +联结表还可以使用 `create_join_table` 方法创建: + +```ruby +class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0] + def change + create_join_table :assemblies, :parts do |t| + t.index :assembly_id + t.index :part_id + end end end ``` -我们把 `id: false` 选项传给 `create_table` 方法,因为这个表不对应模型。只有这样,关联才能正常建立。如果在使用 `has_and_belongs_to_many` 关联时遇到奇怪的表现,例如提示模型 ID 损坏,或 ID 冲突,有可能就是因为创建了主键。 + ### 控制关联的作用域 @@ -603,29 +693,29 @@ end ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account end - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier end end end ``` -上面的代码能正常运行,因为 `Supplier` 和 `Account` 在同一个作用域内。但下面这段代码就不行了,因为 `Supplier` 和 `Account` 在不同的作用域中: +上面的代码能正常运行,因为 `Supplier` 和 `Account` 在同一个作用域中。但下面这段代码就不行了,因为 `Supplier` 和 `Account` 在不同的作用域中: ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account end end module Billing - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier end end @@ -637,14 +727,14 @@ end ```ruby module MyApplication module Business - class Supplier < ActiveRecord::Base + class Supplier < ApplicationRecord has_one :account, class_name: "MyApplication::Billing::Account" end end module Billing - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, class_name: "MyApplication::Business::Supplier" end @@ -652,395 +742,505 @@ module MyApplication end ``` + + ### 双向关联 -一般情况下,都要求能在关联的两端进行操作。例如,有下面的关联声明: +一般情况下,都要求能在关联的两端进行操作,即在两个模型中都要声明关联。 + +```ruby +class Author < ApplicationRecord + has_many :books +end + +class Book < ApplicationRecord + belongs_to :author +end +``` + +通过关联的名称,Active Record 能探知这两个模型之间建立的是双向关联。这样一来,Active Record 只会加载一个 `Author` 对象副本,从而确保应用运行效率更高效,并避免数据不一致。 ```ruby -class Customer < ActiveRecord::Base - has_many :orders +a = Author.first +b = a.books.first +a.first_name == b.author.first_name # => true +a.first_name = 'David' +a.first_name == b.author.first_name # => true +``` + +Active Record 能自动识别多数具有标准名称的双向关联。然而,具有下述选项的关联无法识别: + +* `:conditions` +* `:through` +* `:polymorphic` +* `:class_name` +* `:foreign_key` + +例如,对下属模型来说: + +```ruby +class Author < ApplicationRecord + has_many :books end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' end ``` -默认情况下,Active Record 并不知道这个关联中两个模型之间的联系。可能导致同一对象的两个副本不同步: +Active Record 就无法自动识别这个双向关联: ```ruby -c = Customer.first -o = c.orders.first -c.first_name == o.customer.first_name # => true -c.first_name = 'Manny' -c.first_name == o.customer.first_name # => false +a = Author.first +b = a.books.first +a.first_name == b.writer.first_name # => true +a.first_name = 'David' +a.first_name == b.writer.first_name # => false ``` -之所以会发生这种情况,是因为 `c` 和 `o.customer` 在内存中是同一数据的两钟表示,修改其中一个并不会刷新另一个。Active Record 提供了 `:inverse_of` 选项,可以告知 Rails 两者之间的关系: +Active Record 提供了 `:inverse_of` 选项,可以通过它明确声明双向关联: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books, inverse_of: 'writer' end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' end ``` -这么修改之后,Active Record 就只会加载一个顾客对象,避免数据的不一致性,提高程序的执行效率: +在 `has_many` 声明中指定 `:inverse_of` 选项后,Active Record 便能识别双向关联: ```ruby -c = Customer.first -o = c.orders.first -c.first_name == o.customer.first_name # => true -c.first_name = 'Manny' -c.first_name == o.customer.first_name # => true +a = Author.first +b = a.books.first +a.first_name == b.writer.first_name # => true +a.first_name = 'David' +a.first_name == b.writer.first_name # => true ``` `inverse_of` 有些限制: -* 不能和 `:through` 选项同时使用; -* 不能和 `:polymorphic` 选项同时使用; -* 不能和 `:as` 选项同时使用; -* 在 `belongs_to` 关联中,会忽略 `has_many` 关联的 `inverse_of` 选项; +* 不支持 `:through` 关联; +* 不支持 `:polymorphic` 关联; +* 不支持 `:as` 选项; -每种关联都会尝试自动找到关联的另一端,设置 `:inverse_of` 选项(根据关联的名字)。使用标准名字的关联都有这种功能。但是,如果在关联中设置了下面这些选项,将无法自动设置 `:inverse_of`: + -* `:conditions` -* `:through` -* `:polymorphic` -* `:foreign_key` +## 关联详解 -关联详解 -------- +下面几小节详细说明各种关联,包括添加的方法和声明关联时可以使用的选项。 -下面几节详细说明各种关联,包括添加的方法和声明关联时可以使用的选项。 + ### `belongs_to` 关联详解 -`belongs_to` 关联创建一个模型与另一个模型之间的一对一关系。用数据库的行话来说,就是这个类中包含了外键。如果外键在另一个类中,就应该使用 `has_one` 关联。 +`belongs_to` 关联创建一个模型与另一个模型之间的一对一关系。用数据库术语来说,就是这个类中包含外键。如果外键在另一个类中,应该使用 `has_one` 关联。 + + #### `belongs_to` 关联添加的方法 -声明 `belongs_to` 关联后,所在的类自动获得了五个和关联相关的方法: +声明 `belongs_to` 关联后,所在的类自动获得了五个和关联相关的方法: -* `association(force_reload = false)` -* `association=(associate)` -* `build_association(attributes = {})` -* `create_association(attributes = {})` -* `create_association!(attributes = {})` +* `association` +* `association=(associate)` +* `build_association(attributes = {})` +* `create_association(attributes = {})` +* `create_association!(attributes = {})` -这五个方法中的 `association` 要替换成传入 `belongs_to` 方法的第一个参数。例如,如下的声明: +这五个方法中的 `association` 要替换成传给 `belongs_to` 方法的第一个参数。对下述声明来说: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end ``` -每个 `Order` 模型实例都获得了这些方法: +`Book` 模型的每个实例都获得了这些方法: ```ruby -customer -customer= -build_customer -create_customer -create_customer! +author +author= +build_author +create_author +create_author! ``` NOTE: 在 `has_one` 和 `belongs_to` 关联中,必须使用 `build_*` 方法构建关联对象。`association.build` 方法是在 `has_many` 和 `has_and_belongs_to_many` 关联中使用的。创建关联对象要使用 `create_*` 方法。 -##### `association(force_reload = false)` -如果关联的对象存在,`association` 方法会返回关联对象。如果找不到关联对象,则返回 `nil`。 + + +##### `association` + +如果关联的对象存在,`association` 方法会返回关联的对象。如果找不到关联的对象,返回 `nil`。 ```ruby -@customer = @order.customer +@author = @book.author ``` -如果关联对象之前已经取回,会返回缓存版本。如果不想使用缓存版本,强制重新从数据库中读取,可以把 `force_reload` 参数设为 `true`。 +如果关联的对象之前已经取回,会返回缓存版本。如果不想使用缓存版本(强制读取数据库)在父对象上调用 `#reload` 方法。 + +```ruby +@author = @book.reload.author +``` + + ##### `association=(associate)` -`association=` 方法用来赋值关联的对象。这个方法的底层操作是,从关联对象上读取主键,然后把值赋给该主键对应的对象。 +`association=` 方法用于赋值关联的对象。这个方法的底层操作是,从关联对象上读取主键,然后把值赋给该主键对应的对象。 ```ruby -@order.customer = @customer +@book.author = @author ``` + + ##### `build_association(attributes = {})` -`build_association` 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象连接的外键会自动设置,但关联对象不会存入数据库。 +`build_association` 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,对象的外键会自动设置,但关联对象不会存入数据库。 ```ruby -@customer = @order.build_customer(customer_number: 123, - customer_name: "John Doe") +@author = @book.build_author(author_number: 123, + author_name: "John Doe") ``` + + ##### `create_association(attributes = {})` -`create_association` 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象连接的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。 +`create_association` 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,对象的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。 ```ruby -@customer = @order.create_customer(customer_number: 123, - customer_name: "John Doe") +@author = @book.create_author(author_number: 123, + author_name: "John Doe") ``` + + ##### `create_association!(attributes = {})` -和 `create_association` 方法作用相同,但是如果记录不合法,会抛出 `ActiveRecord::RecordInvalid` 异常。 +与 `create_association` 方法作用相同,但是如果记录无效,会抛出 `ActiveRecord::RecordInvalid` 异常。 + + #### `belongs_to` 方法的选项 -Rails 的默认设置足够智能,能满足常见需求。但有时还是需要定制 `belongs_to` 关联的行为。定制的方法很简单,声明关联时传入选项或者使用代码块即可。例如,下面的关联使用了两个选项: +Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 `belongs_to` 关联的行为。定制的方法很简单,声明关联时传入选项或者使用代码块即可。例如,下面的关联使用了两个选项: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, dependent: :destroy, +class Book < ApplicationRecord + belongs_to :author, dependent: :destroy, counter_cache: true end ``` -`belongs_to` 关联支持以下选项: +`belongs_to` 关联支持下列选项: + +* `:autosave` +* `:class_name` +* `:counter_cache` +* `:dependent` +* `:foreign_key` +* `:primary_key` +* `:inverse_of` +* `:polymorphic` +* `:touch` +* `:validate` +* `:optional` -* `:autosave` -* `:class_name` -* `:counter_cache` -* `:dependent` -* `:foreign_key` -* `:inverse_of` -* `:polymorphic` -* `:touch` -* `:validate` + ##### `:autosave` 如果把 `:autosave` 选项设为 `true`,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。 + + ##### `:class_name` -如果另一个模型无法从关联的名字获取,可以使用 `:class_name` 选项指定模型名。例如,如果订单属于顾客,但表示顾客的模型是 `Patron`,就可以这样声明关联: +如果另一个模型无法从关联的名称获取,可以使用 `:class_name` 选项指定模型名。例如,如果一本书属于一位作者,但是表示作者的模型是 `Patron`,就可以这样声明关联: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, class_name: "Patron" +class Book < ApplicationRecord + belongs_to :author, class_name: "Patron" end ``` + + ##### `:counter_cache` -`:counter_cache` 选项可以提高统计所属对象数量操作的效率。假如如下的模型: +`:counter_cache` 选项可以提高统计所属对象数量操作的效率。以下述模型为例: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -这样声明关联后,如果想知道 `@customer.orders.size` 的结果,就要在数据库中执行 `COUNT(*)` 查询。如果不想执行这个查询,可以在声明 `belongs_to` 关联的模型中加入计数缓存功能: +这样声明关联后,如果想知道 `@author.books.size` 的结果,要在数据库中执行 `COUNT(*)` 查询。如果不想执行这个查询,可以在声明 `belongs_to` 关联的模型中加入计数缓存功能: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, counter_cache: true +class Book < ApplicationRecord + belongs_to :author, counter_cache: true end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -这样声明关联后,Rails 会及时更新缓存,调用 `size` 方法时返回缓存中的值。 +这样声明关联后,Rails 会及时更新缓存,调用 `size` 方法时会返回缓存中的值。 + +虽然 `:counter_cache` 选项在声明 `belongs_to` 关联的模型中设置,但实际使用的字段要添加到所关联的模型中(`has_many` 那一方)。针对上面的例子,要把 `books_count` 字段加入 `Author` 模型。 -虽然 `:counter_cache` 选项在声明 `belongs_to` 关联的模型中设置,但实际使用的字段要添加到关联的模型中。针对上面的例子,要把 `orders_count` 字段加入 `Customer` 模型。这个字段的默认名也是可以设置的: +这个字段的名称也是可以设置的,把 `counter_cache` 选项的值换成列名即可。例如,不使用 `books_count`,而是使用 `count_of_books`: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, counter_cache: :count_of_orders +class Book < ApplicationRecord + belongs_to :author, counter_cache: :count_of_books end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` +NOTE: 只需在关联的 `belongs_to` 一侧指定 `:counter_cache` 选项。 + + 计数缓存字段通过 `attr_readonly` 方法加入关联模型的只读属性列表中。 + + ##### `:dependent` -`:dependent` 选项的值有两个: +`:dependent` 选项控制属主销毁后怎么处理关联的对象: + +* `:destroy`:也销毁关联的对象 +* `:delete_all`:直接从数据库中删除关联的对象(不执行回调) +* `:nullify`:把外键设为 `NULL`(不执行回调) +* `:restrict_with_exception`:如果有关联的记录,抛出异常 +* `:restrict_with_error`:如果有关联的对象,为属主添加一个错误 -* `:destroy`:销毁对象时,也会在关联对象上调用 `destroy` 方法; -* `:delete`:销毁对象时,关联的对象不会调用 `destroy` 方法,而是直接从数据库中删除; +WARNING: 在 `belongs_to` 关联和 `has_many` 关联配对时,不应该设置这个选项,否则会导致数据库中出现无主记录。 -WARNING: 在 `belongs_to` 关联和 `has_many` 关联配对时,不应该设置这个选项,否则会导致数据库中出现孤儿记录。 + + ##### `:foreign_key` 按照约定,用来存储外键的字段名是关联名后加 `_id`。`:foreign_key` 选项可以设置要使用的外键名: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, class_name: "Patron", - foreign_key: "patron_id" +class Book < ApplicationRecord + belongs_to :author, class_name: "Patron", + foreign_key: "patron_id" end ``` TIP: 不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。 + + + +##### `:primary_key` + +按照约定,Rails 假定使用表中的 `id` 列保存主键。使用 `:primary_key` 选项可以指定使用其他列。 + +假如有个 `users` 表使用 `guid` 列存储主键,`todos` 想在 `guid` 列中存储用户的 ID,那么可以使用 `primary_key` 选项设置: + +```ruby +class User < ApplicationRecord + self.primary_key = 'guid' # 主键是 guid,不是 id +end + +class Todo < ApplicationRecord + belongs_to :user, primary_key: 'guid' +end +``` + +执行 `@user.todos.create` 时,`@todo` 记录的用户 ID 是 `@user` 的 `guid` 值。 + + + ##### `:inverse_of` `:inverse_of` 选项指定 `belongs_to` 关联另一端的 `has_many` 和 `has_one` 关联名。不能和 `:polymorphic` 选项一起使用。 ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books, inverse_of: :author end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :author, inverse_of: :books end ``` + + ##### `:polymorphic` -`:polymorphic` 选项为 `true` 时表明这是个多态关联。[前文](#polymorphic-associations)已经详细介绍过多态关联。 +`:polymorphic` 选项为 `true` 时,表明这是个多态关联。[多态关联](#polymorphic-associations)已经详细介绍过多态关联。 + + ##### `:touch` -如果把 `:touch` 选项设为 `true`,保存或销毁对象时,关联对象的 `updated_at` 或 `updated_on` 字段会自动设为当前时间戳。 +如果把 `:touch` 选项设为 `true`,保存或销毁对象时,关联对象的 `updated_at` 或 `updated_on` 字段会自动设为当前时间。 ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, touch: true +class Book < ApplicationRecord + belongs_to :author, touch: true end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -在这个例子中,保存或销毁订单后,会更新关联的顾客中的时间戳。还可指定要更新哪个字段的时间戳: +在这个例子中,保存或销毁一本书后,会更新关联的作者的时间戳。还可指定要更新哪个时间戳字段: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, touch: :orders_updated_at +class Book < ApplicationRecord + belongs_to :author, touch: :books_updated_at end ``` + + ##### `:validate` -如果把 `:validate` 选项设为 `true`,保存对象时,会同时验证关联对象。该选项的默认值是 `false`,保存对象时不验证关联对象。 +如果把 `:validate` 选项设为 `true`,保存对象时,会同时验证关联的对象。该选项的默认值是 `false`,保存对象时不验证关联的对象。 + + + +##### `:optional` + +如果把 `:optional` 选项设为 `true`,不会验证关联的对象是否存在。该选项的默认值是 `false`。 + + #### `belongs_to` 的作用域 -有时可能需要定制 `belongs_to` 关联使用的查询方式,定制的查询可在作用域代码块中指定。例如: +有时可能需要定制 `belongs_to` 关联使用的查询,定制的查询可在作用域代码块中指定。例如: ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, -> { where active: true }, - dependent: :destroy +class Book < ApplicationRecord + belongs_to :author, -> { where active: true }, + dependent: :destroy end ``` -在作用域代码块中可以使用任何一个标准的[查询方法](active_record_querying.html)。下面分别介绍这几个方法: +在作用域代码块中可以使用任何一个标准的[查询方法](active_record_querying.html)。下面分别介绍这几个: + +* `where` +* `includes` +* `readonly` +* `select` -* `where` -* `includes` -* `readonly` -* `select` + ##### `where` `where` 方法指定关联对象必须满足的条件。 ```ruby -class Order < ActiveRecord::Base - belongs_to :customer, -> { where active: true } +class book < ApplicationRecord + belongs_to :author, -> { where active: true } end ``` + + ##### `includes` -`includes` 方法指定使用关联时要按需加载的间接关联。例如,有如下的模型: +`includes` 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型: ```ruby -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -如果经常要直接从商品上获取顾客对象(`@line_item.order.customer`),就可以把顾客引入商品和订单的关联中: +如果经常要直接从商品上获取作者对象(`@line_item.book.author`),就可以在关联中把作者从商品引入图书中: ```ruby -class LineItem < ActiveRecord::Base - belongs_to :order, -> { includes :customer } +class LineItem < ApplicationRecord + belongs_to :book, -> { includes :author } end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -NOTE: 直接关联没必要使用 `includes`。如果 `Order belongs_to :customer`,那么顾客会自动按需加载。 +NOTE: 直接关联没必要使用 `includes`。如果 `Book belongs_to :author`,那么需要使用时会自动及早加载作者。 + + + ##### `readonly` -如果使用 `readonly`,通过关联获取的对象就是只读的。 +如果使用 `readonly`,通过关联获取的对象是只读的。 -##### `select` + -`select` 方法会覆盖获取关联对象使用的 SQL `SELECT` 子句。默认情况下,Rails 会读取所有字段。 +##### `select` -TIP: 如果在 `belongs_to` 关联中使用 `select` 方法,应该同时设置 `:foreign_key` 选项,确保返回正确的结果。 +`select` 方法用于覆盖检索关联对象使用的 SQL `SELECT` 子句。默认情况下,Rails 检索所有字段。 -#### 检查关联的对象是否存在 +TIP: 如果在 `belongs_to` 关联中使用 `select` 方法,应该同时设置 `:foreign_key` 选项,确保返回的结果正确。 -检查关联的对象是否存在可以使用 `association.nil?` 方法: -```ruby -if @order.customer.nil? - @msg = "No customer found for this order" -end -``` + #### 什么时候保存对象 把对象赋值给 `belongs_to` 关联不会自动保存对象,也不会保存关联的对象。 + + ### `has_one` 关联详解 -`has_one` 关联建立两个模型之间的一对一关系。用数据库的行话说,这种关联的意思是外键在另一个类中。如果外键在这个类中,应该使用 `belongs_to` 关联。 +`has_one` 关联建立两个模型之间的一对一关系。用数据库术语来说,这种关联的意思是外键在另一个类中。如果外键在这个类中,应该使用 `belongs_to` 关联。 + + #### `has_one` 关联添加的方法 声明 `has_one` 关联后,声明所在的类自动获得了五个关联相关的方法: -* `association(force_reload = false)` -* `association=(associate)` -* `build_association(attributes = {})` -* `create_association(attributes = {})` -* `create_association!(attributes = {})` +* `association` +* `association=(associate)` +* `build_association(attributes = {})` +* `create_association(attributes = {})` +* `create_association!(attributes = {})` -这五个方法中的 `association` 要替换成传入 `has_one` 方法的第一个参数。例如,如下的声明: +这五个方法中的 `association` 要替换成传给 `has_one` 方法的第一个参数。对如下的声明来说: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end ``` @@ -1057,214 +1257,265 @@ create_account! NOTE: 在 `has_one` 和 `belongs_to` 关联中,必须使用 `build_*` 方法构建关联对象。`association.build` 方法是在 `has_many` 和 `has_and_belongs_to_many` 关联中使用的。创建关联对象要使用 `create_*` 方法。 -##### `association(force_reload = false)` -如果关联的对象存在,`association` 方法会返回关联对象。如果找不到关联对象,则返回 `nil`。 + + +##### `association` + +如果关联的对象存在,`association` 方法会返回关联的对象。如果找不到关联的对象,返回 `nil`。 ```ruby @account = @supplier.account ``` -如果关联对象之前已经取回,会返回缓存版本。如果不想使用缓存版本,强制重新从数据库中读取,可以把 `force_reload` 参数设为 `true`。 +如果关联的对象之前已经取回,会返回缓存版本。如果不想使用缓存版本,而是强制重新从数据库中读取,在父对象上调用 `#reload` 方法。 + +```ruby +@account = @supplier.reload.account +``` + + ##### `association=(associate)` -`association=` 方法用来赋值关联的对象。这个方法的底层操作是,从关联对象上读取主键,然后把值赋给该主键对应的关联对象。 +`association=` 方法用于赋值关联的对象。这个方法的底层操作是,从对象上读取主键,然后把关联的对象的外键设为那个值。 ```ruby @supplier.account = @account ``` + + ##### `build_association(attributes = {})` -`build_association` 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象连接的外键会自动设置,但关联对象不会存入数据库。 +`build_association` 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象链接的外键会自动设置,但关联对象不会存入数据库。 ```ruby @account = @supplier.build_account(terms: "Net 30") ``` + + ##### `create_association(attributes = {})` -`create_association` 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象连接的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。 +`create_association` 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象链接的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。 ```ruby @account = @supplier.create_account(terms: "Net 30") ``` + + ##### `create_association!(attributes = {})` -和 `create_association` 方法作用相同,但是如果记录不合法,会抛出 `ActiveRecord::RecordInvalid` 异常。 +与 `create_association` 方法作用相同,但是如果记录无效,会抛出 `ActiveRecord::RecordInvalid` 异常。 + + #### `has_one` 方法的选项 -Rails 的默认设置足够智能,能满足常见需求。但有时还是需要定制 `has_one` 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项: +Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 `has_one` 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, class_name: "Billing", dependent: :nullify end ``` -`has_one` 关联支持以下选项: +`has_one` 关联支持下列选项: -* `:as` -* `:autosave` -* `:class_name` -* `:dependent` -* `:foreign_key` -* `:inverse_of` -* `:primary_key` -* `:source` -* `:source_type` -* `:through` -* `:validate` +* `:as` +* `:autosave` +* `:class_name` +* `:dependent` +* `:foreign_key` +* `:inverse_of` +* `:primary_key` +* `:source` +* `:source_type` +* `:through` +* `:validate` + + ##### `:as` `:as` 选项表明这是多态关联。[前文](#polymorphic-associations)已经详细介绍过多态关联。 + + ##### `:autosave` 如果把 `:autosave` 选项设为 `true`,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。 + + ##### `:class_name` -如果另一个模型无法从关联的名字获取,可以使用 `:class_name` 选项指定模型名。例如,供应商有一个账户,但表示账户的模型是 `Billing`,就可以这样声明关联: +如果另一个模型无法从关联的名称获取,可以使用 `:class_name` 选项指定模型名。例如,供应商有一个账户,但表示账户的模型是 `Billing`,那么就可以这样声明关联: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, class_name: "Billing" end ``` + + ##### `:dependent` -设置销毁拥有者时要怎么处理关联对象: +控制属主销毁后怎么处理关联的对象: -* `:destroy`:也销毁关联对象; -* `:delete`:直接把关联对象对数据库中删除,因此不会执行回调; -* `:nullify`:把外键设为 `NULL`,不会执行回调; -* `:restrict_with_exception`:有关联的对象时抛出异常; -* `:restrict_with_error`:有关联的对象时,向拥有者添加一个错误; +* `:destroy`:也销毁关联的对象; +* `:delete`:直接把关联的对象从数据库中删除(不执行回调); +* `:nullify`:把外键设为 `NULL`,不执行回调; +* `:restrict_with_exception`:有关联的对象时抛出异常; +* `:restrict_with_error`:有关联的对象时,向属主添加一个错误; -如果在数据库层设置了 `NOT NULL` 约束,就不能使用 `:nullify` 选项。如果 `:dependent` 选项没有销毁关联,就无法修改关联对象,因为关联对象的外键设置为不接受 `NULL`。 +如果在数据库层设置了 `NOT NULL` 约束,就不能使用 `:nullify` 选项。如果 `:dependent` 选项没有销毁关联,就无法修改关联的对象,因为关联的对象的外键设置为不接受 `NULL`。 + + ##### `:foreign_key` -按照约定,在另一个模型中用来存储外键的字段名是模型名后加 `_id`。`:foreign_key` 选项可以设置要使用的外键名: +按照约定,在另一个模型中用来存储外键的字段名是模型名后加 `_id`。`:foreign_key` 选项用于设置要使用的外键名: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, foreign_key: "supp_id" end ``` TIP: 不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。 + + ##### `:inverse_of` `:inverse_of` 选项指定 `has_one` 关联另一端的 `belongs_to` 关联名。不能和 `:through` 或 `:as` 选项一起使用。 ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, inverse_of: :supplier end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier, inverse_of: :account end ``` + + ##### `:primary_key` -按照约定,用来存储该模型主键的字段名 `id`。`:primary_key` 选项可以设置要使用的主键名。 +按照约定,用来存储该模型主键的字段名 `id`。`:primary_key` 选项用于设置要使用的主键名。 + + ##### `:source` -`:source` 选项指定 `has_one :through` 关联的关联源名字。 +`:source` 选项指定 `has_one :through` 关联的源关联名称。 + + ##### `:source_type` -`:source_type` 选项指定 `has_one :through` 关联中用来处理多态关联的关联源类型。 +`:source_type` 选项指定通过多态关联处理 `has_one :through` 关联的源关联类型。 + + ##### `:through` -`:through` 选项指定用来执行查询的连接模型。[前文](#the-has-one-through-association)详细介绍过 `has_one :through` 关联。 +`:through` 选项指定用于执行查询的联结模型。[前文](#the-has-one-through-association)详细介绍过 `has_one :through` 关联。 + + ##### `:validate` -如果把 `:validate` 选项设为 `true`,保存对象时,会同时验证关联对象。该选项的默认值是 `false`,保存对象时不验证关联对象。 +如果把 `:validate` 选项设为 `true`,保存对象时,会同时验证关联的对象。该选项的默认值是 `false`,即保存对象时不验证关联的对象。 + + #### `has_one` 的作用域 -有时可能需要定制 `has_one` 关联使用的查询方式,定制的查询可在作用域代码块中指定。例如: +有时可能需要定制 `has_one` 关联使用的查询。定制的查询在作用域代码块中指定。例如: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { where active: true } end ``` -在作用域代码块中可以使用任何一个标准的[查询方法](active_record_querying.html)。下面分别介绍这几个方法: +在作用域代码块中可以使用任何一个标准的[查询方法](active_record_querying.html)。下面介绍其中几个: + +* `where` +* `includes` +* `readonly` +* `select` -* `where` -* `includes` -* `readonly` -* `select` + ##### `where` -`where` 方法指定关联对象必须满足的条件。 +`where` 方法指定关联的对象必须满足的条件。 ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { where "confirmed = 1" } end ``` + + ##### `includes` -`includes` 方法指定使用关联时要按需加载的间接关联。例如,有如下的模型: +`includes` 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier belongs_to :representative end -class Representative < ActiveRecord::Base +class Representative < ApplicationRecord has_many :accounts end ``` -如果经常要直接获取供应商代表(`@supplier.account.representative`),就可以把代表引入供应商和账户的关联中: +如果经常直接获取供应商代表(`@supplier.account.representative`),可以把代表引入供应商和账户的关联中: ```ruby -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_one :account, -> { includes :representative } end -class Account < ActiveRecord::Base +class Account < ApplicationRecord belongs_to :supplier belongs_to :representative end -class Representative < ActiveRecord::Base +class Representative < ApplicationRecord has_many :accounts end ``` + + ##### `readonly` -如果使用 `readonly`,通过关联获取的对象就是只读的。 +如果使用 `readonly`,通过关联获取的对象是只读的。 + + ##### `select` -`select` 方法会覆盖获取关联对象使用的 SQL `SELECT` 子句。默认情况下,Rails 会读取所有字段。 +`select` 方法会覆盖获取关联对象使用的 SQL `SELECT` 子句。默认情况下,Rails 检索所有列。 + + #### 检查关联的对象是否存在 @@ -1276,518 +1527,638 @@ if @supplier.account.nil? end ``` + + #### 什么时候保存对象 -把对象赋值给 `has_one` 关联时,会自动保存对象(因为要更新外键)。而且所有被替换的对象也会自动保存,因为外键也变了。 +把对象赋值给 `has_one` 关联时,那个对象会自动保存(因为要更新外键)。而且所有被替换的对象也会自动保存,因为外键也变了。 -如果无法通过验证,随便哪一次保存失败了,赋值语句就会返回 `false`,赋值操作会取消。 +如果由于无法通过验证而导致上述保存失败,赋值语句返回 `false`,赋值操作会取消。 如果父对象(`has_one` 关联声明所在的模型)没保存(`new_record?` 方法返回 `true`),那么子对象也不会保存。只有保存了父对象,才会保存子对象。 -如果赋值给 `has_one` 关联时不想保存对象,可以使用 `association.build` 方法。 +如果赋值给 `has_one` 关联时不想保存对象,使用 `association.build` 方法。 + + ### `has_many` 关联详解 -`has_many` 关联建立两个模型之间的一对多关系。用数据库的行话说,这种关联的意思是外键在另一个类中,指向这个类的实例。 +`has_many` 关联建立两个模型之间的一对多关系。用数据库术语来说,这种关联的意思是外键在另一个类中,指向这个类的实例。 + + #### `has_many` 关联添加的方法 声明 `has_many` 关联后,声明所在的类自动获得了 16 个关联相关的方法: -* `collection(force_reload = false)` -* `collection<<(object, ...)` -* `collection.delete(object, ...)` -* `collection.destroy(object, ...)` -* `collection=objects` -* `collection_singular_ids` -* `collection_singular_ids=ids` -* `collection.clear` -* `collection.empty?` -* `collection.size` -* `collection.find(...)` -* `collection.where(...)` -* `collection.exists?(...)` -* `collection.build(attributes = {}, ...)` -* `collection.create(attributes = {})` -* `collection.create!(attributes = {})` +* `collection` +* `collection<<(object, …​)` +* `collection.delete(object, …​)` +* `collection.destroy(object, …​)` +* `collection=(objects)` +* `collection_singular_ids` +* `collection_singular_ids=(ids)` +* `collection.clear` +* `collection.empty?` +* `collection.size` +* `collection.find(…​)` +* `collection.where(…​)` +* `collection.exists?(…​)` +* `collection.build(attributes = {}, …​)` +* `collection.create(attributes = {})` +* `collection.create!(attributes = {})` -这些个方法中的 `collection` 要替换成传入 `has_many` 方法的第一个参数。`collection_singular` 要替换成第一个参数的单数形式。例如,如下的声明: +这些个方法中的 `collection` 要替换成传给 `has_many` 方法的第一个参数。`collection_singular` 要替换成第一个参数的单数形式。对如下的声明来说: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end ``` -每个 `Customer` 模型实例都获得了这些方法: +每个 `Author` 模型实例都获得了这些方法: ```ruby -orders(force_reload = false) -orders<<(object, ...) -orders.delete(object, ...) -orders.destroy(object, ...) -orders=objects -order_ids -order_ids=ids -orders.clear -orders.empty? -orders.size -orders.find(...) -orders.where(...) -orders.exists?(...) -orders.build(attributes = {}, ...) -orders.create(attributes = {}) -orders.create!(attributes = {}) +books +books<<(object, ...) +books.delete(object, ...) +books.destroy(object, ...) +books=(objects) +book_ids +book_ids=(ids) +books.clear +books.empty? +books.size +books.find(...) +books.where(...) +books.exists?(...) +books.build(attributes = {}, ...) +books.create(attributes = {}) +books.create!(attributes = {}) ``` -##### `collection(force_reload = false)` + + +##### `collection` `collection` 方法返回一个数组,包含所有关联的对象。如果没有关联的对象,则返回空数组。 ```ruby -@orders = @customer.orders +@books = @author.books ``` -##### `collection<<(object, ...)` + -`collection<<` 方法向关联对象数组中添加一个或多个对象,并把各所加对象的外键设为调用此方法的模型的主键。 +##### `collection<<(object, …​)` + +`collection<<` 方法向关联对象数组中添加一个或多个对象,并把各个所加对象的外键设为调用此方法的模型的主键。 ```ruby -@customer.orders << @order1 +@author.books << @book1 ``` -##### `collection.delete(object, ...)` + + +##### `collection.delete(object, …​)` `collection.delete` 方法从关联对象数组中删除一个或多个对象,并把删除的对象外键设为 `NULL`。 ```ruby -@customer.orders.delete(@order1) +@author.books.delete(@book1) ``` -WARNING: 如果关联设置了 `dependent: :destroy`,还会销毁关联对象;如果关联设置了 `dependent: :delete_all`,还会删除关联对象。 +WARNING: 如果关联设置了 `dependent: :destroy`,还会销毁关联的对象;如果关联设置了 `dependent: :delete_all`,还会删除关联的对象。 -##### `collection.destroy(object, ...)` + + +##### `collection.destroy(object, …​)` `collection.destroy` 方法在关联对象上调用 `destroy` 方法,从关联对象数组中删除一个或多个对象。 ```ruby -@customer.orders.destroy(@order1) +@author.books.destroy(@book1) ``` -WARNING: 对象会从数据库中删除,忽略 `:dependent` 选项。 +WARNING: 对象始终会从数据库中删除,忽略 `:dependent` 选项。 + + -##### `collection=objects` +##### `collection=(objects)` -`collection=` 让关联对象数组只包含指定的对象,根据需求会添加或删除对象。 +`collection=` 方法让关联对象数组只包含指定的对象,根据需求会添加或删除对象。改动会持久存入数据库。 + + ##### `collection_singular_ids` -`collection_singular_ids` 返回一个数组,包含关联对象数组中各对象的 ID。 +`collection_singular_ids` 方法返回一个数组,包含关联对象数组中各对象的 ID。 ```ruby -@order_ids = @customer.order_ids +@book_ids = @author.book_ids ``` -##### `collection_singular_ids=ids` + + +##### `collection_singular_ids=(ids)` -`collection_singular_ids=` 方法让数组中只包含指定的主键,根据需要增删 ID。 +`collection_singular_ids=` 方法让关联对象数组中只包含指定的主键,根据需要会增删 ID。改动会持久存入数据库。 + + ##### `collection.clear` -`collection.clear` 方法删除数组中的所有对象。如果关联中指定了 `dependent: :destroy` 选项,会销毁关联对象;如果关联中指定了 `dependent: :delete_all` 选项,会直接从数据库中删除对象,然后再把外键设为 `NULL`。 +`collection.clear` 方法根据 `dependent` 选项指定的策略删除集合中的所有对象。如果没有指定这个选项,使用默认策略。`has_many :through` 关联的默认策略是 `delete_all`;`has_many` 关联的默认策略是,把外键设为 `NULL`。 + +```ruby +@author.books.clear +``` + +WARNING: 如果设为 `dependent: :destroy`,对象会被删除,这与 `dependent: :delete_all` 一样。 + + ##### `collection.empty?` -如果关联数组中没有关联对象,`collection.empty?` 方法返回 `true`。 +如果集合中没有关联的对象,`collection.empty?` 方法返回 `true`。 ```erb -<% if @customer.orders.empty? %> - No Orders Found +<% if @author.books.empty? %> + No Books Found <% end %> ``` + + ##### `collection.size` -`collection.size` 返回关联对象数组中的对象数量。 +`collection.size` 返回集合中的对象数量。 ```ruby -@order_count = @customer.orders.size +@book_count = @author.books.size ``` -##### `collection.find(...)` + -`collection.find` 方法在关联对象数组中查找对象,句法和可用选项跟 `ActiveRecord::Base.find` 方法一样。 +##### `collection.find(…​)` + +`collection.find` 方法在集合中查找对象,使用的句法和选项跟 `ActiveRecord::Base.find` 方法一样。 ```ruby -@open_orders = @customer.orders.find(1) +@available_books = @author.books.find(1) ``` -##### `collection.where(...)` + + +#### `collection.where(…​)` -`collection.where` 方法根据指定的条件在关联对象数组中查找对象,但会惰性加载对象,用到对象时才会执行查询。 +`collection.where` 方法根据指定的条件在集合中查找对象,但对象是惰性加载的,即访问对象时才会查询数据库。 ```ruby -@open_orders = @customer.orders.where(open: true) # No query yet -@open_order = @open_orders.first # Now the database will be queried +@available_books = @author.books.where(available: true) # 尚未查询 +@available_book = @available_books.first # 现在查询数据库 ``` -##### `collection.exists?(...)` + -`collection.exists?` 方法根据指定的条件检查关联对象数组中是否有符合条件的对象,句法和可用选项跟 `ActiveRecord::Base.exists?` 方法一样。 +##### `collection.exists?(…​)` -##### `collection.build(attributes = {}, ...)` +`collection.exists?` 方法根据指定的条件检查集合中是否有符合条件的对象,使用的句法和选项跟 [`ActiveRecord::Base.exists?` 方法](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F)一样。 -`collection.build` 方法返回一个或多个此种关联类型的新对象。这些对象会使用传入的属性初始化,还会创建对应的外键,但不会保存关联对象。 + + +##### `collection.build(attributes = {}, …​)` + +`collection.build` 方法返回一个或多个此种关联类型的新对象。这些对象会使用传入的属性初始化,还会创建对应的外键,但不会保存关联的对象。 ```ruby -@order = @customer.orders.build(order_date: Time.now, - order_number: "A12345") +@book = @author.books.build(published_at: Time.now, + book_number: "A12345") + +@books = @author.books.build([ + { published_at: Time.now, book_number: "A12346" }, + { published_at: Time.now, book_number: "A12347" } +]) ``` + + ##### `collection.create(attributes = {})` -`collection.create` 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会创建对应的外键,只要能通过所有数据验证,就会保存关联对象。 +`collection.create` 方法返回一个或多个此种关联类型的新对象。这些对象会使用传入的属性初始化,还会创建对应的外键,只要能通过所有数据验证,就会保存关联的对象。 ```ruby -@order = @customer.orders.create(order_date: Time.now, - order_number: "A12345") +@book = @author.books.create(published_at: Time.now, + book_number: "A12345") + +@books = @author.books.create([ + { published_at: Time.now, book_number: "A12346" }, + { published_at: Time.now, book_number: "A12347" } +]) ``` -##### `collection.create!(attributes = {})` + + +#### `collection.create!(attributes = {})` -作用和 `collection.create` 相同,但如果记录不合法会抛出 `ActiveRecord::RecordInvalid` 异常。 +作用与 `collection.create` 相同,但如果记录无效,会抛出 `ActiveRecord::RecordInvalid` 异常。 + + #### `has_many` 方法的选项 -Rails 的默认设置足够智能,能满足常见需求。但有时还是需要定制 `has_many` 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项: +Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 `has_many` 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, dependent: :delete_all, validate: :false +class Author < ApplicationRecord + has_many :books, dependent: :delete_all, validate: false end ``` `has_many` 关联支持以下选项: -* `:as` -* `:autosave` -* `:class_name` -* `:dependent` -* `:foreign_key` -* `:inverse_of` -* `:primary_key` -* `:source` -* `:source_type` -* `:through` -* `:validate` +* `:as` +* `:autosave` +* `:class_name` +* `:counter_cache` +* `:dependent` +* `:foreign_key` +* `:inverse_of` +* `:primary_key` +* `:source` +* `:source_type` +* `:through` +* `:validate` + + ##### `:as` `:as` 选项表明这是多态关联。[前文](#polymorphic-associations)已经详细介绍过多态关联。 + + ##### `:autosave` 如果把 `:autosave` 选项设为 `true`,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。 + + ##### `:class_name` -如果另一个模型无法从关联的名字获取,可以使用 `:class_name` 选项指定模型名。例如,顾客有多个订单,但表示订单的模型是 `Transaction`,就可以这样声明关联: +如果另一个模型无法从关联的名称获取,可以使用 `:class_name` 选项指定模型名。例如,一位作者有多本图书,但表示图书的模型是 `Transaction`,那么可以这样声明关联: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, class_name: "Transaction" +class Author < ApplicationRecord + has_many :books, class_name: "Transaction" end ``` + + +##### `:counter_cache` + +这个选项用于定制计数缓存列的名称。仅当定制了 `belongs_to` 关联的 `:counter_cache` 选项时才需要设定这个选项。 + + + ##### `:dependent` -设置销毁拥有者时要怎么处理关联对象: +设置销毁属主时怎么处理关联的对象: -* `:destroy`:也销毁所有关联的对象; -* `:delete_all`:直接把所有关联对象对数据库中删除,因此不会执行回调; -* `:nullify`:把外键设为 `NULL`,不会执行回调; -* `:restrict_with_exception`:有关联的对象时抛出异常; -* `:restrict_with_error`:有关联的对象时,向拥有者添加一个错误; +* `:destroy`:也销毁所有关联的对象; +* `:delete_all`:直接把所有关联的对象从数据库中删除(不执行回调); +* `:nullify`:把外键设为 `NULL`,不执行回调; +* `:restrict_with_exception`:有关联的对象时抛出异常; +* `:restrict_with_error`:有关联的对象时,向属主添加一个错误; -NOTE: 如果声明关联时指定了 `:through` 选项,会忽略这个选项。 + ##### `:foreign_key` -按照约定,另一个模型中用来存储外键的字段名是模型名后加 `_id`。`:foreign_key` 选项可以设置要使用的外键名: +按照约定,另一个模型中用来存储外键的字段名是模型名后加 `_id`。`:foreign_key` 选项用于设置要使用的外键名: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, foreign_key: "cust_id" +class Author < ApplicationRecord + has_many :books, foreign_key: "cust_id" end ``` TIP: 不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。 + + ##### `:inverse_of` `:inverse_of` 选项指定 `has_many` 关联另一端的 `belongs_to` 关联名。不能和 `:through` 或 `:as` 选项一起使用。 ```ruby -class Customer < ActiveRecord::Base - has_many :orders, inverse_of: :customer +class Author < ApplicationRecord + has_many :books, inverse_of: :author end -class Order < ActiveRecord::Base - belongs_to :customer, inverse_of: :orders +class Book < ApplicationRecord + belongs_to :author, inverse_of: :books end ``` + + ##### `:primary_key` -按照约定,用来存储该模型主键的字段名 `id`。`:primary_key` 选项可以设置要使用的主键名。 +按照约定,用来存储该模型主键的字段名为 `id`。`:primary_key` 选项用于设置要使用的主键名。 -假设 `users` 表的主键是 `id`,但还有一个 `guid` 字段。根据要求,`todos` 表中应该使用 `guid` 字段,而不是 `id` 字段。这种需求可以这么实现: +假设 `users` 表的主键是 `id`,但还有一个 `guid` 列。根据要求,`todos` 表中应该使用 `guid` 列作为外键,而不是 `id` 列。这种需求可以这么实现: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_many :todos, primary_key: :guid end ``` -如果执行 `@user.todos.create` 创建新的待办事项,那么 `@todo.user_id` 就是 `guid` 字段中的值。 +如果执行 `@todo = @user.todos.create` 创建新的待办事项,那么 `@todo.user_id` 就是 `@user` 记录中 `guid` 字段的值。 + + ##### `:source` -`:source` 选项指定 `has_many :through` 关联的关联源名字。只有无法从关联名种解出关联源的名字时才需要设置这个选项。 +`:source` 选项指定 `has_many :through` 关联的源关联名称。只有无法从关联名中解出源关联的名称时才需要设置这个选项。 + + ##### `:source_type` -`:source_type` 选项指定 `has_many :through` 关联中用来处理多态关联的关联源类型。 +`:source_type` 选项指定通过多态关联处理 `has_many :through` 关联的源关联类型。 + + ##### `:through` -`:through` 选项指定用来执行查询的连接模型。`has_many :through` 关联是实现多对多关联的一种方式,[前文](#the-has-many-through-association)已经介绍过。 +`:through` 选项指定一个联结模型,查询通过它执行。[前文](#the-has-many-through-association)说过,`has_many :through` 关联是实现多对多关联的方式之一。 + + ##### `:validate` -如果把 `:validate` 选项设为 `false`,保存对象时,不会验证关联对象。该选项的默认值是 `true`,保存对象验证关联的对象。 +如果把 `:validate` 选项设为 `false`,保存对象时,不验证关联的对象。该选项的默认值是 `true`,即保存对象时验证关联的对象。 + + #### `has_many` 的作用域 -有时可能需要定制 `has_many` 关联使用的查询方式,定制的查询可在作用域代码块中指定。例如: +有时可能需要定制 `has_many` 关联使用的查询。定制的查询在作用域代码块中指定。例如: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { where processed: true } +class Author < ApplicationRecord + has_many :books, -> { where processed: true } end ``` -在作用域代码块中可以使用任何一个标准的[查询方法](active_record_querying.html)。下面分别介绍这几个方法: +在作用域代码块中可以使用任何一个标准的[查询方法](active_record_querying.html)。下面介绍其中几个: + +* `where` +* `extending` +* `group` +* `includes` +* `limit` +* `offset` +* `order` +* `readonly` +* `select` +* `distinct` -* `where` -* `extending` -* `group` -* `includes` -* `limit` -* `offset` -* `order` -* `readonly` -* `select` -* `uniq` + ##### `where` -`where` 方法指定关联对象必须满足的条件。 +`where` 方法指定关联的对象必须满足的条件。 ```ruby -class Customer < ActiveRecord::Base - has_many :confirmed_orders, -> { where "confirmed = 1" }, - class_name: "Order" +class Author < ApplicationRecord + has_many :confirmed_books, -> { where "confirmed = 1" }, + class_name: "Book" end ``` -条件还可以使用 Hash 的形式指定: +条件还可以使用散列指定: ```ruby -class Customer < ActiveRecord::Base - has_many :confirmed_orders, -> { where confirmed: true }, - class_name: "Order" +class Author < ApplicationRecord + has_many :confirmed_books, -> { where confirmed: true }, + class_name: "Book" end ``` -如果 `where` 使用 Hash 形式,通过这个关联创建的记录会自动使用 Hash 中的作用域。针对上面的例子,使用 `@customer.confirmed_orders.create` 或 `@customer.confirmed_orders.build` 创建订单时,会自动把 `confirmed` 字段的值设为 `true`。 +如果 `where` 使用散列形式,通过这个关联创建的记录会自动使用散列中的作用域。针对上面的例子,使用 `@author.confirmed_books.create` 或 `@author.confirmed_books.build` 创建图书时,会自动把 `confirmed` 列的值设为 `true`。 + + ##### `extending` -`extending` 方法指定一个模块名,用来扩展关联代理。[后文](#association-extensions)会详细介绍关联扩展。 +`extending` 方法指定一个模块名,用于扩展关联代理。[后文](#association-extensions)会详细介绍关联扩展。 + + ##### `group` `group` 方法指定一个属性名,用在 SQL `GROUP BY` 子句中,分组查询结果。 ```ruby -class Customer < ActiveRecord::Base - has_many :line_items, -> { group 'orders.id' }, - through: :orders +class Author < ApplicationRecord + has_many :line_items, -> { group 'books.id' }, + through: :books end ``` + + ##### `includes` -`includes` 方法指定使用关联时要按需加载的间接关联。例如,有如下的模型: +`includes` 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型: ```ruby -class Customer < ActiveRecord::Base - has_many :orders +class Author < ApplicationRecord + has_many :books end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end ``` -如果经常要直接获取顾客购买的商品(`@customer.orders.line_items`),就可以把商品引入顾客和订单的关联中: +如果经常要直接获取作者购买的商品(`@author.books.line_items`),可以把商品引入作者和图书的关联中: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { includes :line_items } +class Author < ApplicationRecord + has_many :books, -> { includes :line_items } end -class Order < ActiveRecord::Base - belongs_to :customer +class Book < ApplicationRecord + belongs_to :author has_many :line_items end -class LineItem < ActiveRecord::Base - belongs_to :order +class LineItem < ApplicationRecord + belongs_to :book end ``` + + ##### `limit` `limit` 方法限制通过关联获取的对象数量。 ```ruby -class Customer < ActiveRecord::Base - has_many :recent_orders, - -> { order('order_date desc').limit(100) }, - class_name: "Order", +class Author < ApplicationRecord + has_many :recent_books, + -> { order('published_at desc').limit(100) }, + class_name: "Book", end ``` + + ##### `offset` `offset` 方法指定通过关联获取对象时的偏移量。例如,`-> { offset(11) }` 会跳过前 11 个记录。 + + ##### `order` -`order` 方法指定获取关联对象时使用的排序方式,用于 SQL `ORDER BY` 子句。 +`order` 方法指定获取关联对象时使用的排序方式,用在 SQL `ORDER BY` 子句中。 ```ruby -class Customer < ActiveRecord::Base - has_many :orders, -> { order "date_confirmed DESC" } +class Author < ApplicationRecord + has_many :books, -> { order "date_confirmed DESC" } end ``` + + ##### `readonly` -如果使用 `readonly`,通过关联获取的对象就是只读的。 +如果使用 `readonly`,通过关联获取的对象是只读的。 + + ##### `select` -`select` 方法用来覆盖获取关联对象数据的 SQL `SELECT` 子句。默认情况下,Rails 会读取所有字段。 +`select` 方法用于覆盖检索关联对象数据的 SQL `SELECT` 子句。默认情况下,Rails 会检索所有列。 + +WARNING: 如果设置 `select` 选项,记得要包含主键和关联模型的外键。否则,Rails 会抛出异常。 -WARNING: 如果设置了 `select`,记得要包含主键和关联模型的外键。否则,Rails 会抛出异常。 + ##### `distinct` -使用 `distinct` 方法可以确保集合中没有重复的对象,和 `:through` 选项一起使用最有用。 +使用 `distinct` 方法可以确保集合中没有重复的对象。与 `:through` 选项一起使用最有用。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :readings - has_many :posts, through: :readings + has_many :articles, through: :readings end person = Person.create(name: 'John') -post = Post.create(name: 'a1') -person.posts << post -person.posts << post -person.posts.inspect # => [#, #] -Reading.all.inspect # => [#, #] +article = Article.create(name: 'a1') +person.articles << article +person.articles << article +person.articles.inspect # => [#
, #
] +Reading.all.inspect # => [#, #] ``` -在上面的代码中,读者读了两篇文章,即使是同一篇文章,`person.posts` 也会返回两个对象。 +在上面的代码中,读者读了两篇文章,即使是同一篇文章,`person.articles` 也会返回两个对象。 -下面我们加入 `distinct` 方法: +下面加入 `distinct` 方法: ```ruby class Person has_many :readings - has_many :posts, -> { distinct }, through: :readings + has_many :articles, -> { distinct }, through: :readings end person = Person.create(name: 'Honda') -post = Post.create(name: 'a1') -person.posts << post -person.posts << post -person.posts.inspect # => [#] -Reading.all.inspect # => [#, #] +article = Article.create(name: 'a1') +person.articles << article +person.articles << article +person.articles.inspect # => [#
] +Reading.all.inspect # => [#, #] ``` -在这段代码中,读者还是读了两篇文章,但 `person.posts` 只返回一个对象,因为加载的集合已经去除了重复元素。 +在这段代码中,读者还是读了两篇文章,但 `person.articles` 只返回一个对象,因为加载的集合已经去除了重复元素。 -如果要确保只把不重复的记录写入关联模型的数据表(这样就不会从数据库中获取重复记录了),需要在数据表上添加唯一性索引。例如,数据表名为 `person_posts`,我们要保证其中所有的文章都没重复,可以在迁移中加入以下代码: +如果要确保只把不重复的记录写入关联模型的数据表(这样就不会从数据库中获取重复记录了),需要在数据表上添加唯一性索引。例如,数据表名为 `readings`,我们要保证其中所有的文章都没重复,可以在迁移中加入以下代码: ```ruby -add_index :person_posts, :post, unique: true +add_index :readings, [:person_id, :article_id], unique: true +``` + +添加唯一性索引之后,尝试为同一个人添加两篇相同的文章会抛出 `ActiveRecord::RecordNotUnique` 异常: + +```ruby +person = Person.create(name: 'Honda') +article = Article.create(name: 'a1') +person.articles << article +person.articles << article # => ActiveRecord::RecordNotUnique ``` 注意,使用 `include?` 等方法检查唯一性可能导致条件竞争。不要使用 `include?` 确保关联的唯一性。还是以前面的文章模型为例,下面的代码会导致条件竞争,因为多个用户可能会同时执行这一操作: ```ruby -person.posts << post unless person.posts.include?(post) +person.articles << article unless person.articles.include?(article) ``` + + #### 什么时候保存对象 把对象赋值给 `has_many` 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存。 -如果无法通过验证,随便哪一次保存失败了,赋值语句就会返回 `false`,赋值操作会取消。 +如果由于无法通过验证而导致保存失败,赋值语句返回 `false`,赋值操作会取消。 如果父对象(`has_many` 关联声明所在的模型)没保存(`new_record?` 方法返回 `true`),那么子对象也不会保存。只有保存了父对象,才会保存子对象。 -如果赋值给 `has_many` 关联时不想保存对象,可以使用 `collection.build` 方法。 +如果赋值给 `has_many` 关联时不想保存对象,使用 `collection.build` 方法。 + + ### `has_and_belongs_to_many` 关联详解 -`has_and_belongs_to_many` 关联建立两个模型之间的多对多关系。用数据库的行话说,这种关联的意思是有个连接数据表包含指向这两个类的外键。 +`has_and_belongs_to_many` 关联建立两个模型之间的多对多关系。用数据库术语来说,这种关联的意思是有个联结表包含指向这两个类的外键。 + + #### `has_and_belongs_to_many` 关联添加的方法 声明 `has_and_belongs_to_many` 关联后,声明所在的类自动获得了 16 个关联相关的方法: -* `collection(force_reload = false)` -* `collection<<(object, ...)` -* `collection.delete(object, ...)` -* `collection.destroy(object, ...)` -* `collection=objects` -* `collection_singular_ids` -* `collection_singular_ids=ids` -* `collection.clear` -* `collection.empty?` -* `collection.size` -* `collection.find(...)` -* `collection.where(...)` -* `collection.exists?(...)` -* `collection.build(attributes = {})` -* `collection.create(attributes = {})` -* `collection.create!(attributes = {})` - -这些个方法中的 `collection` 要替换成传入 `has_and_belongs_to_many` 方法的第一个参数。`collection_singular` 要替换成第一个参数的单数形式。例如,如下的声明: - -```ruby -class Part < ActiveRecord::Base +* `collection` +* `collection<<(object, …​)` +* `collection.delete(object, …​)` +* `collection.destroy(object, …​)` +* `collection=(objects)` +* `collection_singular_ids` +* `collection_singular_ids=(ids)` +* `collection.clear` +* `collection.empty?` +* `collection.size` +* `collection.find(…​)` +* `collection.where(…​)` +* `collection.exists?(…​)` +* `collection.build(attributes = {})` +* `collection.create(attributes = {})` +* `collection.create!(attributes = {})` + +这些个方法中的 `collection` 要替换成传给 `has_and_belongs_to_many` 方法的第一个参数。`collection_singular` 要替换成第一个参数的单数形式。对如下的声明来说: + +```ruby +class Part < ApplicationRecord has_and_belongs_to_many :assemblies end ``` @@ -1795,13 +2166,13 @@ end 每个 `Part` 模型实例都获得了这些方法: ```ruby -assemblies(force_reload = false) +assemblies assemblies<<(object, ...) assemblies.delete(object, ...) assemblies.destroy(object, ...) -assemblies=objects +assemblies=(objects) assembly_ids -assembly_ids=ids +assembly_ids=(ids) assemblies.clear assemblies.empty? assemblies.size @@ -1813,13 +2184,17 @@ assemblies.create(attributes = {}) assemblies.create!(attributes = {}) ``` -##### 额外的字段方法 + + +##### 额外的列方法 + +如果 `has_and_belongs_to_many` 关联使用的联结表中,除了两个外键之外还有其他列,通过关联获取的记录中会包含这些列,但是只读的,因为 Rails 不知道如何保存对这些列的改动。 -如果 `has_and_belongs_to_many` 关联使用的连接数据表中,除了两个外键之外还有其他字段,通过关联获取的记录中会包含这些字段,但是只读字段,因为 Rails 不知道如何保存对这些字段的改动。 +WARNING: 在 `has_and_belongs_to_many` 关联的联结表中使用其他字段的功能已经废弃。如果在多对多关联中需要使用这么复杂的数据表,应该用 `has_many :through` 关联代替 `has_and_belongs_to_many` 关联。 -WARNING: 在 `has_and_belongs_to_many` 关联的连接数据表中使用其他字段的功能已经废弃。如果在多对多关联中需要使用这么复杂的数据表,可以用 `has_many :through` 关联代替 `has_and_belongs_to_many` 关联。 + -##### `collection(force_reload = false)` +##### `collection` `collection` 方法返回一个数组,包含所有关联的对象。如果没有关联的对象,则返回空数组。 @@ -1827,141 +2202,172 @@ WARNING: 在 `has_and_belongs_to_many` 关联的连接数据表中使用其他 @assemblies = @part.assemblies ``` -##### `collection<<(object, ...)` + -`collection<<` 方法向关联对象数组中添加一个或多个对象,并在连接数据表中创建相应的记录。 +##### `collection<<(object, …​)` + +`collection<<` 方法向集合中添加一个或多个对象,并在联结表中创建相应的记录。 ```ruby @part.assemblies << @assembly1 ``` -NOTE: 这个方法与 `collection.concat` 和 `collection.push` 是同名方法。 +NOTE: 这个方法是 `collection.concat` 和 `collection.push` 的别名。 + + -##### `collection.delete(object, ...)` +##### `collection.delete(object, …​)` -`collection.delete` 方法从关联对象数组中删除一个或多个对象,并删除连接数据表中相应的记录。 +`collection.delete` 方法从集合中删除一个或多个对象,并删除联结表中相应的记录,但是不会销毁对象。 ```ruby @part.assemblies.delete(@assembly1) ``` -WARNING: 这个方法不会触发连接记录上的回调。 + -##### `collection.destroy(object, ...)` +##### `collection.destroy(object, …​)` -`collection.destroy` 方法在连接数据表中的记录上调用 `destroy` 方法,从关联对象数组中删除一个或多个对象,还会触发回调。这个方法不会销毁对象本身。 +`collection.destroy` 方法把集合中指定对象在联结表中的记录删除。这个方法不会销毁对象本身。 ```ruby @part.assemblies.destroy(@assembly1) ``` -##### `collection=objects` + + +##### `collection=(objects)` -`collection=` 让关联对象数组只包含指定的对象,根据需求会添加或删除对象。 +`collection=` 方法让集合只包含指定的对象,根据需求会添加或删除对象。改动会持久存入数据库。 + + ##### `collection_singular_ids` -`collection_singular_ids` 返回一个数组,包含关联对象数组中各对象的 ID。 +`collection_singular_ids` 方法返回一个数组,包含集合中各对象的 ID。 ```ruby @assembly_ids = @part.assembly_ids ``` -##### `collection_singular_ids=ids` + + +##### `collection_singular_ids=(ids)` -`collection_singular_ids=` 方法让数组中只包含指定的主键,根据需要增删 ID。 +`collection_singular_ids=` 方法让集合中只包含指定的主键,根据需要会增删 ID。改动会持久存入数据库。 + + ##### `collection.clear` -`collection.clear` 方法删除数组中的所有对象,并把连接数据表中的相应记录删除。这个方法不会销毁关联对象。 +`collection.clear` 方法删除集合中的所有对象,并把联结表中的相应记录删除。这个方法不会销毁关联的对象。 + + ##### `collection.empty?` -如果关联数组中没有关联对象,`collection.empty?` 方法返回 `true`。 +如果集合中没有任何关联的对象,`collection.empty?` 方法返回 `true`。 -```ruby +```erb <% if @part.assemblies.empty? %> This part is not used in any assemblies <% end %> ``` + + ##### `collection.size` -`collection.size` 返回关联对象数组中的对象数量。 +`collection.size` 方法返回集合中的对象数量。 ```ruby @assembly_count = @part.assemblies.size ``` -##### `collection.find(...)` + + +##### `collection.find(…​)` -`collection.find` 方法在关联对象数组中查找对象,句法和可用选项跟 `ActiveRecord::Base.find` 方法一样。同时还限制对象必须在集合中。 +`collection.find` 方法在集合中查找对象,使用的句法和选项跟 `ActiveRecord::Base.find` 方法一样。此外还限制对象必须在集合中。 ```ruby @assembly = @part.assemblies.find(1) ``` -##### `collection.where(...)` + + +##### `collection.where(…​)` -`collection.where` 方法根据指定的条件在关联对象数组中查找对象,但会惰性加载对象,用到对象时才会执行查询。同时还限制对象必须在集合中。 +`collection.where` 方法根据指定的条件在集合中查找对象,但对象是惰性加载的,访问对象时才执行查询。此外还限制对象必须在集合中。 ```ruby @new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago) ``` -##### `collection.exists?(...)` + + +##### `collection.exists?(…​)` + +`collection.exists?` 方法根据指定的条件检查集合中是否有符合条件的对象,使用的句法和选项跟 `ActiveRecord::Base.exists?` 方法一样。 -`collection.exists?` 方法根据指定的条件检查关联对象数组中是否有符合条件的对象,句法和可用选项跟 `ActiveRecord::Base.exists?` 方法一样。 + ##### `collection.build(attributes = {})` -`collection.build` 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在连接数据表中创建对应的记录,但不会保存关联对象。 +`collection.build` 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在联结表中创建对应的记录,但不会保存关联的对象。 ```ruby @assembly = @part.assemblies.build({assembly_name: "Transmission housing"}) ``` + + ##### `collection.create(attributes = {})` -`collection.create` 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在连接数据表中创建对应的记录,只要能通过所有数据验证,就会保存关联对象。 +`collection.create` 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在联结表中创建对应的记录,只要能通过所有数据验证,就保存关联对象。 ```ruby @assembly = @part.assemblies.create({assembly_name: "Transmission housing"}) ``` + + ##### `collection.create!(attributes = {})` -作用和 `collection.create` 相同,但如果记录不合法会抛出 `ActiveRecord::RecordInvalid` 异常。 +作用和 `collection.create` 相同,但如果记录无效,会抛出 `ActiveRecord::RecordInvalid` 异常。 + + #### `has_and_belongs_to_many` 方法的选项 -Rails 的默认设置足够智能,能满足常见需求。但有时还是需要定制 `has_and_belongs_to_many` 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项: +Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 `has_and_belongs_to_many` 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项: ```ruby -class Parts < ActiveRecord::Base - has_and_belongs_to_many :assemblies, autosave: true, - readonly: true +class Parts < ApplicationRecord + has_and_belongs_to_many :assemblies, -> { readonly }, + autosave: true end ``` `has_and_belongs_to_many` 关联支持以下选项: -* `:association_foreign_key` -* `:autosave` -* `:class_name` -* `:foreign_key` -* `:join_table` -* `:validate` -* `:readonly` +* `:association_foreign_key` +* `:autosave` +* `:class_name` +* `:foreign_key` +* `:join_table` +* `:validate` + + ##### `:association_foreign_key` -按照约定,在连接数据表中用来指向另一个模型的外键名是模型名后加 `_id`。`:association_foreign_key` 选项可以设置要使用的外键名: +按照约定,在联结表中用来指向另一个模型的外键名是模型名后加 `_id`。`:association_foreign_key` 选项用于设置要使用的外键名: -TIP: `:foreign_key` 和 `:association_foreign_key` 这两个选项在设置多对多自连接时很有用。 +TIP: `:foreign_key` 和 `:association_foreign_key` 这两个选项在设置多对多自联结时很有用。例如: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_and_belongs_to_many :friends, class_name: "User", foreign_key: "this_user_id", @@ -1969,26 +2375,33 @@ class User < ActiveRecord::Base end ``` + + + ##### `:autosave` 如果把 `:autosave` 选项设为 `true`,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。 + + ##### `:class_name` -如果另一个模型无法从关联的名字获取,可以使用 `:class_name` 选项指定模型名。例如,一个部件由多个装配件组成,但表示装配件的模型是 `Gadget`,就可以这样声明关联: +如果另一个模型无法从关联的名称获取,可以使用 `:class_name` 选项指定。例如,一个部件由多个装配件组成,但表示装配件的模型是 `Gadget`,那么可以这样声明关联: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, class_name: "Gadget" end ``` + + ##### `:foreign_key` -按照约定,在连接数据表中用来指向模型的外键名是模型名后加 `_id`。`:foreign_key` 选项可以设置要使用的外键名: +按照约定,在联结表中用来指向模型的外键名是模型名后加 `_id`。`:foreign_key` 选项用于设置要使用的外键名: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord has_and_belongs_to_many :friends, class_name: "User", foreign_key: "this_user_id", @@ -1996,178 +2409,210 @@ class User < ActiveRecord::Base end ``` + + ##### `:join_table` -如果默认按照字典顺序生成的默认名不能满足要求,可以使用 `:join_table` 选项指定。 +如果默认按照字典顺序生成的联结表名不能满足要求,可以使用 `:join_table` 选项指定。 + + ##### `:validate` -如果把 `:validate` 选项设为 `false`,保存对象时,不会验证关联对象。该选项的默认值是 `true`,保存对象验证关联的对象。 +如果把 `:validate` 选项设为 `false`,保存对象时,不会验证关联的对象。该选项的默认值是 `true`,即保存对象时验证关联的对象。 + + #### `has_and_belongs_to_many` 的作用域 -有时可能需要定制 `has_and_belongs_to_many` 关联使用的查询方式,定制的查询可在作用域代码块中指定。例如: +有时可能需要定制 `has_and_belongs_to_many` 关联使用的查询。定制的查询在作用域代码块中指定。例如: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where active: true } end ``` -在作用域代码块中可以使用任何一个标准的[查询方法](active_record_querying.html)。下面分别介绍这几个方法: +在作用域代码块中可以使用任何一个标准的[查询方法](active_record_querying.html)。下面分别介绍其中几个: -* `where` -* `extending` -* `group` -* `includes` -* `limit` -* `offset` -* `order` -* `readonly` -* `select` -* `uniq` +* `where` +* `extending` +* `group` +* `includes` +* `limit` +* `offset` +* `order` +* `readonly` +* `select` +* `distinct` + + ##### `where` -`where` 方法指定关联对象必须满足的条件。 +`where` 方法指定关联的对象必须满足的条件。 ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where "factory = 'Seattle'" } end ``` -条件还可以使用 Hash 的形式指定: +条件还可以使用散列指定: ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { where factory: 'Seattle' } end ``` -如果 `where` 使用 Hash 形式,通过这个关联创建的记录会自动使用 Hash 中的作用域。针对上面的例子,使用 `@parts.assemblies.create` 或 `@parts.assemblies.build` 创建订单时,会自动把 `factory` 字段的值设为 `"Seattle"`。 +如果 `where` 使用散列形式,通过这个关联创建的记录会自动使用散列中的作用域。针对上面的例子,使用 `@parts.assemblies.create` 或 `@parts.assemblies.build` 创建订单时,会自动把 `factory` 字段的值设为 `"Seattle"`。 + + ##### `extending` `extending` 方法指定一个模块名,用来扩展关联代理。[后文](#association-extensions)会详细介绍关联扩展。 + + ##### `group` `group` 方法指定一个属性名,用在 SQL `GROUP BY` 子句中,分组查询结果。 ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { group "factory" } end ``` + + ##### `includes` -`includes` 方法指定使用关联时要按需加载的间接关联。 +`includes` 方法指定使用关联时要及早加载的间接关联。 + + ##### `limit` `limit` 方法限制通过关联获取的对象数量。 ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { order("created_at DESC").limit(50) } end ``` + + ##### `offset` `offset` 方法指定通过关联获取对象时的偏移量。例如,`-> { offset(11) }` 会跳过前 11 个记录。 + + ##### `order` -`order` 方法指定获取关联对象时使用的排序方式,用于 SQL `ORDER BY` 子句。 +`order` 方法指定获取关联对象时使用的排序方式,用在 SQL `ORDER BY` 子句中。 ```ruby -class Parts < ActiveRecord::Base +class Parts < ApplicationRecord has_and_belongs_to_many :assemblies, -> { order "assembly_name ASC" } end ``` + + ##### `readonly` -如果使用 `readonly`,通过关联获取的对象就是只读的。 +如果使用 `readonly`,通过关联获取的对象是只读的。 + + ##### `select` -`select` 方法用来覆盖获取关联对象数据的 SQL `SELECT` 子句。默认情况下,Rails 会读取所有字段。 +`select` 方法用于覆盖检索关联对象数据的 SQL `SELECT` 子句。默认情况下,Rails 检索所有列。 -##### `uniq` + -`uniq` 方法用来删除集合中重复的对象。 +##### `distinct` + +`distinct` 方法用于删除集合中重复的对象。 + + #### 什么时候保存对象 把对象赋值给 `has_and_belongs_to_many` 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存。 -如果无法通过验证,随便哪一次保存失败了,赋值语句就会返回 `false`,赋值操作会取消。 +如果由于无法通过验证而导致保存失败,赋值语句返回 `false`,赋值操作会取消。 如果父对象(`has_and_belongs_to_many` 关联声明所在的模型)没保存(`new_record?` 方法返回 `true`),那么子对象也不会保存。只有保存了父对象,才会保存子对象。 -如果赋值给 `has_and_belongs_to_many` 关联时不想保存对象,可以使用 `collection.build` 方法。 +如果赋值给 `has_and_belongs_to_many` 关联时不想保存对象,使用 `collection.build` 方法。 + + ### 关联回调 -普通回调会介入 Active Record 对象的生命周期,在很多时刻处理对象。例如,可以使用 `:before_save` 回调在保存对象之前处理对象。 +普通回调会介入 Active Record 对象的生命周期,在多个时刻处理对象。例如,可以使用 `:before_save` 回调在保存对象之前处理对象。 关联回调和普通回调差不多,只不过由集合生命周期中的事件触发。关联回调有四种: -* `before_add` -* `after_add` -* `before_remove` -* `after_remove` +* `before_add` +* `after_add` +* `before_remove` +* `after_remove` 关联回调在声明关联时定义。例如: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, before_add: :check_credit_limit +class Author < ApplicationRecord + has_many :books, before_add: :check_credit_limit - def check_credit_limit(order) + def check_credit_limit(book) ... end end ``` -Rails 会把添加或删除的对象传入回调。 +Rails 会把要添加或删除的对象传入回调。 -同一事件可触发多个回调,多个回调使用数组指定: +同一事件可以触发多个回调,多个回调使用数组指定: ```ruby -class Customer < ActiveRecord::Base - has_many :orders, +class Author < ApplicationRecord + has_many :books, before_add: [:check_credit_limit, :calculate_shipping_charges] - def check_credit_limit(order) + def check_credit_limit(book) ... end - def calculate_shipping_charges(order) + def calculate_shipping_charges(book) ... end end ``` -如果 `before_add` 回调抛出异常,不会把对象加入集合。类似地,如果 `before_remove` 抛出异常,对象不会从集合中删除。 +如果 `before_add` 回调抛出异常,不会把对象添加到集合中。类似地,如果 `before_remove` 抛出异常,对象不会从集合中删除。 + + ### 关联扩展 -Rails 基于关联代理对象自动创建的功能是死的,但是可以通过匿名模块、新的查询方法、创建对象的方法等进行扩展。例如: +Rails 基于关联代理对象自动创建的功能是死的,可以通过匿名模块、新的查找方法、创建对象的方法等进行扩展。例如: ```ruby -class Customer < ActiveRecord::Base - has_many :orders do - def find_by_order_prefix(order_number) - find_by(region_id: order_number[0..2]) +class Author < ApplicationRecord + has_many :books do + def find_by_book_prefix(book_number) + find_by(category_id: book_number[0..2]) end end end @@ -2182,17 +2627,72 @@ module FindRecentExtension end end -class Customer < ActiveRecord::Base - has_many :orders, -> { extending FindRecentExtension } +class Author < ApplicationRecord + has_many :books, -> { extending FindRecentExtension } end -class Supplier < ActiveRecord::Base +class Supplier < ApplicationRecord has_many :deliveries, -> { extending FindRecentExtension } end ``` 在扩展中可以使用如下 `proxy_association` 方法的三个属性获取关联代理的内部信息: -* `proxy_association.owner`:返回关联所属的对象; -* `proxy_association.reflection`:返回描述关联的反射对象; -* `proxy_association.target`:返回 `belongs_to` 或 `has_one` 关联的关联对象,或者 `has_many` 或 `has_and_belongs_to_many` 关联的关联对象集合; +* `proxy_association.owner`:返回关联所属的对象; +* `proxy_association.reflection`:返回描述关联的反射对象; +* `proxy_association.target`:返回 `belongs_to` 或 `has_one` 关联的关联对象,或者 `has_many` 或 `has_and_belongs_to_many` 关联的关联对象集合; + + + +## 单表继承 + +有时可能想在不同的模型中共用相同的字段和行为。假如有 Car、Motorcycle 和 Bicycle 三个模型,我们想在它们中共用 `color` 和 `price` 字段,但是各自的具体行为不同,而且使用不同的控制器。 + +在 Rails 中实现这一需求非常简单。首先,生成基模型 Vehicle: + +```sh +$ rails generate model vehicle type:string color:string price:decimal{10.2} +``` + +注意到了吗,我们添加了一个“type”字段?既然所有模型都保存在这一个数据库表中,Rails 会把保存的模型名存储在这一列中。对这个例子来说,“type”字段的值可能是“Car”、“Motorcycle”或“Bicycle”。如果表中没有“type”字段,单表继承无法工作。 + +然后,生成三个模型,都继承自 Vehicle。为此,可以使用 `parent=PARENT` 选项。这样,生成的模型继承指定的父模型,而且不生成对应的迁移(因为表已经存在)。 + +例如,生成 Car 模型的命令是: + +```sh +$ rails generate model car --parent=Vehicle +``` + +生成的模型如下: + +```ruby +class Car < Vehicle +end +``` + +这意味着,添加到 Vehicle 中的所有行为在 Car 中都可用,例如关联、公开方法,等等。 + +创建一辆汽车,相应的记录保存在 `vehicles` 表中,而且 `type` 字段的值是“Car”: + +```ruby +Car.create(color: 'Red', price: 10000) +``` + +对应的 SQL 如下: + +```sql +INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000) +``` + +查询汽车记录时只会搜索此类车辆: + +```ruby +Car.all +``` + +执行的查询如下: + +```sql +SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car') +``` diff --git a/source/zh-CN/autoloading_and_reloading_constants.md b/source/zh-CN/autoloading_and_reloading_constants.md new file mode 100644 index 0000000..f578331 --- /dev/null +++ b/source/zh-CN/autoloading_and_reloading_constants.md @@ -0,0 +1,1083 @@ +# 自动加载和重新加载常量 + +本文说明常量自动加载和重新加载机制。 + +读完本文后,您将学到: + +* Ruby 常量的关键知识; +* `autoload_paths` 是什么; +* 常量是如何自动加载的; +* `require_dependency` 是什么; +* 常量是如何重新加载的; +* 自动加载常见问题的解决方案。 + +----------------------------------------------------------------------------- + + + +## 简介 + +编写 Ruby on Rails 应用时,代码会预加载。 + +在常规的 Ruby 程序中,类需要加载依赖: + +```ruby +require 'application_controller' +require 'post' + +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +Ruby 程序员的直觉立即就能发现这样做有冗余:如果类定义所在的文件与类名一致,难道不能通过某种方式自动加载吗?我们无需扫描文件寻找依赖,这样不可靠。 + +而且,`Kernel#require` 只加载文件一次,如果修改后无需重启服务器,那么开发的过程就更为平顺。如果能在开发环境中使用 `Kernel#load`,而在生产环境使用 `Kernel#require`,那该多好。 + +其实,Ruby on Rails 就有这样的功能,我们刚才已经用到了: + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +本文说明这一机制的运作原理。 + + + +## 常量刷新程序 + +在多数编程语言中,常量不是那么重要,但在 Ruby 中却是一个内容丰富的话题。 + +本文不会详解 Ruby 常量,但是会重点说明关键的概念。掌握以下几小节的内容对理解常量自动加载和重新加载有所帮助。 + + + +### 嵌套 + +类和模块定义可以嵌套,从而创建命名空间: + +```ruby +module XML + class SAXParser + # (1) + end +end +``` + +类和模块的嵌套由内向外展开。嵌套可以通过 `Module.nesting` 方法审查。例如,在上述示例中,(1) 处的嵌套是 + +``` +[XML::SAXParser, XML] +``` + +注意,组成嵌套的是类和模块“对象”,而不是访问它们的常量,与它们的名称也没有关系。 + +例如,对下面的定义来说 + +```ruby +class XML::SAXParser + # (2) +end +``` + +虽然作用跟前一个示例类似,但是 (2) 处的嵌套是 + +``` +[XML::SAXParser] +``` + +不含“XML”。 + +从这个示例可以看出,嵌套中的类或模块的名称与所在的命名空间没有必然联系。 + +事实上,二者毫无关系。比如说: + +```ruby +module X + module Y + end +end + +module A + module B + end +end + +module X::Y + module A::B + # (3) + end +end +``` + +(3) 处的嵌套包含两个模块对象: + +``` +[A::B, X::Y] +``` + +可以看出,嵌套的最后不是“A”,甚至不含“A”,但是包含 `X::Y`,而且它与 `A::B` 无关。 + +嵌套是解释器维护的一个内部堆栈,根据下述规则修改: + +* 执行 `class` 关键字后面的定义体时,类对象入栈;执行完毕后出栈。 +* 执行 `module` 关键字后面的定义体时,模块对象入栈;执行完毕后出栈。 +* 执行 `class << object` 打开的单例类时,类对象入栈;执行完毕后出栈。 +* 调用 `instance_eval` 时如果传入字符串参数,接收者的单例类入栈求值的代码所在的嵌套层次。调用 `class_eval` 或 `module_eval` 时如果传入字符串参数,接收者入栈求值的代码所在的嵌套层次. +* 顶层代码中由 `Kernel#load` 解释嵌套是空的,除非调用 `load` 时把第二个参数设为真值;如果是这样,Ruby 会创建一个匿名模块,将其入栈。 + +注意,块不会修改嵌套堆栈。尤其要注意的是,传给 `Class.new` 和 `Module.new` 的块不会导致定义的类或模块入栈嵌套堆栈。由此可见,以不同的方式定义类和模块,达到的效果是有区别的。 + + + +### 定义类和模块是为常量赋值 + +假设下面的代码片段是定义一个类(而不是打开类): + +```ruby +class C +end +``` + +Ruby 在 `Object` 中创建一个常量 `C`,并将一个类对象存储在 `C` 常量中。这个类实例的名称是“C”,一个字符串,跟常量名一样。 + +如下的代码: + +```ruby +class Project < ApplicationRecord +end +``` + +这段代码执行的操作等效于下述常量赋值: + +```ruby +Project = Class.new(ApplicationRecord) +``` + +而且有个副作用——设定类的名称: + +```ruby +Project.name # => "Project" +``` + +这得益于常量赋值的一条特殊规则:如果被赋值的对象是匿名类或模块,Ruby 会把对象的名称设为常量的名称。 + +TIP: 自此之后常量和实例发生的事情无关紧要。例如,可以把常量删除,类对象可以赋值给其他常量,或者不再存储于常量中,等等。名称一旦设定就不会再变。 + + +类似地,模块使用 `module` 关键字创建,如下所示: + +```ruby +module Admin +end +``` + +这段代码执行的操作等效于下述常量赋值: + +```ruby +Admin = Module.new +``` + +而且有个副作用——设定模块的名称: + +```ruby +Admin.name # => "Admin" +``` + +WARNING: 传给 `Class.new` 或 `Module.new` 的块与 `class` 或 `module` 关键字的定义体不在完全相同的上下文中执行。但是两种方式得到的结果都是为常量赋值。 + + +因此,当人们说“`String` 类”的时候,真正指的是 `Object` 常量中存储的一个类对象,它存储着常量“String”中存储的一个类对象。而 `String` 是一个普通的 Ruby 常量,与常量有关的一切,例如解析算法,在 `String` 常量上都适用。 + +同样地,在下述控制器中 + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +`Post` 不是调用类的句法,而是一个常规的 Ruby 常量。如果一切正常,这个常量的求值结果是一个能响应 `all` 方法的对象。 + +因此,我们讨论的话题才是“常量”自动加载。Rails 提供了自动加载常量的功能。 + + + +### 常量存储在模块中 + +按字面意义理解,常量属于模块。类和模块有常量表,你可以将其理解为哈希表。 + +下面通过一个示例来理解。通常我们都说“`String` 类”,这样方面,下面的阐述只是为了讲解原理。 + +我们来看看下述模块定义: + +```ruby +module Colors + RED = '0xff0000' +end +``` + +首先,处理 `module` 关键字时,解释器会在 `Object` 常量存储的类对象的常量表中新建一个条目。这个条目把“Colors”与一个新建的模块对象关联起来。而且,解释器把那个新建的模块对象的名称设为字符串“Colors”。 + +随后,解释模块的定义体时,会在 `Colors` 常量中存储的模块对象的常量表中新建一个条目。那个条目把“RED”映射到字符串“0xff0000”上。 + +注意,`Colors::RED` 与其他类或模块对象中的 `RED` 常量完全没有关系。如果存在这样一个常量,它在相应的常量表中,是不同的条目。 + +在前述各段中,尤其要注意类和模块对象、常量名称,以及常量表中与之关联的值对象之间的区别。 + + + +### 解析算法 + + + +#### 相对常量的解析算法 + +在代码中的特定位置,假如使用 cref 表示嵌套中的第一个元素,如果没有嵌套,则表示 `Object`。 + +简单来说,相对常量(relative constant)引用的解析算法如下: + +1. 如果嵌套不为空,在嵌套中按元素顺序查找常量。元素的祖先忽略不计。 +1. 如果未找到,算法向上,进入 cref 的祖先链。 +1. 如果未找到,而且 cref 是个模块,在 `Object` 中查找常量。 +1. 如果未找到,在 cref 上调用 `const_missing` 方法。这个方法的默认行为是抛出 `NameError` 异常,不过可以覆盖。 + +Rails 的自动加载机制没有仿照这个算法,查找的起点是要自动加载的常量名称,即 cref。详情参见 [相对引用](#autoloading-algorithms-relative-references)。 + + + +#### 限定常量的解析算法 + +限定常量(qualified constant)指下面这种: + +```ruby +Billing::Invoice +``` + +`Billing::Invoice` 由两个常量组成,其中 `Billing` 是相对常量,使用前一节所属的算法解析。 + +TIP: 在开头加上两个冒号可以把第一部分的相对常量变成绝对常量,例如 `::Billing::Invoice`。此时,`Billing` 作为顶层常量查找。 + + +而 `Invoice` 由 `Billing` 限定,下面说明它是如何解析的。假定 parent 是限定的类或模块对象,即上例中的 `Billing`。限定常量的解析算法如下: + +1. 在 parent 及其祖先中查找常量。 +1. 如果未找到,调用 parent 的 `const_missing` 方法。这个方法的默认行为是抛出 `NameError` 异常,不过可以覆盖。 + +可以看出,这个算法比相对常量的解析算法简单。毕竟这里不涉及嵌套,而且模块也不是特殊情况,如果二者及其祖先中都找不到常量,不会再查看 `Object`。 + +Rails 的自动加载机制没有仿照这个算法,查找的起点是要自动加载的常量名称和 parent。详情参见 [限定引用](#autoloading-algorithms-qualified-references)。 + + + +## 词汇表 + + + +### 父级命名空间 + +给定常量路径字符串,父级命名空间是把最右边那一部分去掉后余下的字符串。 + +例如,字符串“A::B::C”的父级命名空间是字符串“A::B”,“A::B”的父级命名空间是“A”,“A”的父级命名空间是“”(空)。 + +不过涉及类和模块的父级命名空间解释有点复杂。假设有个名为“A::B”的模块 M: + +* 父级命名空间 “A” 在给定位置可能反应不出嵌套。 +* 某处代码可能把常量 `A` 从 `Object` 中删除了,导致常量 `A` 不存在。 +* 如果 `A` 存在,`A` 中原来有的类或模块可能不再存在。例如,把一个常量删除后再赋值另一个常量,那么存在的可能就不是同一个对象。 +* 这种情形中,重新赋值的 `A` 可能是一个名为“A”的新类或模块。 +* 在上述情况下,无法再通过 `A::B` 访问 `M`,但是模块对象本身可以继续存活于某处,而且名称依然是“A::B”。 + +父级命名空间这个概念是自动加载算法的核心,有助于以直观的方式解释和理解算法,但是并不严谨。由于有边缘情况,本文所说的“父级命名空间”真正指的是具体的字符串来源。 + + + +### 加载机制 + +如果 `config.cache_classes` 的值是 `false`(开发环境的默认值),Rails 使用 `Kernel#load` 自动加载文件,否则使用 `Kernel#require` 自动加载文件(生产环境的默认值)。 + +如果启用了[常量重新加载](#constant-reloading),Rails 通过 `Kernel#load` 多次执行相同的文件。 + +本文使用的“加载”是指解释指定的文件,但是具体使用 `Kernel#load` 还是 `Kernel#require`,取决于配置。 + + + +## 自动加载可用性 + +只要环境允许,Rails 始终会自动加载。例如,`runner` 命令会自动加载: + +```sh +$ bin/rails runner 'p User.column_names' +["id", "email", "created_at", "updated_at"] +``` + +控制台会自动加载,测试组件会自动加载,当然,应用也会自动加载。 + +默认情况下,在生产环境中,Rails 启动时会及早加载应用文件,因此开发环境中的多数自动加载行为不会发生。但是在及早加载的过程中仍然可能会触发自动加载。 + +例如: + +```ruby +class BeachHouse < House +end +``` + +如果及早加载 `app/models/beach_house.rb` 文件之后,`House` 尚不可知,Rails 会自动加载它。 + + + +## `autoload_paths` + +或许你已经知道,使用 `require` 引入相对文件名时,例如 + +```ruby +require 'erb' +``` + +Ruby 在 `$LOAD_PATH` 中列出的目录里寻找文件。即,Ruby 迭代那些目录,检查其中有没有名为“erb.rb”“erb.so”“erb.o”或“erb.dll”的文件。如果在某个目录中找到了,解释器加载那个文件,搜索结束。否则,继续在后面的目录中寻找。如果最后没有找到,抛出 `LoadError` 异常。 + +后面会详述常量自动加载机制,不过整体思路是,遇到未知的常量时,如 `Post`,假如 `app/models` 目录中存在 `post.rb` 文件,Rails 会找到它,执行它,从而定义 `Post` 常量。 + +好吧,其实 Rails 会在一系列目录中查找 `post.rb`,有点类似于 `$LOAD_PATH`。那一系列目录叫做 `autoload_paths`,默认包含: + +* 应用和启动时存在的引擎的 `app` 目录中的全部子目录。例如,`app/controllers`。这些子目录不一定是默认的,可以是任何自定义的目录,如 `app/workers`。`app` 目录中的全部子目录都自动纳入 `autoload_paths`。 +* 应用和引擎中名为 `app/*/concerns` 的二级目录。 +* `test/mailers/previews` 目录。 + +此外,这些目录可以使用 `config.autoload_paths` 配置。例如,以前 `lib` 在这一系列目录中,但是现在不在了。应用可以在 `config/application.rb` 文件中添加下述配置,将其纳入其中: + +```ruby +config.autoload_paths << "#{Rails.root}/lib" +``` + +在各个环境的配置文件中不能配置 `config.autoload_paths`。 + +`autoload_paths` 的值可以审查。在新创建的应用中,它的值是(经过编辑): + +```sh +$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths' +.../app/assets +.../app/controllers +.../app/helpers +.../app/mailers +.../app/models +.../app/controllers/concerns +.../app/models/concerns +.../test/mailers/previews +``` + +TIP: `autoload_paths` 在初始化过程中计算并缓存。目录结构发生变化时,要重启服务器。 + + + + +## 自动加载算法 + + + +### 相对引用 + +相对常量引用可在多处出现,例如: + +```ruby +class PostsController < ApplicationController + def index + @posts = Post.all + end +end +``` + +这里的三个常量都是相对引用。 + + + +#### `class` 和 `module` 关键字后面的常量 + +Ruby 程序会查找 `class` 或 `module` 关键字后面的常量,因为要知道是定义类或模块,还是再次打开。 + +如果常量不被认为是缺失的,不会定义常量,也不会触发自动加载。 + +因此,在上述示例中,解释那个文件时,如果 `PostsController` 未定义,Rails 不会触发自动加载机制,而是由 Ruby 定义那个控制器。 + + + +#### 顶层常量 + +相对地,如果 `ApplicationController` 是未知的,会被认为是缺失的,Rails 会尝试自动加载。 + +为了加载 `ApplicationController`,Rails 会迭代 `autoload_paths`。首先,检查 `app/assets/application_controller.rb` 文件是否存在,如果不存在(通常如此),再检查 `app/controllers/application_controller.rb` 是否存在。 + +如果那个文件定义了 `ApplicationController` 常量,那就没事,否则抛出 `LoadError` 异常: + +``` +unable to autoload constant ApplicationController, expected + to define it (LoadError) +``` + +TIP: Rails 不要求自动加载的常量是类或模块对象。假如在 `app/models/max_clients.rb` 文件中定义了 `MAX_CLIENTS = 100`,Rails 也能自动加载 `MAX_CLIENTS`。 + + + + +#### 命名空间 + +自动加载 `ApplicationController` 时直接检查 `autoload_paths` 里的目录,因为它没有嵌套。`Post` 就不同了,那一行的嵌套是 `[PostsController]`,此时就会使用涉及命名空间的算法。 + +对下述代码来说: + +```ruby +module Admin + class BaseController < ApplicationController + @@all_roles = Role.all + end +end +``` + +为了自动加载 `Role`,要分别检查当前或父级命名空间中有没有定义 `Role`。因此,从概念上讲,要按顺序尝试自动加载下述常量: + +``` +Admin::BaseController::Role +Admin::Role +Role +``` + +为此,Rails 在 `autoload_paths` 中分别查找下述文件名: + +``` +admin/base_controller/role.rb +admin/role.rb +role.rb +``` + +此外还会查找一些其他目录,稍后说明。 + +TIP: 不含扩展名的相对文件路径通过 `'Constant::Name'.underscore` 得到,其中 `Constant::Name` 是已定义的常量。 + + +假设 `app/models/post.rb` 文件中定义了 `Post` 模型,下面说明 Rails 是如何自动加载 `PostsController` 中的 `Post` 常量的。 + +首先,在 `autoload_paths` 中查找 `posts_controller/post.rb`: + +``` +app/assets/posts_controller/post.rb +app/controllers/posts_controller/post.rb +app/helpers/posts_controller/post.rb +... +test/mailers/previews/posts_controller/post.rb +``` + +最后并未找到,因此会寻找一个类似的目录,[下一节](#automatic-modules)说明原因: + +``` +app/assets/posts_controller/post +app/controllers/posts_controller/post +app/helpers/posts_controller/post +... +test/mailers/previews/posts_controller/post +``` + +如果也未找到这样一个目录,Rails 会在父级命名空间中再次查找。对 `Post` 来说,只剩下顶层命名空间了: + +``` +app/assets/post.rb +app/controllers/post.rb +app/helpers/post.rb +app/mailers/post.rb +app/models/post.rb +``` + +这一次找到了 `app/models/post.rb` 文件。查找停止,加载那个文件。如果那个文件中定义了 `Post`,那就没问题,否则抛出 `LoadError` 异常。 + + + +### 限定引用 + +如果缺失限定常量,Rails 不会在父级命名空间中查找。但是有一点要留意:缺失常量时,Rails 不知道它是相对引用还是限定引用。 + +例如: + +```ruby +module Admin + User +end +``` + +和 + +```ruby +Admin::User +``` + +如果 `User` 缺失,在上述两种情况中 Rails 只知道缺失的是“Admin”模块中一个名为“User”的常量。 + +如果 `User` 是顶层常量,对前者来说,Ruby 会解析,但是后者不会。一般来说,Rails 解析常量的算法与 Ruby 不同,但是此时,Rails 尝试使用下述方式处理: + +> 如果类或模块的父级命名空间中没有缺失的常量,Rails 假定引用的是相对常量。否则是限定常量。 + +例如,如果下述代码触发自动加载 + +```ruby +Admin::User +``` + +那么,`Object` 中已经存在 `User` 常量。但是下述代码不会触发自动加载 + +```ruby +module Admin + User +end +``` + +如若不然,Ruby 就能解析出 `User`,也就无需自动加载了。因此,Rails 假定它是限定引用,只会在 `admin/user.rb` 文件和 `admin/user` 目录中查找。 + +其实,只要嵌套匹配全部父级命名空间,而且彼时适用这一规则的常量已知,这种机制便能良好运行。 + +然而,自动加载是按需执行的。如果碰巧顶层 `User` 尚未加载,那么 Rails 就假定它是相对引用。 + +在实际使用中,这种命名冲突很少发生。如果发生,`require_dependency` 提供了解决方案:确保做前述引文中的试探时,在有冲突的地方定义了常量。 + + + +### 自动模块 + +把模块作为命名空间使用时,Rails 不要求应用为之定义一个文件,有匹配命名空间的目录就够了。 + +假设应用有个后台,相关的控制器存储在 `app/controllers/admin` 目录中。遇到 `Admin::UsersController` 时,如果 `Admin` 模块尚未加载,Rails 要先自动加载 `Admin` 常量。 + +如果 `autoload_paths` 中有个名为 `admin.rb` 的文件,Rails 会加载那个文件。如果没有这么一个文件,而且存在名为 `admin` 的目录,Rails 会创建一个空模块,自动将其赋值给 `Admin` 常量。 + + + +### 一般步骤 + +相对引用在 cref 中报告缺失,限定引用在 parent 中报告缺失(cref 的指代参见 [相对常量的解析算法](#resolution-algorithm-for-relative-constants)开头,parent 的指代参见 [限定常量的解析算法](#resolution-algorithm-for-qualified-constants)开头)。 + +在任意的情况下,自动加载常量 C 的步骤如下: + +``` +if the class or module in which C is missing is Object + let ns = '' +else + let M = the class or module in which C is missing + + if M is anonymous + let ns = '' + else + let ns = M.name + end +end + +loop do + # 查找特定的文件 + for dir in autoload_paths + if the file "#{dir}/#{ns.underscore}/c.rb" exists + load/require "#{dir}/#{ns.underscore}/c.rb" + + if C is now defined + return + else + raise LoadError + end + end + end + + # 查找自动模块 + for dir in autoload_paths + if the directory "#{dir}/#{ns.underscore}/c" exists + if ns is an empty string + let C = Module.new in Object and return + else + let C = Module.new in ns.constantize and return + end + end + end + + if ns is empty + # 到顶层了,还未找到常量 + raise NameError + else + if C exists in any of the parent namespaces + # 以限定常量试探 + raise NameError + else + # 在父级命名空间中再试一次 + let ns = the parent namespace of ns and retry + end + end +end +``` + + + +## `require_dependency` + +常量自动加载按需触发,因此使用特定常量的代码可能已经定义了常量,或者触发自动加载。具体情况取决于执行路径,二者之间可能有较大差异。 + +然而,有时执行到某部分代码时想确保特定常量是已知的。`require_dependency` 为此提供了一种方式。它使用目前的[加载机制](#loading-mechanism)加载文件,而且会记录文件中定义的常量,就像是自动加载的一样,而且会按需重新加载。 + +`require_dependency` 很少需要使用,不过 [自动加载和 STI](#autoloading-and-sti)和 [常量未缺失](#when-constants-aren-t-missed)有几个用例。 + +WARNING: 与自动加载不同,`require_dependency` 不期望文件中定义任何特定的常量。但是利用这种行为不好,文件和常量路径应该匹配。 + + + + +## 常量重新加载 + +`config.cache_classes` 设为 `false` 时,Rails 会重新自动加载常量。 + +例如,在控制台会话中编辑文件之后,可以使用 `reload!` 命令重新加载代码: + +```irb +> reload! +``` + +在应用运行的过程中,如果相关的逻辑有变,会重新加载代码。为此,Rails 会监控下述文件: + +* `config/routes.rb` +* 本地化文件 +* `autoload_paths` 中的 Ruby 文件 +* `db/schema.rb` 和 `db/structure.sql` + +如果这些文件中的内容有变,有个中间件会发现,然后重新加载代码。 + +自动加载机制会记录自动加载的常量。重新加载机制使用 `Module#remove_const` 方法把它们从相应的类和模块中删除。这样,运行代码时那些常量就变成未知了,从而按需重新加载文件。 + +TIP: 这是一个极端操作,Rails 重新加载的不只是那些有变化的代码,因为类之间的依赖极难处理。相反,Rails 重新加载一切。 + + + + +## `Module#autoload` 不涉其中 + +`Module#autoload` 提供的是惰性加载常量方式,深置于 Ruby 的常量查找算法、动态常量 API,等等。这一机制相当简单。 + +Rails 内部在加载过程中大量采用这种方式,尽量减少工作量。但是,Rails 的常量自动加载机制不是使用 `Module#autoload` 实现的。 + +如果基于 `Module#autoload` 实现,可以遍历应用树,调用 `autoload` 把文件名和常规的常量名对应起来。 + +Rails 不采用这种实现方式有几个原因。 + +例如,`Module#autoload` 只能使用 `require` 加载文件,因此无法重新加载。不仅如此,它使用的是 `require` 关键字,而不是 `Kernel#require` 方法。 + +因此,删除文件后,它无法移除声明。如果使用 `Module#remove_const` 把常量删除了,不会触发 `Module#autoload`。此外,它不支持限定名称,因此有命名空间的文件要在遍历树时解析,这样才能调用相应的 `autoload` 方法,但是那些文件中可能有尚未配置的常量引用。 + +基于 `Module#autoload` 的实现很棒,但是如你所见,目前还不可能。Rails 的常量自动加载机制使用 `Module#const_missing` 实现,因此才有本文所述的独特算法。 + + + +## 常见问题 + + + +### 嵌套和限定常量 + +假如有下述代码 + +```ruby +module Admin + class UsersController < ApplicationController + def index + @users = User.all + end + end +end +``` + +和 + +```ruby +class Admin::UsersController < ApplicationController + def index + @users = User.all + end +end +``` + +为了解析 `User`,对前者来说,Ruby 会检查 `Admin`,但是后者不会,因为它不在嵌套中(参见 [嵌套](#nesting)和 [解析算法](#resolution-algorithms))。 + +可惜,在缺失常量的地方,Rails 自动加载机制不知道嵌套,因此行为与 Ruby 不同。具体而言,在两种情况下,`Admin::User` 都能自动加载。 + +尽管严格来说某些情况下 `class` 和 `module` 关键字后面的限定常量可以自动加载,但是最好使用相对常量: + +```ruby +module Admin + class UsersController < ApplicationController + def index + @users = User.all + end + end +end +``` + + + +### 自动加载和 STI + +单表继承(Single Table Inheritance,STI)是 Active Record 的一个功能,作用是在一个数据库表中存储具有层次结构的多个模型。这种模型的 API 知道层次结构的存在,而且封装了一些常用的需求。例如,对下面的类来说: + +```ruby +# app/models/polygon.rb +class Polygon < ApplicationRecord +end + +# app/models/triangle.rb +class Triangle < Polygon +end + +# app/models/rectangle.rb +class Rectangle < Polygon +end +``` + +`Triangle.create` 在表中创建一行,表示一个三角形,而 `Rectangle.create` 创建一行,表示一个长方形。如果 `id` 是某个现有记录的 ID,`Polygon.find(id)` 返回的是正确类型的对象。 + +操作集合的方法也知道层次结构。例如,`Polygon.all` 返回表中的全部记录,因为所有长方形和三角形都是多边形。Active Record 负责为结果集合中的各个实例设定正确的类。 + +类型会按需自动加载。例如,如果 `Polygon.first` 是一个长方形,而 `Rectangle` 尚未加载,Active Record 会自动加载它,然后正确实例化记录。 + +目前一切顺利,但是如果在根类上执行查询,需要处理子类,这时情况就复杂了。 + +处理 `Polygon` 时,无需知道全部子代,因为表中的所有记录都是多边形。但是处理子类时, Active Record 需要枚举类型,找到所需的那个。下面看一个例子。 + +`Rectangle.all` 在查询中添加一个类型约束,只加载长方形: + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle") +``` + +下面定义一个 `Rectangle` 的子类: + +```ruby +# app/models/square.rb +class Square < Rectangle +end +``` + +现在,`Rectangle.all` 返回的结果应该既有长方形,也有正方形: + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle", "Square") +``` + +但是这里有个问题:Active Record 怎么知道存在 `Square` 类呢? + +如果 `app/models/square.rb` 文件存在,而且定义了 `Square` 类,但是没有代码使用它,`Rectangle.all` 执行的查询是 + +```sql +SELECT "polygons".* FROM "polygons" +WHERE "polygons"."type" IN ("Rectangle") +``` + +这不是缺陷,查询包含了所有已知的 `Rectangle` 子代。 + +为了确保能正确处理,而不管代码的执行顺序,可以在定义各个中间类的文件底部手动加载子类: + +```ruby +# app/models/rectangle.rb +class Rectangle < Polygon +end +require_dependency 'square' +``` + +每个中间类(首尾之外的类)都要这么做。根类并没有通过类型限定查询,因此无需知道所有子代。 + + + +### 自动加载和 `require` + +通过自动加载机制加载的定义常量的文件一定不能使用 `require` 引入: + +```ruby +require 'user' # 千万别这么做 + +class UsersController < ApplicationController + ... +end +``` + +如果这么做,在开发环境中会导致两个问题: + +1. 如果在执行 `require` 之前自动加载了 `User`,`app/models/user.rb` 会再次运行,因为 `load` 不会更新 `$LOADED_FEATURES`。 +1. 如果 `require` 先执行了,Rails 不会把 `User` 标记为自动加载的常量,因此 `app/models/user.rb` 文件中的改动不会重新加载。 + +我们应该始终遵守规则,使用常量自动加载机制,一定不能混用自动加载和 `require`。底线是,如果一定要加载特定的文件,使用 `require_dependency`,这样能正确利用常量自动加载机制。不过,实际上很少需要这么做。 + +当然,在自动加载的文件中使用 `require` 加载第三方库没问题,Rails 会做区分,不把第三方库里的常量标记为自动加载的。 + + + +### 自动加载和初始化脚本 + +假设 `config/initializers/set_auth_service.rb` 文件中有下述赋值语句: + +```ruby +AUTH_SERVICE = if Rails.env.production? + RealAuthService +else + MockedAuthService +end +``` + +这么做的目的是根据所在环境为 `AUTH_SERVICE` 赋予不同的值。在开发环境中,运行这个初始化脚本时,自动加载 `MockedAuthService`。假如我们发送了几个请求,修改了实现,然后再次运行应用,奇怪的是,改动没有生效。这是为什么呢? + +[从前文得知](#constant-reloading),Rails 会删除自动加载的常量,但是 `AUTH_SERVICE` 存储的还是原来那个类对象。原来那个常量不存在了,但是功能完全不受影响。 + +下述代码概述了这种情况: + +```ruby +class C + def quack + 'quack!' + end +end + +X = C +Object.instance_eval { remove_const(:C) } +X.new.quack # => quack! +X.name # => C +C # => uninitialized constant C (NameError) +``` + +鉴于此,不建议在应用初始化过程中自动加载常量。 + +对上述示例来说,我们可以实现一个动态接入点: + +```ruby +# app/models/auth_service.rb +class AuthService + if Rails.env.production? + def self.instance + RealAuthService + end + else + def self.instance + MockedAuthService + end + end +end +``` + +然后在应用中使用 `AuthService.instance`。这样,`AuthService` 会按需加载,而且能顺利自动加载。 + + + +### `require_dependency` 和初始化脚本 + +前面说过,`require_dependency` 加载的文件能顺利自动加载。但是,一般来说不应该在初始化脚本中使用。 + +有人可能觉得在初始化脚本中调用 [`require_dependency`](#require-dependency) 能确保提前加载特定的常量,例如用于解决 [STI 问题](#autoloading-and-sti)。 + +问题是,在开发环境中,如果文件系统中有相关的改动,[自动加载的常量会被抹除](#constant-reloading)。这样就与使用初始化脚本的初衷背道而驰了。 + +`require_dependency` 调用应该写在能自动加载的地方。 + + + +### 常量未缺失 + + + +#### 相对引用 + +以一个飞行模拟器为例。应用中有个默认的飞行模型: + +```ruby +# app/models/flight_model.rb +class FlightModel +end +``` + +每架飞机都可以将其覆盖,例如: + +```ruby +# app/models/bell_x1/flight_model.rb +module BellX1 + class FlightModel < FlightModel + end +end + +# app/models/bell_x1/aircraft.rb +module BellX1 + class Aircraft + def initialize + @flight_model = FlightModel.new + end + end +end +``` + +初始化脚本想创建一个 `BellX1::FlightModel` 对象,而且嵌套中有 `BellX1`,看起来这没什么问题。但是,如果默认飞行模型加载了,但是 Bell-X1 模型没有,解释器能解析顶层的 `FlightModel`,因此 `BellX1::FlightModel` 不会触发自动加载机制。 + +这种代码取决于执行路径。 + +这种歧义通常可以通过限定常量解决: + +```ruby +module BellX1 + class Plane + def flight_model + @flight_model ||= BellX1::FlightModel.new + end + end +end +``` + +此外,使用 `require_dependency` 也能解决: + +```ruby +require_dependency 'bell_x1/flight_model' + +module BellX1 + class Plane + def flight_model + @flight_model ||= FlightModel.new + end + end +end +``` + + + +#### 限定引用 + +对下述代码来说 + +```ruby +# app/models/hotel.rb +class Hotel +end + +# app/models/image.rb +class Image +end + +# app/models/hotel/image.rb +class Hotel + class Image < Image + end +end +``` + +`Hotel::Image` 这个表达式有歧义,因为它取决于执行路径。 + +[从前文得知](#resolution-algorithm-for-qualified-constants),Ruby 会在 `Hotel` 及其祖先中查找常量。如果加载了 `app/models/image.rb` 文件,但是没有加载 `app/models/hotel/image.rb`,Ruby 在 `Hotel` 中找不到 `Image`,而在 `Object` 中能找到: + +```ruby +$ bin/rails r 'Image; p Hotel::Image' 2>/dev/null +Image # 不是 Hotel::Image! +``` + +若想得到 `Hotel::Image`,要确保 `app/models/hotel/image.rb` 文件已经加载——或许是使用 `require_dependency` 加载的。 + +不过,在这些情况下,解释器会发出提醒: + +``` +warning: toplevel constant Image referenced by Hotel::Image +``` + +任何限定的类都能发现这种奇怪的常量解析行为: + +``` +2.1.5 :001 > String::Array +(irb):1: warning: toplevel constant Array referenced by String::Array + => Array +``` + +WARNING: 为了发现这种问题,限定命名空间必须是类。`Object` 不是模块的祖先。 + + + + +### 单例类中的自动加载 + +假如有下述类定义: + +```ruby +# app/models/hotel/services.rb +module Hotel + class Services + end +end + +# app/models/hotel/geo_location.rb +module Hotel + class GeoLocation + class << self + Services + end + end +end +``` + +如果加载 `app/models/hotel/geo_location.rb` 文件时 `Hotel::Services` 是已知的,`Services` 由 Ruby 解析,因为打开 `Hotel::GeoLocation` 的单例类时,`Hotel` 在嵌套中。 + +但是,如果 `Hotel::Services` 是未知的,Rails 无法自动加载它,应用会抛出 `NameError` 异常。 + +这是因为单例类(匿名的)会触发自动加载,[从前文得知](#generic-procedure),在这种边缘情况下,Rails 只检查顶层命名空间。 + +这个问题的简单解决方案是使用限定常量: + +```ruby +module Hotel + class GeoLocation + class << self + Hotel::Services + end + end +end +``` + + + +### `BasicObject` 中的自动加载 + +`BasicObject` 的直接子代的祖先中没有 `Object`,因此无法解析顶层常量: + +```ruby +class C < BasicObject + String # NameError: uninitialized constant C::String +end +``` + +如果涉及自动加载,情况稍微复杂一些。对下述代码来说 + +```ruby +class C < BasicObject + def user + User # 错误 + end +end +``` + +因为 Rails 会检查顶层命名空间,所以第一次调用 `user` 方法时,`User` 能自动加载。但是,如果 `User` 是已知的,尤其是第二次调用 `user` 方法时,情况就不同了: + +```ruby +c = C.new +c.user # 奇怪的是能正常运行,返回 User +c.user # NameError: uninitialized constant C::User +``` + +因为此时发现父级命名空间中已经有那个常量了(参见 [限定引用](#autoloading-algorithms-qualified-references))。 + +在纯 Ruby 代码中,在 `BasicObject` 的直接子代的定义体中应该始终使用绝对常量路径: + +```ruby +class C < BasicObject + ::String # 正确 + + def user + ::User # 正确 + end +end +``` diff --git a/source/zh-CN/caching_with_rails.md b/source/zh-CN/caching_with_rails.md index e833ead..488b5f6 100644 --- a/source/zh-CN/caching_with_rails.md +++ b/source/zh-CN/caching_with_rails.md @@ -1,153 +1,238 @@ -Rails 缓存简介 -============= +# Rails 缓存概览 -本文要教你如果避免频繁查询数据库,在最短的时间内把真正需要的内容返回给客户端。 +本文简述如何使用缓存提升 Rails 应用的速度。 -读完本文,你将学到: +缓存是指存储请求-响应循环中生成的内容,在类似请求的响应中复用。 -* 页面和动作缓存(在 Rails 4 中被提取成单独的 gem); -* 片段缓存; -* 存储缓存的方法; -* Rails 对条件 GET 请求的支持; +通常,缓存是提升应用性能最有效的方式。通过缓存,在单个服务器中使用单个数据库的网站可以承受数千个用户并发访问。 --------------------------------------------------------------------------------- +Rails 自带了一些缓存功能。本文说明它们的适用范围和作用。掌握这些技术之后,你的 Rails 应用能承受大量访问,而不必花大量时间生成响应,或者支付高昂的服务器账单。 -缓存基础 -------- +读完本文后,您将学到: -本节介绍三种缓存技术:页面,动作和片段。Rails 默认支持片段缓存。如果想使用页面缓存和动作缓存,要在 `Gemfile` 中加入 `actionpack-page_caching` 和 `actionpack-action_caching`。 +* 片段缓存和俄罗斯套娃缓存; +* 如何管理缓存依赖; +* 不同的缓存存储器; +* 对条件 GET 请求的支持。 -在开发环境中若想使用缓存,要把 `config.action_controller.perform_caching` 选项设为 `true`。这个选项一般都在各环境的设置文件(`config/environments/*.rb`)中设置,在开发环境和测试环境默认是禁用的,在生产环境中默认是开启的。 +----------------------------------------------------------------------------- + + + +## 基本缓存 + +本节简介三种缓存技术:页面缓存(page caching)、动作缓存(action caching)和片段缓存(fragment caching)。Rails 默认提供了片段缓存。如果想使用页面缓存或动作缓存,要把 `actionpack-page_caching` 或 `actionpack-action_caching` 添加到 `Gemfile` 中。 + +默认情况下,缓存只在生产环境启用。如果想在本地启用缓存,要在相应的 `config/environments/*.rb` 文件中把 `config.action_controller.perform_caching` 设为 `true`。 ```ruby config.action_controller.perform_caching = true ``` +NOTE: 修改 `config.action_controller.perform_caching` 的值只对 Action Controller 组件提供的缓存有影响。例如,对低层缓存没影响,[下文详述](#low-level-caching)。 + + + + ### 页面缓存 -页面缓存机制允许网页服务器(Apache 或 Nginx 等)直接处理请求,不经 Rails 处理。这么做显然速度超快,但并不适用于所有情况(例如需要身份认证的页面)。服务器直接从文件系统上伺服文件,所以缓存过期是一个很棘手的问题。 +页面缓存时 Rails 提供的一种缓存机制,让 Web 服务器(如 Apache 和 NGINX)直接伺服生成的页面,而不经由 Rails 栈处理。虽然这种缓存的速度超快,但是不适用于所有情况(例如需要验证身份的页面)。此外,因为 Web 服务器直接从文件系统中伺服文件,所以你要自行实现缓存失效机制。 + +TIP: Rails 4 删除了页面缓存。参见 [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching)。 + -NOTE: Rails 4 删除了对页面缓存的支持,如想使用就得安装 [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching)。最新推荐的缓存方法参见 [DHH 对键基缓存过期的介绍](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)。 + ### 动作缓存 -如果动作上有前置过滤器就不能使用页面缓存,例如需要身份认证的页面,这时需要使用动作缓存。动作缓存和页面缓存的工作方式差不多,但请求还是会经由 Rails 处理,所以在伺服缓存之前会执行前置过滤器。使用动作缓存可以执行身份认证等限制,然后再从缓存中取出结果返回客户端。 +有前置过滤器的动作不能使用页面缓存,例如需要验证身份的页面。此时,应该使用动作缓存。动作缓存的工作原理与页面缓存类似,不过入站请求会经过 Rails 栈处理,以便运行前置过滤器,然后再伺服缓存。这样,可以做身份验证和其他限制,同时还能从缓存的副本中伺服结果。 -NOTE: Rails 4 删除了对动作缓存的支持,如想使用就得安装 [actionpack-action_caching gem](https://github.com/rails/actionpack-action_caching)。最新推荐的缓存方法参见 [DHH 对键基缓存过期的介绍](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)。 +TIP: Rails 4 删除了动作缓存。参见 [actionpack-action_caching gem](https://github.com/rails/actionpack-action_caching)。最新推荐的做法参见 DHH 写的“[How key-based cache expiration works](https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works)”一文。 + + + ### 片段缓存 -如果能缓存整个页面或动作的内容,再伺服给客户端,这个世界就完美了。但是,动态网页程序的页面一般都由很多部分组成,使用的缓存机制也不尽相同。在动态生成的页面中,不同的内容要使用不同的缓存方式和过期日期。为此,Rails 提供了一种缓存机制叫做“片段缓存”。 +动态 Web 应用一般使用不同的组件构建页面,不是所有组件都能使用同一种缓存机制。如果页面的不同部分需要使用不同的缓存机制,在不同的条件下失效,可以使用片段缓存。 -片段缓存把视图逻辑的一部分打包放在 `cache` 块中,后续请求都会从缓存中伺服这部分内容。 +片段缓存把视图逻辑的一部分放在 `cache` 块中,下次请求使用缓存存储器中的副本伺服。 -例如,如果想实时显示网站的订单,而且不想缓存这部分内容,但想缓存显示所有可选商品的部分,就可以使用下面这段代码: +例如,如果想缓存页面中的各个商品,可以使用下述代码: ```erb -<% Order.find_recent.each do |o| %> - <%= o.buyer.name %> bought <%= o.product.name %> +<% @products.each do |product| %> + <% cache product do %> + <%= render product %> + <% end %> <% end %> +``` -<% cache do %> - All available products: - <% Product.all.each do |p| %> - <%= link_to p.name, product_url(https://github.com/p) %> - <% end %> +首次访问这个页面时,Rails 会创建一个具有唯一键的缓存条目。缓存键类似下面这种: + +``` +views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901 +``` + +中间的数字是 `product_id` 加上商品记录的 `updated_at` 属性中存储的时间戳。Rails 使用时间戳确保不伺服过期的数据。如果 `updated_at` 的值变了,Rails 会生成一个新键,然后在那个键上写入一个新缓存,旧键上的旧缓存不再使用。这叫基于键的失效方式。 + +视图片段有变化时(例如视图的 HTML 有变),缓存的片段也失效。缓存键末尾那个字符串是模板树摘要,是基于缓存的视图片段的内容计算的 MD5 哈希值。如果视图片段有变化,MD5 哈希值就变了,因此现有文件失效。 + +TIP: Memcached 等缓存存储器会自动删除旧的缓存文件。 + +如果想在特定条件下缓存一个片段,可以使用 `cache_if` 或 `cache_unless`: + +```erb +<% cache_if admin?, product do %> + <%= render product %> <% end %> ``` -上述代码中的 `cache` 块会绑定到调用它的动作上,输出到动作缓存的所在位置。因此,如果要在动作中使用多个片段缓存,就要使用 `action_suffix` 为 `cache` 块指定前缀: + + +#### 集合缓存 + +`render` 辅助方法还能缓存渲染集合的单个模板。这甚至比使用 `each` 的前述示例更好,因为是一次性读取所有缓存模板的,而不是一次读取一个。若想缓存集合,渲染集合时传入 `cached: true` 选项: ```erb -<% cache(action: 'recent', action_suffix: 'all_products') do %> - All available products: +<%= render partial: 'products/product', collection: @products, cached: true %> ``` -`expire_fragment` 方法可以把缓存设为过期,例如: +上述代码中所有的缓存模板一次性获取,速度更快。此外,尚未缓存的模板也会写入缓存,在下次渲染时获取。 -```ruby -expire_fragment(controller: 'products', action: 'recent', action_suffix: 'all_products') + + +### 俄罗斯套娃缓存 + +有时,可能想把缓存的片段嵌套在其他缓存的片段里。这叫俄罗斯套娃缓存(Russian doll caching)。 + +俄罗斯套娃缓存的优点是,更新单个商品后,重新生成外层片段时,其他内存片段可以复用。 + +前一节说过,如果缓存的文件对应的记录的 `updated_at` 属性值变了,缓存的文件失效。但是,内层嵌套的片段不失效。 + +对下面的视图来说: + +```erb +<% cache product do %> + <%= render product.games %> +<% end %> ``` -如果不想把缓存绑定到调用它的动作上,调用 `cahce` 方法时可以使用全局片段名: +而它渲染这个视图: ```erb -<% cache('all_available_products') do %> - All available products: +<% cache game do %> + <%= render game %> <% end %> ``` -在 `ProductsController` 的所有动作中都可以使用片段名调用这个片段缓存,而且过期的设置方式不变: +如果游戏的任何一个属性变了,`updated_at` 的值会设为当前时间,因此缓存失效。然而,商品对象的 `updated_at` 属性不变,因此它的缓存不失效,从而导致应用伺服过期的数据。为了解决这个问题,可以使用 `touch` 方法把模型绑在一起: ```ruby -expire_fragment('all_available_products') +class Product < ApplicationRecord + has_many :games +end + +class Game < ApplicationRecord + belongs_to :product, touch: true +end ``` -如果不想手动设置片段缓存过期,而想每次更新商品后自动过期,可以定义一个帮助方法: +把 `touch` 设为 `true` 后,导致游戏的 `updated_at` 变化的操作,也会修改关联的商品的 `updated_at` 属性,从而让缓存失效。 + + + +### 管理依赖 + +为了正确地让缓存失效,要正确地定义缓存依赖。Rails 足够智能,能处理常见的情况,无需自己指定。但是有时需要处理自定义的辅助方法(以此为例),因此要自行定义。 + + + +#### 隐式依赖 + +多数模板依赖可以从模板中的 `render` 调用中推导出来。下面举例说明 `ActionView::Digestor` 知道如何解码的 `render` 调用: ```ruby -module ProductsHelper - def cache_key_for_products - count = Product.count - max_updated_at = Product.maximum(:updated_at).try(:utc).try(:to_s, :number) - "products/all-#{count}-#{max_updated_at}" - end -end +render partial: "comments/comment", collection: commentable.comments +render "comments/comments" +render 'comments/comments' +render('comments/comments') + +render "header" => render("comments/header") + +render(@topic) => render("topics/topic") +render(topics) => render("topics/topic") +render(message.topics) => render("topics/topic") +``` + +而另一方面,有些调用要做修改方能让缓存正确工作。例如,如果传入自定义的集合,要把下述代码: + +```ruby +render @project.documents.where(published: true) +``` + +改为: + +```ruby +render partial: "documents/document", collection: @project.documents.where(published: true) ``` -这个方法生成一个缓存键,用于所有商品的缓存。在视图中可以这么做: + + +#### 显式依赖 + +有时,模板依赖推导不出来。在辅助方法中渲染时经常是这样。下面举个例子: ```erb -<% cache(cache_key_for_products) do %> - All available products: -<% end %> +<%= render_sortable_todolists @project.todolists %> ``` -如果想在满足某个条件时缓存片段,可以使用 `cache_if` 或 `cache_unless` 方法: +此时,要使用一种特殊的注释格式: ```erb -<% cache_if (condition, cache_key_for_products) do %> - All available products: -<% end %> +<%# Template Dependency: todolists/todolist %> +<%= render_sortable_todolists @project.todolists %> ``` -缓存的键名还可使用 Active Record 模型: +某些情况下,例如设置单表继承,可能要显式定义一堆依赖。此时无需写出每个模板,可以使用通配符匹配一个目录中的全部模板: ```erb -<% Product.all.each do |p| %> - <% cache(p) do %> - <%= link_to p.name, product_url(https://github.com/p) %> - <% end %> +<%# Template Dependency: events/* %> +<%= render_categorizable_events @person.events %> +``` + +对集合缓存来说,如果局部模板不是以干净的缓存调用开头,依然可以使用集合缓存,不过要在模板中的任意位置添加一种格式特殊的注释,如下所示: + +```erb +<%# Template Collection: notification %> +<% my_helper_that_calls_cache(some_arg, notification) do %> + <%= notification.name %> <% end %> ``` -Rails 会在模型上调用 `cache_key` 方法,返回一个字符串,例如 `products/23-20130109142513`。键名中包含模型名,ID 以及 `updated_at` 字段的时间戳。所以更新商品后会自动生成一个新片段缓存,因为键名变了。 + + +#### 外部依赖 -上述两种缓存机制还可以结合在一起使用,这叫做“俄罗斯套娃缓存”(Russian Doll Caching): +如果在缓存的块中使用辅助方法,而后更新了辅助方法,还要更新缓存。具体方法不限,只要能改变模板文件的 MD5 值就行。推荐的方法之一是添加一个注释,如下所示: ```erb -<% cache(cache_key_for_products) do %> - All available products: - <% Product.all.each do |p| %> - <% cache(p) do %> - <%= link_to p.name, product_url(https://github.com/p) %> - <% end %> - <% end %> -<% end %> +<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %> +<%= some_helper_method(person) %> ``` -之所以叫“俄罗斯套娃缓存”,是因为嵌套了多个片段缓存。这种缓存的优点是,更新单个商品后,重新生成外层片段缓存时可以继续使用内层片段缓存。 + -### 底层缓存 +### 低层缓存 -有时不想缓存视图片段,只想缓存特定的值或者查询结果。Rails 中的缓存机制可以存储各种信息。 +有时需要缓存特定的值或查询结果,而不是缓存视图片段。Rails 的缓存机制能存储任何类型的信息。 -实现底层缓存最有效地方式是使用 `Rails.cache.fetch` 方法。这个方法既可以从缓存中读取数据,也可以把数据写入缓存。传入单个参数时,读取指定键对应的值。传入代码块时,会把代码块的计算结果存入缓存的指定键中,然后返回计算结果。 +实现低层缓存最有效的方式是使用 `Rails.cache.fetch` 方法。这个方法既能读取也能写入缓存。传入单个参数时,获取指定的键,返回缓存中的值。如果传入块,块中的代码在缓存缺失时执行。块返回的值将写入缓存,存在指定键的名下,然后返回那个返回值。如果命中缓存,直接返回缓存的值,而不执行块中的代码。 -以下面的代码为例。程序中有个 `Product` 模型,其中定义了一个实例方法,用来查询竞争对手网站上的商品价格。这个方法的返回结果最好使用底层缓存: +下面举个例子。应用中有个 `Product` 模型,它有个实例方法,在竞争网站中查找商品的价格。这个方法返回的数据特别适合使用低层缓存: ```ruby -class Product < ActiveRecord::Base +class Product < ApplicationRecord def competing_price Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do Competitor::API.find_price(id) @@ -156,11 +241,14 @@ class Product < ActiveRecord::Base end ``` -NOTE: 注意,在这个例子中使用了 `cache_key` 方法,所以得到的缓存键名是这种形式:`products/233-20140225082222765838000/competing_price`。`cache_key` 方法根据模型的 `id` 和 `updated_at` 属性生成键名。这是最常见的做法,因为商品更新后,缓存就失效了。一般情况下,使用底层缓存保存实例的相关信息时,都要生成缓存键。 +NOTE: 注意,这个示例使用了 `cache_key` 方法,因此得到的缓存键类似这种:`products/233-20140225082222765838000/competing_price`。`cache_key` 方法根据模型的 `id` 和 `updated_at` 属性生成一个字符串。这是常见的约定,有个好处是,商品更新后缓存自动失效。一般来说,使用低层缓存缓存实例层信息时,需要生成缓存键。 + + + ### SQL 缓存 -查询缓存是 Rails 的一个特性,把每次查询的结果缓存起来,如果在同一次请求中遇到相同的查询,直接从缓存中读取结果,不用再次查询数据库。 +查询缓存是 Rails 提供的一个功能,把各个查询的结果集缓存起来。如果在同一个请求中遇到了相同的查询,Rails 会使用缓存的结果集,而不再次到数据库中运行查询。 例如: @@ -168,159 +256,146 @@ NOTE: 注意,在这个例子中使用了 `cache_key` 方法,所以得到的 class ProductsController < ApplicationController def index - # Run a find query + # 运行查找查询 @products = Product.all ... - # Run the same query again + # 再次运行相同的查询 @products = Product.all end end ``` -缓存的存储方式 ------------- +再次运行相同的查询时,根本不会发给数据库。首次运行查询得到的结果存储在查询缓存中(内存里),第二次查询从内存中获取。 -Rails 为动作缓存和片段缓存提供了不同的存储方式。 +然而要知道,查询缓存在动作开头创建,到动作末尾销毁,只在动作的存续时间内存在。如果想持久化存储查询结果,使用低层缓存也能实现。 -TIP: 页面缓存全部存储在硬盘中。 + -### 设置 +## 缓存存储器 -程序默认使用的缓存存储方式可以在文件 `config/application.rb` 的 `Application` 类中或者环境设置文件(`config/environments/*.rb`)的 `Application.configure` 代码块中调用 `config.cache_store=` 方法设置。该方法的第一个参数是存储方式,后续参数都是传给对应存储方式构造器的参数。 +Rails 为存储缓存数据(SQL 缓存和页面缓存除外)提供了不同的存储器。 + + + +### 配置 + +`config.cache_store` 配置选项用于设定应用的默认缓存存储器。可以设定其他参数,传给缓存存储器的构造方法: ```ruby -config.cache_store = :memory_store +config.cache_store = :memory_store, { size: 64.megabytes } ``` -NOTE: 在设置代码块外部可以调用 `ActionController::Base.cache_store` 方法设置存储方式。 - -缓存中的数据通过 `Rails.cache` 方法获取。 +NOTE: 此外,还可以在配置块外部调用 `ActionController::Base.cache_store`。 -### ActiveSupport::Cache::Store +缓存存储器通过 `Rails.cache` 访问。 -这个类提供了在 Rails 中和缓存交互的基本方法。这是个抽象类,不能直接使用,应该使用针对各存储引擎的具体实现。Rails 实现了几种存储方式,介绍参见后几节。 + -和缓存交互常用的方法有:`read`,`write`,`delete`,`exist?`,`fetch`。`fetch` 方法接受一个代码块,如果缓存中有对应的数据,将其返回;否则,执行代码块,把结果写入缓存。 +### `ActiveSupport::Cache::Store` -Rails 实现的所有存储方式都共用了下面几个选项。这些选项可以传给构造器,也可传给不同的方法,和缓存中的记录交互。 +这个类是在 Rails 中与缓存交互的基础。这是个抽象类,不能直接使用。你必须根据存储器引擎具体实现这个类。Rails 提供了几个实现,说明如下。 -* `:namespace`:在缓存存储中创建命名空间。如果和其他程序共用同一个存储,可以使用这个选项。 +主要调用的方法有 `read`、`write`、`delete`、`exist?` 和 `fetch`。`fetch` 方法接受一个块,返回缓存中现有的值,或者把新值写入缓存。 -* `:compress`:是否压缩缓存。便于在低速网络中传输大型缓存记录。 +所有缓存实现有些共用的选项,可以传给构造方法,或者传给与缓存条目交互的各个方法。 -* `:compress_threshold`:结合 `:compress` 选项使用,设定一个阈值,低于这个值就不压缩缓存。默认为 16 KB。 +* `:namespace`:在缓存存储器中创建命名空间。如果与其他应用共用同一个缓存存储器,这个选项特别有用。 +* `:compress`:指定压缩缓存。通过缓慢的网络传输大量缓存时用得着。 +* `:compress_threshold`:与 `:compress` 选项搭配使用,指定一个阈值,未达到时不压缩缓存。默认为 16 千字节。 +* `:expires_in`:为缓存条目设定失效时间(秒数),失效后自动从缓存中删除。 +* `:race_condition_ttl`:与 `:expires_in` 选项搭配使用。避免多个进程同时重新生成相同的缓存条目(也叫 dog pile effect),防止让缓存条目过期时出现条件竞争。这个选项设定在重新生成新值时失效的条目还可以继续使用多久(秒数)。如果使用 `:expires_in` 选项, 最好也设定这个选项。 -* `:expires_in`:为缓存记录设定一个过期时间,单位为秒,过期后把记录从缓存中删除。 + -* `:race_condition_ttl`:结合 `:expires_in` 选项使用。缓存过期后,禁止多个进程同时重新生成同一个缓存记录(叫做 dog pile effect),从而避免条件竞争。这个选项设置一个秒数,在这个时间之后才能再次使用重新生成的新值。如果设置了 `:expires_in` 选项,最好也设置这个选项。 +#### 自定义缓存存储器 -### ActiveSupport::Cache::MemoryStore +缓存存储器可以自己定义,只需扩展 `ActiveSupport::Cache::Store` 类,实现相应的方法。这样,你可以把任何缓存技术带到你的 Rails 应用中。 -这种存储方式在 Ruby 进程中把缓存保存在内存中。存储空间的大小由 `:size` 选项指定,默认为 32MB。如果超出分配的大小,系统会清理缓存,把最不常使用的记录删除。 +若想使用自定义的缓存存储器,只需把 `cache_store` 设为自定义类的实例: ```ruby -config.cache_store = :memory_store, { size: 64.megabytes } +config.cache_store = MyCacheStore.new ``` -如果运行多个 Rails 服务器进程(使用 mongrel_cluster 或 Phusion Passenger 时),进程间无法共用缓存数据。这种存储方式不适合在大型程序中使用,不过很适合只有几个服务器进程的小型、低流量网站,也可在开发环境和测试环境中使用。 + -### ActiveSupport::Cache::FileStore +### `ActiveSupport::Cache::MemoryStore` -这种存储方式使用文件系统保存缓存。缓存文件的存储位置必须在初始化时指定。 +这个缓存存储器把缓存条目放在内存中,与 Ruby 进程放在一起。可以把 `:size` 选项传给构造方法,指定缓存的大小限制(默认为 32Mb)。超过分配的大小后,会清理缓存,把最不常用的条目删除。 ```ruby -config.cache_store = :file_store, "/path/to/cache/directory" +config.cache_store = :memory_store, { size: 64.megabytes } ``` -使用这种存储方式,同一主机上的服务器进程之间可以共用缓存。运行在不同主机上的服务器进程之间也可以通过共享的文件系统共用缓存,但这种用法不是最好的方式,因此不推荐使用。这种存储方式适合在只用了一到两台主机的中低流量网站中使用。 +如果运行多个 Ruby on Rails 服务器进程(例如使用 Phusion Passenger 或 Puma 集群模式),各个实例之间无法共享缓存数据。这个缓存存储器不适合大型应用使用。不过,适合只有几个服务器进程的低流量小型应用使用,也适合在开发环境和测试环境中使用。 -注意,如果不定期清理,缓存会不断增多,最终会用完硬盘空间。 + -这是默认使用的缓存存储方式。 +### `ActiveSupport::Cache::FileStore` -### ActiveSupport::Cache::MemCacheStore - -这种存储方式使用 Danga 开发的 `memcached` 服务器,为程序提供一个中心化的缓存存储。Rails 默认使用附带安装的 `dalli` gem 实现这种存储方式。这是目前在生产环境中使用最广泛的缓存存储方式,可以提供单个缓存存储,或者共享的缓存集群,性能高,冗余度低。 - -初始化时要指定集群中所有 memcached 服务器的地址。如果没有指定地址,默认运行在本地主机的默认端口上,这对大型网站来说不是个好主意。 - -在这种缓存存储中使用 `write` 和 `fetch` 方法还可指定两个额外的选项,充分利用 memcached 的特有功能。指定 `:raw` 选项可以直接把没有序列化的数据传给 memcached 服务器。在这种类型的数据上可以使用 memcached 的原生操作,例如 `increment` 和 `decrement`。如果不想让 memcached 覆盖已经存在的记录,可以指定 `:unless_exist` 选项。 +这个缓存存储器使用文件系统存储缓存条目。初始化这个存储器时,必须指定存储文件的目录: ```ruby -config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" +config.cache_store = :file_store, "/path/to/cache/directory" ``` -### ActiveSupport::Cache::EhcacheStore +使用这个缓存存储器时,在同一台主机中运行的多个服务器进程可以共享缓存。这个缓存存储器适合一到两个主机的中低流量网站使用。运行在不同主机中的多个服务器进程若想共享缓存,可以使用共享的文件系统,但是不建议这么做。 -如果在 JRuby 平台上运行程序,可以使用 Terracotta 开发的 Ehcache 存储缓存。Ehcache 是使用 Java 开发的开源缓存存储,同时也提供企业版,增强了稳定性、操作便利性,以及商用支持。使用这种存储方式要先安装 `jruby-ehcache-rails3` gem(1.1.0 及以上版本)。 +缓存量一直增加,直到填满磁盘,所以建议你定期清理旧缓存条目。 -```ruby -config.cache_store = :ehcache_store -``` +这是默认的缓存存储器。 + + -初始化时,可以使用 `:ehcache_config` 选项指定 Ehcache 设置文件的位置(默认为 Rails 程序根目录中的 `ehcache.xml`),还可使用 `:cache_name` 选项定制缓存名(默认为 `rails_cache`)。 +### `ActiveSupport::Cache::MemCacheStore` -使用 `write` 方法时,除了可以使用通用的 `:expires_in` 选项之外,还可指定 `:unless_exist` 选项,让 Ehcache 使用 `putIfAbsent` 方法代替 `put` 方法,不覆盖已经存在的记录。除此之外,`write` 方法还可接受 [Ehcache Element 类](http://ehcache.org/apidocs/net/sf/ehcache/Element.html)开放的所有属性,包括: +这个缓存存储器使用 Danga 的 `memcached` 服务器为应用提供中心化缓存。Rails 默认使用自带的 `dalli` gem。这是生产环境的网站目前最常使用的缓存存储器。通过它可以实现单个共享的缓存集群,效率很高,有较好的冗余。 -| 属性 | 参数类型 | 说明 | -| --------------------------- | ------------------- | ----------------------------------------------------------- | -| elementEvictionData | ElementEvictionData | 设置元素的 eviction 数据实例 | -| eternal | boolean | 设置元素是否为 eternal | -| timeToIdle, tti | int | 设置空闲时间 | -| timeToLive, ttl, expires_in | int | 设置在线时间 | -| version | long | 设置 ElementAttributes 对象的 `version` 属性 | +初始化这个缓存存储器时,要指定集群中所有 memcached 服务器的地址。如果不指定,假定 memcached 运行在本地的默认端口上,但是对大型网站来说,这样做并不好。 -这些选项通过 Hash 传给 `write` 方法,可以使用驼峰式或者下划线分隔形式。例如: +这个缓存存储器的 `write` 和 `fetch` 方法接受两个额外的选项,以便利用 memcached 的独有特性。指定 `:raw` 时,直接把值发给服务器,不做序列化。值必须是字符串或数字。memcached 的直接操作,如 `increment` 和 `decrement`,只能用于原始值。还可以指定 `:unless_exist` 选项,不让 memcached 覆盖现有条目。 ```ruby -Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds) -caches_action :index, expires_in: 60.seconds, unless_exist: true +config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" ``` -关于 Ehcache 更多的介绍,请访问 。关于如何在运行于 JRuby 平台之上的 Rails 中使用 Ehcache,请访问 。 + -### ActiveSupport::Cache::NullStore +### `ActiveSupport::Cache::NullStore` -这种存储方式只可在开发环境和测试环境中使用,并不会存储任何数据。如果在开发过程中必须和 `Rails.cache` 交互,而且会影响到修改代码后的效果,使用这种存储方式尤其方便。使用这种存储方式时调用 `fetch` 和 `read` 方法没有实际作用。 +这个缓存存储器只应该在开发或测试环境中使用,它并不存储任何信息。在开发环境中,如果代码直接与 `Rails.cache` 交互,但是缓存可能对代码的结果有影响,可以使用这个缓存存储器。在这个缓存存储器上调用 `fetch` 和 `read` 方法不返回任何值。 ```ruby config.cache_store = :null_store ``` -### 自建存储方式 - -要想自建缓存存储方式,可以继承 `ActiveSupport::Cache::Store` 类,并实现相应的方法。自建存储方式时,可以使用任何缓存技术。 + -使用自建的存储方式,把 `cache_store` 设为类的新实例即可。 +## 缓存键 -```ruby -config.cache_store = MyCacheStore.new -``` +缓存中使用的键可以是能响应 `cache_key` 或 `to_param` 方法的任何对象。如果想定制生成键的方式,可以覆盖 `cache_key` 方法。Active Record 根据类名和记录 ID 生成缓存键。 -### 缓存键 - -缓存中使用的键可以是任意对象,只要能响应 `:cache_key` 或 `:to_param` 方法即可。如果想生成自定义键,可以在类中定义 `:cache_key` 方法。Active Record 根据类名和记录的 ID 生成缓存键。 - -缓存键也可使用 Hash 或者数组。 +缓存键的值可以是散列或数组: ```ruby -# This is a legal cache key +# 这是一个有效的缓存键 Rails.cache.read(site: "mysite", owners: [owner_1, owner_2]) ``` -`Rails.cache` 方法中使用的键和保存到存储引擎中的键并不一样。保存时,可能会根据命名空间或引擎的限制做修改。也就是说,不能使用 `memcache-client` gem 调用 `Rails.cache` 方法保存缓存再尝试读取缓存。不过,无需担心会超出 memcached 的大小限制,或者违反句法规则。 +`Rails.cache` 使用的键与存储引擎使用的并不相同,存储引擎使用的键可能含有命名空间,或者根据后端的限制做调整。这意味着,使用 `Rails.cache` 存储值时使用的键可能无法用于供 `dalli` gem 获取缓存条目。然而,你也无需担心会超出 memcached 的大小限制,或者违背句法规则。 -支持条件 GET 请求 ---------------- + -条件请求是 HTTP 规范的一个特性,网页服务器告诉浏览器 GET 请求的响应自上次请求以来没有发生变化,可以直接读取浏览器缓存中的副本。 +## 对条件 GET 请求的支持 -条件请求通过 `If-None-Match` 和 `If-Modified-Since` 报头实现,这两个报头的值分别是内容的唯一 ID 和上次修改内容的时间戳,在服务器和客户端之间来回传送。如果浏览器发送的请求中内容 ID(ETag)或上次修改时间戳和服务器上保存的值一样,服务器只需返回一个空响应,并把状态码设为未修改。 +条件 GET 请求是 HTTP 规范的一个特性,以此告诉 Web 浏览器,GET 请求的响应自上次请求之后没有变化,可以放心从浏览器的缓存中读取。 -服务器负责查看上次修改时间戳和 `If-None-Match` 报头的值,决定是否返回完整的响应。在 Rails 中使用条件 GET 请求很简单: +为此,要传递 `HTTP_IF_NONE_MATCH` 和 `HTTP_IF_MODIFIED_SINCE` 首部,其值分别为唯一的内容标识符和上一次改动时的时间戳。浏览器发送的请求,如果内容标识符(etag)或上一次修改的时间戳与服务器中的版本匹配,那么服务器只需返回一个空响应,把状态设为未修改。 + +服务器(也就是我们自己)要负责查看最后修改时间戳和 `HTTP_IF_NONE_MATCH` 首部,判断要不要返回完整的响应。既然 Rails 支持条件 GET 请求,那么这个任务就非常简单: ```ruby class ProductsController < ApplicationController @@ -328,40 +403,44 @@ class ProductsController < ApplicationController def show @product = Product.find(params[:id]) - # If the request is stale according to the given timestamp and etag value - # (i.e. it needs to be processed again) then execute this block + # 如果根据指定的时间戳和 etag 值判断请求的内容过期了 + # (即需要重新处理)执行这个块 if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key) respond_to do |wants| - # ... normal response processing + # ... 正常处理响应 end end - # If the request is fresh (i.e. it's not modified) then you don't need to do - # anything. The default render checks for this using the parameters - # used in the previous call to stale? and will automatically send a - # :not_modified. So that's it, you're done. + # 如果请求的内容还新鲜(即未修改),无需做任何事 + # render 默认使用前面 stale? 中的参数做检查,会自动发送 :not_modified 响应 + # 就这样,工作结束 end end ``` -如果不想使用 Hash,还可直接传入模型实例,Rails 会调用 `updated_at` 和 `cache_key` 方法分别设置 `last_modified` 和 `etag`: +除了散列,还可以传入模型。Rails 会使用 `updated_at` 和 `cache_key` 方法设定 `last_modified` 和 `etag`: ```ruby class ProductsController < ApplicationController def show @product = Product.find(params[:id]) - respond_with(@product) if stale?(@product) + + if stale?(@product) + respond_to do |wants| + # ... 正常处理响应 + end + end end end ``` -如果没有使用特殊的方式处理响应,使用默认的渲染机制(例如,没有使用 `respond_to` 代码块,或者没有手动调用 `render` 方法),还可使用十分便利的 `fresh_when` 方法: +如果无需特殊处理响应,而且使用默认的渲染机制(即不使用 `respond_to`,或者不自己调用 `render`),可以使用 `fresh_when` 简化这个过程: ```ruby class ProductsController < ApplicationController - # This will automatically send back a :not_modified if the request is fresh, - # and will render the default template (product.*) if it's stale. + # 如果请求的内容是新鲜的,自动返回 :not_modified + # 否则渲染默认的模板(product.*) def show @product = Product.find(params[:id]) @@ -369,3 +448,72 @@ class ProductsController < ApplicationController end end ``` + +有时,我们需要缓存响应,例如永不过期的静态页面。为此,可以使用 `http_cache_forever` 辅助方法,让浏览器和代理无限期缓存。 + +默认情况下,缓存的响应是私有的,只在用户的 Web 浏览器中缓存。如果想让代理缓存响应,设定 `public: true`,让代理把缓存的响应提供给所有用户。 + +使用这个辅助方法时,`last_modified` 首部的值被设为 `Time.new(2011, 1, 1).utc`,`expires` 首部的值被设为 100 年。 + +WARNING: 使用这个方法时要小心,因为浏览器和代理不会作废缓存的响应,除非强制清除浏览器缓存。 + +```ruby +class HomeController < ApplicationController + def index + http_cache_forever(public: true) do + render + end + end +end +``` + + + +### 强 Etag 与弱 Etag + +Rails 默认生成弱 ETag。这种 Etag 允许语义等效但主体不完全匹配的响应具有相同的 Etag。如果响应主体有微小改动,而不想重新渲染页面,可以使用这种 Etag。 + +为了与强 Etag 区别,弱 Etag 前面有 `W/`。 + +``` +W/"618bbc92e2d35ea1945008b42799b0e7" => 弱 ETag +"618bbc92e2d35ea1945008b42799b0e7" => 强 ETag +``` + +与弱 Etag 不同,强 Etag 要求响应完全一样,不能有一个字节的差异。在大型视频或 PDF 文件内部做 Range 查询时用得到。有些 CDN,如 Akamai,只支持强 Etag。如果确实想生成强 Etag,可以这么做: + +```ruby +class ProductsController < ApplicationController + def show + @product = Product.find(params[:id]) + fresh_when last_modified: @product.published_at.utc, strong_etag: @product + end +end +``` + +也可以直接在响应上设定强 Etag: + +```ruby +response.strong_etag = response.body +# => "618bbc92e2d35ea1945008b42799b0e7" +``` + + + +## 在开发环境中测试缓存 + +我们经常需要在开发模式中测试应用采用的缓存策略。Rails 提供的 Rake 任务 `dev:cache` 能轻易启停缓存。 + +```sh +$ bin/rails dev:cache +Development mode is now being cached. +$ bin/rails dev:cache +Development mode is no longer being cached. +``` + + + +## 参考资源 + +* [DHH 写的文章:How key-based cache expiration works](https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) +* [Railscast 中介绍缓存摘要的视频](http://railscasts.com/episodes/387-cache-digests) diff --git a/source/zh-CN/command_line.md b/source/zh-CN/command_line.md index dc7f974..fdb727f 100644 --- a/source/zh-CN/command_line.md +++ b/source/zh-CN/command_line.md @@ -1,44 +1,47 @@ -Rails 命令行 -=========== +# Rails 命令行 -读完本文,你将学到: +读完本文后,您将学到: -* 如何新建 Rails 程序; -* 如何生成模型、控制器、数据库迁移和单元测试; -* 如何启动开发服务器; -* 如果在交互 shell 中测试对象; -* 如何分析、评测程序; +* 如何新建 Rails 应用; +* 如何生成模型、控制器、数据库迁移和单元测试; +* 如何启动开发服务器; +* 如果在交互式 shell 中测试对象; --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -NOTE: 阅读本文前要具备一些 Rails 基础知识,可以阅读“[Rails 入门](getting_started.html)”一文。 +NOTE: 阅读本文前请阅读[Rails 入门](getting_started.html),掌握一些 Rails 基础知识。 -命令行基础 ---------- + + + +## 命令行基础 有些命令在 Rails 开发过程中经常会用到,下面按照使用频率倒序列出: -* `rails console` -* `rails server` -* `rake` -* `rails generate` -* `rails dbconsole` -* `rails new app_name` +* `rails console` +* `rails server` +* `bin/rails` +* `rails generate` +* `rails dbconsole` +* `rails new app_name` -这些命令都可指定 `-h` 或 `--help` 选项显示具体用法。 +这些命令都可指定 `-h` 或 `--help` 选项列出更多信息。 -下面我们来新建一个 Rails 程序,介绍各命令的用法。 +下面我们新建一个 Rails 应用,通过它介绍各个命令的用法。 + + ### `rails new` -安装 Rails 后首先要做的就是使用 `rails new` 命令新建 Rails 程序。 +安装 Rails 后首先要做的就是使用 `rails new` 命令新建 Rails 应用。 + +TIP: 如果还没安装 Rails ,可以执行 `gem install rails` 命令安装。 -NOTE: 如果还没安装 Rails ,可以执行 `gem install rails` 命令安装。 -```bash +```sh $ rails new commandsapp create - create README.rdoc + create README.md create Rakefile create config.ru create .gitignore @@ -50,46 +53,54 @@ $ rails new commandsapp run bundle install ``` -这个简单的命令会生成很多文件,组成一个完整的 Rails 程序,直接就可运行。 +这个简单的命令会生成很多文件,组成一个完整的 Rails 应用目录结构,直接就可运行。 + + ### `rails server` -`rails server` 命令会启动 Ruby 内建的小型服务器 WEBrick。要想在浏览器中访问程序,就要执行这个命令。 +`rails server` 命令用于启动 Rails 自带的 Puma Web 服务器。若想在浏览器中访问应用,就要执行这个命令。 -无需其他操作,执行 `rails server` 命令后就能运行刚创建的 Rails 程序: +无需其他操作,执行 `rails server` 命令后就能运行刚才创建的 Rails 应用: -```bash +```sh $ cd commandsapp -$ rails server -=> Booting WEBrick -=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 -=> Call with -d to detach -=> Ctrl-C to shutdown server -[2013-08-07 02:00:01] INFO WEBrick 1.3.1 -[2013-08-07 02:00:01] INFO ruby 2.0.0 (2013-06-27) [x86_64-darwin11.2.0] -[2013-08-07 02:00:01] INFO WEBrick::HTTPServer#start: pid=69680 port=3000 +$ bin/rails server +=> Booting Puma +=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000 +=> Run `rails server -h` for more startup options +Puma starting in single mode... +* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas +* Min threads: 5, max threads: 5 +* Environment: development +* Listening on tcp://localhost:3000 +Use Ctrl-C to stop ``` -只执行了三个命令,我们就启动了一个 Rails 服务器,监听端口 3000。打开浏览器,访问 ,会看到一个简单的 Rails 程序。 +只执行了三个命令,我们就启动了一个 Rails 服务器,监听着 3000 端口。打开浏览器,访问 ,你会看到一个简单的 Rails 应用。 + +TIP: 启动服务器的命令还可使用别名“s”:`rails s`。 -NOTE: 启动服务器的命令还可使用别名“s”:`rails s`。 -如果想让服务器监听其他端口,可通过 `-p` 选项指定。所处的环境可由 `-e` 选项指定。 +如果想让服务器监听其他端口,可通过 `-p` 选项指定。所处的环境(默认为开发环境)可由 `-e` 选项指定。 -```bash -$ rails server -e production -p 4000 +```sh +$ bin/rails server -e production -p 4000 ``` -`-b` 选项把 Rails 绑定到指定的 IP,默认 IP 是 0.0.0.0。指定 `-d` 选项后,服务器会以守护进程的形式运行。 +`-b` 选项把 Rails 绑定到指定的 IP(默认为 localhost)。指定 `-d` 选项后,服务器会以守护进程的形式运行。 + + ### `rails generate` -`rails generate` 使用模板生成很多东西。单独执行 `rails generate` 命令,会列出可用的生成器: +`rails generate` 目录使用模板生成很多东西。单独执行 `rails generate` 命令,会列出可用的生成器: + +TIP: 还可使用别名“g”执行生成器命令:`rails g`。 -NOTE: 还可使用别名“g”执行生成器命令:`rails g`。 -```bash -$ rails generate +```sh +$ bin/rails generate Usage: rails generate GENERATOR [args] [options] ... @@ -107,14 +118,16 @@ Rails: NOTE: 使用其他生成器 gem 可以安装更多的生成器,或者使用插件中提供的生成器,甚至还可以自己编写生成器。 -使用生成器可以节省大量编写程序骨架的时间。 -下面我们使用控制器生成器生成控制器。但应该使用哪个命令呢?我们问一下生成器: +使用生成器可以节省大量编写样板代码(即应用运行必须的代码)的时间。 -NOTE: 所有的 Rails 命令都有帮助信息。和其他 *nix 命令一样,可以在命令后加上 `--help` 或 `-h` 选项,例如 `rails server --help`。 +下面我们使用控制器生成器生成一个控制器。不过,应该使用哪个命令呢?我们问一下生成器: -```bash -$ rails generate controller +TIP: 所有 Rails 命令都有帮助信息。和其他 *nix 命令一样,可以在命令后加上 `--help` 或 `-h` 选项,例如 `rails server --help`。 + + +```sh +$ bin/rails generate controller Usage: rails generate controller NAME [action action] [options] ... @@ -123,25 +136,24 @@ Usage: rails generate controller NAME [action action] [options] Description: ... - To create a controller within a module, specify the controller name as a - path like 'parent_module/controller_name'. + To create a controller within a module, specify the controller name as a path like 'parent_module/controller_name'. ... Example: - `rails generate controller CreditCard open debit credit close` + `rails generate controller CreditCards open debit credit close` - Credit card controller with URLs like /credit_card/debit. - Controller: app/controllers/credit_card_controller.rb - Test: test/controllers/credit_card_controller_test.rb - Views: app/views/credit_card/debit.html.erb [...] - Helper: app/helpers/credit_card_helper.rb + Credit card controller with URLs like /credit_cards/debit. + Controller: app/controllers/credit_cards_controller.rb + Test: test/controllers/credit_cards_controller_test.rb + Views: app/views/credit_cards/debit.html.erb [...] + Helper: app/helpers/credit_cards_helper.rb ``` -控制器生成器接受的参数形式是 `generate controller ControllerName action1 action2`。下面我们来生成 `Greetings` 控制器,包含一个动作 `hello`,跟读者打个招呼。 +控制器生成器接受的参数形式是 `generate controller ControllerName action1 action2`。下面我们来生成 `Greetings` 控制器,包含一个动作 `hello`,通过它跟读者打个招呼。 -```bash -$ rails generate controller Greetings hello +```sh +$ bin/rails generate controller Greetings hello create app/controllers/greetings_controller.rb route get "greetings/hello" invoke erb @@ -151,16 +163,14 @@ $ rails generate controller Greetings hello create test/controllers/greetings_controller_test.rb invoke helper create app/helpers/greetings_helper.rb - invoke test_unit - create test/helpers/greetings_helper_test.rb invoke assets invoke coffee - create app/assets/javascripts/greetings.js.coffee + create app/assets/javascripts/greetings.coffee invoke scss - create app/assets/stylesheets/greetings.css.scss + create app/assets/stylesheets/greetings.scss ``` -这个命令生成了什么呢?在程序中创建了一堆文件夹,还有控制器文件、视图文件、功能测试文件、视图帮助方法文件、JavaScript 文件盒样式表文件。 +这个命令生成了什么呢?它在应用中创建了一堆目录,还有控制器文件、视图文件、功能测试文件、视图辅助方法文件、JavaScript 文件和样式表文件。 打开控制器文件(`app/controllers/greetings_controller.rb`),做些改动: @@ -181,19 +191,20 @@ end 执行 `rails server` 命令启动服务器: -```bash -$ rails server -=> Booting WEBrick... +```sh +$ bin/rails server +=> Booting Puma... ``` -要查看的地址是 。 +要查看的 URL 是 。 + +TIP: 在常规的 Rails 应用中,URL 的格式是 http://(host)/(controller)/(action),访问 http://(host)/(controller) 这样的 URL 会进入控制器的 `index` 动作。 -NOTE: 在常规的 Rails 程序中,URL 的格式是 http://(host)/(controller)/(action),访问 http://(host)/(controller) 会进入控制器的 `index` 动作。 Rails 也为数据模型提供了生成器。 -```bash -$ rails generate model +```sh +$ bin/rails generate model Usage: rails generate model NAME [field[:type][:index] field[:type][:index]] [options] @@ -209,14 +220,15 @@ Description: Create rails files for model generator. ``` -NOTE: 全部可用的字段类型,请查看 `TableDefinition#column` 方法的[文档](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html#method-i-column)。 +NOTE: `type` 参数可用的全部字段类型参见 `SchemaStatements` 模块中 [`add_column` 方法的 API 文档](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_column)。`index` 参数为相应的列生成索引。 + -不过我们暂且不单独生成模型(后文再生成),先使用脚手架。Rails 中的脚手架会生成资源所需的全部文件,包括:模型,模型所用的迁移,处理模型的控制器,查看数据的视图,以及测试组件。 +不过我们暂且不直接生成模型(后文再生成),先来使用脚手架(scaffold)。Rails 中的脚手架会生成资源所需的全部文件,包括模型、模型所用的迁移、处理模型的控制器、查看数据的视图,以及各部分的测试组件。 我们要创建一个名为“HighScore”的资源,记录视频游戏的最高得分。 -```bash -$ rails generate scaffold HighScore game:string score:integer +```sh +$ bin/rails generate scaffold HighScore game:string score:integer invoke active_record create db/migrate/20130717151933_create_high_scores.rb create app/models/high_score.rb @@ -238,93 +250,137 @@ $ rails generate scaffold HighScore game:string score:integer create test/controllers/high_scores_controller_test.rb invoke helper create app/helpers/high_scores_helper.rb - invoke test_unit - create test/helpers/high_scores_helper_test.rb invoke jbuilder create app/views/high_scores/index.json.jbuilder create app/views/high_scores/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/high_scores.js.coffee + create app/assets/javascripts/high_scores.coffee invoke scss - create app/assets/stylesheets/high_scores.css.scss + create app/assets/stylesheets/high_scores.scss invoke scss - identical app/assets/stylesheets/scaffolds.css.scss + identical app/assets/stylesheets/scaffolds.scss ``` -这个生成器检测到以下各组件对应的文件夹已经存储在:模型,控制器,帮助方法,布局,功能测试,单元测试,样式表。然后创建“HighScore”资源的视图、控制器、模型和迁移文件(用来创建 `high_scores` 数据表和字段),并设置好路由,以及测试等。 +这个生成器检测到以下各组件对应的目录已经存在:模型、控制器、辅助方法、布局、功能测试、单元测试和样式表。然后创建“HighScore”资源的视图、控制器、模型和数据库迁移(用于创建 `high_scores` 数据表和字段),并设置好路由,以及测试等。 -我们要运行迁移,执行文件 `20130717151933_create_high_scores.rb` 中的代码,这才能修改数据库的模式。那么要修改哪个数据库呢?执行 `rake db:migrate` 命令后会生成 SQLite3 数据库。稍后再详细介绍 Rake。 +我们要运行迁移,执行文件 `20130717151933_create_high_scores.rb` 中的代码,这样才能修改数据库的模式。那么要修改哪个数据库呢?执行 `bin/rails db:migrate` 命令后会生成 SQLite3 数据库。稍后再详细说明 `bin/rails`。 -```bash -$ rake db:migrate +```sh +$ bin/rails db:migrate == CreateHighScores: migrating =============================================== -- create_table(:high_scores) -> 0.0017s == CreateHighScores: migrated (0.0019s) ====================================== ``` -NOTE: 介绍一下单元测试。单元测试是用来测试代码、做断定的代码。在单元测试中,我们只关注代码的一部分,例如模型中的一个方法,测试其输入和输出。单元测试是你的好伙伴,你逐渐会意识到,单元测试的程度越高,生活的质量才能提上来。真的。稍后我们会编写一个单元测试。 +TIP: 介绍一下单元测试。单元测试是用来测试和做断言的代码。在单元测试中,我们只关注代码的一小部分,例如模型中的一个方法,测试其输入和输出。单元测试是你的好伙伴,你逐渐会意识到,单元测试的程度越高,生活的质量越高。真的。关于单元测试的详情,参阅[Rails 应用测试指南](testing.html)。 + 我们来看一下 Rails 创建的界面。 -```bash -$ rails server +```sh +$ bin/rails server ``` 打开浏览器,访问 ,现在可以创建新的最高得分了(太空入侵者得了 55,160 分)。 + + ### `rails console` -执行 `console` 命令后,可以在命令行中和 Rails 程序交互。`rails` console` 使用的是 IRB,所以如果你用过 IRB 的话,操作起来很顺手。在终端里可以快速测试想法,或者修改服务器端的数据,而无需在网站中操作。 +执行 `console` 命令后,可以在命令行中与 Rails 应用交互。`rails console` 使用的是 IRB,所以如果你用过 IRB 的话,操作起来很顺手。在控制台里可以快速测试想法,或者修改服务器端数据,而无需在网站中操作。 -NOTE: 这个命令还可以使用别名“c”:`rails c`。 +TIP: 这个命令还可以使用别名“c”:`rails c`。 -执行 `console` 命令时可以指定终端在哪个环境中打开: -```bash -$ rails console staging +执行 `console` 命令时可以指定在哪个环境中打开控制台: + +```sh +$ bin/rails console staging ``` -如果你想测试一些代码,但不想改变存储的数据,可以执行 `rails console --sandbox`。 +如果你想测试一些代码,但不想改变存储的数据,可以执行 `rails console --sandbox` 命令。 -```bash -$ rails console --sandbox -Loading development environment in sandbox (Rails 4.2.0) +```sh +$ bin/rails console --sandbox +Loading development environment in sandbox (Rails 5.1.0) Any modifications you make will be rolled back on exit irb(main):001:0> ``` + + +#### `app` 和 `helper` 对象 + +在控制台中可以访问 `app` 和 `helper` 对象。 + +通过 `app` 可以访问 URL 和路径辅助方法,还可以发送请求。 + +```irb +>> app.root_path +=> "/" + +>> app.get _ +Started GET "/" for 127.0.0.1 at 2014-06-19 10:41:57 -0300 +... +``` + +通过 `helper` 可以访问 Rails 和应用定义的辅助方法。 + +```irb +>> helper.time_ago_in_words 30.days.ago +=> "about 1 month" + +>> helper.my_custom_helper +=> "my custom helper" +``` + + + ### `rails dbconsole` -`rails dbconsole` 能检测到你正在使用的数据库类型(还能理解传入的命令行参数),然后进入该数据库的命令行界面。该命令支持 MySQL,PostgreSQL,SQLite 和 SQLite3。 +`rails dbconsole` 能检测到你正在使用的数据库类型(还能理解传入的命令行参数),然后进入该数据库的命令行界面。该命令支持 MySQL(包括 MariaDB)、PostgreSQL 和 SQLite3。 -NOTE: 这个命令还可使用别名“db”:`rails db`。 +TIP: 这个命令还可以使用别名“db”:`rails db`。 + + + ### `rails runner` -`runner` 可以以非交互的方式在 Rails 中运行 Ruby 代码。例如: +`runner` 能以非交互的方式在 Rails 中运行 Ruby 代码。例如: -```bash -$ rails runner "Model.long_running_method" +```sh +$ bin/rails runner "Model.long_running_method" ``` -NOTE: 这个命令还可使用别名“r”:`rails r`。 +TIP: 这个命令还可以使用别名“r”:`rails r`。 + + +可以使用 `-e` 选项指定 `runner` 命令在哪个环境中运行。 -可使用 `-e` 选项指定 `runner` 命令在哪个环境中运行。 +```sh +$ bin/rails runner -e staging "Model.long_running_method" +``` + +甚至还可以执行文件中的 Ruby 代码: -```bash -$ rails runner -e staging "Model.long_running_method" +```sh +$ bin/rails runner lib/code_to_be_run.rb ``` + + ### `rails destroy` -`destroy` 可以理解成 `generate` 的逆操作,能识别生成了什么,然后将其删除。 +`destroy` 可以理解成 `generate` 的逆操作,它能识别生成了什么,然后撤销。 + +TIP: 这个命令还可以使用别名“d”:`rails d`。 -NOTE: 这个命令还可使用别名“d”:`rails d`。 -```bash -$ rails generate model Oops +```sh +$ bin/rails generate model Oops invoke active_record create db/migrate/20120528062523_create_oops.rb create app/models/oops.rb @@ -333,8 +389,8 @@ $ rails generate model Oops create test/fixtures/oops.yml ``` -```bash -$ rails destroy model Oops +```sh +$ bin/rails destroy model Oops invoke active_record remove db/migrate/20120528062523_create_oops.rb remove app/models/oops.rb @@ -343,79 +399,98 @@ $ rails destroy model Oops remove test/fixtures/oops.yml ``` -Rake ----- + -Rake 是 Ruby 领域的 Make,是个独立的 Ruby 工具,目的是代替 Unix 中的 make。Rake 根据 `Rakefile` 和 `.rake` 文件构建任务。Rails 使用 Rake 实现常见的管理任务,尤其是较为复杂的任务。 +## `bin/rails` -执行 `rake -- tasks` 命令可以列出所有可用的 Rake 任务,具体的任务根据所在文件夹会有所不同。每个任务都有描述信息,帮助你找到所需的命令。 +从 Rails 5.0+ 起,rake 命令内建到 `rails` 可执行文件中了,因此现在应该使用 `bin/rails` 执行命令。 + +`bin/rails` 支持的任务列表可通过 `bin/rails --help` 查看(可用的任务根据所在的目录有所不同)。每个任务都有描述,应该能帮助你找到所需的那个。 + +```sh +$ bin/rails --help +Usage: rails COMMAND [ARGS] + +The most common rails commands are: +generate Generate new code (short-cut alias: "g") +console Start the Rails console (short-cut alias: "c") +server Start the Rails server (short-cut alias: "s") +... -要想查看执行 Rake 任务时的完整调用栈,可以在命令中使用 `--trace` 选项,例如 `rake db:create --trace`。 +All commands can be run with -h (or --help) for more information. -```bash -$ rake --tasks -rake about # List versions of all Rails frameworks and the environment -rake assets:clean # Remove compiled assets -rake assets:precompile # Compile all the assets named in config.assets.precompile -rake db:create # Create the database from config/database.yml for the current Rails.env +In addition to those commands, there are: +about List versions of all Rails ... +assets:clean[keep] Remove old compiled assets +assets:clobber Remove compiled assets +assets:environment Load asset compile environment +assets:precompile Compile all the assets ... ... -rake log:clear # Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development) -rake middleware # Prints out your Rack middleware stack +db:fixtures:load Loads fixtures into the ... +db:migrate Migrate the database ... +db:migrate:status Display status of migrations +db:rollback Rolls the schema back to ... +db:schema:cache:clear Clears a db/schema_cache.yml file +db:schema:cache:dump Creates a db/schema_cache.yml file +db:schema:dump Creates a db/schema.rb file ... +db:schema:load Loads a schema.rb file ... +db:seed Loads the seed data ... +db:structure:dump Dumps the database structure ... +db:structure:load Recreates the databases ... +db:version Retrieves the current schema ... ... -rake tmp:clear # Clear session, cache, and socket files from tmp/ (narrow w/ tmp:sessions:clear, tmp:cache:clear, tmp:sockets:clear) -rake tmp:create # Creates tmp directories for sessions, cache, sockets, and pids +restart Restart app by touching ... +tmp:create ``` -NOTE: 还可以执行 `rake -T` 查看所有任务。 +TIP: 还可以使用 `bin/rails -T` 列出所有任务。 + + + ### `about` -`rake about` 任务输出以下信息:Ruby、RubyGems、Rails 的版本号,Rails 使用的组件,程序所在的文件夹,Rails 当前所处的环境名,程序使用的数据库适配器,数据库模式版本号。如果想向他人需求帮助,检查安全补丁是否影响程序,或者需要查看现有 Rails 程序的信息,可以使用这个任务。 +`bin/rails about` 输出以下信息:Ruby、RubyGems、Rails 的版本号,Rails 使用的组件,应用所在的文件夹,Rails 当前所处的环境名,应用使用的数据库适配器,以及数据库模式版本号。如果想向他人需求帮助,检查安全补丁对你是否有影响,或者需要查看现有 Rails 应用的状态,就可以使用这个任务。 -```bash -$ rake about +```sh +$ bin/rails about About your application's environment -Ruby version 1.9.3 (x86_64-linux) -RubyGems version 1.3.6 -Rack version 1.3 -Rails version 4.2.0 +Rails version 5.1.0 +Ruby version 2.2.2 (x86_64-linux) +RubyGems version 2.4.6 +Rack version 2.0.1 JavaScript Runtime Node.js (V8) -Active Record version 4.2.0 -Action Pack version 4.2.0 -Action View version 4.2.0 -Action Mailer version 4.2.0 -Active Support version 4.2.0 -Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag +Middleware: Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActiveSupport::Cache::Strategy::LocalCache::Middleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Sprockets::Rails::QuietAssets, Rails::Rack::Logger, ActionDispatch::ShowExceptions, WebConsole::Middleware, ActionDispatch::DebugExceptions, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development Database adapter sqlite3 Database schema version 20110805173523 ``` + + ### `assets` -`rake assets:precompile` 任务会预编译 `app/assets` 文件夹中的静态资源文件。`rake assets:clean` 任务会把编译好的静态资源文件删除。 +`bin/rails assets:precompile` 用于预编译 `app/assets` 文件夹中的静态资源文件。`bin/rails assets:clean` 用于把之前编译好的静态资源文件删除。滚动部署时应该执行 `assets:clean`,以防仍然链接旧的静态资源文件。 -### `db` +如果想完全清空 `public/assets` 目录,可以使用 `bin/rails assets:clobber`。 -Rake 命名空间 `db:` 中最常用的任务是 `migrate` 和 `create`,这两个任务会尝试运行所有迁移相关的 Rake 任务(`up`,`down`,`redo`,`reset`)。`rake db:version` 在排查问题时很有用,会输出数据库的当前版本。 + -关于数据库迁移的更多介绍,参阅“[Active Record 数据库迁移](migrations.html)”一文。 +### `db` -### `doc` +`bin/rails` 命名空间 `db:` 中最常用的任务是 `migrate` 和 `create`,这两个任务会尝试运行所有迁移相关的任务(`up`、`down`、`redo`、`reset`)。`bin/rails db:version` 在排查问题时很有用,它会输出数据库的当前版本。 -`doc:` 命名空间中的任务可以生成程序的文档,Rails API 文档和 Rails 指南。生成的文档可以随意分割,减少程序的大小,适合在嵌入式平台使用。 +关于数据库迁移的进一步说明,参阅[Active Record 迁移](active_record_migrations.html)。 -* `rake doc:app` 在 `doc/app` 文件夹中生成程序的文档; -* `rake doc:guides` 在 `doc/guides` 文件夹中生成 Rails 指南; -* `rake doc:rails` 在 `doc/api` 文件夹中生成 Rails API 文档; + ### `notes` -`rake notes` 会搜索整个程序,寻找以 FIXME、OPTIMIZE 或 TODO 开头的注释。搜索的文件包括 `.builder`,`.rb`,`.erb`,`.haml`,`.slim`,`.css`,`.scss`,`.js`,`.coffee`,`.rake`,`.sass` 和 `.less`。搜索的内容包括默认注解和自定义注解。 +`bin/rails notes` 在代码中搜索以 FIXME、OPTIMIZE 或 TODO 开头的注释。搜索的文件类型包括 `.builder`、`.rb`、`.rake`、`.yml`、`.yaml`、`.ruby`、`.css`、`.js` 和 `.erb`,搜索的注解包括默认的和自定义的。 -```bash -$ rake notes +```sh +$ bin/rails notes (in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [ 20] [TODO] any other way to do this? @@ -426,10 +501,16 @@ app/models/school.rb: * [ 17] [FIXME] ``` -如果想查找特定的注解,例如 FIXME,可以执行 `rake notes:fixme` 任务。注意,在命令中注解的名字要使用小写形式。 +可以使用 `config.annotations.register_extensions` 选项添加新的文件扩展名。这个选项的值是扩展名列表和对应的正则表达式。 + +```ruby +config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } +``` + +如果想查看特定类型的注解,如 FIXME,可以使用 `bin/rails notes:fixme`。注意,注解的名称是小写形式。 -```bash -$ rake notes:fixme +```sh +$ bin/rails notes:fixme (in /home/foobar/commandsapp) app/controllers/admin/users_controller.rb: * [132] high priority for next deploy @@ -438,22 +519,29 @@ app/models/school.rb: * [ 17] ``` -在代码中可以使用自定义的注解,然后执行 `rake notes:custom` 任务,并使用 `ANNOTATION` 环境变量指定要查找的注解。 +此外,还可以在代码中使用自定义的注解,然后使用 `bin/rails notes:custom`,并通过 `ANNOTATION` 环境变量指定注解类型,将其列出。 -```bash -$ rake notes:custom ANNOTATION=BUG +```sh +$ bin/rails notes:custom ANNOTATION=BUG (in /home/foobar/commandsapp) -app/models/post.rb: +app/models/article.rb: * [ 23] Have to fix this one before pushing! ``` -NOTE: 注意,不管查找的是默认的注解还是自定义的直接,注解名(例如 FIXME,BUG 等)不会在输出结果中显示。 +NOTE: 使用内置的注解或自定义的注解时,注解的名称(FIXME、BUG 等)不会在输出中显示。 + + +默认情况下,`rails notes` 在 `app`、`config`、`db`、`lib` 和 `test` 目录中搜索。如果想搜索其他目录,可以通过 `config.annotations.register_directories` 选项配置。 + +```ruby +config.annotations.register_directories("spec", "vendor") +``` -默认情况下,`rake notes` 会搜索 `app`、`config`、`lib`、`bin` 和 `test` 这几个文件夹中的文件。如果想在其他的文件夹中查找,可以使用 `SOURCE_ANNOTATION_DIRECTORIES` 环境变量指定一个以逗号分隔的列表。 +此外,还可以通过 `SOURCE_ANNOTATION_DIRECTORIES` 环境变量指定,目录之间使用逗号分开。 -```bash +```sh $ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor' -$ rake notes +$ bin/rails notes (in /home/foobar/commandsapp) app/models/user.rb: * [ 35] [FIXME] User should have a subscription at this point @@ -461,51 +549,61 @@ spec/models/user_spec.rb: * [122] [TODO] Verify the user that has a subscription works ``` + + ### `routes` -`rake routes` 会列出程序中定义的所有路由,可为解决路由问题提供帮助,还可以让你对程序中的所有 URL 有个整体了解。 +`rails routes` 列出应用中定义的所有路由,可为解决路由问题提供帮助,还可以让你对应用中的所有 URL 有个整体了解。 + + ### `test` -NOTE: Rails 中的单元测试详情,参见“[Rails 程序测试指南](testing.html)”一文。 +TIP: Rails 中的单元测试详情,参见[Rails 应用测试指南](testing.html)。 -Rails 提供了一个名为 Minitest 的测试组件。Rails 的稳定性也由测试决定。`test:` 命名空间中的任务可用于运行各种测试。 + +Rails 提供了一个名为 Minitest 的测试组件。Rails 的稳定性由测试决定。`test:` 命名空间中的任务可用于运行各种测试。 + + ### `tmp` -`Rails.root/tmp` 文件夹和 *nix 中的 `/tmp` 作用相同,用来存放临时文件,例如会话(如果使用文件存储会话)、PID 文件和缓存文件等。 +`Rails.root/tmp` 目录和 *nix 系统中的 `/tmp` 目录作用相同,用于存放临时文件,例如 PID 文件和缓存的动作等。 -`tmp:` 命名空间中的任务可以清理或创建 `Rails.root/tmp` 文件夹: +`tmp:` 命名空间中的任务可以清理或创建 `Rails.root/tmp` 目录: -* `rake tmp:cache:clear` 清理 `tmp/cache` 文件夹; -* `rake tmp:sessions:clear` 清理 `tmp/sessions` 文件夹; -* `rake tmp:sockets:clear` 清理 `tmp/sockets` 文件夹; -* `rake tmp:clear` 清理以上三个文件夹; -* `rake tmp:create` 创建会话、缓存、套接字和 PID 所需的临时文件夹; +* `rails tmp:cache:clear` 清空 `tmp/cache` 目录; +* `rails tmp:sockets:clear` 清空 `tmp/sockets` 目录; +* `rails tmp:clear` 清空所有缓存和套接字文件; +* `rails tmp:create` 创建缓存、套接字和 PID 所需的临时目录; + + ### 其他任务 -* `rake stats` 用来统计代码状况,显示千行代码数和测试比例等; -* `rake secret` 会生成一个伪随机字符串,作为会话的密钥; -* `rake time:zones:all` 列出 Rails 能理解的所有时区; +* `rails stats` 用于统计代码状况,显示千行代码数和测试比例等; +* `rails secret` 生成一个伪随机字符串,作为会话的密钥; +* `rails time:zones:all` 列出 Rails 能理解的所有时区; + + -### 编写 Rake 任务 +### 自定义 Rake 任务 -自己编写的 Rake 任务保存在 `Rails.root/lib/tasks` 文件夹中,文件的扩展名是 `.rake`。执行 `bin/rails generate task` 命令会生成一个新的自定义任务文件。 +自定义的 Rake 任务保存在 `Rails.root/lib/tasks` 目录中,文件的扩展名是 `.rake`。执行 `bin/rails generate task` 命令会生成一个新的自定义任务文件。 ```ruby desc "I am short, but comprehensive description for my cool task" task task_name: [:prerequisite_task, :another_task_we_depend_on] do - # All your magic here - # Any valid Ruby code is allowed + # 在这里定义任务 + # 可以使用任何有效的 Ruby 代码 end ``` -向自定义的任务中传入参数的方式如下: +向自定义的任务传入参数的方式如下: ```ruby -task :task_name, [:arg_1] => [:pre_1, :pre_2] do |t, args| - # You can use args from here +task :task_name, [:arg_1] => [:prerequisite_1, :prerequisite_2] do |task, args| + argument_1 = args.arg_1 end ``` @@ -515,33 +613,37 @@ end namespace :db do desc "This task does nothing" task :nothing do - # Seriously, nothing + # 确实什么也没做 end end ``` 执行任务的方法如下: -```bash -rake task_name -rake "task_name[value 1]" # entire argument string should be quoted -rake db:nothing +```sh +$ bin/rails task_name +$ bin/rails "task_name[value 1]" # 整个参数字符串应该放在引号内 +$ bin/rails db:nothing ``` -NOTE: 如果在任务中要和程序的模型交互,例如查询数据库等,可以使用 `environment` 任务,加载程序代码。 +NOTE: 如果在任务中要与应用的模型交互、查询数据库等,可以使用 `environment` 任务加载应用代码。 -Rails 命令行高级用法 ------------------- + + + +## Rails 命令行高级用法 Rails 命令行的高级用法就是找到实用的参数,满足特定需求或者工作流程。下面是一些常用的高级命令。 -### 新建程序时指定数据库和源码管理系统 + -新建程序时,可设置一些选项指定使用哪种数据库和源码管理系统。这么做可以节省一点时间,减少敲击键盘的次数。 +### 新建应用时指定数据库和源码管理系统 + +新建 Rails 应用时,可以设定一些选项指定使用哪种数据库和源码管理系统。这么做可以节省一点时间,减少敲击键盘的次数。 我们来看一下 `--git` 和 `--database=postgresql` 选项有什么作用: -```bash +```sh $ mkdir gitapp $ cd gitapp $ git init @@ -556,8 +658,8 @@ $ rails new . --git --database=postgresql create tmp/pids create Rakefile add 'Rakefile' - create README.rdoc -add 'README.rdoc' + create README.md +add 'README.md' create app/controllers/application_controller.rb add 'app/controllers/application_controller.rb' create app/helpers/application_helper.rb @@ -566,11 +668,11 @@ add 'app/controllers/application_controller.rb' add 'log/test.log' ``` -上面的命令先新建一个 `gitapp` 文件夹,初始化一个空的 git 仓库,然后再把 Rails 生成的文件加入仓库。再来看一下在数据库设置文件中添加了什么: +上面的命令先新建 `gitapp` 文件夹,初始化一个空的 git 仓库,然后再把 Rails 生成的文件纳入仓库。再来看一下它在数据库配置文件中添加了什么: -```bash +```sh $ cat config/database.yml -# PostgreSQL. Versions 8.2 and up are supported. +# PostgreSQL. Versions 9.1 and up are supported. # # Install the pg driver: # gem install pg @@ -597,6 +699,7 @@ development: ... ``` -这个命令还根据我们选择的 PostgreSQL 数据库在 `database.yml` 中添加了一些设置。 +这个命令还根据我们选择的 PostgreSQL 数据库在 `database.yml` 中添加了一些配置。 + +NOTE: 指定源码管理系统选项时唯一的不便是,要先新建存放应用的目录,再初始化源码管理系统,然后才能执行 `rails new` 命令生成应用骨架。 -NOTE: 指定源码管理系统选项时唯一的不便是,要先新建程序的文件夹,再初始化源码管理系统,然后才能执行 `rails new` 命令生成程序骨架。 diff --git a/source/zh-CN/configuring.md b/source/zh-CN/configuring.md index 943e202..9972921 100644 --- a/source/zh-CN/configuring.md +++ b/source/zh-CN/configuring.md @@ -1,168 +1,163 @@ -设置 Rails 程序 -============== +# 配置 Rails 应用 -本文介绍 Rails 程序的设置和初始化。 +本文涵盖 Rails 应用可用的配置和初始化功能。 -读完本文,你将学到: +读完本文后,您将学到: -* 如何调整 Rails 程序的表现; -* 如何在程序启动时运行其他代码; +* 如何调整 Rails 应用的行为; +* 如何增加额外代码,在应用启动时运行。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -初始化代码的存放位置 ------------------ + -Rails 的初始化代码存放在四个标准位置: +## 初始化代码的存放位置 -* `config/application.rb` 文件 -* 针对特定环境的设置文件; -* 初始化脚本; -* 后置初始化脚本; +Rails 为初始化代码提供了四个标准位置: -加载 Rails 前运行代码 -------------------- +* `config/application.rb` +* 针对各环境的配置文件 +* 初始化脚本 +* 后置初始化脚本 -如果想在加载 Rails 之前运行代码,可以把代码添加到 `config/application.rb` 文件的 `require 'rails/all'` 之前。 + -设置 Rails 组件 --------------- +## 在 Rails 之前运行代码 -总的来说,设置 Rails 的工作包括设置 Rails 的组件以及 Rails 本身。在设置文件 `config/application.rb` 和针对特定环境的设置文件(例如 `config/environments/production.rb`)中可以指定传给各个组件的不同设置项目。 +虽然在加载 Rails 自身之前运行代码很少见,但是如果想这么做,可以把代码添加到 `config/application.rb` 文件中 `require 'rails/all'` 的前面。 -例如,在文件 `config/application.rb` 中有下面这个设置: + -```ruby -config.autoload_paths += %W(#{config.root}/extras) -``` - -这是针对 Rails 本身的设置项目。如果想设置单独的 Rails 组件,一样可以在 `config/application.rb` 文件中使用同一个 `config` 对象: - -```ruby -config.active_record.schema_format = :ruby -``` - -Rails 会使用指定的设置配置 Active Record。 - -###3常规选项 - -下面这些设置方法在 `Rails::Railtie` 对象上调用,例如 `Rails::Engine` 或 `Rails::Application` 的子类。 - -* `config.after_initialize`:接受一个代码块,在 Rails 初始化程序之后执行。初始化的过程包括框架本身,引擎,以及 `config/initializers` 文件夹中所有的初始化脚本。注意,Rake 任务也会执行代码块中的代码。常用于设置初始化脚本用到的值。 - -```ruby -config.after_initialize do - ActionView::Base.sanitized_allowed_tags.delete 'div' -end -``` - -* `config.asset_host`:设置静态资源的主机。可用于设置静态资源所用的 CDN,或者通过不同的域名绕过浏览器对并发请求数量的限制。是 `config.action_controller.asset_host` 的简化。 - -* `config.autoload_once_paths`:一个由路径组成的数组,Rails 从这些路径中自动加载常量,且在多次请求之间一直可用。只有 `config.cache_classes` 为 `false`(开发环境中的默认值)时才有效。如果为 `true`,所有自动加载的代码每次请求时都会重新加载。这个数组中的路径必须出现在 `autoload_paths` 设置中。默认为空数组。 - -* `config.autoload_paths`:一个由路径组成的数组,Rails 从这些路径中自动加载常量。默认值为 `app` 文件夹中的所有子文件夹。 - -* `config.cache_classes`:决定程序中的类和模块在每次请求中是否要重新加载。在开发环境中的默认值是 `false`,在测试环境和生产环境中的默认值是 `true`。调用 `threadsafe!` 方法的作用和设为 `true` 一样。 - -* `config.action_view.cache_template_loading`:决定模板是否要在每次请求时重新加载。默认值等于 `config.cache_classes` 的值。 - -* `config.beginning_of_week`:设置一周从哪天开始。可使用的值是一周七天名称的符号形式,例如 `:monday`。 +## 配置 Rails 组件 -* `config.cache_store`:设置 Rails 缓存的存储方式。可选值有:`:memory_store`,`:file_store`,`:mem_cache_store`,`:null_store`,以及实现了缓存 API 的对象。如果文件夹 `tmp/cache` 存在,默认值为 `:file_store`,否则为 `:memory_store`。 +一般来说,配置 Rails 的意思是配置 Rails 的组件和 Rails 自身。传给各个组件的设置在 `config/application.rb` 配置文件或者针对各环境的配置文件(如 `config/environments/production.rb`)中指定。 -* `config.colorize_logging`:设定日志信息是否使用 ANSI 颜色代码。默认值为 `true`。 - -* `config.consider_all_requests_local`:如果设为 `true`,在 HTTP 响应中会显示详细的调试信息,而且 `Rails::Info` 控制器会在地址 `/rails/info/properties` 上显示程序的运行时上下文。在开发环境和测试环境中默认值为 `true`,在生产环境中默认值为 `false`。要想更精确的控制,可以把这个选项设为 `false`,然后在控制器中实现 `local_request?` 方法,指定哪些请求要显示调试信息。 - -* `config.console`:设置执行 `rails console` 命令时使用哪个类实现控制台,最好在 `console` 代码块中设置: +例如,`config/application.rb` 文件中有下述设置: ```ruby -console do - # this block is called only when running console, - # so we can safely require pry here - require "pry" - config.console = Pry -end +config.time_zone = 'Central Time (US & Canada)' ``` -* `config.dependency_loading`:设为 `false` 时禁止自动加载常量。只有 `config.cache_classes` 为 `true`(生产环境的默认值)时才有效。`config.threadsafe!` 为 `true` 时,这个选项为 `false`。 - -* `config.eager_load`:设为 `true` 是按需加载 `config.eager_load_namespaces` 中的所有命名空间,包括程序本身、引擎、Rails 框架和其他注册的命名空间。 - -* `config.eager_load_namespaces`:注册命名空间,`config.eager_load` 为 `true` 时按需加载。所有命名空间都要能响应 `eager_load!` 方法。 - -* `config.eager_load_paths`:一个由路径组成的数组,`config.cache_classes` 为 `true` 时,Rails 启动时按需加载对应的代码。 - -* `config.encoding`:设置程序全局编码,默认为 UTF-8。 - -* `config.exceptions_app`:设置抛出异常后中间件 ShowException 调用哪个异常处理程序。默认为 `ActionDispatch::PublicExceptions.new(Rails.public_path)`。 - -* `config.file_watcher`:设置监视文件系统上文件变化使用的类,`config.reload_classes_only_on_change` 为 `true` 时才有效。指定的类必须符合 `ActiveSupport::FileUpdateChecker` API。 - -* `config.filter_parameters`:过滤不想写入日志的参数,例如密码,信用卡卡号。把 `config.filter_parameters+=[:password]` 加入文件 `config/initializers/filter_parameter_logging.rb`,可以过滤密码。 - -* `config.force_ssl`:强制所有请求使用 HTTPS 协议,通过 `ActionDispatch::SSL` 中间件实现。 - -* `config.log_formatter`:设置 Rails 日志的格式化工具。在生产环境中默认值为 `Logger::Formatter`,其他环境默认值为 `ActiveSupport::Logger::SimpleFormatter`。 - -* `config.log_level`:设置 Rails 日志等级。在生产环境中默认值为 `:info`,其他环境默认值为 `:debug`。 - -* `config.log_tags`:一组可响应 `request` 对象的方法。可在日志消息中加入更多信息,例如二级域名和请求 ID,便于调试多用户程序。 - -* `config.logger`:接受一个实现了 Log4r 接口的类,或者使用默认的 `Logger` 类。默认值为 `ActiveSupport::Logger`,在生产环境中关闭了自动冲刷功能。 - -* `config.middleware`:设置程序使用的中间件。详情参阅“[设置中间件](#configuring-middleware)”一节。 - -* `config.reload_classes_only_on_change`:只当监视的文件变化时才重新加载。默认值为 `true`,监视 `autoload_paths` 中所有路径。如果 `config.cache_classes` 为 `true`,忽略这个设置。 - -* `secrets.secret_key_base`: -指定一个密令,和已知的安全密令比对,防止篡改会话。新建程序时会生成一个随机密令,保存在文件 `config/secrets.yml` 中。 - -* `config.serve_static_assets`:让 Rails 伺服静态资源文件。默认值为 `true`,但在生产环境中为 `false`,因为应该使用服务器软件(例如 Nginx 或 Apache)伺服静态资源文件。 如果测试程序,或者在生产环境中使用 WEBrick(极力不推荐),应该设为 `true`,否则无法使用页面缓存,请求 `public` 文件夹中的文件时也会经由 Rails 处理。 - -* `config.session_store`:一般在 `config/initializers/session_store.rb` 文件中设置,指定使用什么方式存储会话。可用值有:`:cookie_store`(默认),`:mem_cache_store` 和 `:disabled`。`:disabled` 指明不让 Rails 处理会话。当然也可指定自定义的会话存储: +这是针对 Rails 自身的设置。如果想把设置传给某个 Rails 组件,依然是在 `config/application.rb` 文件中通过 `config` 对象去做: ```ruby -config.session_store :my_custom_store +config.active_record.schema_format = :ruby ``` -这个自定义的存储方式必须定义为 `ActionDispatch::Session::MyCustomStore`。 +Rails 会使用这个设置配置 Active Record。 -* `config.time_zone`:设置程序使用的默认时区,也让 Active Record 使用这个时区。 + -###3设置静态资源 +### Rails 的一般性配置 -* `config.assets.enabled`:设置是否启用 Asset Pipeline。默认启用。 +这些配置方法在 `Rails::Railtie` 对象上调用,例如 `Rails::Engine` 或 `Rails::Application` 的子类。 -* `config.assets.raise_runtime_errors`:设为 `true`,启用额外的运行时错误检查。建议在 `config/environments/development.rb` 中设置,这样可以尽量减少部署到生产环境后的异常表现。 +* `config.after_initialize` 接受一个块,在 Rails 初始化应用之后运行。初始化过程包括初始化框架自身、引擎和 `config/initializers` 目录中的全部初始化脚本。注意,这个块会被 Rake 任务运行。可用于配置其他初始化脚本设定的值: -* `config.assets.compress`:是否压缩编译后的静态资源文件。在 `config/environments/production.rb` 中为 `true`。 - -* `config.assets.css_compressor`:设定使用的 CSS 压缩程序,默认为 `sass-rails`。目前,唯一可用的另一个值是 `:yui`,使用 `yui-compressor` gem 压缩文件。 - -* `config.assets.js_compressor`:设定使用的 JavaScript 压缩程序。可用值有:`:closure`,`:uglifier` 和 `:yui`。分别需要安装 `closure-compiler`,`uglifier` 和 `yui-compressor` 这三个 gem。 - -* `config.assets.paths`:查找静态资源文件的路径。Rails 会在这个选项添加的路径中查找静态资源文件。 - -* `config.assets.precompile`:指定执行 `rake assets:precompile` 任务时除 `application.css` 和 `application.js` 之外要编译的其他资源文件。 - -* `config.assets.prefix`:指定伺服静态资源文件时使用的地址前缀,默认为 `/assets`。 - -* `config.assets.digest`:在静态资源文件名中加入 MD5 指纹。在 `production.rb` 中默认设为 `true`。 - -* `config.assets.debug`:禁止合并和压缩静态资源文件。在 `development.rb` 中默认设为 `true`。 - -* `config.assets.cache_store`:设置 Sprockets 使用的缓存方式,默认使用文件存储。 - -* `config.assets.version`:生成 MD5 哈希时用到的一个字符串。可用来强制重新编译所有文件。 - -* `config.assets.compile`:布尔值,用于在生产环境中启用 Sprockets 实时编译功能。 - -* `config.assets.logger`:接受一个实现了 Log4r 接口的类,或者使用默认的 `Logger` 类。默认值等于 `config.logger` 选项的值。把 `config.assets.logger` 设为 `false`,可以关闭静态资源相关的日志。 - -###3设置生成器 - -Rails 允许使用 `config.generators` 方法设置使用的生成器。这个方法接受一个代码块: + ```ruby + config.after_initialize do + ActionView::Base.sanitized_allowed_tags.delete 'div' + end + ``` + + +* `config.asset_host` 设定静态资源文件的主机名。使用 CDN 贮存静态资源文件,或者想绕开浏览器对同一域名的并发连接数的限制时可以使用这个选项。这是 `config.action_controller.asset_host` 的简短版本。 +* `config.autoload_once_paths` 接受一个路径数组,告诉 Rails 自动加载常量后不在每次请求中都清空。如果 `config.cache_classes` 的值为 `false`(开发环境的默认值),这个选项有影响。否则,都只自动加载一次。这个数组的全部元素都要在 `autoload_paths` 中。默认值为一个空数组。 +* `config.autoload_paths` 接受一个路径数组,让 Rails 自动加载里面的常量。默认值是 `app` 目录中的全部子目录。 +* `config.cache_classes` 控制每次请求是否重新加载应用的类和模块。在开发环境中默认为 `false`,在测试和生产环境中默认为 `true`。 +* `config.action_view.cache_template_loading` 控制每次请求是否重新加载模板。默认值为 `config.cache_classes` 的值。 +* `config.beginning_of_week` 设定一周从周几开始。可接受的值是有效的周几符号(如 `:monday`)。 +* `config.cache_store` 配置 Rails 缓存使用哪个存储器。可用的选项有:`:memory_store`、`:file_store`、`:mem_cache_store`、`:null_store`,或者实现了缓存 API 的对象。默认值为 `:file_store`。 +* `config.colorize_logging` 指定在日志中记录信息时是否使用 ANSI 颜色代码。默认值为 `true`。 +* `config.consider_all_requests_local` 是一个旗标。如果设为 `true`,发生任何错误都会把详细的调试信息转储到 HTTP 响应中,而且 `Rails::Info` 控制器会在 `/rails/info/properties` 中显示应用的运行时上下文。开发和测试环境中默认为 `true`,生产环境默认为 `false`。如果想精细控制,把这个选项设为 `false`,然后在控制器中实现 `local_request?` 方法,指定哪些请求应该在出错时显示调试信息。 +* `config.console` 设定 `rails console` 命令所用的控制台类。最好在 `console` 块中运行: + + ```ruby + console do + # 这个块只在运行控制台时运行 + # 因此可以安全引入 pry + require "pry" + config.console = Pry + end + ``` + + +* `config.eager_load` 设为 `true` 时,及早加载注册的全部 `config.eager_load_namespaces`。包括应用、引擎、Rails 框架和注册的其他命名空间。 +* `config.eager_load_namespaces` 注册命名空间,当 `config.eager_load` 为 `true` 时及早加载。这里列出的所有命名空间都必须响应 `eager_load!` 方法。 +* `config.eager_load_paths` 接受一个路径数组,如果启用类缓存,启动 Rails 时会及早加载。默认值为 `app` 目录中的全部子目录。 +* `config.enable_dependency_loading` 设为 `true` 时,即便应用及早加载了,而且把 `config.cache_classes` 设为 `true`,也自动加载。默认值为 `false`。 +* `config.encoding` 设定应用全局编码。默认为 UTF-8。 +* `config.exceptions_app` 设定出现异常时 ShowException 中间件调用的异常应用。默认为 `ActionDispatch::PublicExceptions.new(Rails.public_path)`。 +* `config.debug_exception_response_format` 设定开发环境中出错时响应的格式。只提供 API 的应用默认值为 `:api`,常规应用的默认值为 `:default`。 +* `config.file_watcher` 指定一个类,当 `config.reload_classes_only_on_change` 设为 `true` 时用于检测文件系统中文件的变动。Rails 提供了 `ActiveSupport::FileUpdateChecker`(默认)和 `ActiveSupport::EventedFileUpdateChecker`(依赖 [listen](https://github.com/guard/listen) gem)。自定义的类必须符合 `ActiveSupport::FileUpdateChecker` API。 +* `config.filter_parameters` 用于过滤不想记录到日志中的参数,例如密码或信用卡卡号。默认,Rails 把 `Rails.application.config.filter_parameters += [:password]` 添加到 `config/initializers/filter_parameter_logging.rb` 文件中,过滤密码。过滤的参数部分匹配正则表达式。 +* `config.force_ssl` 强制所有请求经由 `ActionDispatch::SSL` 中间件处理,即通过 HTTPS 伺服,而且把 `config.action_mailer.default_url_options` 设为 `{ protocol: 'https' }`。SSL 通过设定 `config.ssl_options` 选项配置,详情参见 [`ActionDispatch::SSL` 的文档](http://api.rubyonrails.org/classes/ActionDispatch/SSL.html)。 +* `config.log_formatter` 定义 Rails 日志记录器的格式化程序。这个选项的默认值在所有环境中都是 `ActiveSupport::Logger::SimpleFormatter` 的实例。如果为 `config.logger` 设定了值,必须在包装到 `ActiveSupport::TaggedLogging` 实例中之前手动把格式化程序的值传给日志记录器,Rails 不会为你代劳。 +* `config.log_level` 定义 Rails 日志记录器的详细程度。在所有环境中,这个选项的默认值都是 `:debug`。可用的日志等级有 `:debug`、`:info`、`:warn`、`:error`、`:fatal` 和 `:unknown`。 +* `config.log_tags` 的值可以是一组 `request` 对象响应的方法,可以是一个接受 `request` 对象的 `Proc`,也可以是能响应 `to_s` 方法的对象。这样便于为包含调试信息的日志行添加标签,例如二级域名和请求 ID——二者对调试多用户应用十分有用。 +* `config.logger` 指定 `Rails.logger` 和与 Rails 有关的其他日志(`ActiveRecord::Base.logger`)所用的日志记录器。默认值为 `ActiveSupport::TaggedLogging` 实例,包装 `ActiveSupport::Logger` 实例,把日志存储在 `log/` 目录中。你可以提供自定义的日志记录器,但是为了完全兼容,必须遵照下述指导方针: + + * 为了支持格式化程序,必须手动把 `config.log_formatter` 指定的格式化程序赋值给日志记录器。 + * 为了支持日志标签,日志实例必须使用 `ActiveSupport::TaggedLogging` 包装。 + * 为了支持静默,日志记录器必须引入 `LoggerSilence` 和 `ActiveSupport::LoggerThreadSafeLevel` 模块。`ActiveSupport::Logger` 类已经引入这两个模块。 + + ```ruby + class MyLogger < ::Logger + include ActiveSupport::LoggerThreadSafeLevel + include LoggerSilence + end + + mylogger = MyLogger.new(STDOUT) + mylogger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(mylogger) + ``` + + + + +* `config.middleware` 用于配置应用的中间件。详情参见 [配置中间件](#configuring-middleware)。 +* `config.reload_classes_only_on_change` 设定仅在跟踪的文件有变化时是否重新加载类。默认跟踪自动加载路径中的一切文件,这个选项的值为 `true`。如果把 `config.cache_classes` 设为 `true`,这个选项将被忽略。 +* `secrets.secret_key_base` 用于指定一个密钥,检查应用的会话,防止篡改。`secrets.secret_key_base` 的值一开始是个随机的字符串,存储在 `config/secrets.yml` 文件中。 +* `config.public_file_server.enabled` 配置 Rails 从 public 目录中伺服静态文件。这个选项的默认值是 `false`,但在生产环境中设为 `false`,因为应该使用运行应用的服务器软件(如 NGINX 或 Apache)伺服静态文件。在生产环境中如果使用 WEBrick 运行或测试应用(不建议在生产环境中使用 WEBrick),把这个选项设为 `true`。否则无法使用页面缓存,也无法请求 public 目录中的文件。 +* `config.session_store` 指定使用哪个类存储会话。可用的值有 `:cookie_store`(默认值)、`:mem_cache_store` 和 `:disabled`。最后一个值告诉 Rails 不处理会话。cookie 存储器中的会话键默认使用应用的名称。也可以指定自定义的会话存储器: + + ```ruby + config.session_store :my_custom_store + ``` + + 这个自定义的存储器必须定义为 `ActionDispatch::Session::MyCustomStore`。 + + +* `config.time_zone` 设定应用的默认时区,并让 Active Record 知道。 + + + +### 配置静态资源 + +* `config.assets.enabled` 是个旗标,控制是否启用 Asset Pipeline。默认值为 `true`。 +* `config.assets.raise_runtime_errors` 设为 `true` 时启用额外的运行时错误检查。推荐在 `config/environments/development.rb` 中设定,以免部署到生产环境时遇到意料之外的错误。 +* `config.assets.css_compressor` 定义所用的 CSS 压缩程序。默认设为 `sass-rails`。目前唯一的另一个值是 `:yui`,使用 `yui-compressor` gem 压缩。 +* `config.assets.js_compressor` 定义所用的 JavaScript 压缩程序。可用的值有 `:closure`、`:uglifier` 和 `:yui`,分别使用 `closure-compiler`、`uglifier` 和 `yui-compressor` gem。 +* `config.assets.gzip` 是一个旗标,设定在静态资源的常规版本之外是否创建 gzip 版本。默认为 `true`。 +* `config.assets.paths` 包含查找静态资源的路径。在这个配置选项中追加的路径,会在里面寻找静态资源。 +* `config.assets.precompile` 设定运行 `rake assets:precompile` 任务时要预先编译的其他静态资源(除 `application.css` 和 `application.js` 之外)。 +* `config.assets.unknown_asset_fallback` 在使用 sprockets-rails 3.2.0 或以上版本时用于修改 Asset Pipeline 找不到静态资源时的行为。默认为 `true`。 +* `config.assets.prefix` 定义伺服静态资源的前缀。默认为 `/assets`。 +* `config.assets.manifest` 定义静态资源预编译器使用的清单文件的完整路径。默认为 `public` 文件夹中 `config.assets.prefix` 设定的目录中的 `manifest-.json`。 +* `config.assets.digest` 设定是否在静态资源的名称中包含 SHA256 指纹。默认为 `true`。 +* `config.assets.debug` 禁止拼接和压缩静态文件。在 `development.rb` 文件中默认设为 `true`。 + +`config.assets.version` 是在生成 SHA256 哈希值过程中使用的一个字符串。修改这个值可以强制重新编译所有文件。 + +* `config.assets.compile` 是一个旗标,设定在生产环境中是否启用实时 Sprockets 编译。 +* `config.assets.logger` 接受一个符合 Log4r 接口的日志记录器,或者默认的 Ruby `Logger` 类。默认值与 `config.logger` 相同。如果设为 `false`,不记录对静态资源的伺服。 +* `config.assets.quiet` 禁止在日志中记录对静态资源的请求。在 `development.rb` 文件中默认设为 `true`。 + + + +### 配置生成器 + +Rails 允许通过 `config.generators` 方法调整生成器的行为。这个方法接受一个块: ```ruby config.generators do |g| @@ -171,337 +166,491 @@ config.generators do |g| end ``` -在代码块中可用的方法如下所示: - -* `assets`:是否允许脚手架创建静态资源文件,默认为 `true`。 -* `force_plural`:是否允许使用复数形式的模型名,默认为 `false`。 -* `helper`:是否生成帮助方法文件,默认为 `true`。 -* `integration_tool`:设置使用哪个集成工具,默认为 `nil`。 -* `javascripts`:是否允许脚手架创建 JavaScript 文件,默认为 `true`。 -* `javascript_engine`:设置生成静态资源文件时使用的预处理引擎(例如 CoffeeScript),默认为 `nil`。 -* `orm`:设置使用哪个 ORM。默认为 `false`,使用 Active Record。 -* `resource_controller`:设定执行 `rails generate resource` 命令时使用哪个生成器生成控制器,默认为 `:controller`。 -* `scaffold_controller`:和 `resource_controller` 不同,设定执行 `rails generate scaffold` 命令时使用哪个生成器生成控制器,默认为 `:scaffold_controller`。 -* `stylesheets`:是否启用生成器中的样式表文件钩子,在执行脚手架时使用,也可用于其他生成器,默认值为 `true`。 -* `stylesheet_engine`:设置生成静态资源文件时使用的预处理引擎(例如 Sass),默认为 `:css`。 -* `test_framework`:设置使用哪个测试框架,默认为 `false`,使用 Test::Unit。 -* `template_engine`:设置使用哪个模板引擎,例如 ERB 或 Haml,默认为 `:erb`。 - -###3设置中间件 - -每个 Rails 程序都使用了一组标准的中间件,在开发环境中的加载顺序如下: - -* `ActionDispatch::SSL`:强制使用 HTTPS 协议处理每个请求。`config.force_ssl` 设为 `true` 时才可用。`config.ssl_options` 选项的值会传给这个中间件。 -* `ActionDispatch::Static`:用来伺服静态资源文件。如果 `config.serve_static_assets` 设为 `false`,则不会使用这个中间件。 -* `Rack::Lock`:把程序放入互斥锁中,一次只能在一个线程中运行。`config.cache_classes` 设为 `false` 时才会使用这个中间件。 -* `ActiveSupport::Cache::Strategy::LocalCache`:使用内存存储缓存。这种存储方式对线程不安全,而且只能在单个线程中做临时存储。 -* `Rack::Runtime`:设定 `X-Runtime` 报头,其值为处理请求花费的时间,单位为秒。 -* `Rails::Rack::Logger`:开始处理请求时写入日志,请求处理完成后冲刷所有日志。 -* `ActionDispatch::ShowExceptions`:捕获程序抛出的异常,如果在本地处理请求,或者 `config.consider_all_requests_local` 设为 `true`,会渲染一个精美的异常页面。如果 `config.action_dispatch.show_exceptions` 设为 `false`,则会直接抛出异常。 -* `ActionDispatch::RequestId`:在响应中加入一个唯一的 `X-Request-Id` 报头,并启用 `ActionDispatch::Request#uuid` 方法。 -* `ActionDispatch::RemoteIp`:从请求报头中获取正确的 `client_ip`,检测 IP 地址欺骗攻击。通过 `config.action_dispatch.ip_spoofing_check` 和 `config.action_dispatch.trusted_proxies` 设置。 -* `Rack::Sendfile`:响应主体为一个文件,并设置 `X-Sendfile` 报头。通过 `config.action_dispatch.x_sendfile_header` 设置。 -* `ActionDispatch::Callbacks`:处理请求之前运行指定的回调。 -* `ActiveRecord::ConnectionAdapters::ConnectionManagement`:每次请求后都清理可用的连接,除非把在请求环境变量中把 `rack.test` 键设为 `true`。 -* `ActiveRecord::QueryCache`:缓存请求中使用的 `SELECT` 查询。如果用到了 `INSERT` 或 `UPDATE` 语句,则清除缓存。 -* `ActionDispatch::Cookies`:设置请求的 cookie。 -* `ActionDispatch::Session::CookieStore`:把会话存储在 cookie 中。`config.action_controller.session_store` 设为其他值时则使用其他中间件。`config.action_controller.session_options` 的值会传给这个中间件。 -* `ActionDispatch::Flash`:设定 `flash` 键。必须为 `config.action_controller.session_store` 设置一个值,才能使用这个中间件。 -* `ActionDispatch::ParamsParser`:解析请求中的参数,生成 `params`。 -* `Rack::MethodOverride`:如果设置了 `params[:_method]`,则使用相应的方法作为此次请求的方法。这个中间件提供了对 PATCH、PUT 和 DELETE 三个 HTTP 请求方法的支持。 -* `ActionDispatch::Head`:把 HEAD 请求转换成 GET 请求,并处理请求。 - -除了上述标准中间件之外,还可使用 `config.middleware.use` 方法添加其他中间件: +在这个块中可以使用的全部方法如下: + +* `assets` 指定在生成脚手架时是否创建静态资源。默认为 `true`。 +* `force_plural` 指定模型名是否允许使用复数。默认为 `false`。 +* `helper` 指定是否生成辅助模块。默认为 `true`。 +* `integration_tool` 指定使用哪个集成工具生成集成测试。默认为 `:test_unit`。 +* `javascripts` 启用生成器中的 JavaScript 文件钩子。在 Rails 中供 `scaffold` 生成器使用。默认为 `true`。 +* `javascript_engine` 配置生成静态资源时使用的脚本引擎(如 coffee)。默认为 `:js`。 +* `orm` 指定使用哪个 ORM。默认为 `false`,即使用 Active Record。 +* `resource_controller` 指定 `rails generate resource` 使用哪个生成器生成控制器。默认为 `:controller`。 +* `resource_route` 指定是否生成资源路由。默认为 `true`。 +* `scaffold_controller` 与 `resource_controller` 不同,它指定 `rails generate scaffold` 使用哪个生成器生成脚手架中的控制器。默认为 `:scaffold_controller`。 +* `stylesheets` 启用生成器中的样式表钩子。在 Rails 中供 `scaffold` 生成器使用,不过也可以供其他生成器使用。默认为 `true`。 +* `stylesheet_engine` 配置生成静态资源时使用的样式表引擎(如 sass)。默认为 `:css`。 +* `scaffold_stylesheet` 生成脚手架中的资源时创建 `scaffold.css`。默认为 `true`。 +* `test_framework` 指定使用哪个测试框架。默认为 `false`,即使用 Minitest。 +* `template_engine` 指定使用哪个模板引擎,例如 ERB 或 Haml。默认为 `:erb`。 + + + +### 配置中间件 + +每个 Rails 应用都自带一系列中间件,在开发环境中按下述顺序使用: + +* `ActionDispatch::SSL` 强制使用 HTTPS 伺服每个请求。`config.force_ssl` 设为 `true` 时启用。传给这个中间件的选项通过 `config.ssl_options` 配置。 +* `ActionDispatch::Static` 用于伺服静态资源。`config.public_file_server.enabled` 设为 `false` 时禁用。如果静态资源目录的索引文件不是 `index`,使用 `config.public_file_server.index_name` 指定。例如,请求目录时如果想伺服 `main.html`,而不是 `index.html`,把 `config.public_file_server.index_name` 设为 `"main"`。 +* `ActionDispatch::Executor` 以线程安全的方式重新加载代码。`onfig.allow_concurrency` 设为 `false` 时禁用,此时加载 `Rack::Lock`。`Rack::Lock` 把应用包装在 mutex 中,因此一次只能被一个线程调用。 +* `ActiveSupport::Cache::Strategy::LocalCache` 是基本的内存后端缓存。这个缓存对线程不安全,只应该用作单线程的临时内存缓存。 +* `Rack::Runtime` 设定 `X-Runtime` 首部,包含执行请求的时间(单位为秒)。 +* `Rails::Rack::Logger` 通知日志请求开始了。请求完成后,清空相关日志。 +* `ActionDispatch::ShowExceptions` 拯救应用抛出的任何异常,在本地或者把 `config.consider_all_requests_local` 设为 `true` 时渲染精美的异常页面。如果把 `config.action_dispatch.show_exceptions` 设为 `false`,异常总是抛出。 +* `ActionDispatch::RequestId` 在响应中添加 `X-Request-Id` 首部,并且启用 `ActionDispatch::Request#uuid` 方法。 +* `ActionDispatch::RemoteIp` 检查 IP 欺骗攻击,从请求首部中获取有效的 `client_ip`。可通过 `config.action_dispatch.ip_spoofing_check` 和 `config.action_dispatch.trusted_proxies` 配置。 +* `Rack::Sendfile` 截获从文件中伺服内容的响应,将其替换成服务器专属的 `X-Sendfile` 首部。可通过 `config.action_dispatch.x_sendfile_header` 配置。 +* `ActionDispatch::Callbacks` 在伺服请求之前运行准备回调。 +* `ActionDispatch::Cookies` 为请求设定 cookie。 +* `ActionDispatch::Session::CookieStore` 负责把会话存储在 cookie 中。可以把 `config.action_controller.session_store` 改为其他值,换成其他中间件。此外,可以使用 `config.action_controller.session_options` 配置传给这个中间件的选项。 +* `ActionDispatch::Flash` 设定 `flash` 键。仅当为 `config.action_controller.session_store` 设定值时可用。 +* `Rack::MethodOverride` 在设定了 `params[:_method]` 时允许覆盖请求方法。这是支持 PATCH、PUT 和 DELETE HTTP 请求的中间件。 +* `Rack::Head` 把 HEAD 请求转换成 GET 请求,然后以 GET 请求伺服。 + +除了这些常规中间件之外,还可以使用 `config.middleware.use` 方法添加: ```ruby config.middleware.use Magical::Unicorns ``` -上述代码会把中间件 `Magical::Unicorns` 放入中间件列表的最后。如果想在某个中间件之前插入中间件,可以使用 `insert_before`: +上述代码把 `Magical::Unicorns` 中间件添加到栈的末尾。如果想把中间件添加到另一个中间件的前面,可以使用 `insert_before`: ```ruby -config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns +config.middleware.insert_before Rack::Head, Magical::Unicorns ``` -如果想在某个中间件之后插入中间件,可以使用 `insert_after`: +也可以使用索引把中间件插入指定的具体位置。例如,若想把 `Magical::Unicorns` 中间件插入栈顶,可以这么做: ```ruby -config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns +config.middleware.insert_before 0, Magical::Unicorns ``` -中间件还可替换成其他中间件: +此外,还有 `insert_after`。它把中间件添加到另一个中间件的后面: ```ruby -config.middleware.swap ActionController::Failsafe, Lifo::Failsafe +config.middleware.insert_after Rack::Head, Magical::Unicorns ``` -也可从中间件列表中删除: +中间件也可以完全替换掉: ```ruby -config.middleware.delete "Rack::MethodOverride" +config.middleware.swap ActionController::Failsafe, Lifo::Failsafe ``` -###3设置 i18n - -下述设置项目都针对 `I18n` 代码库。 - -* `config.i18n.available_locales`:设置程序可用本地语言的白名单。默认值为可在本地化文件中找到的所有本地语言,在新建程序中一般是 `:en`。 - -* `config.i18n.default_locale`:设置程序的默认本地化语言,默认值为 `:en`。 - -* `config.i18n.enforce_available_locales`:确保传给 i18n 的本地语言在 `available_locales` 列表中,否则抛出 `I18n::InvalidLocale` 异常。默认值为 `true`。除非特别需要,不建议禁用这个选项,因为这是一项安全措施,能防止用户提供不可用的本地语言。 - -* `config.i18n.load_path`:设置 Rails 搜寻本地化文件的路径。默认为 config/locales/*.{yml,rb}`。 - -###3设置 Active Record - -`config.active_record` 包含很多设置项: - -* `config.active_record.logger`:接受一个实现了 Log4r 接口的类,或者使用默认的 `Logger` 类,然后传给新建的数据库连接。在 Active Record 模型类或模型实例上调用 `logger` 方法可以获取这个日志类。设为 `nil` 禁用日志。 - -* `config.active_record.primary_key_prefix_type`:调整主键的命名方式。默认情况下,Rails 把主键命名为 `id`(无需设置这个选项)。除此之外还有另外两个选择: - * `:table_name`:`Customer` 模型的主键为 `customerid`; - * `:table_name_with_underscore`: `Customer` 模型的主键为 `customer_id`; - -* `config.active_record.table_name_prefix`:设置一个全局字符串,作为数据表名的前缀。如果设为 `northwest_`,那么 `Customer` 模型对应的表名为 `northwest_customers`。默认为空字符串。 - -* `config.active_record.table_name_suffix`:设置一个全局字符串,作为数据表名的后缀。如果设为 `_northwest`,那么 `Customer` 模型对应的表名为 `customers_northwest`。默认为空字符串。 - -* `config.active_record.schema_migrations_table_name`:设置模式迁移数据表的表名。 - -* `config.active_record.pluralize_table_names`:设置 Rails 在数据库中要寻找单数形式还是复数形式的数据表。如果设为 `true`(默认值),`Customer` 类对应的数据表是 `customers`。如果设为 `false`,`Customer` 类对应的数据表是 `customer`。 - -* `config.active_record.default_timezone`:从数据库中查询日期和时间时使用 `Time.local`(设为 `:local` 时)还是 `Time.utc`(设为 `:utc` 时)。默认为 `:utc`。 - -* `config.active_record.schema_format`:设置导出数据库模式到文件时使用的格式。可选项包括:`:ruby`,默认值,根据迁移导出模式,与数据库种类无关;`:sql`,导出为 SQL 语句,受数据库种类影响。 - -* `config.active_record.timestamped_migrations`:设置迁移编号使用连续的数字还是时间戳。默认值为 `true`,使用时间戳。如果有多名开发者协作,建议使用时间戳。 - -* `config.active_record.lock_optimistically`:设置 Active Record 是否使用乐观锁定,默认使用。 - -* `config.active_record.cache_timestamp_format`:设置缓存键中使用的时间戳格式,默认为 `:number`。 - -* `config.active_record.record_timestamps`:设置是否记录 `create` 和 `update` 动作的时间戳。默认为 `true`。 - -* `config.active_record.partial_writes`: 布尔值,设置是否局部写入(例如,只更新有变化的属性)。注意,如果使用局部写入,还要使用乐观锁定,因为并发更新写入的数据可能已经过期。默认值为 `true`。 - -* `config.active_record.attribute_types_cached_by_default`:设置读取时 `ActiveRecord::AttributeMethods` 缓存的字段类型。默认值为 `[:datetime, :timestamp, :time, :date]`。 - -* `config.active_record.maintain_test_schema`:设置运行测试时 Active Record 是否要保持测试数据库的模式和 `db/schema.rb` 文件(或 `db/structure.sql`)一致,默认为 `true`。 - -* `config.active_record.dump_schema_after_migration`:设置运行迁移后是否要导出数据库模式到文件 `db/schema.rb` 或 `db/structure.sql` 中。这项设置在 Rails 生成的 `config/environments/production.rb` 文件中为 `false`。如果不设置这个选项,则值为 `true`。 - -MySQL 适配器添加了一项额外设置: - -* `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans`:设置 Active Record 是否要把 MySQL 数据库中 `tinyint(1)` 类型的字段视为布尔值,默认为 `true`。 +还可以从栈中移除: -模式导出程序添加了一项额外设置: - -* `ActiveRecord::SchemaDumper.ignore_tables`:指定一个由数据表组成的数组,导出模式时不会出现在模式文件中。仅当 `config.active_record.schema_format == :ruby` 时才有效。 - -###3设置 Action Controller - -`config.action_controller` 包含以下设置项: - -* `config.action_controller.asset_host`:设置静态资源的主机,不用程序的服务器伺服静态资源,而使用 CDN。 - -* `config.action_controller.perform_caching`:设置程序是否要缓存。在开发模式中为 `false`,生产环境中为 `true`。 - -* `config.action_controller.default_static_extension`:设置缓存文件的扩展名,默认为 `.html`。 - -* `config.action_controller.default_charset`:设置默认字符集,默认为 `utf-8`。 - -* `config.action_controller.logger`:接受一个实现了 Log4r 接口的类,或者使用默认的 `Logger` 类,用于写 Action Controller 中的日志消息。设为 `nil` 禁用日志。 - -* `config.action_controller.request_forgery_protection_token`:设置请求伪造保护的权标参数名。默认情况下调用 `protect_from_forgery` 方法,将其设为 `:authenticity_token`。 - -* `config.action_controller.allow_forgery_protection`:是否启用跨站请求伪造保护功能。在测试环境中默认为 `false`,其他环境中默认为 `true`。 - -* `config.action_controller.relative_url_root`:用来告知 Rails 程序[部署在子目录中](#deploy-to-a-subdirectory-relative-url-root)。默认值为 `ENV['RAILS_RELATIVE_URL_ROOT']`。 - -* `config.action_controller.permit_all_parameters`:设置默认允许在批量赋值中使用的参数,默认为 `false`。 - -* `config.action_controller.action_on_unpermitted_parameters`:发现禁止使用的参数时,写入日志还是抛出异常(分别设为 `:log` 和 `:raise`)。在开发环境和测试环境中的默认值为 `:log`,在其他环境中的默认值为 `false`。 +```ruby +config.middleware.delete Rack::MethodOverride +``` -###3设置 Action Dispatch + + +### 配置 i18n + +这些配置选项都委托给 `I18n` 库。 + +* `config.i18n.available_locales` 设定应用可用的本地化白名单。默认为在本地化文件中找到的全部本地化键,在新应用中通常只有 `:en`。 +* `config.i18n.default_locale` 设定供 i18n 使用的默认本地化。默认为 `:en`。 +* `config.i18n.enforce_available_locales` 确保传给 i18n 的本地化必须在 `available_locales` 声明的列表中,否则抛出 `I18n::InvalidLocale` 异常。默认为 `true`。除非有特别的原因,否则不建议禁用这个选项,因为这是一项安全措施,能防止用户输入无效的本地化。 +* `config.i18n.load_path` 设定 Rails 寻找本地化文件的路径。默认为 `config/locales/*.{yml,rb}`。 +* `config.i18n.fallbacks` 设定没有翻译时的回落行为。下面是这个选项的单个使用示例: + + * 设为 `true`,回落到默认区域设置: + + ```ruby + config.i18n.fallbacks = true + ``` + + + * 设为一个区域设置数据: + + ```ruby + config.i18n.fallbacks = [:tr, :en] + ``` + + + * 还可以为各个区域设置设定不同的回落语言。例如,如果想把 `:tr` 作为 `:az` 的回落语言,把 `:de` 和 :en` 作为 `:da` 的回落语言,可以这么做: + + ```ruby + config.i18n.fallbacks = { az: :tr, da: [:de, :en] } + # 或 + config.i18n.fallbacks.map = { az: :tr, da: [:de, :en] } + ``` + + + + + + + + +### 配置 Active Record + +`config.active_record` 包含众多配置选项: + +* `config.active_record.logger` 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby `Logger` 类,然后传给新的数据库连接。可以在 Active Record 模型类或实例上调用 `logger` 方法获取日志记录器。设为 `nil` 时禁用日志。 +* `config.active_record.primary_key_prefix_type` 用于调整主键列的名称。默认情况下,Rails 假定主键列名为 `id`(无需配置)。此外有两个选择: + + * 设为 `:table_name` 时,`Customer` 类的主键为 `customerid`。 + * 设为 `:table_name_with_underscore` 时,`Customer` 类的主键为 `customer_id`。 + + +* `config.active_record.table_name_prefix` 设定一个全局字符串,放在表名前面。如果设为 `northwest_`,`Customer` 类对应的表是 `northwest_customers`。默认为空字符串。 +* `config.active_record.table_name_suffix` 设定一个全局字符串,放在表名后面。如果设为 `_northwest`,`Customer` 类对应的表是 `customers_northwest`。默认为空字符串。 +* `config.active_record.schema_migrations_table_name` 设定模式迁移表的名称。 +* `config.active_record.pluralize_table_names` 指定 Rails 在数据库中寻找单数还是复数表名。如果设为 `true`(默认),那么 `Customer` 类使用 `customers` 表。如果设为 `false`,`Customer` 类使用 `customer` 表。 +* `config.active_record.default_timezone` 设定从数据库中检索日期和时间时使用 `Time.local`(设为 `:local` 时)还是 `Time.utc`(设为 `:utc` 时)。默认为 `:utc`。 +* `config.active_record.schema_format` 控制把数据库模式转储到文件中时使用的格式。可用的值有:`:ruby`(默认),与所用的数据库无关;`:sql`,转储 SQL 语句(可能与数据库有关)。 +* `config.active_record.error_on_ignored_order_or_limit` 指定批量查询时如果忽略顺序是否抛出错误。设为 `true` 时抛出错误,设为 `false` 时发出提醒。默认为 `false`。 +* `config.active_record.timestamped_migrations` 控制迁移使用整数还是时间戳编号。默认为 `true`,使用时间戳。如果有多个开发者共同开发同一个应用,建议这么设置。 +* `config.active_record.lock_optimistically` 控制 Active Record 是否使用乐观锁。默认为 `true`。 +* `config.active_record.cache_timestamp_format` 控制缓存键中时间戳的格式。默认为 `:nsec`。 +* `config.active_record.record_timestamps` 是个布尔值选项,控制 `create` 和 `update` 操作是否更新时间戳。默认值为 `true`。 +* `config.active_record.partial_writes` 是个布尔值选项,控制是否使用部分写入(partial write,即更新时是否只设定有变化的属性)。注意,使用部分写入时,还应该使用乐观锁(`config.active_record.lock_optimistically`),因为并发更新可能写入过期的属性。默认值为 `true`。 +* `config.active_record.maintain_test_schema` 是个布尔值选项,控制 Active Record 是否应该在运行测试时让测试数据库的模式与 `db/schema.rb`(或 `db/structure.sql`)保持一致。默认为 `true`。 +* `config.active_record.dump_schema_after_migration` 是个旗标,控制运行迁移后是否转储模式(`db/schema.rb` 或 `db/structure.sql`)。生成 Rails 应用时,`config/environments/production.rb` 文件中把它设为 `false`。如果不设定这个选项,默认为 `true`。 +* `config.active_record.dump_schemas` 控制运行 `db:structure:dump` 任务时转储哪些数据库模式。可用的值有:`:schema_search_path`(默认),转储 `schema_search_path` 列出的全部模式;`:all`,不考虑 `schema_search_path`,始终转储全部模式;以逗号分隔的模式字符串。 +* `config.active_record.belongs_to_required_by_default` 是个布尔值选项,控制没有 `belongs_to` 关联时记录的验证是否失败。 +* `config.active_record.warn_on_records_fetched_greater_than` 为查询结果的数量设定一个提醒阈值。如果查询返回的记录数量超过这一阈值,在日志中记录一个提醒。可用于标识可能导致内存泛用的查询。 +* `config.active_record.index_nested_attribute_errors` 让嵌套的 `has_many` 关联错误显示索引。默认为 `false`。 +* `config.active_record.use_schema_cache_dump` 设为 `true` 时,用户可以从 `db/schema_cache.yml` 文件中获取模式缓存信息,而不用查询数据库。默认为 `true`。 + +MySQL 适配器添加了一个配置选项: + +* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` 控制 Active Record 是否把 `tinyint(1)` 类型的列当做布尔值。默认为 `true`。 + +模式转储程序添加了一个配置选项: + +* `ActiveRecord::SchemaDumper.ignore_tables` 指定一个表数组,不包含在生成的模式文件中。如果 `config.active_record.schema_format` 的值不是 `:ruby`,这个设置会被忽略。 + + + +### 配置 Action Controller + +`config.action_controller` 包含众多配置选项: + +* `config.action_controller.asset_host` 设定静态资源的主机。不使用应用自身伺服静态资源,而是通过 CDN 伺服时设定。 +* `config.action_controller.perform_caching` 配置应用是否使用 Action Controller 组件提供的缓存功能。默认在开发环境中为 `false`,在生产环境中为 `true`。 +* `config.action_controller.default_static_extension` 配置缓存页面的扩展名。默认为 `.html`。 +* `config.action_controller.include_all_helpers` 配置视图辅助方法在任何地方都可用,还是只在相应的控制器中可用。如果设为 `false`,`UsersHelper` 模块中的方法只在 `UsersController` 的视图中可用。如果设为 `true`,`UsersHelper` 模块中的方法在任何地方都可用。默认的行为(不明确设为 `true` 或 `false`)是视图辅助方法在每个控制器中都可用。 +* `config.action_controller.logger` 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby `Logger` 类,用于记录 Action Controller 的信息。设为 `nil` 时禁用日志。 +* `config.action_controller.request_forgery_protection_token` 设定请求伪造的令牌参数名称。调用 `protect_from_forgery` 默认把它设为 `:authenticity_token`。 +* `config.action_controller.allow_forgery_protection` 启用或禁用 CSRF 防护。在测试环境中默认为 `false`,其他环境默认为 `true`。 +* `config.action_controller.forgery_protection_origin_check` 配置是否检查 HTTP `Origin` 首部与网站的源一致,作为一道额外的 CSRF 防线。 +* `config.action_controller.per_form_csrf_tokens` 控制 CSRF 令牌是否只在生成它的方法(动作)中有效。 +* `config.action_controller.relative_url_root` 用于告诉 Rails 你把应用[部署到子目录中](#deploy-to-a-subdirectory-relative-url-root)。默认值为 `ENV['RAILS_RELATIVE_URL_ROOT']`。 +* `config.action_controller.permit_all_parameters` 设定默认允许批量赋值全部参数。默认值为 `false`。 +* `config.action_controller.action_on_unpermitted_parameters` 设定在发现没有允许的参数时记录日志还是抛出异常。设为 `:log` 或 `:raise` 时启用。开发和测试环境的默认值是 `:log`,其他环境的默认值是 `false`。 +* `config.action_controller.always_permitted_parameters` 设定一组默认允许传入的参数白名单。默认值为 `['controller', 'action']`。 +* `config.action_controller.enable_fragment_cache_logging` 指明是否像下面这样在日志中详细记录片段缓存的读写操作: + + ``` + Read fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/d0bdf2974e1ef6d31685c3b392ad0b74 (0.6ms) + Rendered messages/_message.html.erb in 1.2 ms [cache hit] + Write fragment views/v1/2914079/v1/2914079/recordings/70182313-20160225015037000000/3b4e249ac9d168c617e32e84b99218b5 (1.1ms) + Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss] + Rendered messages/_message.html.erb in 1.2 ms [cache hit] + Rendered recordings/threads/_thread.html.erb in 1.5 ms [cache miss] + ``` + + + + + +### 配置 Action Dispatch + +* `config.action_dispatch.session_store` 设定存储会话数据的存储器。默认为 `:cookie_store`;其他有效的值包括 `:active_record_store`、`:mem_cache_store` 或自定义类的名称。 +* `config.action_dispatch.default_headers` 的值是一个散列,设定每个响应默认都有的 HTTP 首部。默认定义的首部有: + + ```ruby + config.action_dispatch.default_headers = { + 'X-Frame-Options' => 'SAMEORIGIN', + 'X-XSS-Protection' => '1; mode=block', + 'X-Content-Type-Options' => 'nosniff' + } + ``` + + +* `config.action_dispatch.default_charset` 指定渲染时使用的默认字符集。默认为 `nil`。 +* `config.action_dispatch.tld_length` 设定应用的 TLD(top-level domain,顶级域名)长度。默认为 `1`。 +* `config.action_dispatch.ignore_accept_header` 设定是否忽略请求中的 Accept 首部。默认为 `false`。 +* `config.action_dispatch.x_sendfile_header` 指定服务器具体使用的 X-Sendfile 首部。通过服务器加速发送文件时用得到。例如,使用 Apache 时设为 'X-Sendfile'。 +* `config.action_dispatch.http_auth_salt` 设定 HTTP Auth 的盐值。默认为 `'http authentication'`。 +* `config.action_dispatch.signed_cookie_salt` 设定签名 cookie 的盐值。默认为 `'signed cookie'`。 +* `config.action_dispatch.encrypted_cookie_salt` 设定加密 cookie 的盐值。默认为 `'encrypted cookie'`。 +* `config.action_dispatch.encrypted_signed_cookie_salt` 设定签名加密 cookie 的盐值。默认为 `'signed encrypted cookie'`。 +* `config.action_dispatch.perform_deep_munge` 配置是否在参数上调用 `deep_munge` 方法。详情参见 [生成不安全的查询](security.html#unsafe-query-generation)。默认为 `true`。 +* `config.action_dispatch.rescue_responses` 设定异常与 HTTP 状态的对应关系。其值为一个散列,指定异常和状态之间的映射。默认的定义如下: + + ```ruby + config.action_dispatch.rescue_responses = { + 'ActionController::RoutingError' => :not_found, + 'AbstractController::ActionNotFound' => :not_found, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::UnknownHttpMethod' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::UnknownFormat' => :not_acceptable, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity, + 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity, + 'ActionDispatch::Http::Parameters::ParseError' => :bad_request, + 'ActionController::BadRequest' => :bad_request, + 'ActionController::ParameterMissing' => :bad_request, + 'Rack::QueryParser::ParameterTypeError' => :bad_request, + 'Rack::QueryParser::InvalidParameterError' => :bad_request, + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity + } + ``` + + 没有配置的异常映射为 500 Internal Server Error。 + + +* `ActionDispatch::Callbacks.before` 接受一个代码块,在请求之前运行。 +* `ActionDispatch::Callbacks.to_prepare` 接受一个块,在 `ActionDispatch::Callbacks.before` 之后、请求之前运行。在开发环境中每个请求都会运行,但在生产环境或 `cache_classes` 设为 `true` 的环境中只运行一次。 +* `ActionDispatch::Callbacks.after` 接受一个代码块,在请求之后运行。 + + + +### 配置 Action View + +`config.action_view` 有一些配置选项: + +* `config.action_view.field_error_proc` 提供一个 HTML 生成器,用于显示 Active Model 抛出的错误。默认为: + + ```ruby + Proc.new do |html_tag, instance| + %Q(
#{html_tag}
).html_safe + end + ``` -* `config.action_dispatch.session_store`:设置存储会话的方式,默认为 `:cookie_store`,其他可用值有:`:active_record_store`,`:mem_cache_store`,以及自定义类的名字。 -* `config.action_dispatch.default_headers`:一个 Hash,设置响应的默认报头。默认设定的报头为: +* `config.action_view.default_form_builder` 告诉 Rails 默认使用哪个表单构造器。默认为 `ActionView::Helpers::FormBuilder`。如果想在初始化之后加载表单构造器类,把值设为一个字符串。 +* `config.action_view.logger` 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby `Logger` 类,用于记录 Action View 的信息。设为 `nil` 时禁用日志。 +* `config.action_view.erb_trim_mode` 让 ERB 使用修剪模式。默认为 `'-'`,使用 `<%= -%>` 或 `<%= =%>` 时裁掉尾部的空白和换行符。详情参见 [Erubis 的文档](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces)。 +* `config.action_view.embed_authenticity_token_in_remote_forms` 设定具有 `remote: true` 选项的表单中 `authenticity_token` 的默认行为。默认设为 `false`,即远程表单不包含 `authenticity_token`,对表单做片段缓存时可以这么设。远程表单从 `meta` 标签中获取真伪令牌,因此除非要支持没有 JavaScript 的浏览器,否则不应该内嵌在表单中。如果想支持没有 JavaScript 的浏览器,可以在表单选项中设定 `authenticity_token: true`,或者把这个配置设为 `true`。 +* `config.action_view.prefix_partial_path_with_controller_namespace` 设定渲染嵌套在命名空间中的控制器时是否在子目录中寻找局部视图。例如,`Admin::ArticlesController` 渲染这个模板: -```ruby -config.action_dispatch.default_headers = { - 'X-Frame-Options' => 'SAMEORIGIN', - 'X-XSS-Protection' => '1; mode=block', - 'X-Content-Type-Options' => 'nosniff' -} -``` + ```erb + <%= render @article %> + ``` + + 默认设置是 `true`,使用局部视图 `/admin/articles/_article.erb`。设为 `false` 时,渲染 `/articles/_article.erb`——这与渲染没有放入命名空间中的控制器一样,例如 `ArticlesController`。 -* `config.action_dispatch.tld_length`:设置顶级域名(top-level domain,简称 TLD)的长度,默认为 `1`。 -* `config.action_dispatch.http_auth_salt`:设置 HTTP Auth 认证的加盐值,默认为 `'http authentication'`。 +* `config.action_view.raise_on_missing_translations` 设定缺少翻译时是否抛出错误。 +* `config.action_view.automatically_disable_submit_tag` 设定点击提交按钮(`submit_tag`)时是否自动将其禁用。默认为 `true`。 +* `config.action_view.debug_missing_translation` 设定是否把缺少的翻译键放在 `` 标签中。默认为 `true`。 +* `config.action_view.form_with_generates_remote_forms` 指明 `form_with` 是否生成远程表单。默认为 `true`。 -* `config.action_dispatch.signed_cookie_salt`:设置签名 cookie 的加盐值,默认为 `'signed cookie'`。 + -* `config.action_dispatch.encrypted_cookie_salt`:设置加密 cookie 的加盐值,默认为 `'encrypted cookie'`。 +### 配置 Action Mailer -* `config.action_dispatch.encrypted_signed_cookie_salt`:设置签名加密 cookie 的加盐值,默认为 `'signed encrypted cookie'`。 +`config.action_mailer` 有一些配置选项: -* `config.action_dispatch.perform_deep_munge`:设置是否在参数上调用 `deep_munge` 方法。详情参阅“[Rails 安全指南](security.html#unsafe-query-generation)”一文。默认值为 `true`。 +* `config.action_mailer.logger` 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby `Logger` 类,用于记录 Action Mailer 的信息。设为 `nil` 时禁用日志。 +* `config.action_mailer.smtp_settings` 用于详细配置 `:smtp` 发送方法。值是一个选项散列,包含下述选项: -* `ActionDispatch::Callbacks.before`:设置在处理请求前运行的代码块。 + * `:address`:设定远程邮件服务器的地址。默认为 localhost。 + * `:port`:如果邮件服务器不在 25 端口上(很少发生),可以修改这个选项。 + * `:domain`:如果需要指定 HELO 域名,通过这个选项设定。 + * `:user_name`:如果邮件服务器需要验证身份,通过这个选项设定用户名。 + * `:password`:如果邮件服务器需要验证身份,通过这个选项设定密码。 + * `:authentication`:如果邮件服务器需要验证身份,要通过这个选项设定验证类型。这个选项的值是一个符号,可以是 `:plain`、`:login` 或 `:cram_md5`。 + * `:enable_starttls_auto`:检测 SMTP 服务器是否启用了 STARTTLS,如果启用就使用。默认为 `true`。 + * `:openssl_verify_mode`:使用 TLS 时可以设定 OpenSSL 检查证书的方式。需要验证自签名或通配证书时用得到。值为 `:none` 或 `:peer`,或相应的常量 OpenSSL::SSL::VERIFY_NONE` 或 `OpenSSL::SSL::VERIFY_PEER`。 + * `:ssl/:tls`:通过 SMTP/TLS 连接 SMTP。 -* `ActionDispatch::Callbacks.to_prepare`:设置在 `ActionDispatch::Callbacks.before` 之后、处理请求之前运行的代码块。这个代码块在开发环境中的每次请求中都会运行,但在生产环境或 `cache_classes` 设为 `true` 的环境中只运行一次。 -* `ActionDispatch::Callbacks.after`:设置处理请求之后运行的代码块。 +* `config.action_mailer.sendmail_settings` 用于详细配置 `sendmail` 发送方法。值是一个选项散列,包含下述选项: -###3设置 Action View + * `:location`:sendmail 可执行文件的位置。默认为 `/usr/sbin/sendmail`。 + * `:arguments`:命令行参数。默认为 `-i`。 -`config.action_view` 包含以下设置项: -* `config.action_view.field_error_proc`:设置用于生成 Active Record 表单错误的 HTML,默认为: +* `config.action_mailer.raise_delivery_errors` 指定无法发送电子邮件时是否抛出错误。默认为 `true`。 +* `config.action_mailer.delivery_method` 设定发送方法,默认为 `:smtp`。详情参见 [配置 Action Mailer](action_mailer_basics.html#action-mailer-configuration)。 +* `config.action_mailer.perform_deliveries` 指定是否真的发送邮件,默认为 `true`。测试时建议设为 `false`。 +* `config.action_mailer.default_options` 配置 Action Mailer 的默认值。用于为每封邮件设定 `from` 或 `reply_to` 等选项。设定的默认值为: -```ruby -Proc.new do |html_tag, instance| - %Q(
#{html_tag}
).html_safe -end -``` + ```ruby + mime_version: "1.0", + charset: "UTF-8", + content_type: "text/plain", + parts_order: ["text/plain", "text/enriched", "text/html"] + ``` + + 若想设定额外的选项,使用一个散列: + + ```ruby + config.action_mailer.default_options = { + from: "noreply@example.com" + } + ``` -* `config.action_view.default_form_builder`:设置默认使用的表单构造器。默认值为 `ActionView::Helpers::FormBuilder`。如果想让表单构造器在程序初始化完成后加载(在开发环境中每次请求都会重新加载),可使用字符串形式。 -* `config.action_view.logger`:接受一个实现了 Log4r 接口的类,或者使用默认的 `Logger` 类,用于写入来自 Action View 的日志。设为 `nil` 禁用日志。 +* `config.action_mailer.observers` 注册观测器(observer),发送邮件时收到通知。 -* `config.action_view.erb_trim_mode`:设置 ERB 使用的删除空白模式,默认为 `'-'`,使用 `<%= -%>` 或 `<%= =%>` 时,删除行尾的空白和换行。详情参阅 [Erubis 的文档](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces)。 + ```ruby + config.action_mailer.observers = ["MailObserver"] + ``` -* `config.action_view.embed_authenticity_token_in_remote_forms`:设置启用 `:remote => true` 选项的表单如何处理 `authenticity_token` 字段。默认值为 `false`,即不加入 `authenticity_token` 字段,有助于使用片段缓存缓存表单。远程表单可从 `meta` 标签中获取认证权标,因此没必要再加入 `authenticity_token` 字段,除非要支持没启用 JavaScript 的浏览器。如果要支持没启用 JavaScript 的浏览器,可以在表单的选项中加入 `:authenticity_token => true`,或者把这个设置设为 `true`。 -* `config.action_view.prefix_partial_path_with_controller_namespace`:设置渲染命名空间中的控制器时是否要在子文件夹中查找局部视图。例如,控制器名为 `Admin::PostsController`,渲染了以下视图: +* `config.action_mailer.interceptors` 注册侦听器(interceptor),在发送邮件前调用。 -```erb -<%= render @post %> -``` + ```ruby + config.action_mailer.interceptors = ["MailInterceptor"] + ``` -这个设置的默认值为 `true`,渲染的局部视图为 `/admin/posts/_post.erb`。如果设为 `false`,就会渲染 `/posts/_post.erb`,和没加命名空间的控制器(例如 `PostsController`)行为一致。 -* `config.action_view.raise_on_missing_translations`:找不到翻译时是否抛出异常。 +* `config.action_mailer.preview_path` 指定邮件程序预览的位置。 -###3设置 Action Mailer + ```ruby + config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + ``` -`config.action_mailer` 包含以下设置项: -* `config.action_mailer.logger`:接受一个实现了 Log4r 接口的类,或者使用默认的 `Logger` 类,用于写入来自 Action Mailer 的日志。设为 `nil` 禁用日志。 +* `config.action_mailer.show_previews` 启用或禁用邮件程序预览。开发环境默认为 `true`。 -* `config.action_mailer.smtp_settings`:详细设置 `:smtp` 发送方式。接受一个 Hash,包含以下选项: - * `:address`:设置远程邮件服务器,把默认值 `"localhost"` 改成所需值即可; - * `:port`:如果邮件服务器不使用端口 25,可通过这个选项修改; - * `:domain`:如果想指定一个 HELO 域名,可通过这个选项修改; - * `:user_name`:如果所用邮件服务器需要身份认证,可通过这个选项设置用户名; - * `:password`:如果所用邮件服务器需要身份认证,可通过这个选项设置密码; - * `:authentication`:如果所用邮件服务器需要身份认证,可通过这个选项指定认证类型,可选值包括:`:plain`,`:login`,`:cram_md5`; + ```ruby + config.action_mailer.show_previews = false + ``` -* `config.action_mailer.sendmail_settings`:详细设置 `sendmail` 发送方式。接受一个 Hash,包含以下选项: - * `:location`:`sendmail` 可执行文件的位置,默认为 `/usr/sbin/sendmail`; - * `:arguments`:传入命令行的参数,默认为 `-i -t`; -* `config.action_mailer.raise_delivery_errors`:如果无法发送邮件,是否抛出异常。默认为 `true`。 +* `config.action_mailer.deliver_later_queue_name` 设定邮件程序的队列名称。默认为 `mailers`。 +* `config.action_mailer.perform_caching` 指定是否片段缓存邮件模板。在所有环境中默认为 `false`。 -* `config.action_mailer.delivery_method`:设置发送方式,默认为 `:smtp`。详情参阅“Action Mailer 基础”一文中的“[设置](action_mailer_basics.html#action-mailer-configuration)”一节。。 + -* `config.action_mailer.perform_deliveries`:设置是否真的发送邮件,默认为 `true`。测试时可设为 `false`。 +### 配置 Active Support -* `config.action_mailer.default_options`:设置 Action Mailer 的默认选项。可设置各个邮件发送程序的 `from` 或 `reply_to` 等选项。默认值为: +Active Support 有一些配置选项: -```ruby -mime_version: "1.0", -charset: "UTF-8", -content_type: "text/plain", -parts_order: ["text/plain", "text/enriched", "text/html"] -``` +* `config.active_support.bare` 指定在启动 Rails 时是否加载 `active_support/all`。默认为 `nil`,即加载 `active_support/all`。 +* `config.active_support.test_order` 设定执行测试用例的顺序。可用的值是 `:random` 和 `:sorted`。默认为 `:random`。 +* `config.active_support.escape_html_entities_in_json` 指定在 JSON 序列化中是否转义 HTML 实体。默认为 `true`。 +* `config.active_support.use_standard_json_time_format` 指定是否把日期序列化成 ISO 8601 格式。默认为 `true`。 +* `config.active_support.time_precision` 设定 JSON 编码的时间值的精度。默认为 `3`。 +* `ActiveSupport::Logger.silencer` 设为 `false` 时静默块的日志。默认为 `true`。 +* `ActiveSupport::Cache::Store.logger` 指定缓存存储操作使用的日志记录器。 +* `ActiveSupport::Deprecation.behavior` 的作用与 `config.active_support.deprecation` 相同,用于配置 Rails 弃用提醒的行为。 +* `ActiveSupport::Deprecation.silence` 接受一个块,块里的所有弃用提醒都静默。 +* `ActiveSupport::Deprecation.silenced` 设定是否显示弃用提醒。 - 设置时要使用 Hash: + -```ruby -config.action_mailer.default_options = { - from: "noreply@example.com" -} -``` +### 配置 Active Job -* `config.action_mailer.observers`:注册邮件发送后触发的监控器。 +`config.active_job` 提供了下述配置选项: -```ruby -config.action_mailer.observers = ["MailObserver"] -``` +* `config.active_job.queue_adapter` 设定队列后端的适配器。默认的适配器是 `:async`。最新的内置适配器参见 [`ActiveJob::QueueAdapters` 的 API 文档](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html)。 -* `config.action_mailer.interceptors`:注册发送邮件前调用的拦截程序。 + ```ruby + # 要把适配器的 gem 写入 Gemfile + # 请参照适配器的具体安装和部署说明 + config.active_job.queue_adapter = :sidekiq + ``` -```ruby -config.action_mailer.interceptors = ["MailInterceptor"] -``` -###3设置 Active Support +* `config.active_job.default_queue_name` 用于修改默认的队列名称。默认为 `"default"`。 -Active Support 包含以下设置项: + ```ruby + config.active_job.default_queue_name = :medium_priority + ``` -* `config.active_support.bare`:启动 Rails 时是否加载 `active_support/all`。默认值为 `nil`,即加载 `active_support/all`。 -* `config.active_support.escape_html_entities_in_json`:在 JSON 格式的数据中是否转义 HTML 实体。默认为 `false`。 +* `config.active_job.queue_name_prefix` 用于为所有作业设定队列名称的前缀(可选)。默认为空,不使用前缀。 -* `config.active_support.use_standard_json_time_format`:在 JSON 格式的数据中是否把日期转换成 ISO 8601 格式。默认为 `true`。 + 做下述配置后,在生产环境中运行时把指定作业放入 `production_high_priority` 队列中: + + ```ruby + config.active_job.queue_name_prefix = Rails.env + ``` + + ```ruby + class GuestsCleanupJob < ActiveJob::Base + queue_as :high_priority + #.... + end + ``` + + +* `config.active_job.queue_name_delimiter` 的默认值是 `'_'`。如果设定了 `queue_name_prefix`,使用 `queue_name_delimiter` 连接前缀和队列名。 + + 下述配置把指定作业放入 `video_server.low_priority` 队列中: + + ```ruby + # 设定了前缀才会使用分隔符 + config.active_job.queue_name_prefix = 'video_server' + config.active_job.queue_name_delimiter = '.' + ``` + + ```ruby + class EncoderJob < ActiveJob::Base + queue_as :low_priority + #.... + end + ``` -* `config.active_support.time_precision`:设置 JSON 编码的时间精度,默认为 `3`。 -* `ActiveSupport::Logger.silencer`:设为 `false` 可以静默代码块中的日志消息。默认为 `true`。 +* `config.active_job.logger` 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby `Logger` 类,用于记录 Action Job 的信息。在 Active Job 类或实例上调用 `logger` 方法可以获取日志记录器。设为 `nil` 时禁用日志。 -* `ActiveSupport::Cache::Store.logger`:设置缓存存储中使用的写日志程序。 + -* `ActiveSupport::Deprecation.behavior`:作用和 `config.active_support.deprecation` 一样,设置是否显示 Rails 废弃提醒。 +### 配置 Action Cable -* `ActiveSupport::Deprecation.silence`:接受一个代码块,静默废弃提醒。 +* `config.action_cable.url` 的值是一个 URL 字符串,指定 Action Cable 服务器的地址。如果 Action Cable 服务器与主应用的服务器不同,可以使用这个选项。 +* `config.action_cable.mount_path` 的值是一个字符串,指定把 Action Cable 挂载在哪里,作为主服务器进程的一部分。默认为 `/cable`。可以设为 `nil`,不把 Action Cable 挂载为常规 Rails 服务器的一部分。 -* `ActiveSupport::Deprecation.silenced`:设置是否显示废弃提醒。 + -###3设置数据库 +### 配置数据库 -几乎每个 Rails 程序都要用到数据库。数据库信息可以在环境变量 `ENV['DATABASE_URL']` 中设定,也可在 `config/database.yml` 文件中设置。 +几乎所有 Rails 应用都要与数据库交互。可以通过环境变量 `ENV['DATABASE_URL']` 或 `config/database.yml` 配置文件中的信息连接数据库。 -在 `config/database.yml` 文件中可以设置连接数据库所需的所有信息: +在 `config/database.yml` 文件中可以指定访问数据库所需的全部信息: -```yaml +```yml development: adapter: postgresql database: blog_development pool: 5 ``` -上述设置使用 `postgresql` 适配器连接名为 `blog_development` 的数据库。这些信息也可存储在 URL 中,通过下面的环境变量提供: +此时使用 `postgresql` 适配器连接名为 `blog_development` 的数据库。这些信息也可以存储在一个 URL 中,然后通过环境变量提供,如下所示: -```ruby +``` > puts ENV['DATABASE_URL'] postgresql://localhost/blog_development?pool=5 ``` -`config/database.yml` 文件包含三个区域,分别对应 Rails 中的三个默认环境: +`config/database.yml` 文件分成三部分,分别对应 Rails 默认支持的三个环境: -* `development` 环境在本地开发电脑上运行,手动与程序交互; -* `test` 环境用于运行自动化测试; -* `production` 环境用于部署后的程序; +* `development` 环境在开发(本地)电脑中使用,手动与应用交互。 +* `test` 环境用于运行自动化测试。 +* `production` 环境在把应用部署到线上时使用。 -如果需要使用 URL 形式,也可在 `config/database.yml` 文件中按照下面的方式设置: +如果愿意,可以在 `config/database.yml` 文件中指定连接 URL: -``` +```yml development: url: postgresql://localhost/blog_development?pool=5 ``` -`config/database.yml` 文件中可以包含 ERB 标签 `<%= %>`。这个标签中的代码被视为 Ruby 代码。使用 ERB 标签可以从环境变量中获取数据,或者计算所需的连接信息。 +`config/database.yml` 文件中可以包含 ERB 标签 `<%= %>`。这个标签中的内容作为 Ruby 代码执行。可以使用这个标签从环境变量中获取数据,或者执行计算,生成所需的连接信息。 + +TIP: 无需自己动手更新数据库配置。如果查看应用生成器的选项,你会发现其中一个名为 `--database`。通过这个选项可以从最常使用的关系数据库中选择一个。甚至还可以重复运行这个生成器:`cd .. && rails new blog --database=mysql`。同意重写 `config/database.yml` 文件后,应用的配置会针对 MySQL 更新。常见的数据库连接示例参见下文。 -TIP: 你无须手动更新数据库设置信息。查看新建程序生成器,会发现一个名为 `--database` 的选项。使用这个选项可以从一组常用的关系型数据库中选择想用的数据库。甚至还可重复执行生成器:`cd .. && rails new blog --database=mysql`。确认覆盖文件 `config/database.yml` 后,程序就设置成使用 MySQL,而不是 SQLite。常用数据库的设置如下所示。 -###3连接设置 + -既然数据库的连接信息有两种设置方式,就要知道两者之间的关系。 +### 连接配置的优先级 -如果 `config/database.yml` 文件为空,而且设置了环境变量 `ENV['DATABASE_URL']`,Rails 就会使用环境变量连接数据库: +因为有两种配置连接的方式(使用 `config/database.yml` 文件或者一个环境变量),所以要明白二者之间的关系。 + +如果 `config/database.yml` 文件为空,而 `ENV['DATABASE_URL']` 有值,那么 Rails 使用环境变量连接数据库: ```sh $ cat config/database.yml @@ -510,7 +659,7 @@ $ echo $DATABASE_URL postgresql://localhost/my_database ``` -如果 `config/database.yml` 文件存在,且没有设置环境变量 `ENV['DATABASE_URL']`,Rails 会使用设置文件中的信息连接数据库: +如果在 `config/database.yml` 文件中做了配置,而 `ENV['DATABASE_URL']` 没有值,那么 Rails 使用这个文件中的信息连接数据库: ```sh $ cat config/database.yml @@ -522,9 +671,9 @@ development: $ echo $DATABASE_URL ``` -如果有 `config/database.yml` 文件,也设置了环境变量 `ENV['DATABASE_URL']`,Rails 会合并二者提供的信息。下面举个例子说明。 +如果 `config/database.yml` 文件中做了配置,而且 `ENV['DATABASE_URL']` 有值,Rails 会把二者合并到一起。为了更好地理解,必须看些示例。 -如果二者提供的信息有重复,环境变量中的信息优先级更高: +如果连接信息有重复,环境变量中的信息优先级高: ```sh $ cat config/database.yml @@ -536,13 +685,13 @@ development: $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' +$ bin/rails runner 'puts ActiveRecord::Base.configurations' {"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}} ``` -这里的适配器、主机和数据库名都和 `ENV['DATABASE_URL']` 中的信息一致。 +可以看出,适配器、主机和数据库与 `ENV['DATABASE_URL']` 中的信息匹配。 -如果没有重复,则会从这两个信息源获取信息。如果有冲突,环境变量的优先级更高。 +如果信息无重复,都是唯一的,遇到冲突时还是环境变量中的信息优先级高: ```sh $ cat config/database.yml @@ -553,29 +702,29 @@ development: $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' +$ bin/rails runner 'puts ActiveRecord::Base.configurations' {"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}} ``` -因为 `ENV['DATABASE_URL']` 中没有提供数据库连接池信息,所以从设置文件中获取。二者都提供了 `adapter` 信息,但使用的是 `ENV['DATABASE_URL']` 中的信息。 +`ENV['DATABASE_URL']` 没有提供连接池数量,因此从文件中获取。而两处都有 `adapter`,因此 `ENV['DATABASE_URL']` 中的连接信息胜出。 -如果完全不想使用 `ENV['DATABASE_URL']` 中的信息,要使用 `url` 子建指定一个 URL: +如果不想使用 `ENV['DATABASE_URL']` 中的连接信息,唯一的方法是使用 `"url"` 子键指定一个 URL: ```sh $ cat config/database.yml development: - url: sqlite3://localhost/NOT_my_database + url: sqlite3:NOT_my_database $ echo $DATABASE_URL postgresql://localhost/my_database -$ rails runner 'puts ActiveRecord::Base.connections' -{"development"=>{"adapter"=>"sqlite3", "host"=>"localhost", "database"=>"NOT_my_database"}} +$ bin/rails runner 'puts ActiveRecord::Base.configurations' +{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}} ``` -如上所示,`ENV['DATABASE_URL']` 中的连接信息被忽略了,使用了不同的适配器和数据库名。 +这里,`ENV['DATABASE_URL']` 中的连接信息被忽略了。注意,适配器和数据库名称不同了。 -既然 `config/database.yml` 文件中可以使用 ERB,最好使用 `ENV['DATABASE_URL']` 中的信息连接数据库。这种方式在生产环境中特别有用,因为我们并不想把数据库密码等信息纳入版本控制系统(例如 Git)。 +因为在 `config/database.yml` 文件中可以内嵌 ERB,所以最好明确表明使用 `ENV['DATABASE_URL']` 连接数据库。这在生产环境中特别有用,因为不应该把机密信息(如数据库密码)提交到源码控制系统中(如 Git)。 ```sh $ cat config/database.yml @@ -583,15 +732,17 @@ production: url: <%= ENV['DATABASE_URL'] %> ``` -注意,这种设置方式很明确,只使用 `ENV['DATABASE_URL']` 中的信息。 +现在的行为很明确,只使用 `<%= ENV['DATABASE_URL'] %>` 中的连接信息。 -####4设置 SQLite3 数据库 + -Rails 内建支持 [SQLite3](http://www.sqlite.org)。SQLite 是个轻量级数据库,无需单独的服务器。大型线上环境可能并不适合使用 SQLite,但在开发环境和测试环境中使用却很便利。新建程序时,Rails 默认使用 SQLite,但可以随时换用其他数据库。 +#### 配置 SQLite3 数据库 -下面是默认的设置文件(`config/database.yml`)中针对开发环境的数据库设置: +Rails 内建支持 [SQLite3](http://www.sqlite.org/),这是一个轻量级无服务器数据库应用。SQLite 可能无法负担生产环境,但是在开发和测试环境中用着很好。新建 Rails 项目时,默认使用 SQLite 数据库,不过之后可以随时更换。 -```yaml +下面是默认配置文件(`config/database.yml`)中开发环境的连接信息: + +```yml development: adapter: sqlite3 database: db/development.sqlite3 @@ -599,13 +750,16 @@ development: timeout: 5000 ``` -NOTE: Rails 默认使用 SQLite3 存储数据,因为 SQLite3 无需设置即可使用。Rails 还内建支持 MySQL 和 PostgreSQL。还提供了很多插件,支持更多的数据库系统。如果在生产环境中使用了数据库,Rails 很可能已经提供了对应的适配器。 +NOTE: Rails 默认使用 SQLite3 存储数据,因为它无需配置,立即就能使用。Rails 还原生支持 MySQL(含 MariaDB)和 PostgreSQL,此外还有针对其他多种数据库系统的插件。在生产环境中使用的数据库,基本上都有相应的 Rails 适配器。 -####4设置 MySQL 数据库 -如果不想使用 SQLite3,而是使用 MySQL,`config/database.yml` 文件的内容会有些不同。下面是针对开发环境的设置: + -```yaml +#### 配置 MySQL 或 MariaDB 数据库 + +如果选择使用 MySQL 或 MariaDB,而不是 SQLite3,`config/database.yml` 文件的内容稍有不同。下面是开发环境的连接信息: + +```yml development: adapter: mysql2 encoding: utf8 @@ -616,33 +770,45 @@ development: socket: /tmp/mysql.sock ``` -如果开发电脑中的 MySQL 使用 root 用户,且没有密码,可以直接使用上述设置。否则就要相应的修改用户名和密码。 +如果开发数据库使用 root 用户,而且没有密码,这样配置就行了。否则,要相应地修改 `development` 部分的用户名和密码。 -####4设置 PostgreSQL 数据库 + -如果选择使用 PostgreSQL,`config/database.yml` 会准备好连接 PostgreSQL 数据库的信息: +#### 配置 PostgreSQL 数据库 -```yaml +如果选择使用 PostgreSQL,`config/database.yml` 文件会针对 PostgreSQL 数据库定制: + +```yml development: adapter: postgresql encoding: unicode database: blog_development pool: 5 - username: blog - password: ``` -`PREPARE` 语句可使用下述方法禁用: +PostgreSQL 默认启用预处理语句(prepared statement)。若想禁用,把 `prepared_statements` 设为 `false`: -```yaml +```yml production: adapter: postgresql prepared_statements: false ``` -####4在 JRuby 平台上设置 SQLite3 数据库 +如果启用,Active Record 默认最多为一个数据库连接创建 1000 个预处理语句。若想修改,可以把 `statement_limit` 设定为其他值: + +```yml +production: + adapter: postgresql + statement_limit: 200 +``` + +预处理语句的数量越多,数据库消耗的内存越多。如果 PostgreSQL 数据库触及内存上限,尝试降低 `statement_limit` 的值,或者禁用预处理语句。 + + -如果在 JRuby 中使用 SQLite3,`config/database.yml` 文件的内容会有点不同。下面是针对开发环境的设置: +#### 为 JRuby 平台配置 SQLite3 数据库 + +如果选择在 JRuby 中使用 SQLite3,`config/database.yml` 文件的内容稍有不同。下面是 `development` 部分: ```yaml development: @@ -650,11 +816,13 @@ development: database: db/development.sqlite3 ``` -####4在 JRuby 平台上设置 MySQL 数据库 + -如果在 JRuby 中使用 MySQL,`config/database.yml` 文件的内容会有点不同。下面是针对开发环境的设置: +#### 为 JRuby 平台配置 MySQL 或 MariaDB 数据库 -```yaml +如果选择在 JRuby 中使用 MySQL 或 MariaDB,`config/database.yml` 文件的内容稍有不同。下面是 `development` 部分: + +```yml development: adapter: jdbcmysql database: blog_development @@ -662,11 +830,13 @@ development: password: ``` -####4在 JRuby 平台上设置 PostgreSQL 数据库 + -如果在 JRuby 中使用 PostgreSQL,`config/database.yml` 文件的内容会有点不同。下面是针对开发环境的设置: +#### 为 JRuby 平台配置 PostgreSQL 数据库 -```yaml +如果选择在 JRuby 中使用 PostgreSQL,`config/database.yml` 文件的内容稍有不同。下面是 `development` 部分: + +```yml development: adapter: jdbcpostgresql encoding: unicode @@ -675,107 +845,144 @@ development: password: ``` -请相应地修改 `development` 区中的用户名和密码。 +请根据需要修改 `development` 部分的用户名和密码。 + + + +### 创建 Rails 环境 -###3新建 Rails 环境 +Rails 默认提供三个环境:开发环境、测试环境和生产环境。多数情况下,这就够用了,但有时可能需要更多环境。 -默认情况下,Rails 提供了三个环境:开发,测试和生产。这三个环境能满足大多数需求,但有时需要更多的环境。 +比如说想要一个服务器,镜像生产环境,但是只用于测试。这样的服务器通常称为“交付准备服务器”。如果想为这个服务器创建名为“staging”的环境,只需创建 `config/environments/staging.rb` 文件。请参照 `config/environments` 目录中的现有文件,根据需要修改。 -假设有个服务器镜像了生产环境,但只用于测试。这种服务器一般叫做“交付准备服务器”(staging server)。要想为这个服务器定义一个名为“staging”的环境,新建文件 `config/environments/staging.rb` 即可。请使用 `config/environments` 文件夹中的任一文件作为模板,以此为基础修改设置。 +自己创建的环境与默认的没有区别,启动服务器使用 `rails server -e staging`,启动控制台使用 `rails console staging`,`Rails.env.staging?` 也能正常使用,等等。 -新建的环境和默认提供的环境没什么区别,可以执行 `rails server -e staging` 命令启动服务器,执行 `rails console staging` 命令进入控制台,`Rails.env.staging?` 也可使用。 + -###3部署到子目录中 +### 部署到子目录(URL 相对于根路径) -默认情况下,Rails 在根目录(例如 `/`)中运行程序。本节说明如何在子目录中运行程序。 +默认情况下,Rails 预期应用在根路径(即 `/`)上运行。本节说明如何在目录中运行应用。 -假设想把网站部署到 `/app1` 目录中。生成路由时,Rails 要知道这个目录: +假设我们想把应用部署到“/app1”。Rails 要知道这个目录,这样才能生成相应的路由: ```ruby config.relative_url_root = "/app1" ``` -或者,设置环境变量 `RAILS_RELATIVE_URL_ROOT` 也行。 +此外,也可以设定 `RAILS_RELATIVE_URL_ROOT` 环境变量。 + +现在生成链接时,Rails 会在前面加上“/app1”。 + + + +#### 使用 Passenger + +使用 Passenger 在子目录中运行应用很简单。相关配置参阅 [Passenger 手册](https://www.phusionpassenger.com/library/deploy/apache/deploy/ruby/#deploying-an-app-to-a-sub-uri-or-subdirectory)。 + + + +#### 使用反向代理 + +使用反向代理部署应用比传统方式有明显的优势:对服务器有更好的控制,因为应用所需的组件可以分层。 -这样设置之后,Rails 生成的链接都会加上前缀 `/app1`。 +有很多现代的 Web 服务器可以用作代理服务器,用来均衡第三方服务器,如缓存服务器或应用服务器。 -####4使用 Passenger +[Unicorn](http://unicorn.bogomips.org/) 就是这样的应用服务器,在反向代理后面运行。 -使用 Passenger 时,在子目录中运行程序更简单。具体做法参见 [Passenger 手册](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri)。 +此时,要配置代理服务器(NGINX、Apache,等等),让它接收来自应用服务器(Unicorn)的连接。Unicorn 默认监听 8080 端口上的 TCP 连接,不过可以更换端口,或者换用套接字。 -####4使用反向代理 +详情参阅 [Unicorn 的自述文件](http://unicorn.bogomips.org/README.html),还可以了解[背后的哲学](http://unicorn.bogomips.org/PHILOSOPHY.html)。 -TODO +配置好应用服务器之后,还要相应配置 Web 服务器,把请求代理过去。例如,NGINX 的配置可能包含: -####4部署到子目录时的注意事项 +```nginx +upstream application_server { + server 0.0.0.0:8080 +} + +server { + listen 80; + server_name localhost; -在生产环境中部署到子目录中会影响 Rails 的多个功能: + root /root/path/to/your_app/public; -* 开发环境 -* 测试环境 -* 伺服静态资源文件 -* Asset Pipeline + try_files $uri/index.html $uri.html @app; -Rails 环境设置 -------------- + location @app { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://application_server; + } -Rails 的某些功能只能通过外部的环境变量设置。下面介绍的环境变量可以被 Rails 识别: + # 其他配置 +} +``` -* `ENV["RAILS_ENV"]`:指定 Rails 运行在哪个环境中:生成环境,开发环境,测试环境等。 +最新的信息参阅 [NGINX 的文档](http://nginx.org/en/docs/)。 -* `ENV["RAILS_RELATIVE_URL_ROOT"]`:[部署到子目录](#deploy-to-a-subdirectory-relative-url-root)时,路由用来识别 URL。 + -* `ENV["RAILS_CACHE_ID"]` 和 `ENV["RAILS_APP_VERSION"]`:用于生成缓存扩展键。允许在同一程序中使用多个缓存。 +## Rails 环境设置 -使用初始化脚本 ------------- +Rails 的某些部分还可以通过环境变量在外部配置。Rails 能识别下述几个环境变量: -加载完框架以及程序中使用的 gem 后,Rails 会加载初始化脚本。初始化脚本是个 Ruby 文件,存储在程序的 `config/initializers` 文件夹中。初始化脚本可在框架和 gem 加载完成后做设置。 +* `ENV["RAILS_ENV"]` 定义在哪个环境(生产环境、开发环境、测试环境,等等)中运行 Rails。 +* `ENV["RAILS_RELATIVE_URL_ROOT"]` 在[部署到子目录](#deploy-to-a-subdirectory-relative-url-root)中时供路由代码识别 URL。 +* `ENV["RAILS_CACHE_ID"]` 和 `ENV["RAILS_APP_VERSION"]` 供 Rails 的缓存代码生成扩张的缓存键。这样可以在同一个应用中使用多个单独的缓存。 -NOTE: 如果有需求,可以使用子文件夹组织初始化脚本,Rails 会加载整个 `config/initializers` 文件夹中的内容。 + -TIP: 如果对初始化脚本的加载顺序有要求,可以通过文件名控制。初始化脚本的加载顺序按照文件名的字母表顺序进行。例如,`01_critical.rb` 在 `02_normal.rb` 之前加载。 +## 使用初始化脚本文件 -初始化事件 --------- +加载完框架和应用依赖的 gem 之后,Rails 开始加载初始化脚本。初始化脚本是 Ruby 文件,存储在应用的 `config/initializers` 目录中。可以在初始化脚本中存放应该于加载完框架和 gem 之后设定的配置,例如配置各部分的设置项目的选项。 -Rails 提供了 5 个初始化事件,可做钩子使用。下面按照事件的加载顺序介绍: +NOTE: 如果愿意,可以使用子文件夹组织初始化脚本,Rails 会自上而下查找整个文件夹层次结构。 -* `before_configuration`:程序常量继承自 `Rails::Application` 之后立即运行。`config` 方法在此事件之前调用。 -* `before_initialize`:在程序初始化过程中的 `:bootstrap_hook` 之前运行,接近初始化过程的开头。 +TIP: 如果初始化脚本有顺序要求,可以通过名称控制加载顺序。初始化脚本文件按照路径的字母表顺序加载。例如,`01_critical.rb` 在 `02_normal.rb` 前面加载。 -* `to_prepare`:所有 Railtie(包括程序本身)的初始化都运行完之后,但在按需加载代码和构建中间件列表之前运行。更重要的是,在开发环境中,每次请求都会运行,但在生产环境和测试环境中只运行一次(在启动阶段)。 -* `before_eager_load`:在按需加载代码之前运行。这是在生产环境中的默认表现,但在开发环境中不是。 + -* `after_initialize`:在程序初始化完成之后运行,即 `config/initializers` 文件夹中的初始化脚本运行完毕之后。 +## 初始化事件 -要想为这些钩子定义事件,可以在 `Rails::Application`、`Rails::Railtie` 或 `Rails::Engine` 的子类中使用代码块: +Rails 有 5 个初始化事件(按运行顺序列出): + +* `before_configuration`:在应用常量继承 `Rails::Application` 时立即运行。`config` 调用在此之前执行。 +* `before_initialize`:直接在应用初始化过程之前运行,与 Rails 初始化过程靠近开头的 `:bootstrap_hook` 初始化脚本一起运行。 +* `to_prepare`:在所有 Railtie(包括应用自身)的初始化脚本运行结束之后、及早加载和构架中间件栈之前运行。更重要的是,在开发环境中每次请求都运行,而在生产和测试环境只运行一次(在启动过程中)。 +* `before_eager_load`:在及早加载之前直接运行。这是生产环境的默认行为,开发环境则不然。 +* `after_initialize`:在应用初始化之后、`config/initializers` 中的初始化脚本都运行完毕后直接运行。 + +若想为这些钩子定义事件,在 `Rails::Application`、`Rails::Railtie` 或 `Rails::Engine` 子类中使用块句法: ```ruby module YourApp class Application < Rails::Application config.before_initialize do - # initialization code goes here + # 在这编写初始化代码 end end end ``` -或者,在 `Rails.application` 对象上调用 `config` 方法: +此外,还可以通过 `Rails.application` 对象的 `config` 方法定义: ```ruby Rails.application.config.before_initialize do - # initialization code goes here + # 在这编写初始化代码 end ``` -WARNING: 程序的某些功能,尤其是路由,在 `after_initialize` 之后还不可用。 +WARNING: 调用 `after_initialize` 块时,应用的某些部分,尤其是路由,尚不可用。 + -###3`Rails::Railtie#initializer` + -Rails 中有几个初始化脚本使用 `Rails::Railtie` 的 `initializer` 方法定义,在程序启动时运行。下面这段代码摘自 Action Controller 中的 `set_helpers_path` 初始化脚本: +### `Rails::Railtie#initializer` + +有几个在启动时运行的 Rails 初始化脚本使用 `Rails::Railtie` 对象的 `initializer` 方法定义。下面以 Action Controller 中的 `set_helpers_path` 初始化脚本为例: ```ruby initializer "action_controller.set_helpers_path" do |app| @@ -783,133 +990,184 @@ initializer "action_controller.set_helpers_path" do |app| end ``` -`initializer` 方法接受三个参数,第一个是初始化脚本的名字,第二个是选项 Hash(上述代码中没用到),第三个参数是代码块。参数 Hash 中的 `:before` 键指定在特定的初始化脚本之前运行,`:after` 键指定在特定的初始化脚本之后运行。 - -使用 `initializer` 方法定义的初始化脚本按照定义的顺序运行,但指定 `:before` 或 `:after` 参数的初始化脚本例外。 - -WARNING: 初始化脚本可放在任一初始化脚本的前面或后面,只要符合逻辑即可。假设定义了四个初始化脚本,名字为 `"one"` 到 `"four"`(就按照这个顺序定义),其中 `"four"` 在 `"four"` 之前,且在 `"three"` 之后,这就不符合逻辑,Rails 无法判断初始化脚本的加载顺序。 - -`initializer` 方法的代码块参数是程序实例,因此可以调用 `config` 方法,如上例所示。 - -因为 `Rails::Application` 直接继承自 `Rails::Railtie`,因此可在文件 `config/application.rb` 中使用 `initializer` 方法定义程序的初始化脚本。 - -###3初始化脚本 - -下面列出了 Rails 中的所有初始化脚本,按照定义的顺序,除非特别说明,也按照这个顺序执行。 - -* `load_environment_hook`:只是一个占位符,让 `:load_environment_config` 在其前运行。 - -* `load_active_support`:加载 `active_support/dependencies`,加入 Active Support 的基础。如果 `config.active_support.bare` 为非真值(默认),还会加载 `active_support/all`。 - -* `initialize_logger`:初始化日志程序(`ActiveSupport::Logger` 对象),可通过 `Rails.logger` 调用。在此之前的初始化脚本中不能定义 `Rails.logger`。 - -* `initialize_cache`:如果还没创建 `Rails.cache`,使用 `config.cache_store` 指定的方式初始化缓存,并存入 `Rails.cache`。如果对象能响应 `middleware` 方法,对应的中间件会插入 `Rack::Runtime` 之前。 - -* `set_clear_dependencies_hook`:为 `active_record.set_dispatch_hooks` 提供钩子,在本初始化脚本之前运行。这个初始化脚本只有当 `cache_classes` 为 `false` 时才会运行,使用 `ActionDispatch::Callbacks.after` 从对象空间中删除请求过程中已经引用的常量,以便下次请求重新加载。 - -* `initialize_dependency_mechanism`:如果 `config.cache_classes` 为 `true`,设置 `ActiveSupport::Dependencies.mechanism` 使用 `require` 而不是 `load` 加载依赖件。 - -* `bootstrap_hook`:运行所有 `before_initialize` 代码块。 - -* `i18n.callbacks`:在开发环境中,设置一个 `to_prepare` 回调,调用 `I18n.reload!` 加载上次请求后修改的本地化翻译。在生产环境中这个回调只会在首次请求时运行。 - -* `active_support.deprecation_behavior`:设置各环境的废弃提醒方式,开发环境的方式为 `:log`,生产环境的方式为 `:notify`,测试环境的方式为 `:stderr`。如果没有为 `config.active_support.deprecation` 设定值,这个初始化脚本会提醒用户在当前环境的设置文件中设置。可以设为一个数组。 - -* `active_support.initialize_time_zone`:根据 `config.time_zone` 设置程序的默认时区,默认值为 `"UTC"`。 - -* `active_support.initialize_beginning_of_week`:根据 `config.beginning_of_week` 设置程序默认使用的一周开始日,默认值为 `:monday`。 - -* `action_dispatch.configure`:把 `ActionDispatch::Http::URL.tld_length` 设置为 `config.action_dispatch.tld_length` 指定的值。 - -* `action_view.set_configs`:根据 `config.action_view` 设置 Action View,把指定的方法名做为赋值方法发送给 `ActionView::Base`,并传入指定的值。 - -* `action_controller.logger`:如果还未创建,把 `ActionController::Base.logger` 设为 `Rails.logger`。 - -* `action_controller.initialize_framework_caches`:如果还未创建,把 `ActionController::Base.cache_store` 设为 `Rails.cache`。 - -* `action_controller.set_configs`:根据 `config.action_controller` 设置 Action Controller,把指定的方法名作为赋值方法发送给 `ActionController::Base`,并传入指定的值。 - -* `action_controller.compile_config_methods`:初始化指定的设置方法,以便快速访问。 - -* `active_record.initialize_timezone`:把 `ActiveRecord::Base.time_zone_aware_attributes` 设为 `true`,并把 `ActiveRecord::Base.default_timezone` 设为 UTC。从数据库中读取数据时,转换成 `Time.zone` 中指定的时区。 - -* `active_record.logger`:如果还未创建,把 `ActiveRecord::Base.logger` 设为 `Rails.logger`。 - -* `active_record.set_configs`:根据 `config.active_record` 设置 Active Record,把指定的方法名作为赋值方法传给 `ActiveRecord::Base`,并传入指定的值。 - -* `active_record.initialize_database`:从 `config/database.yml` 中加载数据库设置信息,并为当前环境建立数据库连接。 +`initializer` 方法接受三个参数,第一个是初始化脚本的名称,第二个是选项散列(上例中没有),第三个是一个块。选项散列的 `:before` 键指定在哪个初始化脚本之前运行,`:after` 键指定在哪个初始化脚本之后运行。 + +`initializer` 方法定义的初始化脚本按照定义的顺序运行,除非指定了 `:before` 或 `:after` 键。 + +WARNING: 只要符合逻辑,可以设定一个初始化脚本在另一个之前或之后运行。假如有四个初始化脚本,名称分别为“one”到“four”(按照这个顺序定义)。如果定义“four”在“four”之前、“three”之后运行就不合逻辑,Rails 无法确定初始化脚本的执行顺序。 + + +`initializer` 方法的块参数是应用自身的实例,因此可以像示例中那样使用 `config` 方法访问配置。 + +因为 `Rails::Application`(间接)继承自 `Rails::Railtie`,所以可以在 `config/application.rb` 文件中使用 `initializer` 方法为应用定义初始化脚本。 + + + +### 初始化脚本 + +下面按定义顺序(因此以此顺序运行,除非另行说明)列出 Rails 中的全部初始化脚本: + +* `load_environment_hook`:一个占位符,让 `:load_environment_config` 在此之前运行。 +* `load_active_support`:引入 `active_support/dependencies`,设置 Active Support 的基本功能。如果 `config.active_support.bare` 为假值(默认),引入 `active_support/all`。 +* `initialize_logger`:初始化应用的日志记录器(一个 `ActiveSupport::Logger` 对象),可通过 `Rails.logger` 访问。假定在此之前的初始化脚本没有定义 `Rails.logger`。 +* `initialize_cache`:如果没有设置 `Rails.cache`,使用 `config.cache_store` 的值初始化缓存,把结果存储为 `Rails.cache`。如果这个对象响应 `middleware` 方法,它的中间件插入 `Rack::Runtime` 之前。 +* `set_clear_dependencies_hook`:这个初始化脚本(仅当 `cache_classes` 设为 `false` 时运行)使用 `ActionDispatch::Callbacks.after` 从对象空间中删除请求过程中引用的常量,以便在后续请求中重新加载。 +* `initialize_dependency_mechanism`:如果 `config.cache_classes` 为真,配置 `ActiveSupport::Dependencies.mechanism` 使用 `require` 引入依赖,而不使用 `load`。 +* `bootstrap_hook`:运行配置的全部 `before_initialize` 块。 +* `i18n.callbacks`:在开发环境中设置一个 `to_prepare` 回调,如果自上次请求后本地化有变,调用 `I18n.reload!`。在生产环境,这个回调只在第一次请求时运行。 +* `active_support.deprecation_behavior`:设定各个环境报告弃用的方式,在开发环境中默认为 `:log`,在生产环境中默认为 `:notify`,在测试环境中默认为 `:stderr`。如果没为 `config.active_support.deprecation` 设定一个值,这个初始化脚本提示用户在当前环境的配置文件(`config/environments` 目录里)中设定。可以设为一个数组。 +* `active_support.initialize_time_zone`:根据 `config.time_zone` 设置为应用设定默认的时区。默认为“UTC”。 +* `active_support.initialize_beginning_of_week`:根据 `config.beginning_of_week` 设置为应用设定一周从哪一天开始。默认为 `:monday`。 +* `active_support.set_configs`:使用 `config.active_support` 设置 Active Support,把方法名作为设值方法发给 `ActiveSupport`,并传入选项的值。 +* `action_dispatch.configure`:配置 `ActionDispatch::Http::URL.tld_length`,设为 `config.action_dispatch.tld_length` 的值。 +* `action_view.set_configs`:使用 `config.action_view` 设置 Action View,把方法名作为设值方法发给 `ActionView::Base`,并传入选项的值。 +* `action_controller.assets_config`:如果没有明确配置,把 `config.actions_controller.assets_dir` 设为应用的 `public` 目录。 +* `action_controller.set_helpers_path`:把 Action Controller 的 `helpers_path` 设为应用的 `helpers_path`。 +* `action_controller.parameters_config`:为 `ActionController::Parameters` 配置健壮参数选项。 +* `action_controller.set_configs`:使用 `config.action_controller` 设置 Action Controller,把方法名作为设值方法发给 `ActionController::Base`,并传入选项的值。 +* `action_controller.compile_config_methods`:初始化指定的配置选项,得到方法,以便快速访问。 +* `active_record.initialize_timezone`:把 `ActiveRecord::Base.time_zone_aware_attributes` 设为 `true`,并把 `ActiveRecord::Base.default_timezone` 设为 UTC。从数据库中读取属性时,转换成 `Time.zone` 指定的时区。 +* `active_record.logger`:把 `ActiveRecord::Base.logger` 设为 `Rails.logger`(如果还未设定)。 +* `active_record.migration_error`:配置中间件,检查待运行的迁移。 +* `active_record.check_schema_cache_dump`:如果配置了,而且有缓存,加载模式缓存转储。 +* `active_record.warn_on_records_fetched_greater_than`:查询返回大量记录时启用提醒。 +* `active_record.set_configs`:使用 `config.active_record` 设置 Active Record,把方法名作为设值方法发给 `ActiveRecord::Base`,并传入选项的值。 +* `active_record.initialize_database`:从 `config/database.yml` 中加载数据库配置,并在当前环境中连接数据库。 +* `active_record.log_runtime`:引入 `ActiveRecord::Railties::ControllerRuntime`,把 Active Record 调用的耗时记录到日志中。 +* `active_record.set_reloader_hooks`:如果 `config.cache_classes` 设为 `false`,还原所有可重新加载的数据库连接。 +* `active_record.add_watchable_files`:把 `schema.rb` 和 `structure.sql` 添加到可监视的文件列表中。 +* `active_job.logger`:把 `ActiveJob::Base.logger` 设为 `Rails.logger`(如果还未设定)。 +* `active_job.set_configs`:使用 `config.active_job` 设置 Active Job,把方法名作为设值方法发给 `ActiveJob::Base`,并传入选项的值。 +* `action_mailer.logger`:把 `ActionMailer::Base.logger` 设为 `Rails.logger`(如果还未设定)。 +* `action_mailer.set_configs`:使用 `config.action_mailer` 设定 Action Mailer,把方法名作为设值方法发给 `ActionMailer::Base`,并传入选项的值。 +* `action_mailer.compile_config_methods`:初始化指定的配置选项,得到方法,以便快速访问。 +* `set_load_path`:在 `bootstrap_hook` 之前运行。把 `config.load_paths` 指定的路径和所有自动加载路径添加到 `$LOAD_PATH` 中。 +* `set_autoload_paths`:在 `bootstrap_hook` 之前运行。把 `app` 目录中的所有子目录,以及 `config.autoload_paths`、`config.eager_load_paths` 和 `config.autoload_once_paths` 指定的路径添加到 `ActiveSupport::Dependencies.autoload_paths` 中。 +* `add_routing_paths`:加载所有的 `config/routes.rb` 文件(应用和 Railtie 中的,包括引擎),然后设置应用的路由。 +* `add_locales`:把(应用、Railtie 和引擎的)`config/locales` 目录中的文件添加到 `I18n.load_path` 中,让那些文件中的翻译可用。 +* `add_view_paths`:把应用、Railtie 和引擎的 `app/views` 目录添加到应用查找视图文件的路径中。 +* `load_environment_config`:加载 `config/environments` 目录中针对当前环境的配置文件。 +* `prepend_helpers_path`:把应用、Railtie 和引擎中的 `app/helpers` 目录添加到应用查找辅助方法的路径中。 +* `load_config_initializers`:加载应用、Railtie 和引擎中 `config/initializers` 目录里的全部 Ruby 文件。这个目录中的文件可用于存放应该在加载完全部框架之后设定的设置。 +* `engines_blank_point`:在初始化过程中提供一个点,以便在加载引擎之前做些事情。在这一点之后,运行所有 Railtie 和引擎初始化脚本。 +* `add_generator_templates`:寻找应用、Railtie 和引擎中 `lib/templates` 目录里的生成器模板,把它们添加到 `config.generators.templates` 设置中,供所有生成器引用。 +* `ensure_autoload_once_paths_as_subset`:确保 `config.autoload_once_paths` 只包含 `config.autoload_paths` 中的路径。如果有额外路径,抛出异常。 +* `add_to_prepare_blocks`:把应用、Railtie 或引擎中的每个 `config.to_prepare` 调用都添加到 Action Dispatch 的 `to_prepare` 回调中。这些回调在开发环境中每次请求都运行,在生产环境中只在第一次请求之前运行。 +* `add_builtin_route`:如果应用在开发环境中运行,把针对 `rails/info/properties` 的路由添加到应用的路由中。这个路由在 Rails 应用的 `public/index.html` 文件中提供一些详细信息,例如 Rails 和 Ruby 的版本。 +* `build_middleware_stack`:为应用构建中间件栈,返回一个对象,它有一个 `call` 方法,参数是请求的 Rack 环境对象。 +* `eager_load!`:如果 `config.eager_load` 为 `true`,运行 `config.before_eager_load` 钩子,然后调用 `eager_load!`,加载全部 `config.eager_load_namespaces`。 +* `finisher_hook`:在应用初始化过程结束的位置提供一个钩子,并且运行应用、Railtie 和引擎的所有 `config.after_initialize` 块。 +* `set_routes_reloader_hook`:让 Action Dispatch 使用 `ActionDispatch::Callbacks.to_prepare` 重新加载路由文件。 +* `disable_dependency_loading`:如果 `config.eager_load` 为 `true`,禁止自动加载依赖。 + + + +## 数据库池 + +Active Record 数据库连接由 `ActiveRecord::ConnectionAdapters::ConnectionPool` 管理,确保连接池的线程访问量与有限个数据库连接数同步。这一限制默认为 5,可以在 `database.yml` 文件中配置。 + +```yml +development: + adapter: sqlite3 + database: db/development.sqlite3 + pool: 5 + timeout: 5000 +``` -* `active_record.log_runtime`:引入 `ActiveRecord::Railties::ControllerRuntime`,这个模块负责把 Active Record 查询花费的时间写入日志。 +连接池默认在 Active Record 内部处理,因此所有应用服务器(Thin、Puma、Unicorn,等等)的行为应该一致。数据库连接池一开始是空的,随着连接数的增加,会不断创建,直至连接池上限。 -* `active_record.set_dispatch_hooks`:如果 `config.cache_classes` 为 `false`,重置所有可重新加载的数据库连接。 +每个请求在首次访问数据库时会检出连接,请求结束再检入连接。这样,空出的连接位置就可以提供给队列中的下一个请求使用。 -* `action_mailer.logger`:如果还未创建,把 `ActionMailer::Base.logger` 设为 `Rails.logger`。 +如果连接数超过可用值,Active Record 会阻塞,等待池中有空闲的连接。如果无法获得连接,会抛出类似下面的超时错误。 -* `action_mailer.set_configs`:根据 `config.action_mailer` 设置 Action Mailer,把指定的方法名作为赋值方法发送给 `ActionMailer::Base`,并传入指定的值。 +``` +ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5.000 seconds (waited 5.000 seconds) +``` -* `action_mailer.compile_config_methods`:初始化指定的设置方法,以便快速访问。 +如果出现上述错误,可以考虑增加连接池的数量,即在 `database.yml` 文件中增加 `pool` 选项的值。 -* `set_load_path`:在 `bootstrap_hook` 之前运行。把 `vendor` 文件夹、`lib` 文件夹、`app` 文件夹中的所有子文件夹,以及 `config.load_paths` 中指定的路径加入 `$LOAD_PATH`。 +NOTE: 如果是多线程环境,有可能多个线程同时访问多个连接。因此,如果请求量很大,极有可能发生多个线程争夺有限个连接的情况。 -* `set_autoload_paths`:在 `bootstrap_hook` 之前运行。把 `app` 文件夹中的所有子文件夹,以及 `config.autoload_paths` 指定的路径加入 `ActiveSupport::Dependencies.autoload_paths`。 -* `add_routing_paths`:加载所有 `config/routes.rb` 文件(程序中的,Railtie 中的,以及引擎中的),并创建程序的路由。 + -* `add_locales`:把 `config/locales` 文件夹中的所有文件(程序中的,Railties 中的,以及引擎中的)加入 `I18n.load_path`,让这些文件中的翻译可用。 +## 自定义配置 -* `add_view_paths`:把程序、Railtie 和引擎中的 `app/views` 文件夹加入视图文件查找路径。 +我们可以通过 Rails 配置对象为自己的代码设定配置。如下所示: -* `load_environment_config`:加载 `config/environments` 文件夹中当前环境对应的设置文件。 +```ruby +config.payment_processing.schedule = :daily +config.payment_processing.retries = 3 +config.super_debugger = true +``` -* `append_asset_paths`:查找程序的静态资源文件路径,Railtie 中的静态资源文件路径,以及 `config.static_asset_paths` 中可用的文件夹。 +这些配置选项可通过配置对象访问: -* `prepend_helpers_path`:把程序、Railtie、引擎中的 `app/helpers` 文件夹加入帮助文件查找路径。 +```ruby +Rails.configuration.payment_processing.schedule # => :daily +Rails.configuration.payment_processing.retries # => 3 +Rails.configuration.super_debugger # => true +Rails.configuration.super_debugger.not_set # => nil +``` -* `load_config_initializers`:加载程序、Railtie、引擎中 `config/initializers` 文件夹里所有的 Ruby 文件。这些文件可在框架加载后做设置。 +还可以使用 `Rails::Application.config_for` 加载整个配置文件: -* `engines_blank_point`:在初始化过程加入一个时间点,以防加载引擎之前要做什么处理。在这一点之后,会运行所有 Railtie 和引擎的初始化脚本。 +```yml +# config/payment.yml: +production: + environment: production + merchant_id: production_merchant_id + public_key: production_public_key + private_key: production_private_key +development: + environment: sandbox + merchant_id: development_merchant_id + public_key: development_public_key + private_key: development_private_key +``` -* `add_generator_templates`:在程序、Railtie、引擎的 `lib/templates` 文件夹中查找生成器使用的模板,并把这些模板添加到 `config.generators.templates`,让所有生成器都能使用。 +```ruby +# config/application.rb +module MyApp + class Application < Rails::Application + config.payment = config_for(:payment) + end +end +``` -* `ensure_autoload_once_paths_as_subset`:确保 `config.autoload_once_paths` 只包含 `config.autoload_paths` 中的路径。如果包含其他路径,会抛出异常。 +```ruby +Rails.configuration.payment['merchant_id'] # => production_merchant_id or development_merchant_id +``` -* `add_to_prepare_blocks`:把程序、Railtie、引擎中的 `config.to_prepare` 加入 Action Dispatch 的 `to_prepare` 回调中,这些回调在开发环境中每次请求都会运行,但在生产环境中只在首次请求前运行。 + -* `add_builtin_route`:如果程序运行在开发环境中,这个初始化脚本会把 `rails/info/properties` 添加到程序的路由中。这个路由对应的页面显示了程序的详细信息,例如 Rails 和 Ruby 版本。 +## 搜索引擎索引 -* `build_middleware_stack`:构建程序的中间件列表,返回一个对象,可响应 `call` 方法,参数为 Rack 请求的环境对象。 +有时,你可能不想让应用中的某些页面出现在搜索网站中,如 Google、Bing、Yahoo 或 Duck Duck Go。索引网站的机器人首先分析 `http://your-site.com/robots.txt` 文件,了解允许它索引哪些页面。 -* `eager_load!`:如果 `config.eager_load` 为 `true`,运行 `config.before_eager_load` 钩子,然后调用 `eager_load!`,加载所有 `config.eager_load_namespaces` 中的命名空间。 +Rails 为你创建了这个文件,在 `/public` 文件夹中。默认情况下,允许搜索引擎索引应用的所有页面。如果不想索引应用的任何页面,使用下述内容: -* `finisher_hook`:为程序初始化完成点提供一个钩子,还会运行程序、Railtie、引擎中的所有 `config.after_initialize` 代码块。 +``` +User-agent: * +Disallow: / +``` -* `set_routes_reloader`:设置 Action Dispatch 使用 `ActionDispatch::Callbacks.to_prepare` 重新加载路由文件。 +若想禁止索引指定的页面,需要使用更复杂的句法。详情参见[官方文档](http://www.robotstxt.org/robotstxt.html)。 -* `disable_dependency_loading`:如果 `config.eager_load` 为 `true`,禁止自动加载依赖件。 + -数据库连接池 ----------- +## 事件型文件系统监控程序 -Active Record 数据库连接由 `ActiveRecord::ConnectionAdapters::ConnectionPool` 管理,确保一个连接池的线程量限制在有限的数据库连接数之内。这个限制量默认为 5,但可以在文件 `database.yml` 中设置。 +如果加载了 listen gem,而且 `config.cache_classes` 为 `false`,Rails 使用一个事件型文件系统监控程序监测变化: ```ruby -development: - adapter: sqlite3 - database: db/development.sqlite3 - pool: 5 - timeout: 5000 +group :development do + gem 'listen', '>= 3.0.5', '< 3.2' +end ``` -因为连接池在 Active Record 内部处理,因此程序服务器(Thin,mongrel,Unicorn 等)要表现一致。一开始数据库连接池是空的,然后按需创建更多的链接,直到达到连接池数量限制为止。 - -任何一个请求在初次需要连接数据库时都要检查连接,请求处理完成后还会再次检查,确保后续连接可使用这个连接池。 - -如果尝试使用比可用限制更多的连接,Active Record 会阻塞连接,等待连接池分配新的连接。如果无法获得连接,会抛出如下所示的异常。 - -```ruby -ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it: -``` +否则,每次请求 Rails 都会遍历应用树,检查有没有变化。 -如果看到以上异常,可能需要增加连接池限制数量,方法是修改 `database.yml` 文件中的 `pool` 选项。 +在 Linux 和 macOS 中无需额外的 gem,[*BSD](https://github.com/guard/listen#on-bsd) 和 [Windows](https://github.com/guard/listen#on-windows) 可能需要。 -NOTE: 如果在多线程环境中运行程序,有可能多个线程同时使用多个连接。所以,如果程序的请求量很大,有可能出现多个线程抢用有限的连接。 +注意,[某些设置不支持](https://github.com/guard/listen#issues—​limitations)。 diff --git a/source/zh-CN/contributing_to_ruby_on_rails.md b/source/zh-CN/contributing_to_ruby_on_rails.md index 133ef58..e0d9391 100644 --- a/source/zh-CN/contributing_to_ruby_on_rails.md +++ b/source/zh-CN/contributing_to_ruby_on_rails.md @@ -1,344 +1,391 @@ -Contributing to Ruby on Rails -============================= +# 为 Ruby on Rails 做贡献 -This guide covers ways in which _you_ can become a part of the ongoing development of Ruby on Rails. +本文介绍几种参与 Ruby on Rails 开发的方式。 -After reading this guide, you will know: +读完本文后,您将学到: -* How to use GitHub to report issues. -* How to clone master and run the test suite. -* How to help resolve existing issues. -* How to contribute to the Ruby on Rails documentation. -* How to contribute to the Ruby on Rails code. +* 如何使用 GitHub 报告问题; +* 如何克隆 master,运行测试组件; +* 如何帮助解决现有问题; +* 如何为 Ruby on Rails 文档做贡献; +* 如何为 Ruby on Rails 代码做贡献。 -Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches. +Ruby on Rails 不是某一个人的框架。这些年,有成百上千个人为 Ruby on Rails 做贡献,小到修正一个字符,大到调整重要的架构或文档——目的都是把 Ruby on Rails 变得更好,适合所有人使用。即便你现在不想编写代码或文档,也能通过其他方式做贡献,例如报告问题和测试补丁。 --------------------------------------------------------------------------------- +[Rails 的自述文件](https://github.com/rails/rails/blob/master/README.md)说道,参与 Rails 及其子项目代码基开发的人,参与问题追踪系统、聊天室和邮件列表的人,都要遵守 Rails 的[行为准则](http://rubyonrails.org/conduct/)。 -Reporting an Issue ------------------- +----------------------------------------------------------------------------- -Ruby on Rails uses [GitHub Issue Tracking](https://github.com/rails/rails/issues) to track issues (primarily bugs and contributions of new code). If you've found a bug in Ruby on Rails, this is the place to start. You'll need to create a (free) GitHub account in order to submit an issue, to comment on them or to create pull requests. + -NOTE: Bugs in the most recent released version of Ruby on Rails are likely to get the most attention. Also, the Rails core team is always interested in feedback from those who can take the time to test _edge Rails_ (the code for the version of Rails that is currently under development). Later in this guide you'll find out how to get edge Rails for testing. +## 报告错误 -### Creating a Bug Report +Ruby on Rails 使用 [GitHub 的问题追踪系统](https://github.com/rails/rails/issues)追踪问题(主要是解决缺陷和贡献新代码)。如果你发现 Ruby on Rails 有缺陷,首先应该发布到这个系统中。若想提交问题、评论问题或创建拉取请求, 你要注册一个 GitHub 账户(免费)。 -If you've found a problem in Ruby on Rails which is not a security risk, do a search in GitHub under [Issues](https://github.com/rails/rails/issues) in case it has already been reported. If you do not find any issue addressing it you may proceed to [open a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.) +NOTE: Ruby on Rails 最新版的缺陷最受关注。此外,Rails 核心团队始终欢迎能对最新开发版做测试的人反馈。本文后面会说明如何测试最新开发版。 -Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to replicate the bug and figure out a fix. -Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment. + -### Create a Self-Contained gist for Active Record and Action Controller Issues +### 创建一个缺陷报告 -If you are filing a bug report, please use -[Active Record template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) or -[Action Controller template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) -if the bug is found in a published gem, and -[Active Record template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) or -[Action Controller template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) -if the bug happens in the master branch. +如果你在 Ruby on Rails 中发现一个没有安全风险的问题,在 [GitHub 的问题追踪系统](https://github.com/rails/rails/issues)中搜索一下,说不定已经有人报告了。如果之前没有人报告,接下来你要[创建一个](https://github.com/rails/rails/issues/new)。(报告安全问题的方法参见下一节。) -### Special Treatment for Security Issues +问题报告应该包含标题,而且要使用简洁的语言描述问题。你应该尽量多提供相关的信息,而且至少要有一个代码示例,演示所述的问题。如果能提供一个单元测试,说明预期行为更好。你的目标是让你自己以及其他人能重现缺陷,找出修正方法。 -WARNING: Please do not report security vulnerabilities with public GitHub issue reports. The [Rails security policy page](http://rubyonrails.org/security) details the procedure to follow for security issues. +然后,耐心等待。除非你报告的是紧急问题,会导致世界末日,否则你要等待可能有其他人也遇到同样的问题,与你一起去解决。不要期望你报告的问题能立即得到关注,有人立刻着手解决。像这样报告问题基本上是让自己迈出修正问题的第一步,并且让其他遇到同样问题的人复议。 -### What about Feature Requests? + -Please don't put "feature request" items into GitHub Issues. If there's a new -feature that you want to see added to Ruby on Rails, you'll need to write the -code yourself - or convince someone else to partner with you to write the code. -Later in this guide you'll find detailed instructions for proposing a patch to -Ruby on Rails. If you enter a wish list item in GitHub Issues with no code, you -can expect it to be marked "invalid" as soon as it's reviewed. +### 创建可执行的测试用例 -Sometimes, the line between 'bug' and 'feature' is a hard one to draw. -Generally, a feature is anything that adds new behavior, while a bug is -anything that fixes already existing behavior that is misbehaving. Sometimes, -the core team will have to make a judgement call. That said, the distinction -generally just affects which release your patch will get in to; we love feature -submissions! They just won't get backported to maintenance branches. +提供重现问题的方式有助于别人帮你确认、研究并最终解决问题。为此,你可以提供可执行的测试用例。为了简化这一过程,我们准备了几个缺陷报告模板供你参考: -If you'd like feedback on an idea for a feature before doing the work for make -a patch, please send an email to the [rails-core mailing -list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You -might get no response, which means that everyone is indifferent. You might find -someone who's also interested in building that feature. You might get a "This -won't be accepted." But it's the proper place to discuss new ideas. GitHub -Issues are not a particularly good venue for the sometimes long and involved -discussions new features require. +* 报告 Active Record(模型、数据库)问题的模板:[gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) +* 报告 Active Record(迁移)问题的模板:[gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_migrations_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_migrations_master.rb) +* 报告 Action Pack(控制器、路由)问题的模板:[gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb) +* 报告 Active Job 问题的模板:[gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_job_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_job_master.rb) +* 其他问题的通用模板:[gem](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_gem.rb) / [master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/generic_master.rb) +这些模板包含样板代码,供你着手编写测试用例,分别针对 Rails 的发布版(`*_gem.rb`)和最新开发版(`*_master.rb`)。 -Helping to Resolve Existing Issues ----------------------------------- +你只需把相应模板中的内容复制到一个 `.rb` 文件中,然后做必要的改动,说明问题。如果想运行测试,只需在终端里执行 `ruby the_file.rb`。如果一切顺利,测试用例应该失败。 -As a next step beyond reporting issues, you can help the core team resolve existing issues. If you check the [Everyone's Issues](https://github.com/rails/rails/issues) list in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually: +随后,可以通过一个 [gist](https://gist.github.com/) 分享你的可执行测试用例,或者直接粘贴到问题描述中。 -### Verifying Bug Reports + -For starters, it helps just to verify bug reports. Can you reproduce the reported issue on your own computer? If so, you can add a comment to the issue saying that you're seeing the same thing. +### 特殊对待安全问题 -If something is very vague, can you help squash it down into something specific? Maybe you can provide additional information to help reproduce a bug, or help by eliminating needless steps that aren't required to demonstrate the problem. +WARNING: 请不要在公开的 GitHub 问题报告中报告安全漏洞。安全问题的报告步骤在 [Rails 安全方针页面](http://rubyonrails.org/security)中有详细说明。 -If you find a bug report without a test, it's very useful to contribute a failing test. This is also a great way to get started exploring the source code: looking at the existing test files will teach you how to write more tests. New tests are best contributed in the form of a patch, as explained later on in the "Contributing to the Rails Code" section. -Anything you can do to make bug reports more succinct or easier to reproduce is a help to folks trying to write code to fix those bugs - whether you end up writing the code yourself or not. + -### Testing Patches +### 功能请求怎么办? -You can also help out by examining pull requests that have been submitted to Ruby on Rails via GitHub. To apply someone's changes you need first to create a dedicated branch: +请勿在 GitHub 问题追踪系统中请求新功能。如果你想把新功能添加到 Ruby on Rails 中,你要自己编写代码,或者说服他人与你一起编写代码。本文后面会详述如何为 Ruby on Rails 提请补丁。如果在 GitHub 问题追踪系统发布希望含有的功能,但是没有提供代码,在审核阶段会将其标记为“无效”。 -```bash +有时,很难区分“缺陷”和“功能”。一般来说,功能是为了添加新行为,而缺陷是导致不正确行为的缘由。有时,核心团队会做判断。尽管如此,区别通常影响的是补丁放在哪个发布版中。我们十分欢迎你提交功能!只不过,新功能不会添加到维护分支中。 + +如果你想在着手打补丁之前征询反馈,请向 [rails-core 邮件列表](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core)发送电子邮件。你可能得不到回应,这表明大家是中立的。你可能会发现有人对你提议的功能感兴趣;可能会有人说你的提议不可行。但是新想法就应该在那里讨论。GitHub 问题追踪系统不是集中讨论特性请求的正确场所。 + + + +## 帮助解决现有问题 + +除了报告问题之外,你还可以帮助核心团队解决现有问题。如果查看 GitHub 中的[问题列表](https://github.com/rails/rails/issues),你会发现很多问题都得到了关注。为此你能做些什么呢?其实,你能做的有很多。 + + + +### 确认缺陷报告 + +对新人来说,帮助确认缺陷报告就行了。你能在自己的电脑中重现报告的问题吗?如果能,可以在问题的评论中说你发现了同样的问题。 + +如果问题描述不清,你能帮忙说得更具体些吗?或许你可以提供额外的信息,帮助重现缺陷,或者去掉说明问题所不需要的步骤。 + +如果发现缺陷报告中没有测试,你可以贡献一个失败测试。这是学习源码的好机会:查看现有的测试文件能让你学到如何编写更好的测试。新测试最好以补丁的形式提供,详情参阅 [为 Rails 代码做贡献](#contributing-to-the-rails-code)。 + +不管你自己写不写代码,只要你能把缺陷报告变得更简洁、更便于重现,就能为尝试修正缺陷的人提供帮助。 + + + +### 测试补丁 + +你还可以帮忙检查通过 GitHub 为 Ruby on Rails 提交的拉取请求。在使用别人的改动之前,你要创建一个专门的分支: + +```sh $ git checkout -b testing_branch ``` -Then you can use their remote branch to update your codebase. For example, let's say the GitHub user JohnSmith has forked and pushed to a topic branch "orange" located at https://github.com/JohnSmith/rails. +然后可以使用他们的远程分支更新代码基。假如 GitHub 用户 JohnSmith 派生了 Rails 源码,地址是 https://github.com/JohnSmith/rails,然后推送到主题分支“orange”: -```bash -$ git remote add JohnSmith git://github.com/JohnSmith/rails.git +```sh +$ git remote add JohnSmith https://github.com/JohnSmith/rails.git $ git pull JohnSmith orange ``` -After applying their branch, test it out! Here are some things to think about: +然后,使用主题分支中的代码做测试。下面是一些考虑的事情: + +* 改动可用吗? +* 你对测试满意吗?你能理解测试吗?缺少测试吗? +* 有适度的文档覆盖度吗?其他地方的文档需要更新吗? +* 你喜欢他的实现方式吗?你能以更好或更快的方式实现部分改动吗? + +拉取请求中的改动让你满意之后,在 GitHub 问题追踪系统中发表评论,表明你赞成。你的评论应该说你喜欢这个改动,以及你的观点。比如说: + +> 我喜欢你对 generate_finder_sql 这部分代码的调整,现在更好了。测试也没问题。 + +如果你的评论只是说“+1”,其他评审很难严肃对待。你要表明你花时间审查拉取请求了。 -* Does the change actually work? -* Are you happy with the tests? Can you follow what they're testing? Are there any tests missing? -* Does it have the proper documentation coverage? Should documentation elsewhere be updated? -* Do you like the implementation? Can you think of a nicer or faster way to implement a part of their change? + -Once you're happy that the pull request contains a good change, comment on the GitHub issue indicating your approval. Your comment should indicate that you like the change and what you like about it. Something like: +## 为 Rails 文档做贡献 -
-I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. -
+Ruby on Rails 主要有两份文档:这份指南,帮你学习 Ruby on Rails;API,作为参考资料。 -If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. +你可以帮助改进这份 Rails 指南,把它变得更简单、更为一致,也更易于理解。你可以添加缺少的信息、更正错误、修正错别字或者针对最新的 Rails 开发版做更新。 -Contributing to the Rails Documentation ---------------------------------------- +为此,可以向 [Rails 项目](http://github.com/rails/rails)发送拉取请求。 -Ruby on Rails has two main sets of documentation: the guides, which help you -learn about Ruby on Rails, and the API, which serves as a reference. +如果你想为文档做贡献,请阅读[API 文档指导方针](api_documentation_guidelines.html)和[Ruby on Rails 指南指导方针](ruby_on_rails_guides_guidelines.html)。 -You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/rails/docrails/translating-rails-guides). +NOTE: 为了减轻 CI 服务器的压力,关于文档的提交消息中应该包含 `[ci skip]`,跳过构建步骤。只修改文档的提交一定要这么做。 -You can either open a pull request to [Rails](http://github.com/rails/rails) or -ask the [Rails core team](http://rubyonrails.org/core) for commit access on -[docrails](http://github.com/rails/docrails) if you contribute regularly. -Please do not open pull requests in docrails, if you'd like to get feedback on your -change, ask for it in [Rails](http://github.com/rails/rails) instead. -Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation. + -If you are unsure of the documentation changes, you can create an issue in the [Rails](https://github.com/rails/rails/issues) issues tracker on GitHub. +## 翻译 Rails 指南 -When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html). +我们欢迎人们自发把 Rails 指南翻译成其他语言。翻译时请遵照下述步骤: -NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements. +* 派生 https://github.com/rails/rails 项目 +* 为你的语言添加一个文件夹,例如针对意大利语的 guides/source/it-IT +* 把 guides/source 中的内容复制到你创建的文件夹中,然后翻译 +* 不要翻译 HTML 文件,因为那是自动生成的 -NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes. +注意,翻译不提交到 Rails 仓库中。如前所述,翻译在你派生的项目中操作。这么做的原因是,或许只有英语文档适合通过补丁维护。 -WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails. +如果想生成这份指南的 HTML 格式,进入 guides 目录,然后执行(以 it-IT 为例): -Contributing to the Rails Code ------------------------------- +```sh +$ bundle install +$ bundle exec rake guides:generate:html GUIDES_LANGUAGE=it-IT +``` + +上述命令在 output 目录中生成这份指南。 + +NOTE: 上述说明针对 Rails 4 及以上版本。Redcarpet gem 无法在 JRuby 中使用。 + + +已知的翻译成果: + +* 意大利语: +* 西班牙语: +* 波兰语: +* 法语: +* 捷克语: +* 土耳其语: +* 韩语: +* 简体中文: +* 繁体中文: +* 俄语: +* 日语: + + + +## 为 Rails 代码做贡献 + + -### Setting Up a Development Environment +### 搭建开发环境 -To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to setup the tests on your own computer. +过了提交缺陷这个初级阶段之后,若想帮助解决现有问题,或者为 Ruby on Rails 贡献自己的代码,必须要能运行测试组件。这一节教你在自己的电脑中搭建测试的环境。 -#### The Easy Way + -The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box). +#### 简单方式 -#### The Hard Way +搭建开发环境最简单、也是推荐的方式是使用 [Rails 开发虚拟机](https://github.com/rails/rails-dev-box)。 -In case you can't use the Rails development box, see [this other guide](development_dependencies_install.html). + -### Clone the Rails Repository +#### 笨拙方式 -To be able to contribute code, you need to clone the Rails repository: +如果你不便使用 Rails 开发虚拟机,请阅读[安装开发依赖](development_dependencies_install.html)。 -```bash -$ git clone git://github.com/rails/rails.git + + +### 克隆 Rails 仓库 + +若想贡献代码,需要克隆 Rails 仓库: + +```sh +$ git clone https://github.com/rails/rails.git ``` -and create a dedicated branch: +然后创建一个专门的分支: -```bash +```sh $ cd rails $ git checkout -b my_new_branch ``` -It doesn't matter much what name you use, because this branch will only exist on your local computer and your personal repository on GitHub. It won't be part of the Rails Git repository. +分支的名称无关紧要,因为这个分支只存在于你的本地电脑和你在 GitHub 上的个人仓库中,不会出现在 Rails 的 Git 仓库里。 + + + +### bundle install + +安装所需的 gem: + +```sh +$ bundle install +``` + + -### Running an Application Against Your Local Branch +### 使用本地分支运行应用 -In case you need a dummy Rails app to test changes, the `--dev` flag of `rails new` generates an application that uses your local branch: +如果想使用虚拟的 Rails 应用测试改动,执行 `rails new` 命令时指定 `--dev` 旗标,使用本地分支生成一个应用: -```bash +```sh $ cd rails $ bundle exec rails new ~/my-test-app --dev ``` -The application generated in `~/my-test-app` runs against your local branch -and in particular sees any modifications upon server reboot. +上述命令使用本地分支在 `~/my-test-app` 目录中生成一个应用,重启服务器后便能看到改动的效果。 -### Write Your Code + -Now get busy and add/edit code. You're on your branch now, so you can write whatever you want (make sure you're on the right branch with `git branch -a`). But if you're planning to submit your change back for inclusion in Rails, keep a few things in mind: +### 编写你的代码 -* Get the code right. -* Use Rails idioms and helpers. -* Include tests that fail without your code, and pass with it. -* Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution. +现在可以着手添加和编辑代码了。你处在自己的分支中,可以编写任何你想编写的代码(使用 `git branch -a` 确定你处于正确的分支中)。不过,如果你打算把你的改动提交到 Rails 中,要注意几点: +* 代码要写得正确。 +* 使用 Rails 习惯用法和辅助方法。 +* 包含测试,在没有你的代码时失败,添加之后则通过。 +* 更新(相应的)文档、别处的示例和指南。只要受你的代码影响,都更新。 -TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted. +TIP: 装饰性的改动,没有为 Rails 的稳定性、功能或可测试性做出实质改进的改动一般不会接受(关于这一决定的讨论参见[这里](https://github.com/rails/rails/pull/13771#issuecomment-32746700))。 -#### Follow the Coding Conventions -Rails follows a simple set of coding style conventions: + -* Two spaces, no tabs (for indentation). -* No trailing whitespace. Blank lines should not have any spaces. -* Indent after private/protected. -* Use Ruby >= 1.9 syntax for hashes. Prefer `{ a: :b }` over `{ :a => :b }`. -* Prefer `&&`/`||` over `and`/`or`. -* Prefer class << self over self.method for class methods. -* Use `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`. -* Use `a = b` and not `a=b`. -* Use assert_not methods instead of refute. -* Prefer `method { do_stuff }` instead of `method{do_stuff}` for single-line blocks. -* Follow the conventions in the source you see used already. +#### 遵守编程约定 -The above are guidelines - please use your best judgment in using them. +Rails 遵守下述简单的编程风格约定: -### Benchmark Your Code +* (缩进)使用两个空格,不用制表符。 +* 行尾没有空白。空行不能有任何空白。 +* 私有和受保护的方法多一层缩进。 +* 使用 Ruby 1.9 及以上版本采用的散列句法。使用 `{ a: :b }`,而非 `{ :a => :b }`。 +* 较之 `and`/`or`,尽量使用 `&&`/`||`。 +* 编写类方法时,较之 `self.method`,尽量使用 `class << self`。 +* 使用 `my_method(my_arg)`,而非 `my_method( my_arg )` 或 `my_method my_arg`。 +* 使用 `a = b`,而非 `a=b`。 +* 使用 `assert_not` 方法,而非 `refute`。 +* 编写单行块时,较之 `method{do_stuff}`,尽量使用 `method { do_stuff }`。 +* 遵照源码中在用的其他约定。 -If your change has an impact on the performance of Rails, please use the -[benchmark-ips](https://github.com/evanphx/benchmark-ips) gem to provide -benchmark results for comparison. +以上是指导方针,使用时请灵活应变。 -Here's an example of using benchmark-ips: + -```ruby -require 'benchmark/ips' +### 对你的代码做基准测试 -Benchmark.ips do |x| - x.report('addition') { 1 + 2 } - x.report('addition with send') { 1.send(:+, 2) } -end -``` +如果你的改动对 Rails 的性能有影响,请对你的代码做基准测试,衡量影响。请把基准测试脚本与结果一起分享出来。应该考虑把这个信息写入提交消息,以便后续开发者验证你的发现,确定是否仍有必要修改。(例如,Ruby VM 最新的优化出来后,以前的优化可能就没必要了。) -This will generate a report with the following information: +针对你所关注的情况做优化十分简单,但是在其他情况下可能导致回归错误。英雌,应该在一些典型的情况下测试你的改动。理想情况下,你应该在从生产应用中抽离出来的真实场景中测试。 -``` -Calculating ------------------------------------- - addition 69114 i/100ms - addition with send 64062 i/100ms -------------------------------------------------- - addition 5307644.4 (±3.5%) i/s - 26539776 in 5.007219s - addition with send 3702897.9 (±3.5%) i/s - 18513918 in 5.006723s -``` +你可以从[基准测试模板](https://github.com/rails/rails/blob/master/guides/bug_report_templates/benchmark.rb)入手,模板中有使用 [benchmark-ips](https://github.com/evanphx/benchmark-ips) gem 的样板代码。这个模板针对相对独立的改动,可以直接放在脚本中。 -Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information. + -### Running Tests +### 运行测试 -It is not customary in Rails to run the full test suite before pushing -changes. The railties test suite in particular takes a long time, and even -more if the source code is mounted in `/vagrant` as happens in the recommended -workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box). +在推送改动之前,通常不运行整个测试组件。railties 的测试组件所需的时间特别长,如果按照推荐的工作流程,使用 [rails-dev-box](https://github.com/rails/rails-dev-box) 把源码挂载到 `/vagrant`,时间更长。 -As a compromise, test what your code obviously affects, and if the change is -not in railties, run the whole test suite of the affected component. If all -tests are passing, that's enough to propose your contribution. We have -[Travis CI](https://travis-ci.org/rails/rails) as a safety net for catching -unexpected breakages elsewhere. +作为一种折中方案,应该测试明显受到影响的代码;如果不是改动 railties,运行受影响的组件的整个测试组件。如果所有测试都能通过,表明你可以提请你的贡献了。为了捕获别处预料之外的问题,我们配备了 [Travis CI](https://travis-ci.org/rails/rails),作为一个安全保障。 -#### Entire Rails: + -To run all the tests, do: +#### 整个 Rails -```bash +运行全部测试: + +```sh $ cd rails $ bundle exec rake test ``` -#### For a Particular Component + + +#### 某个组件 -You can run tests only for a particular component (e.g. Action Pack). For example, -to run Action Mailer tests: +可以只运行某个组件(如 Action Pack)的测试。例如,运行 Action Mailer 的测试: -```bash +```sh $ cd actionmailer $ bundle exec rake test ``` -#### Running a Single Test + -You can run a single test through ruby. For instance: +#### 运行单个测试 -```bash +可以通过 `ruby` 运行单个测试。例如: + +```sh $ cd actionmailer -$ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout +$ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout ``` -The `-n` option allows you to run a single method instead of the whole -file. +`-n` 选项指定运行单个方法,而非整个文件。 + + + +#### 测试 Active Record + +首先,创建所需的数据库。必要的表名、用户名和密码参见 `activerecord/test/config.example.yml`。 -##### Testing Active Record +对 MySQL 和 PostgreSQL 来说,运行 SQL 语句 `create database activerecord_unittest` 和 `create database activerecord_unittest2` 就行。SQLite3 无需这一步。 -This is how you run the Active Record test suite only for SQLite3: +只使用 SQLite3 运行 Active Record 的测试组件: -```bash +```sh $ cd activerecord $ bundle exec rake test:sqlite3 ``` -You can now run the tests as you did for `sqlite3`. The tasks are respectively +然后分别运行: -```bash -test:mysql +``` test:mysql2 test:postgresql ``` -Finally, +最后,一次运行前述三个测试: -```bash +```sh $ bundle exec rake test ``` -will now run the four of them in turn. +也可以单独运行某个测试: + +```sh +$ ARCONN=sqlite3 bundle exec ruby -Itest test/cases/associations/has_many_associations_test.rb +``` -You can also run any single test separately: +使用全部适配器运行某个测试: -```bash -$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb +```sh +$ bundle exec rake TEST=test/cases/associations/has_many_associations_test.rb ``` -You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server. +此外,还可以调用 `test_jdbcmysql`、`test_jdbcsqlite3` 或 `test_jdbcpostgresql`。针对其他数据库的测试参见 `activerecord/RUNNING_UNIT_TESTS.rdoc` 文件,持续集成服务器运行的测试组件参见 `ci/travis.rb` 文件。 -### Warnings + -The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings. +### 提醒 -If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag: +运行测试组件的命令启用了提醒。理想情况下,Ruby on Rails 不应该发出提醒,不过你可能会见到一些,其中部分可能来自第三方库。如果看到提醒,请忽略(或修正),然后提交不发出提醒的补丁。 -```bash +如果确信自己在做什么,想得到干净的输出,可以覆盖这个旗标: + +```sh $ RUBYOPT=-W0 bundle exec rake test ``` -### Updating the CHANGELOG + + +### 更新 CHANGELOG -The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version. +CHANGELOG 是每次发布的重要一环,保存着每个 Rails 版本的改动列表。 -You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG. +如果添加或删除了功能、提交了缺陷修正,或者添加了弃用提示,应该在框架的 CHANGELOG 顶部添加一条记录。重构和文档修改一般不应该在 CHANGELOG 中记录。 -A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry: +CHANGELOG 中的记录应该概述所做的改动,并且在末尾加上作者的名字。如果需要,可以写成多行,也可以缩进四个空格,添加代码示例。如果改动与某个工单有关,应该加上工单号。下面是一条 CHANGELOG 记录示例: ``` * Summary of a change that briefly describes what was changed. You can use multiple @@ -355,196 +402,183 @@ A CHANGELOG entry should summarize what was changed and should end with author's *Your Name* ``` -Your name can be added directly after the last word if you don't provide any code examples or don't need multiple paragraphs. Otherwise, it's best to make as a new paragraph. +如果没有代码示例,或者没有分成多行,可以直接在最后一个词后面加上作者的名字。否则,最好新起一段。 -### Sanity Check + -You should not be the only person who looks at the code before you submit it. -If you know someone else who uses Rails, try asking them if they'll check out -your work. If you don't know anyone else using Rails, try hopping into the IRC -room or posting about your idea to the rails-core mailing list. Doing this in -private before you push a patch out publicly is the "smoke test" for a patch: -if you can't convince one other developer of the beauty of your code, you’re -unlikely to convince the core team either. +### 更新 Gemfile.lock -### Commit Your Changes +有些改动需要更新依赖。此时,要执行 `bundle update` 命令,获取依赖的正确版本,并且随改动一起提交 `Gemfile.lock` 文件。 -When you're happy with the code on your computer, you need to commit the changes to Git: + -```bash +### 提交改动 + +在自己的电脑中对你的代码满意之后,要把改动提交到 Git 仓库中: + +```sh $ git commit -a ``` -At this point, your editor should be fired up and you can write a message for this commit. Well formatted and descriptive commit messages are extremely helpful for the others, especially when figuring out why given change was made, so please take the time to write it. +上述命令会启动编辑器,让你编写一个提交消息。写完之后,保存并关闭编辑器,然后继续往下做。 -Good commit message should be formatted according to the following example: +行文好,而且具有描述性的提交消息有助于别人理解你为什么做这项改动,因此请认真对待提交消息。 + +好的提交消息类似下面这样: ``` Short summary (ideally 50 characters or less) -More detailed description, if necessary. It should be wrapped to 72 -characters. Try to be as descriptive as you can, even if you think that -the commit content is obvious, it may not be obvious to others. You -should add such description also if it's already present in bug tracker, -it should not be necessary to visit a webpage to check the history. +More detailed description, if necessary. It should be wrapped to +72 characters. Try to be as descriptive as you can. Even if you +think that the commit content is obvious, it may not be obvious +to others. Add any description that is already present in the +relevant issues; it should not be necessary to visit a webpage +to check the history. + +The description section can have multiple paragraphs. -Description can have multiple paragraphs and you can use code examples -inside, just indent it with 4 spaces: +Code examples can be embedded by indenting them with 4 spaces: class ArticlesController def index - respond_with Article.limit(10) + render json: Article.limit(10) end end You can also add bullet points: -- you can use dashes or asterisks +- make a bullet point by starting a line with either a dash (-) + or an asterisk (*) -- also, try to indent next line of a point for readability, if it's too - long to fit in 72 characters +- wrap lines at 72 characters, and indent any additional lines + with 2 spaces for readability ``` -TIP. Please squash your commits into a single commit when appropriate. This simplifies future cherry picks, and also keeps the git log clean. +TIP: 如果合适,请把多条提交压缩成一条提交。这样便于以后挑选,而且能保持 Git 日志整洁。 -### Update Your Branch -It's pretty likely that other changes to master have happened while you were working. Go get them: + -```bash +### 更新你的分支 + +你在改动的过程中,master 分支很有可能有变化。请获取这些变化: + +```sh $ git checkout master $ git pull --rebase ``` -Now reapply your patch on top of the latest changes: +然后在最新的改动上重新应用你的补丁: -```bash +```sh $ git checkout my_new_branch $ git rebase master ``` -No conflicts? Tests still pass? Change still seems reasonable to you? Then move on. +没有冲突?测试依旧能通过?你的改动依然合理?那就往下走。 + + -### Fork +### 派生 -Navigate to the Rails [GitHub repository](https://github.com/rails/rails) and press "Fork" in the upper right hand corner. +打开 [GitHub 中的 Rails 仓库](https://github.com/rails/rails),点击右上角的“Fork”按钮。 -Add the new remote to your local repository on your local machine: +把派生的远程仓库添加到本地设备中的本地仓库里: -```bash -$ git remote add mine git@github.com:/rails.git +```sh +$ git remote add mine https://github.com:/rails.git ``` -Push to your remote: +推送到你的远程仓库: -```bash +```sh $ git push mine my_new_branch ``` -You might have cloned your forked repository into your machine and might want to add the original Rails repository as a remote instead, if that's the case here's what you have to do. +你可能已经把派生的仓库克隆到本地设备中了,因此想把 Rails 仓库添加为远程仓库。此时,要这么做。 -In the directory you cloned your fork: +在你克隆的派生仓库的目录中: -```bash -$ git remote add rails git://github.com/rails/rails.git +```sh +$ git remote add rails https://github.com/rails/rails.git ``` -Download new commits and branches from the official repository: +从官方仓库中下载新提交和分支: -```bash +```sh $ git fetch rails ``` -Merge the new content: +合并新内容: -```bash +```sh $ git checkout master $ git rebase rails/master ``` -Update your fork: +更新你派生的仓库: -```bash +```sh $ git push origin master ``` -If you want to update another branch: +如果想更新另一个分支: -```bash +```sh $ git checkout branch_name $ git rebase rails/branch_name $ git push origin branch_name ``` + + +### 创建拉取请求 + +打开你刚刚推送的目标仓库(例如 https://github.com/your-user-name/rails),点击“New pull request”按钮。 -### Issue a Pull Request +如果需要修改比较的分支(默认比较 master 分支),点击“Edit”,然后点击“Click to create a pull request for this comparison”。 -Navigate to the Rails repository you just pushed to (e.g. -https://github.com/your-user-name/rails) and click on "Pull Requests" seen in -the right panel. On the next page, press "New pull request" in the upper right -hand corner. +确保包含你所做的改动。填写补丁的详情,以及一个有意义的标题。然后点击“Send pull request”。Rails 核心团队会收到关于此次提交的通知。 -Click on "Edit", if you need to change the branches being compared (it compares -"master" by default) and press "Click to create a pull request for this -comparison". + -Ensure the changesets you introduced are included. Fill in some details about -your potential patch including a meaningful title. When finished, press "Send -pull request". The Rails core team will be notified about your submission. +### 获得反馈 -### Get some Feedback +多数拉取请求在合并之前会经过几轮迭代。不同的贡献者有时有不同的观点,而且有些补丁要重写之后才能合并。 -Most pull requests will go through a few iterations before they get merged. -Different contributors will sometimes have different opinions, and often -patches will need revised before they can get merged. +有些 Rails 贡献者开启了 GitHub 的邮件通知,有些则没有。此外,Rails 团队中(几乎)所有人都是志愿者,因此你的拉取请求可能要等几天才能得到第一个反馈。别失望!有时快,有时慢。这就是开源世界的日常。 -Some contributors to Rails have email notifications from GitHub turned on, but -others do not. Furthermore, (almost) everyone who works on Rails is a -volunteer, and so it may take a few days for you to get your first feedback on -a pull request. Don't despair! Sometimes it's quick, sometimes it's slow. Such -is the open source life. +如果过了一周还是无人问津,你可以尝试主动推进。你可以在 [rubyonrails-core 邮件列表](http://groups.google.com/group/rubyonrails-core/)中发消息,也可以在拉取请求中发一个评论。 -If it's been over a week, and you haven't heard anything, you might want to try -and nudge things along. You can use the [rubyonrails-core mailing -list](http://groups.google.com/group/rubyonrails-core/) for this. You can also -leave another comment on the pull request. +在你等待反馈的过程中,可以再创建其他拉取请求,也可以给别人的拉取请求反馈。我想,他们会感激你的,正如你会感激给你反馈的人一样。 -While you're waiting for feedback on your pull request, open up a few other -pull requests and give someone else some! I'm sure they'll appreciate it in -the same way that you appreciate feedback on your patches. + -### Iterate as Necessary +### 必要时做迭代 -It's entirely possible that the feedback you get will suggest changes. Don't get discouraged: the whole point of contributing to an active open source project is to tap into the knowledge of the community. If people are encouraging you to tweak your code, then it's worth making the tweaks and resubmitting. If the feedback is that your code doesn't belong in the core, you might still think about releasing it as a gem. +很有可能你得到的反馈是让你修改。别灰心,为活跃的开源项目做贡献就要跟上社区的步伐。如果有人建议你调整代码,你应该做调整,然后重新提交。如果你得到的反馈是,你的代码不应该添加到核心中,或许你可以考虑发布成一个 gem。 -#### Squashing commits + -One of the things that we may ask you to do is to "squash your commits", which -will combine all of your commits into a single commit. We prefer pull requests -that are a single commit. This makes it easier to backport changes to stable -branches, squashing makes it easier to revert bad commits, and the git history -can be a bit easier to follow. Rails is a large project, and a bunch of -extraneous commits can add a lot of noise. +#### 压缩提交 -In order to do this, you'll need to have a git remote that points at the main -Rails repository. This is useful anyway, but just in case you don't have it set -up, make sure that you do this first: +我们要求你做的一件事可能是让你“压缩提交”,把你的全部提交合并成一个提交。我们喜欢只有一个提交的拉取请求。这样便于把改动逆向移植(backport)到稳定分支中,压缩后易于还原不良提交,而且 Git 历史条理更清晰。Rails 是个大型项目,过多无关的提交容易扰乱视线。 -```bash +为此,Git 仓库中要有一个指向官方 Rails 仓库的远程仓库。这样做是有必要的,如果你还没有这么做,确保先执行下述命令: + +```sh $ git remote add upstream https://github.com/rails/rails.git ``` -You can call this remote whatever you'd like, but if you don't use `upstream`, -then change the name to your own in the instructions below. +这个远程仓库的名称随意,如果你使用的不是 `upstream`,请相应修改下述说明。 -Given that your remote branch is called `my_pull_request`, then you can do the -following: +假设你的远程分支是 `my_pull_request`,你要这么做: -```bash +```sh $ git fetch upstream $ git checkout my_pull_request -$ git rebase upstream/master -$ git rebase -i +$ git rebase -i upstream/master < Choose 'squash' for all of your commits except the first one. > < Edit the commit message to make sense, and describe all your changes. > @@ -552,52 +586,69 @@ $ git rebase -i $ git push origin my_pull_request -f ``` -You should be able to refresh the pull request on GitHub and see that it has -been updated. +此时,GitHub 中的拉取请求会刷新,更新为最新的提交。 + + -### Older Versions of Ruby on Rails +#### 更新拉取请求 -If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 4-0-stable branch: +有时,你得到的反馈是让你修改已经提交的代码。此时可能需要修正现有的提交。在这种情况下,Git 不允许你推送改动,因为你推送的分支和本地分支不匹配。你无须重新发起拉取请求,而是可以强制推送到 GitHub 中的分支,如前一节的压缩提交命令所示: -```bash +```sh +$ git push origin my_pull_request -f +``` + +这个命令会更新 GitHub 中的分支和拉取请求。不过注意,强制推送可能会导致远程分支中的提交丢失。使用时要小心。 + + + +### 旧版 Ruby on Rails + +如果想修正旧版 Ruby on Rails,要创建并切换到本地跟踪分支(tracking branch)。下例切换到 4-0-stable 分支: + +```sh $ git branch --track 4-0-stable origin/4-0-stable $ git checkout 4-0-stable ``` -TIP: You may want to [put your Git branch name in your shell prompt](http://qugstart.com/blog/git-and-svn/add-colored-git-branch-name-to-your-shell-prompt/) to make it easier to remember which version of the code you're working with. +TIP: 为了明确知道你处于代码的哪个版本,可以[把 Git 分支名放到 shell 提示符中](http://qugstart.com/blog/git-and-svn/add-colored-git-branch-name-to-your-shell-prompt/)。 + -#### Backporting + -Changes that are merged into master are intended for the next major release of Rails. Sometimes, it might be beneficial for your changes to propagate back to the maintenance releases for older stable branches. Generally, security fixes and bug fixes are good candidates for a backport, while new features and patches that introduce a change in behavior will not be accepted. When in doubt, it is best to consult a Rails team member before backporting your changes to avoid wasted effort. +#### 逆向移植 -For simple fixes, the easiest way to backport your changes is to [extract a diff from your changes in master and apply them to the target branch](http://ariejan.net/2009/10/26/how-to-create-and-apply-a-patch-with-git). +合并到 master 分支中的改动针对 Rails 的下一个主发布版。有时,你的改动可能需要逆向移植到旧的稳定分支中。一般来说,安全修正和缺陷修正会做逆向移植,而新特性和引入行为变化的补丁不会这么做。如果不确定,在逆向移植之前最好询问一位 Rails 团队成员,以免浪费精力。 -First make sure your changes are the only difference between your current branch and master: +对简单的修正来说,逆向移植最简单的方法是根据 master 分支的改动提取差异(diff),然后在目标分支应用改动。 -```bash +首先,确保你的改动是当前分支与 master 分支之间的唯一差别: + +```sh $ git log master..HEAD ``` -Then extract the diff: +然后,提取差异: -```bash +```sh $ git format-patch master --stdout > ~/my_changes.patch ``` -Switch over to the target branch and apply your changes: +切换到目标分支,然后应用改动: -```bash -$ git checkout -b my_backport_branch 3-2-stable +```sh +$ git checkout -b my_backport_branch 4-2-stable $ git apply ~/my_changes.patch ``` -This works well for simple changes. However, if your changes are complicated or if the code in master has deviated significantly from your target branch, it might require more work on your part. The difficulty of a backport varies greatly from case to case, and sometimes it is simply not worth the effort. +简单的改动可以这么做。然而,如果改动较为复杂,或者 master 分支的代码与目标分支之间差异巨大,你可能要做更多的工作。逆向移植的工作量有大有小,有时甚至不值得为此付出精力。 + +解决所有冲突,并且确保测试都能通过之后,推送你的改动,然后为逆向移植单独发起一个拉取请求。还应注意,旧分支的构建目标可能与 master 分支不同。如果可能,提交拉取请求之前最好在本地使用 `.travis.yml` 文件中给出的 Ruby 版本测试逆向移植。 -Once you have resolved all conflicts and made sure all the tests are passing, push your changes and open a separate pull request for your backport. It is also worth noting that older branches might have a different set of build targets than master. When possible, it is best to first test your backport locally against the Ruby versions listed in `.travis.yml` before submitting your pull request. +然后……可以思考下一次贡献了! -And then... think about your next contribution! + -Rails Contributors ------------------- +## Rails 贡献者 -All contributions, either via master or docrails, get credit in [Rails Contributors](http://contributors.rubyonrails.org). +所有贡献者都在 [Rails Contributors 页面](http://contributors.rubyonrails.org/)中列出。 diff --git a/source/zh-CN/credits.html.erb b/source/zh-CN/credits.html.erb index d5deb29..6f8302b 100644 --- a/source/zh-CN/credits.html.erb +++ b/source/zh-CN/credits.html.erb @@ -1,15 +1,15 @@ <% content_for :page_title do %> -Ruby on Rails 指南:致谢 +Ruby on Rails Guides: Credits <% end %> <% content_for :header_section do %> -

致谢

+

Credits

-

感谢以下人士孜孜不倦为本项目贡献

+

We'd like to thank the following people for their tireless contributions to this project.

<% end %> -

Rails 指南审阅者

+

Rails Guides Reviewers

<%= author('Vijay Dev', 'vijaydev', 'vijaydev.jpg') do %> Vijayakumar, found as Vijay Dev on the web, is a web applications developer and an open source enthusiast who lives in Chennai, India. He started using Rails in 2009 and began actively contributing to Rails documentation in late 2010. He tweets a lot and also blogs. @@ -19,16 +19,16 @@ Ruby on Rails 指南:致谢 Xavier Noria has been into Ruby on Rails since 2005. He is a Rails core team member and enjoys combining his passion for Rails and his past life as a proofreader of math textbooks. Xavier is currently an independent Ruby on Rails consultant. Oh, he also tweets and can be found everywhere as "fxn". <% end %> -

Rails 指南设计师

+

Rails Guides Designers

<%= author('Jason Zimdars', 'jz') do %> Jason Zimdars is an experienced creative director and web designer who has lead UI and UX design for numerous websites and web applications. You can see more of his design and writing at Thinkcage.com or follow him on Twitter. <% end %> -

Rails 指南作者群

+

Rails Guides Authors

<%= author('Ryan Bigg', 'radar', 'radar.png') do %> - Ryan Bigg works as the Community Manager at Spree Commerce and has been working with Rails since 2006. He's the author of Multi Tenancy With Rails and co-author of Rails 4 in Action. He's written many gems which can be seen on his GitHub page and he also tweets prolifically as @ryanbigg. + Ryan Bigg works as a Rails developer at Marketplacer and has been working with Rails since 2006. He's the author of Multi Tenancy With Rails and co-author of Rails 4 in Action. He's written many gems which can be seen on his GitHub page and he also tweets prolifically as @ryanbigg. <% end %> <%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %> @@ -40,7 +40,7 @@ Oscar Del Ben is a software engineer at Wi <% end %> <%= author('Tore Darell', 'toretore') do %> - Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. His home on the Internet is his blog Sneaky Abstractions. + Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. You can follow him on Twitter. <% end %> <%= author('Jeff Dean', 'zilkey') do %> @@ -78,3 +78,23 @@ Oscar Del Ben is a software engineer at Wi <%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %> Akshay Surve is the Founder at DeltaX, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on Twitter, Linkedin, Personal Blog or Quora. <% end %> + +

Rails 指南中文译者

+ +
+ Akshay Surve +

安道

+

+ 高校老师 / 自由翻译,翻译了大量 Ruby 资料。博客 +

+
+ +
+ Akshay Surve +

chinakr

+

+ GitHub +

+
+ +

其他贡献者

diff --git a/source/zh-CN/debugging_rails_applications.md b/source/zh-CN/debugging_rails_applications.md index 399e828..cb7af16 100644 --- a/source/zh-CN/debugging_rails_applications.md +++ b/source/zh-CN/debugging_rails_applications.md @@ -1,42 +1,44 @@ -调试 Rails 程序 -============== +# 调试 Rails 应用 -本文介绍如何调试 Rails 程序。 +本文介绍如何调试 Rails 应用。 -读完本文,你将学到: +读完本文后,您将学到: -* 调试的目的; -* 如何追查测试没有发现的问题; -* 不同的调试方法; -* 如何分析调用堆栈; +* 调试的目的; +* 如何追查测试没有发现的问题; +* 不同的调试方法; +* 如何分析堆栈跟踪。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -调试相关的视图帮助方法 -------------------- + -调试一个常见的需求是查看变量的值。在 Rails 中,可以使用下面这三个方法: +## 调试相关的视图辅助方法 -* `debug` -* `to_yaml` -* `inspect` +一个常见的需求是查看变量的值。在 Rails 中,可以使用下面这三个方法: + +* `debug` +* `to_yaml` +* `inspect` + + ### `debug` -`debug` 方法使用 YAML 格式渲染对象,把结果包含在 `
` 标签中,可以把任何对象转换成人类可读的数据格式。例如,在视图中有以下代码:
+`debug` 方法使用 YAML 格式渲染对象,把结果放在 `
` 标签中,可以把任何对象转换成人类可读的数据格式。例如,在视图中有以下代码:
 
 ```erb
-<%= debug @post %>
+<%= debug @article %>
 

Title: - <%= @post.title %> + <%= @article.title %>

``` 渲染后会看到如下结果: ```yaml ---- !ruby/object:Post +--- !ruby/object Article attributes: updated_at: 2008-09-05 22:55:47 body: It's a very helpful guide for debugging your Rails app. @@ -50,24 +52,24 @@ attributes_cache: {} Title: Rails debugging guide ``` + + ### `to_yaml` -使用 YAML 格式显示实例变量、对象的值或者方法的返回值,可以这么做: +在任何对象上调用 `to_yaml` 方法可以把对象转换成 YAML。转换得到的对象可以传给 `simple_format` 辅助方法,格式化输出。`debug` 就是这么做的: ```erb -<%= simple_format @post.to_yaml %> +<%= simple_format @article.to_yaml %>

Title: - <%= @post.title %> + <%= @article.title %>

``` -`to_yaml` 方法把对象转换成可读性较好地 YAML 格式,`simple_format` 方法按照终端中的方式渲染每一行。`debug` 方法就是包装了这两个步骤。 - -上述代码在渲染后的页面中会显示如下内容: +渲染后得到的结果如下: ```yaml ---- !ruby/object:Post +--- !ruby/object Article attributes: updated_at: 2008-09-05 22:55:47 body: It's a very helpful guide for debugging your Rails app. @@ -80,15 +82,17 @@ attributes_cache: {} Title: Rails debugging guide ``` + + ### `inspect` -另一个用于显示对象值的方法是 `inspect`,显示数组和 Hash 时使用这个方法特别方便。`inspect` 方法以字符串的形式显示对象的值。例如: +另一个用于显示对象值的方法是 `inspect`,显示数组和散列时使用这个方法特别方便。`inspect` 方法以字符串的形式显示对象的值。例如: ```erb <%= [1, 2, 3, 4, 5].inspect %>

Title: - <%= @post.title %> + <%= @article.title %>

``` @@ -100,42 +104,58 @@ Title: Rails debugging guide Title: Rails debugging guide ``` -Logger ------- + + +## 日志记录器 -运行时把信息写入日志文件也很有用。Rails 分别为各运行环境都维护着单独的日志文件。 +运行时把信息写入日志文件也很有用。Rails 分别为各个运行时环境维护着单独的日志文件。 -### Logger 是什么 + -Rails 使用 `ActiveSupport::Logger` 类把信息写入日志。当然也可换用其他代码库,比如 `Log4r`。 +### 日志记录器是什么? -替换日志代码库可以在 `environment.rb` 或其他环境文件中设置: +Rails 使用 `ActiveSupport::Logger` 类把信息写入日志。当然也可以换用其他库,比如 `Log4r`。 + +若想替换日志库,可以在 `config/application.rb` 或其他环境的配置文件中设置,例如: + +```ruby +config.logger = Logger.new(STDOUT) +config.logger = Log4r::Logger.new("Application Log") +``` + +或者在 `config/environment.rb` 中添加下述代码中的某一行: ```ruby Rails.logger = Logger.new(STDOUT) Rails.logger = Log4r::Logger.new("Application Log") ``` -TIP: 默认情况下,日志文件都保存在 `Rails.root/log/` 文件夹中,日志文件名为 `environment_name.log`。 +TIP: 默认情况下,日志文件都保存在 `Rails.root/log/` 目录中,日志文件的名称对应于各个环境。 + + + ### 日志等级 如果消息的日志等级等于或高于设定的等级,就会写入对应的日志文件中。如果想知道当前的日志等级,可以调用 `Rails.logger.level` 方法。 -可用的日志等级包括:`:debug`,`:info`,`:warn`,`:error`,`:fatal` 和 `:unknown`,分别对应数字 0-5。修改默认日志等级的方式如下: +可用的日志等级包括 `:debug`、`:info`、`:warn`、`:error`、`:fatal` 和 `:unknown`,分别对应数字 0-5。修改默认日志等级的方式如下: ```ruby -config.log_level = :warn # In any environment initializer, or -Rails.logger.level = 0 # at any time +config.log_level = :warn # 在环境的配置文件中 +Rails.logger.level = 0 # 任何时候 ``` 这么设置在开发环境和交付准备环境中很有用,在生产环境中则不会写入大量不必要的信息。 -TIP: Rails 为生产环境设置的默认日志等级是 `info`,生产环境和测试环境的默认日志等级是 `debug`。 +TIP: Rails 为所有环境设定的默认日志等级是 `debug`。 -### 写日志 -把消息写入日志文件可以在控制器、模型或邮件发送程序中调用 `logger.(debug|info|warn|error|fatal)` 方法。 + + +### 发送消息 + +把消息写入日志文件可以在控制器、模型或邮件程序中调用 `logger.(debug|info|warn|error|fatal)` 方法。 ```ruby logger.debug "Person attributes hash: #{@person.attributes.inspect}" @@ -146,18 +166,18 @@ logger.fatal "Terminating application, raised unrecoverable error!!!" 下面这个例子增加了额外的写日志功能: ```ruby -class PostsController < ApplicationController +class ArticlesController < ApplicationController # ... def create - @post = Post.new(params[:post]) - logger.debug "New post: #{@post.attributes.inspect}" - logger.debug "Post should be valid: #{@post.valid?}" - - if @post.save - flash[:notice] = 'Post was successfully created.' - logger.debug "The post was saved and now the user is going to be redirected..." - redirect_to(@post) + @article = Article.new(params[:article]) + logger.debug "New article: #{@article.attributes.inspect}" + logger.debug "Article should be valid: #{@article.valid?}" + + if @article.save + flash[:notice] = 'Article was successfully created.' + logger.debug "The article was saved and now the user is going to be redirected..." + redirect_to(@article) else render action: "new" end @@ -170,28 +190,30 @@ end 执行上述动作后得到的日志如下: ``` -Processing PostsController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST] +Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST] Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4 - Parameters: {"commit"=>"Create", "post"=>{"title"=>"Debugging Rails", + Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>"0"}, - "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"posts"} -New post: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", + "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"articles"} +New article: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>false, "created_at"=>nil} -Post should be valid: true - Post Create (0.000443) INSERT INTO "posts" ("updated_at", "title", "body", "published", +Article should be valid: true + Article Create (0.000443) INSERT INTO "articles" ("updated_at", "title", "body", "published", "created_at") VALUES('2008-09-08 14:52:54', 'Debugging Rails', 'I''m learning how to print in logs!!!', 'f', '2008-09-08 14:52:54') -The post was saved and now the user is going to be redirected... -Redirected to # -Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/posts] +The article was saved and now the user is going to be redirected... +Redirected to # Article:0x20af760> +Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/articles] ``` 加入这种日志信息有助于发现异常现象。如果添加了额外的日志消息,记得要合理设定日志等级,免得把大量无用的消息写入生产环境的日志文件。 -### 日志标签 + + +### 为日志打标签 -运行多用户/多账户的程序时,使用自定义的规则筛选日志信息能节省很多时间。Active Support 中的 `TaggedLogging` 模块可以实现这种功能,可以在日志消息中加入二级域名、请求 ID 等有助于调试的信息。 +运行多用户、多账户的应用时,使用自定义的规则筛选日志信息能节省很多时间。Active Support 中的 `TaggedLogging` 模块可以实现这种功能,可以在日志消息中加入二级域名、请求 ID 等有助于调试的信息。 ```ruby logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) @@ -200,9 +222,11 @@ logger.tagged("BCX", "Jason") { logger.info "Stuff" } # Logs " logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff" ``` + + ### 日志对性能的影响 -如果把日志写入硬盘,肯定会对程序有点小的性能影响。不过可以做些小调整:`:debug` 等级比 `:fatal` 等级对性能的影响更大,因为写入的日志消息量更多。 +如果把日志写入磁盘,肯定会对应用有点小的性能影响。不过可以做些小调整:`:debug` 等级比 `:fatal` 等级对性能的影响更大,因为写入的日志消息量更多。 如果按照下面的方式大量调用 `Logger`,也有潜在的问题: @@ -210,393 +234,474 @@ logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs " logger.debug "Person attributes hash: #{@person.attributes.inspect}" ``` -在上述代码中,即使日志等级不包含 `:debug` 也会对性能产生影响。因为 Ruby 要初始化字符串,再花时间做插值。因此推荐把代码块传给 `logger` 方法,只有等于或大于设定的日志等级时才会执行其中的代码。重写后的代码如下: +在上述代码中,即使日志等级不包含 `:debug` 也会对性能产生影响。这是因为 Ruby 要初始化字符串,再花时间做插值。因此建议把代码块传给 `logger` 方法,只有等于或大于设定的日志等级时才执行其中的代码。重写后的代码如下: ```ruby logger.debug {"Person attributes hash: #{@person.attributes.inspect}"} ``` -代码块中的内容,即字符串插值,仅当允许 `:debug` 日志等级时才会执行。这种降低性能的方式只有在日志量比较大时才能体现出来,但却是个好的编程习惯。 +代码块中的内容,即字符串插值,仅当允许 `:debug` 日志等级时才会执行。这种节省性能的方式只有在日志量比较大时才能体现出来,但却是个好的编程习惯。 + + + +## 使用 `byebug` gem 调试 -使用 `debugger` gem 调试 ------------------------ +如果代码表现异常,可以在日志或控制台中诊断问题。但有时使用这种方法效率不高,无法找到导致问题的根源。如果需要检查源码,`byebug` gem 可以助你一臂之力。 -如果代码表现异常,可以在日志文件或者控制台查找原因。但有时使用这种方法效率不高,无法找到导致问题的根源。如果需要检查源码,`debugger` gem 可以助你一臂之力。 +如果想学习 Rails 源码但却无从下手,也可使用 `byebug` gem。随便找个请求,然后按照这里介绍的方法,从你编写的代码一直研究到 Rails 框架的代码。 -如果想学习 Rails 源码但却无从下手,也可使用 `debugger` gem。随便找个请求,然后按照这里介绍的方法,从你编写的代码一直研究到 Rails 框架的代码。 + ### 安装 -`debugger` gem 可以设置断点,实时查看执行的 Rails 代码。安装方法如下: +`byebug` gem 可以设置断点,实时查看执行的 Rails 代码。安装方法如下: -```bash -$ gem install debugger +```sh +$ gem install byebug ``` -从 2.0 版本开始,Rails 内置了调试功能。在任何 Rails 程序中都可以使用 `debugger` 方法调出调试器。 +在任何 Rails 应用中都可以使用 `byebug` 方法呼出调试器。 下面举个例子: ```ruby class PeopleController < ApplicationController def new - debugger + byebug @person = Person.new end end ``` -然后就能在控制台或者日志中看到如下信息: + -``` -***** Debugger requested, but was not available: Start server with --debugger to enable ***** -``` +### Shell -记得启动服务器时要加上 `--debugger` 选项: +在应用中调用 `byebug` 方法后,在启动应用的终端窗口中会启用调试器 shell,并显示调试器的提示符 `(byebug)`。提示符前面显示的是即将执行的代码,当前行以“=>”标记,例如: -```bash -$ rails server --debugger -=> Booting WEBrick -=> Rails 4.2.0 application starting on http://0.0.0.0:3000 -=> Debugger enabled -... ``` +[1, 10] in /PathTo/project/app/controllers/articles_controller.rb + 3: + 4: # GET /articles + 5: # GET /articles.json + 6: def index + 7: byebug +=> 8: @articles = Article.find_recent + 9: + 10: respond_to do |format| + 11: format.html # index.html.erb + 12: format.json { render json: @articles } -TIP: 在开发环境中,如果启动服务器时没有指定 `--debugger` 选项,不用重启服务器,加入 `require "debugger"` 即可。 - -### Shell - -在程序中调用 `debugger` 方法后,会在启动程序所在的终端窗口中启用调试器 shell,并进入调试器的终端 `(rdb:n)` 中。其中 `n` 是线程编号。在调试器的终端中会显示接下来要执行哪行代码。 +(byebug) +``` -如果在浏览器中执行的请求触发了调试器,当前浏览器选项卡会处于停顿状态,等待调试器启动,跟踪完整个请求。 +如果是浏览器中执行的请求到达了那里,当前浏览器标签页会处于挂起状态,等待调试器完工,跟踪完整个请求。 例如: -```bash -@posts = Post.all -(rdb:7) -``` - -现在可以深入分析程序的代码了。首先我们来查看一下调试器的帮助信息,输入 `help`: - -```bash -(rdb:7) help -ruby-debug help v0.10.2 -Type 'help ' for help on a specific command - -Available commands: -backtrace delete enable help next quit show trace -break disable eval info p reload source undisplay -catch display exit irb pp restart step up -condition down finish list ps save thread var -continue edit frame method putl set tmate where -``` - -TIP: 要想查看某个命令的帮助信息,可以在终端里输入 `help `,例如 `help var`。 - -接下来要学习最有用的命令之一:`list`。调试器中的命令可以使用简写形式,只要输入的字母数量足够和其他命令区分即可。因此,可使用 `l` 代替 `list`。 - -`list` 命令输出当前执行代码的前后 5 行代码。下面的例子中,当前行是第 6 行,前面用 `=>` 符号标记。 - -```bash -(rdb:7) list -[1, 10] in /PathTo/project/app/controllers/posts_controller.rb - 1 class PostsController < ApplicationController - 2 # GET /posts - 3 # GET /posts.json - 4 def index - 5 debugger -=> 6 @posts = Post.all - 7 - 8 respond_to do |format| - 9 format.html # index.html.erb - 10 format.json { render json: @posts } -``` - -如果再次执行 `list` 命令,请用 `l` 试试。接下来要执行的 10 行代码会显示出来: - -```bash -(rdb:7) l -[11, 20] in /PathTo/project/app/controllers/posts_controller.rb - 11 end - 12 end - 13 - 14 # GET /posts/1 - 15 # GET /posts/1.json - 16 def show - 17 @post = Post.find(params[:id]) - 18 - 19 respond_to do |format| - 20 format.html # show.html.erb -``` - -可以一直这么执行下去,直到文件的末尾。如果到文件末尾了,`list` 命令会回到该文件的开头,再次从头开始执行一遍,把文件视为一个环形缓冲。 - -如果想查看前面 10 行代码,可以输入 `list-`(或者 `l-`): - -```bash -(rdb:7) l- -[1, 10] in /PathTo/project/app/controllers/posts_controller.rb - 1 class PostsController < ApplicationController - 2 # GET /posts - 3 # GET /posts.json - 4 def index - 5 debugger - 6 @posts = Post.all - 7 - 8 respond_to do |format| - 9 format.html # index.html.erb - 10 format.json { render json: @posts } -``` - -使用 `list` 命令可以在文件中来回移动,查看 `debugger` 方法所在位置前后的代码。如果想知道 `debugger` 方法在文件的什么位置,可以输入 `list=`: - -```bash -(rdb:7) list= -[1, 10] in /PathTo/project/app/controllers/posts_controller.rb - 1 class PostsController < ApplicationController - 2 # GET /posts - 3 # GET /posts.json - 4 def index - 5 debugger -=> 6 @posts = Post.all - 7 - 8 respond_to do |format| - 9 format.html # index.html.erb - 10 format.json { render json: @posts } ``` +=> Booting Puma +=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000 +=> Run `rails server -h` for more startup options +Puma starting in single mode... +* Version 3.4.0 (ruby 2.3.1-p112), codename: Owl Bowl Brawl +* Min threads: 5, max threads: 5 +* Environment: development +* Listening on tcp://localhost:3000 +Use Ctrl-C to stop +Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200 + ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations" +Processing by ArticlesController#index as HTML + +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb + 3: + 4: # GET /articles + 5: # GET /articles.json + 6: def index + 7: byebug +=> 8: @articles = Article.find_recent + 9: + 10: respond_to do |format| + 11: format.html # index.html.erb + 12: format.json { render json: @articles } +(byebug) +``` + +现在可以深入分析应用的代码了。首先我们来查看一下调试器的帮助信息,输入 `help`: + +``` +(byebug) help + + break -- Sets breakpoints in the source code + catch -- Handles exception catchpoints + condition -- Sets conditions on breakpoints + continue -- Runs until program ends, hits a breakpoint or reaches a line + debug -- Spawns a subdebugger + delete -- Deletes breakpoints + disable -- Disables breakpoints or displays + display -- Evaluates expressions every time the debugger stops + down -- Moves to a lower frame in the stack trace + edit -- Edits source files + enable -- Enables breakpoints or displays + finish -- Runs the program until frame returns + frame -- Moves to a frame in the call stack + help -- Helps you using byebug + history -- Shows byebug's history of commands + info -- Shows several informations about the program being debugged + interrupt -- Interrupts the program + irb -- Starts an IRB session + kill -- Sends a signal to the current process + list -- Lists lines of source code + method -- Shows methods of an object, class or module + next -- Runs one or more lines of code + pry -- Starts a Pry session + quit -- Exits byebug + restart -- Restarts the debugged program + save -- Saves current byebug session to a file + set -- Modifies byebug settings + show -- Shows byebug settings + source -- Restores a previously saved byebug session + step -- Steps into blocks or methods one or more times + thread -- Commands to manipulate threads + tracevar -- Enables tracing of a global variable + undisplay -- Stops displaying all or some expressions when program stops + untracevar -- Stops tracing a global variable + up -- Moves to a higher frame in the stack trace + var -- Shows variables and its values + where -- Displays the backtrace + +(byebug) +``` + +如果想查看前面十行代码,输入 `list-`(或 `l-`)。 + +``` +(byebug) l- + +[1, 10] in /PathTo/project/app/controllers/articles_controller.rb + 1 class ArticlesController < ApplicationController + 2 before_action :set_article, only: [:show, :edit, :update, :destroy] + 3 + 4 # GET /articles + 5 # GET /articles.json + 6 def index + 7 byebug + 8 @articles = Article.find_recent + 9 + 10 respond_to do |format| +``` + +这样我们就可以在文件内移动,查看 `byebug` 所在行上面的代码。如果想查看你在哪一行,输入 `list=`: + +``` +(byebug) list= + +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb + 3: + 4: # GET /articles + 5: # GET /articles.json + 6: def index + 7: byebug +=> 8: @articles = Article.find_recent + 9: + 10: respond_to do |format| + 11: format.html # index.html.erb + 12: format.json { render json: @articles } +(byebug) +``` + + ### 上下文 -开始调试程序时,会进入堆栈中不同部分对应的不同上下文。 +开始调试应用时,会进入堆栈中不同部分对应的不同上下文。 -到达一个停止点或者触发某个事件时,调试器就会创建一个上下文。上下文中包含被终止程序的信息,调试器用这些信息审查调用帧,计算变量的值,以及调试器在程序的什么地方终止执行。 +到达一个停止点或者触发某个事件时,调试器就会创建一个上下文。上下文中包含被终止应用的信息,调试器用这些信息审查帧堆栈,计算变量的值,以及调试器在应用的什么地方终止执行。 -任何时候都可执行 `backtrace` 命令(简写形式为 `where`)显示程序的调用堆栈。这有助于理解如何执行到当前位置。只要你想知道程序是怎么执行到当前代码的,就可以通过 `backtrace` 命令获得答案。 +任何时候都可执行 `backtrace` 命令(或别名 `where`)打印应用的回溯信息。这有助于理解是如何执行到当前位置的。只要你想知道应用是怎么执行到当前代码的,就可以通过 `backtrace` 命令获得答案。 -```bash -(rdb:5) where - #0 PostsController.index - at line /PathTo/project/app/controllers/posts_controller.rb:6 - #1 Kernel.send - at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175 - #2 ActionController::Base.perform_action_without_filters - at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175 - #3 ActionController::Filters::InstanceMethods.call_filters(chain#ActionController::Fil...,...) - at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/filters.rb:617 +``` +(byebug) where +--> #0 ArticlesController.index + at /PathToProject/app/controllers/articles_controller.rb:8 + #1 ActionController::BasicImplicitRender.send_action(method#String, *args#Array) + at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/basic_implicit_render.rb:4 + #2 AbstractController::Base.process_action(action#NilClass, *args#Array) + at /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb:181 + #3 ActionController::Rendering.process_action(action, *args) + at /PathToGems/actionpack-5.1.0/lib/action_controller/metal/rendering.rb:30 ... ``` -执行 `frame n` 命令可以进入指定的调用帧,其中 `n` 为帧序号。 +当前帧使用 `-->` 标记。在回溯信息中可以执行 `frame n` 命令移动(从而改变上下文),其中 `n` 为帧序号。如果移动了,`byebug` 会显示新的上下文。 -```bash -(rdb:5) frame 2 -#2 ActionController::Base.perform_action_without_filters - at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175 +``` +(byebug) frame 2 + +[176, 185] in /PathToGems/actionpack-5.1.0/lib/abstract_controller/base.rb + 176: # is the intended way to override action dispatching. + 177: # + 178: # Notice that the first argument is the method to be dispatched + 179: # which is *not* necessarily the same as the action name. + 180: def process_action(method_name, *args) +=> 181: send_action(method_name, *args) + 182: end + 183: + 184: # Actually call the method associated with the action. Override + 185: # this method if you wish to change how action methods are called, +(byebug) ``` 可用的变量和逐行执行代码时一样。毕竟,这就是调试的目的。 -向前或向后移动调用帧可以执行 `up [n]`(简写形式为 `u`)和 `down [n]` 命令,分别向前或向后移动 n 帧。n 的默认值为 1。向前移动是指向更高的帧数移动,向下移动是指向更低的帧数移动。 +向前或向后移动帧可以执行 `up [n]` 或 `down [n]` 命令,分别向前或向后移动 n 帧。n 的默认值为 1。向前移动是指向较高的帧数移动,向下移动是指向较低的帧数移动。 + + ### 线程 -`thread` 命令(缩略形式为 `th`)可以列出所有线程,停止线程,恢复线程,或者在线程之间切换。其选项如下: +`thread` 命令(缩写为 `th`)可以列出所有线程、停止线程、恢复线程,或者在线程之间切换。其选项如下: + +* `thread`:显示当前线程; +* `thread list`:列出所有线程及其状态,`+` 符号表示当前线程; +* `thread stop n`:停止线程 `n`; +* `thread resume n`:恢复线程 `n`; +* `thread switch n`:把当前线程切换到线程 `n`; -* `thread`:显示当前线程; -* `thread list`:列出所有线程及其状态,`+` 符号和数字表示当前线程; -* `thread stop n`:停止线程 `n`; -* `thread resume n`:恢复线程 `n`; -* `thread switch n`:把当前线程切换到线程 `n`; +调试并发线程时,如果想确认代码中没有条件竞争,使用这个命令十分方便。 -`thread` 命令有很多作用。调试并发线程时,如果想确认代码中没有条件竞争,使用这个命令十分方便。 + ### 审查变量 -任何表达式都可在当前上下文中运行。如果想计算表达式的值,直接输入表达式即可。 +任何表达式都可在当前上下文中求值。如果想计算表达式的值,直接输入表达式即可。 -下面这个例子说明如何查看在当前上下文中 `instance_variables` 的值: +下面这个例子说明如何查看当前上下文中实例变量的值: ``` -@posts = Post.all -(rdb:11) instance_variables -["@_response", "@action_name", "@url", "@_session", "@_cookies", "@performed_render", "@_flash", "@template", "@_params", "@before_filter_chain_aborted", "@request_origin", "@_headers", "@performed_redirect", "@_request"] +[3, 12] in /PathTo/project/app/controllers/articles_controller.rb + 3: + 4: # GET /articles + 5: # GET /articles.json + 6: def index + 7: byebug +=> 8: @articles = Article.find_recent + 9: + 10: respond_to do |format| + 11: format.html # index.html.erb + 12: format.json { render json: @articles } + +(byebug) instance_variables +[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context, + :@_action_name, :@_response_body, :@marked_for_same_origin_verification, + :@_config] ``` -你可能已经看出来了,在控制器中可使用的所有实例变量都显示出来了。这个列表随着代码的执行会动态更新。例如,使用 `next` 命令执行下一行代码: +你可能已经看出来了,在控制器中可以使用的实例变量都显示出来了。这个列表随着代码的执行会动态更新。例如,使用 `next` 命令(本文后面会进一步说明这个命令)执行下一行代码: ``` -(rdb:11) next -Processing PostsController#index (for 127.0.0.1 at 2008-09-04 19:51:34) [GET] - Session ID: BAh7BiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7AA==--b16e91b992453a8cc201694d660147bba8b0fd0e - Parameters: {"action"=>"index", "controller"=>"posts"} -/PathToProject/posts_controller.rb:8 -respond_to do |format| +(byebug) next + +[5, 14] in /PathTo/project/app/controllers/articles_controller.rb + 5 # GET /articles.json + 6 def index + 7 byebug + 8 @articles = Article.find_recent + 9 +=> 10 respond_to do |format| + 11 format.html # index.html.erb + 12 format.json { render json: @articles } + 13 end + 14 end + 15 +(byebug) ``` 然后再查看 `instance_variables` 的值: ``` -(rdb:11) instance_variables.include? "@posts" -true +(byebug) instance_variables +[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context, + :@_action_name, :@_response_body, :@marked_for_same_origin_verification, + :@_config, :@articles] ``` -实例变量中出现了 `@posts`,因为执行了定义这个变量的代码。 +实例变量中出现了 `@articles`,因为执行了定义它的代码。 + +TIP: 执行 `irb` 命令可进入 **irb** 模式(这不显然吗),irb 会话使用当前上下文。 -TIP: 执行 `irb` 命令可进入 **irb** 模式,irb 会话使用当前上下文。警告:这是实验性功能。 `var` 命令是显示变量值最便捷的方式: ``` -var -(rdb:1) v[ar] const show constants of object -(rdb:1) v[ar] g[lobal] show global variables -(rdb:1) v[ar] i[nstance] show instance variables of object -(rdb:1) v[ar] l[ocal] show local variables +(byebug) help var + + [v]ar + + Shows variables and its values + + + var all -- Shows local, global and instance variables of self. + var args -- Information about arguments of the current scope + var const -- Shows constants of an object. + var global -- Shows global variables. + var instance -- Shows instance variables of self or a specific object. + var local -- Shows local variables in current scope. ``` -上述方法可以很轻易的查看当前上下文中的变量值。例如: +上述方法可以很轻易查看当前上下文中的变量值。例如,下述代码确认没有局部变量: ``` -(rdb:9) var local - __dbg_verbose_save => false +(byebug) var local +(byebug) ``` -审查对象的方法可以使用下述方式: +审查对象的方法也可以使用这个命令: ``` -(rdb:9) var instance Post.new -@attributes = {"updated_at"=>nil, "body"=>nil, "title"=>nil, "published"=>nil, "created_at"... -@attributes_cache = {} +(byebug) var instance Article.new +@_start_transaction_state = {} +@aggregation_cache = {} +@association_cache = {} +@attributes = ## ### 逐步执行 -现在你知道在运行代码的什么位置,以及如何查看变量的值。下面我们继续执行程序。 +现在你知道在运行代码的什么位置,以及如何查看变量的值了。下面我们继续执行应用。 -`step` 命令(缩写形式为 `s`)可以一直执行程序,直到下一个逻辑停止点,再把控制权交给调试器。 +`step` 命令(缩写为 `s`)可以一直执行应用,直到下一个逻辑停止点,再把控制权交给调试器。`next` 命令的作用和 `step` 命令类似,但是 `step` 命令会在执行下一行代码之前停止,一次只执行一步,而 `next` 命令会执行下一行代码,但不跳出方法。 -TIP: `step+ n` 和 `step- n` 可以相应的向前或向后 `n` 步。 +我们来看看下面这种情形: -`next` 命令的作用和 `step` 命令类似,但执行的方法不会停止。和 `step` 命令一样,也可使用加号前进 `n` 步。 +``` +Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200 +Processing by ArticlesController#index as HTML -`next` 命令和 `step` 命令的区别是,`step` 命令会在执行下一行代码之前停止,一次只执行一步;`next` 命令会执行下一行代码,但不跳出方法。 +[1, 6] in /PathToProject/app/models/article.rb + 1: class Article < ApplicationRecord + 2: def self.find_recent(limit = 10) + 3: byebug +=> 4: where('created_at > ?', 1.week.ago).limit(limit) + 5: end + 6: end -例如,下面这段代码调用了 `debugger` 方法: +(byebug) +``` -```ruby -class Author < ActiveRecord::Base - has_one :editorial - has_many :comments +如果使用 `next`,不会深入方法调用,`byebug` 会进入同一上下文中的下一行。这里,进入的是当前方法的最后一行,因此 `byebug` 会返回调用方的下一行。 - def find_recent_comments(limit = 10) - debugger - @recent_comments ||= comments.where("created_at > ?", 1.week.ago).limit(limit) - end -end ``` +(byebug) next +[4, 13] in /PathToProject/app/controllers/articles_controller.rb + 4: # GET /articles + 5: # GET /articles.json + 6: def index + 7: @articles = Article.find_recent + 8: +=> 9: respond_to do |format| + 10: format.html # index.html.erb + 11: format.json { render json: @articles } + 12: end + 13: end -TIP: 在控制台中也可启用调试器,但要记得在调用 `debugger` 方法之前先 `require "debugger"`。 - -```bash -$ rails console -Loading development environment (Rails 4.2.0) ->> require "debugger" -=> [] ->> author = Author.first -=> # ->> author.find_recent_comments -/PathTo/project/app/models/author.rb:11 -) +(byebug) ``` -停止执行代码时,看一下输出: +如果使用 `step`,`byebug` 会进入要执行的下一个 Ruby 指令——这里是 Active Support 的 `week` 方法。 -```bash -(rdb:1) list -[2, 9] in /PathTo/project/app/models/author.rb - 2 has_one :editorial - 3 has_many :comments - 4 - 5 def find_recent_comments(limit = 10) - 6 debugger -=> 7 @recent_comments ||= comments.where("created_at > ?", 1.week.ago).limit(limit) - 8 end - 9 end ``` +(byebug) step -在方法内的最后一行停止了。但是这行代码执行了吗?你可以审查一下实例变量。 - -```bash -(rdb:1) var instance -@attributes = {"updated_at"=>"2008-07-31 12:46:10", "id"=>"1", "first_name"=>"Bob", "las... -@attributes_cache = {} +[49, 58] in /PathToGems/activesupport-5.1.0/lib/active_support/core_ext/numeric/time.rb + 49: + 50: # Returns a Duration instance matching the number of weeks provided. + 51: # + 52: # 2.weeks # => 14 days + 53: def weeks +=> 54: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) + 55: end + 56: alias :week :weeks + 57: + 58: # Returns a Duration instance matching the number of fortnights provided. +(byebug) ``` -`@recent_comments` 还未定义,所以这行代码还没执行。执行 `next` 命令执行这行代码: +逐行执行代码是找出代码缺陷的最佳方式。 -```bash -(rdb:1) next -/PathTo/project/app/models/author.rb:12 -@recent_comments -(rdb:1) var instance -@attributes = {"updated_at"=>"2008-07-31 12:46:10", "id"=>"1", "first_name"=>"Bob", "las... -@attributes_cache = {} -@comments = [] -@recent_comments = [] -``` +TIP: 还可以使用 `step n` 或 `next n` 一次向前移动 `n` 步。 -现在看以看到,因为执行了这行代码,所以加载了 `@comments` 关联,也定义了 `@recent_comments`。 -如果想深入方法和 Rails 代码执行堆栈,可以使用 `step` 命令,一步一步执行。这是发现代码问题(或者 Rails 框架问题)最好的方式。 + ### 断点 -断点设置在何处终止执行代码。调试器会在断点设定行调用。 +断点设置在何处终止执行代码。调试器会在设定断点的行呼出。 + +断点可以使用 `break` 命令(缩写为 `b`)动态添加。添加断点有三种方式: -断点可以使用 `break` 命令(缩写形式为 `b`)动态添加。设置断点有三种方式: +* `break n`:在当前源码文件的第 `n` 行设定断点。 +* `break file:n [if expression]`:在文件 `file` 的第 `n` 行设定断点。如果指定了表达式 `expression`,其返回结果必须为 `true` 才会启动调试器。 +* `break class(.|#)method [if expression]`:在 `class` 类的 `method` 方法中设置断点,`.` 和 `#` 分别表示类和实例方法。表达式 `expression` 的作用与 `file:n` 中的一样。 -* `break line`:在当前源码文件的第 `line` 行设置断点; -* `break file:line [if expression]`:在文件 `file` 的第 `line` 行设置断点。如果指定了表达式 `expression`,其返回结果必须为 `true` 才会启动调试器; -* `break class(.|\#)method [if expression]`:在 `class` 类的 `method` 方法中设置断点,`.` 和 `\#` 分别表示类和实例方法。表达式 `expression` 的作用和上个命令一样; +例如,在前面的情形下: -```bash -(rdb:5) break 10 -Breakpoint 1 file /PathTo/project/vendor/rails/actionpack/lib/action_controller/filters.rb, line 10 ``` +[4, 13] in /PathToProject/app/controllers/articles_controller.rb + 4: # GET /articles + 5: # GET /articles.json + 6: def index + 7: @articles = Article.find_recent + 8: +=> 9: respond_to do |format| + 10: format.html # index.html.erb + 11: format.json { render json: @articles } + 12: end + 13: end -`info breakpoints n` 或 `info break n` 命令可以列出断点。如果指定了数字 `n`,只会列出对应的断点,否则列出所有断点。 +(byebug) break 11 +Successfully created breakpoint with id 1 +``` -```bash -(rdb:5) info breakpoints +使用 `info breakpoints` 命令可以列出断点。如果指定了数字,只会列出对应的断点,否则列出所有断点。 + +``` +(byebug) info breakpoints Num Enb What - 1 y at filters.rb:10 +1 y at /PathToProject/app/controllers/articles_controller.rb:11 ``` -如果想删除断点,可以执行 `delete n` 命令,删除编号为 `n` 的断点。如果不指定数字 `n`,则删除所有在用的断点。 +如果想删除断点,使用 `delete n` 命令,删除编号为 `n` 的断点。如果不指定数字,则删除所有在用的断点。 -```bash -(rdb:5) delete 1 -(rdb:5) info breakpoints +``` +(byebug) delete 1 +(byebug) info breakpoints No breakpoints. ``` -启用和禁用断点的方法如下: +断点也可以启用或禁用: + +* `enable breakpoints [n [m […​]]]`:在指定的断点列表或者所有断点处停止应用。这是创建断点后的默认状态。 +* `disable breakpoints [n [m […​]]]`:让指定的断点(或全部断点)在应用中不起作用。 -* `enable breakpoints`:允许使用指定的断点列表或者所有断点终止执行程序。这是创建断点后的默认状态。 -* `disable breakpoints`:指定的断点 `breakpoints` 在程序中不起作用。 + ### 捕获异常 @@ -604,81 +709,165 @@ No breakpoints. 执行 `catch` 命令可以列出所有可用的捕获点。 + + ### 恢复执行 -有两种方法可以恢复被调试器终止执行的程序: +有两种方法可以恢复被调试器终止执行的应用: + +* `continue [n]`(或 `c`):从停止的地方恢复执行程序,设置的断点失效。可选的参数 `n` 指定一个行数,设定一个一次性断点,应用执行到这一行时,断点会被删除。 +* `finish [n]`:一直执行,直到指定的堆栈帧返回为止。如果没有指定帧序号,应用会一直执行,直到当前堆栈帧返回为止。当前堆栈帧就是最近刚使用过的帧,如果之前没有移动帧的位置(执行 `up`、`down` 或 `frame` 命令),就是第 0 帧。如果指定了帧序号,则运行到指定的帧返回为止。 -* `continue [line-specification]`(或 `c`):从停止的地方恢复执行程序,设置的断点失效。可选的参数 `line-specification` 指定一个代码行数,设定一个一次性断点,程序执行到这一行时,断点会被删除。 -* `finish [frame-number]`(或 `fin`):一直执行程序,直到指定的堆栈帧结束为止。如果没有指定 `frame-number` 参数,程序会一直执行,直到当前堆栈帧结束为止。当前堆栈帧就是最近刚使用过的帧,如果之前没有移动帧的位置(执行 `up`,`down` 或 `frame` 命令),就是第 0 帧。如果指定了帧数,则运行到指定的帧结束为止。 + ### 编辑 -下面两种方法可以从调试器中使用编辑器打开源码: +下面这个方法可以在调试器中使用编辑器打开源码: + +* `edit [file:n]`:使用环境变量 `EDITOR` 指定的编辑器打开文件 `file`。还可指定行数 `n`。 -* `edit [file:line]`:使用环境变量 `EDITOR` 指定的编辑器打开文件 `file`。还可指定文件的行数(`line`)。 -* `tmate n`(简写形式为 `tm`):在 TextMate 中打开当前文件。如果指定了参数 `n`,则使用第 n 帧。 + ### 退出 -要想退出调试器,请执行 `quit` 命令(缩写形式为 `q`),或者别名 `exit`。 +若想退出调试器,使用 `quit` 命令(缩写为 `q`)。也可以输入 `q!`,跳过 `Really quit? (y/n)` 提示,无条件地退出。 -退出后会终止所有线程,所以服务器也会被停止,因此需要重启。 +退出后会终止所有线程,因此服务器也会停止,需要重启。 + + ### 设置 -`debugger` gem 能自动显示你正在分析的代码,在编辑器中修改代码后,还会重新加载源码。下面是可用的选项: +`byebug` 有几个选项,可用于调整行为: + +``` +(byebug) help set + + set + + Modifies byebug settings + + Boolean values take "on", "off", "true", "false", "1" or "0". If you + don't specify a value, the boolean setting will be enabled. Conversely, + you can use "set no" to disable them. -* `set reload`:修改代码后重新加载; -* `set autolist`:在每个断点处执行 `list` 命令; -* `set listsize n`:设置显示 `n` 行源码; -* `set forcestep`:强制 `next` 和 `step` 命令移到终点后的下一行; + You can see these environment settings with the "show" command. -执行 `help set` 命令可以查看完整说明。执行 `help set subcommand` 可以查看 `subcommand` 的帮助信息。 + List of supported settings: -TIP: 设置可以保存到家目录中的 `.rdebugrc` 文件中。启动调试器时会读取这个文件中的全局设置。 + autosave -- Automatically save command history record on exit + autolist -- Invoke list command on every stop + width -- Number of characters per line in byebug's output + autoirb -- Invoke IRB on every stop + basename -- : information after every stop uses short paths + linetrace -- Enable line execution tracing + autopry -- Invoke Pry on every stop + stack_on_error -- Display stack trace when `eval` raises an exception + fullpath -- Display full file names in backtraces + histfile -- File where cmd history is saved to. Default: ./.byebug_history + listsize -- Set number of source lines to list by default + post_mortem -- Enable/disable post-mortem mode + callstyle -- Set how you want method call parameters to be displayed + histsize -- Maximum number of commands that can be stored in byebug history + savefile -- File where settings are saved to. Default: ~/.byebug_save +``` -下面是 `.rdebugrc` 文件示例: +TIP: 可以把这些设置保存在家目录中的 `.byebugrc` 文件里。启动时,调试器会读取这些全局设置。例如: -```bash -set autolist -set forcestep +``` +set callstyle short set listsize 25 ``` -调试内存泄露 ------------ -Ruby 程序(Rails 或其他)可能会导致内存泄露,泄露可能由 Ruby 代码引起,也可能由 C 代码引起。 + + +## 使用 `web-console` gem 调试 + +Web Console 的作用与 `byebug` 有点类似,不过它在浏览器中运行。在任何页面中都可以在视图或控制器的上下文中请求控制台。控制台在 HTML 内容下面渲染。 + + + +### 控制台 + +在任何控制器动作或视图中,都可以调用 `console` 方法呼出控制台。 + +例如,在一个控制器中: + +```ruby +class PostsController < ApplicationController + def new + console + @post = Post.new + end +end +``` + +或者在一个视图中: + +```erb +<% console %> + +

New Post

+``` + +控制台在视图中渲染。调用 `console` 的位置不用担心,它不会在调用的位置显示,而是显示在 HTML 内容下方。 + +控制台可以执行纯 Ruby 代码,你可以定义并实例化类、创建新模型或审查变量。 + +NOTE: 一个请求只能渲染一个控制台,否则 `web-console` 会在第二个 `console` 调用处抛出异常。 + + + + +### 审查变量 + +可以调用 `instance_variables` 列出当前上下文中的全部实例变量。如果想列出全部局部变量,调用 `local_variables`。 + + + +### 设置 + +* `config.web_console.whitelisted_ips`:授权的 IPv4 或 IPv6 地址和网络列表(默认值:`127.0.0.1/8, ::1`)。 +* `config.web_console.whiny_requests`:禁止渲染控制台时记录一条日志(默认值:`true`)。 + +`web-console` 会在远程服务器中执行 Ruby 代码,因此别在生产环境中使用。 + + + +## 调试内存泄露 + +Ruby 应用(Rails 或其他)可能会导致内存泄露,泄露可能由 Ruby 代码引起,也可能由 C 代码引起。 本节介绍如何使用 Valgrind 等工具查找并修正内存泄露问题。 + + ### Valgrind -[Valgrind](http://valgrind.org/) 这个程序只能在 Linux 系统中使用,用于侦察 C 语言层的内存泄露和条件竞争。 +[Valgrind](http://valgrind.org/) 应用能检测 C 语言层的内存泄露和条件竞争。 + +Valgrind 提供了很多工具,能自动检测很多内存管理和线程问题,也能详细分析程序。例如,如果 C 扩展调用了 `malloc()` 函数,但没调用 `free()` 函数,这部分内存就会一直被占用,直到应用终止执行。 + +关于如何安装以及如何在 Ruby 中使用 Valgrind,请阅读 Evan Weaver 写的 [Valgrind and Ruby](http://blog.evanweaver.com/articles/2008/02/05/valgrind-and-ruby/) 一文。 -Valgrind 提供了很多工具,可用来侦察内存管理和线程问题,也能详细分析程序。例如,如果 C 扩展调用了 `malloc()` 函数,但没调用 `free()` 函数,这部分内存就会一直被占用,直到程序结束。 + -关于如何安装 Valgrind 及在 Ruby 中使用,请阅读 Evan Weaver 编写的 [Valgrind and Ruby](http://blog.evanweaver.com/articles/2008/02/05/valgrind-and-ruby/) 一文。 +## 用于调试的插件 -用于调试的插件 ------------- +有很多 Rails 插件可以帮助你查找问题和调试应用。下面列出一些有用的调试插件: -有很多 Rails 插件可以帮助你查找问题和调试程序。下面列出一些常用的调试插件: +* [Footnotes](https://github.com/josevalim/rails-footnotes):在应用的每个页面底部显示请求信息,并链接到源码(可通过 TextMate 打开); +* [Query Trace](https://github.com/ruckus/active-record-query-trace/tree/master):在日志中写入请求源信息; +* [Query Reviewer](https://github.com/nesquena/query_reviewer):这个 Rails 插件在开发环境中会在每个 `SELECT` 查询前执行 `EXPLAIN` 查询,并在每个页面中添加一个 `div` 元素,显示分析到的查询问题; +* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master):提供了一个邮件程序和一组默认的邮件模板,Rails 应用出现问题后发送邮件通知; +* [Better Errors](https://github.com/charliesome/better_errors):使用全新的页面替换 Rails 默认的错误页面,显示更多的上下文信息,例如源码和变量的值; +* [RailsPanel](https://github.com/dejan/rails_panel):一个 Chrome 扩展,在浏览器的开发者工具中显示 `development.log` 文件的内容,显示的内容包括:数据库查询时间、渲染时间、总时间、参数列表、渲染的视图,等等。 +* [Pry](https://github.com/pry/pry):一个 IRB 替代品,可作为开发者的运行时控制台。 -* [Footnotes](https://github.com/josevalim/rails-footnotes):在程序的每个页面底部显示请求信息,并链接到 TextMate 中的源码; -* [Query Trace](https://github.com/ntalbott/query_trace/tree/master):在日志中写入请求源信息; -* [Query Reviewer](https://github.com/nesquena/query_reviewer):这个 Rails 插件在开发环境中会在每个 `SELECT` 查询前执行 `EXPLAIN` 查询,并在每个页面中添加一个 `div` 元素,显示分析到的查询问题; -* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master):提供了一个邮件发送程序和一组默认的邮件模板,Rails 程序出现问题后发送邮件提醒; -* [Better Errors](https://github.com/charliesome/better_errors):使用全新的页面替换 Rails 默认的错误页面,显示更多的上下文信息,例如源码和变量的值; -* [RailsPanel](https://github.com/dejan/rails_panel):一个 Chrome 插件,在浏览器的开发者工具中显示 `development.log` 文件的内容,显示的内容包括:数据库查询时间,渲染时间,总时间,参数列表,渲染的视图等。 + -参考资源 -------- +## 参考资源 -* [ruby-debug 首页](http://bashdb.sourceforge.net/ruby-debug/home-page.html) -* [debugger 首页](https://github.com/cldwalker/debugger) -* [文章:使用 ruby-debug 调试 Rails 程序](http://www.sitepoint.com/debug-rails-app-ruby-debug/) -* [Ryan Bates 制作的视频“Debugging Ruby (revised)”](http://railscasts.com/episodes/54-debugging-ruby-revised) -* [Ryan Bates 制作的视频“The Stack Trace”](http://railscasts.com/episodes/24-the-stack-trace) -* [Ryan Bates 制作的视频“The Logger”](http://railscasts.com/episodes/56-the-logger) -* [使用 ruby-debug 调试](http://bashdb.sourceforge.net/ruby-debug.html) +* [byebug 首页](https://github.com/deivid-rodriguez/byebug) +* [web-console 首页](https://github.com/rails/web-console) diff --git a/source/zh-CN/development_dependencies_install.md b/source/zh-CN/development_dependencies_install.md index b134c9d..cf92164 100644 --- a/source/zh-CN/development_dependencies_install.md +++ b/source/zh-CN/development_dependencies_install.md @@ -1,239 +1,250 @@ -Development Dependencies Install -================================ +# 安装开发依赖 -This guide covers how to setup an environment for Ruby on Rails core development. +本文说明如何搭建 Ruby on Rails 核心开发环境。 -After reading this guide, you will know: +读完本文后,您将学到: -* How to set up your machine for Rails development -* How to run specific groups of unit tests from the Rails test suite -* How the ActiveRecord portion of the Rails test suite operates +* 如何设置你的设备供 Rails 开发; +* 如何运行 Rails 测试组件中特定的单元测试组; +* Rails 测试组件中的 Active Record 部分是如何运作的。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -The Easy Way ------------- + -The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box). +## 简单方式 -The Hard Way ------------- +搭建开发环境最简单、也是推荐的方式是使用 [Rails 开发虚拟机](https://github.com/rails/rails-dev-box)。 -In case you can't use the Rails development box, see section above, these are the steps to manually build a development box for Ruby on Rails core development. + -### Install Git +## 笨拙方式 -Ruby on Rails uses Git for source code control. The [Git homepage](http://git-scm.com/) has installation instructions. There are a variety of resources on the net that will help you get familiar with Git: +如果你不便使用 Rails 开发虚拟机,参见下述说明。这些步骤说明如何自己动手搭建开发环境,供 Ruby on Rails 核心开发使用。 -* [Try Git course](http://try.github.io/) is an interactive course that will teach you the basics. -* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git -* [Everyday Git](http://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by. -* The [PeepCode screencast](https://peepcode.com/products/git) on Git is easier to follow. -* [GitHub](http://help.github.com) offers links to a variety of Git resources. -* [Pro Git](http://git-scm.com/book) is an entire book about Git with a Creative Commons license. + -### Clone the Ruby on Rails Repository +### 安装 Git -Navigate to the folder where you want the Ruby on Rails source code (it will create its own `rails` subdirectory) and run: +Ruby on Rails 使用 Git 做源码控制。Git 的安装说明参见[官网](http://git-scm.com/)。网上有很多学习 Git 的资源: -```bash -$ git clone git://github.com/rails/rails.git -$ cd rails -``` +* [Try Git](http://try.github.io/) 是个交互式课程,教你基本用法。 +* [官方文档](http://git-scm.com/documentation)十分全面,也有一些 Git 基本用法的视频。 +* [Everyday Git](http://schacon.github.io/git/everyday.html) 教你一些技能,足够日常使用。 +* [GitHub 帮助页面](http://help.github.com/)中有很多 Git 资源的链接。 +* [Pro Git](http://git-scm.com/book) 是一本讲解 Git 的书,基于知识共享许可证发布。 -### Set up and Run the Tests + -The test suite must pass with any submitted code. No matter whether you are writing a new patch, or evaluating someone else's, you need to be able to run the tests. +### 克隆 Ruby on Rails 仓库 -Install first libxml2 and libxslt together with their development files for Nokogiri. In Ubuntu that's +进入你想保存 Ruby on Rails 源码的文件夹,然后执行(会创建 `rails` 子目录): -```bash -$ sudo apt-get install libxml2 libxml2-dev libxslt1-dev +```sh +$ git clone git://github.com/rails/rails.git +$ cd rails ``` -If you are on Fedora or CentOS, you can run + -```bash -$ sudo yum install libxml2 libxml2-devel libxslt libxslt-devel -``` - -If you are running Arch Linux, you're done with: +### 准备工作和运行测试 -```bash -$ sudo pacman -S libxml2 libxslt -``` +提交的代码必须通过测试组件。不管你是编写新的补丁,还是评估别人的代码,都要运行测试。 -On FreeBSD, you just have to run: +首先,安装 `sqlite3` gem 所需的 SQLite3 及其开发文件 。macOS 用户这么做: -```bash -# pkg_add -r libxml2 libxslt +```sh +$ brew install sqlite3 ``` -Alternatively, you can install the `textproc/libxml2` and `textproc/libxslt` -ports. - -If you have any problems with these libraries, you can install them manually by compiling the source code. Just follow the instructions at the [Red Hat/CentOS section of the Nokogiri tutorials](http://nokogiri.org/tutorials/installing_nokogiri.html#red_hat__centos) . +Ubuntu 用户这么做: -Also, SQLite3 and its development files for the `sqlite3-ruby` gem - in Ubuntu you're done with just - -```bash +```sh $ sudo apt-get install sqlite3 libsqlite3-dev ``` -And if you are on Fedora or CentOS, you're done with +Fedora 或 CentOS 用户这么做: -```bash +```sh $ sudo yum install sqlite3 sqlite3-devel ``` -If you are on Arch Linux, you will need to run: +Arch Linux 用户要这么做: -```bash +```sh $ sudo pacman -S sqlite ``` -For FreeBSD users, you're done with: +FreeBSD 用户这么做: -```bash -# pkg_add -r sqlite3 +```sh +# pkg install sqlite3 ``` -Or compile the `databases/sqlite3` port. +或者编译 `databases/sqlite3` port。 -Get a recent version of [Bundler](http://gembundler.com/) +然后安装最新版 [Bundler](http://bundler.io/): -```bash +```sh $ gem install bundler $ gem update bundler ``` -and run: +再执行: -```bash +```sh $ bundle install --without db ``` -This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon. +这个命令会安装除了 MySQL 和 PostgreSQL 的 Ruby 驱动之外的所有依赖。稍后再安装那两个驱动。 -NOTE: If you would like to run the tests that use memcached, you need to ensure that you have it installed and running. +NOTE: 如果想运行使用 memcached 的测试,要安装并运行 memcached。 -You can use [Homebrew](http://brew.sh/) to install memcached on OSX: +在 macOS 中可以使用 [Homebrew](http://brew.sh/) 安装 memcached: -```bash +```sh $ brew install memcached ``` -On Ubuntu you can install it with apt-get: +在 Ubuntu 中可以使用 apt-get 安装 memcached: -```bash +```sh $ sudo apt-get install memcached ``` -Or use yum on Fedora or CentOS: +在 Fedora 或 CentOS 中这么做: -```bash +```sh $ sudo yum install memcached ``` -With the dependencies now installed, you can run the test suite with: +在 Arch Linux 中这么做: + +```sh +$ sudo pacman -S memcached +``` + +在 FreeBSD 中这么做: + +```sh +# pkg install memcached +``` + +或者编译 `databases/memcached` port。 + -```bash +安装好依赖之后,可以执行下述命令运行测试组件: + +```sh $ bundle exec rake test ``` -You can also run tests for a specific component, like Action Pack, by going into its directory and executing the same command: +还可以运行某个组件(如 Action Pack)的测试,方法是进入组件所在的目录,然后执行相同的命令: -```bash +```sh $ cd actionpack $ bundle exec rake test ``` -If you want to run the tests located in a specific directory use the `TEST_DIR` environment variable. For example, this will run the tests in the `railties/test/generators` directory only: +如果想运行某个目录中的测试,使用 `TEST_DIR` 环境变量指定。例如,下述命令只运行 `railties/test/generators` 目录中的测试: -```bash +```sh $ cd railties $ TEST_DIR=generators bundle exec rake test ``` -You can run the tests for a particular file by using: +可以像下面这样运行某个文件中的测试: -```bash +```sh $ cd actionpack $ bundle exec ruby -Itest test/template/form_helper_test.rb ``` -Or, you can run a single test in a particular file: +还可以运行某个文件中的某个测试: -```bash +```sh $ cd actionpack $ bundle exec ruby -Itest path/to/test.rb -n test_name ``` -### Active Record Setup + + +### 为 Railties 做准备 + +有些 Railties 测试依赖 JavaScript 运行时环境,因此要安装 [Node.js](https://nodejs.org/)。 + + + +### 为 Active Record 做准备 + +Active Record 的测试组件运行三次:一次针对 SQLite3,一次针对 MySQL,还有一次针对 PostgreSQL。下面说明如何为这三种数据库搭建环境。 + +WARNING: 编写 Active Record 代码时,必须确保测试至少能在 MySQL、PostgreSQL 和 SQLite3 中通过。如果只使用 MySQL 测试,虽然测试能通过,但是不同适配器之间的差异没有考虑到。 + -The test suite of Active Record attempts to run four times: once for SQLite3, once for each of the two MySQL gems (`mysql` and `mysql2`), and once for PostgreSQL. We are going to see now how to set up the environment for them. + -WARNING: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. +#### 数据库配置 -#### Database Configuration +Active Record 测试组件需要一个配置文件:`activerecord/test/config.yml`。`activerecord/test/config.example.yml` 文件中有些示例。你可以复制里面的内容,然后根据你的环境修改。 -The Active Record test suite requires a custom config file: `activerecord/test/config.yml`. An example is provided in `activerecord/test/config.example.yml` which can be copied and used as needed for your environment. + -#### MySQL and PostgreSQL +#### MySQL 和 PostgreSQL -To be able to run the suite for MySQL and PostgreSQL we need their gems. Install first the servers, their client libraries, and their development files. In Ubuntu just run +为了运行针对 MySQL 和 PostgreSQL 的测试组件,要安装相应的 gem。首先安装服务器、客户端库和开发文件。 -```bash -$ sudo apt-get install mysql-server libmysqlclient15-dev +在 macOS 中可以这么做: + +```sh +$ brew install mysql +$ brew install postgresql +``` + +然后按照 Homebrew 给出的说明做。 + +在 Ubuntu 中只需这么做: + +```sh +$ sudo apt-get install mysql-server libmysqlclient-dev $ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev ``` -On Fedora or CentOS, just run: +在 Fedora 或 CentOS 中只需这么做: -```bash +```sh $ sudo yum install mysql-server mysql-devel $ sudo yum install postgresql-server postgresql-devel ``` -If you are running Arch Linux, MySQL isn't supported anymore so you will need to -use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)): +MySQL 不再支持 Arch Linux,因此你要使用 MariaDB(参见[这个声明](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)): -```bash +```sh $ sudo pacman -S mariadb libmariadbclient mariadb-clients $ sudo pacman -S postgresql postgresql-libs ``` -FreeBSD users will have to run the following: +FreeBSD 用户要这么做: -```bash -# pkg_add -r mysql56-client mysql56-server -# pkg_add -r postgresql92-client postgresql92-server +```sh +# pkg install mysql56-client mysql56-server +# pkg install postgresql94-client postgresql94-server ``` -You can use [Homebrew](http://brew.sh/) to install MySQL and PostgreSQL on OSX: +或者通过 port 安装(在 `databases` 文件夹中)。在安装 MySQL 的过程中如何遇到问题,请查阅 [MySQL 文档](http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html)。 -```bash -$ brew install mysql -$ brew install postgresql -``` -Follow instructions given by [Homebrew](http://brew.sh/) to start these. - -Or install them through ports (they are located under the `databases` folder). -If you run into troubles during the installation of MySQL, please see -[the MySQL documentation](http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html). - -After that, run: +安装好之后,执行下述命令: -```bash +```sh $ rm .bundle/config $ bundle install ``` -First, we need to delete `.bundle/config` because Bundler remembers in that file that we didn't want to install the "db" group (alternatively you can edit the file). +首先,我们要删除 `.bundle/config` 文件,因为 Bundler 记得那个文件中的配置。我们前面配置了,不安装“db”分组(此外也可以修改那个文件)。 -In order to be able to run the test suite against MySQL you need to create a user named `rails` with privileges on the test databases: +为了使用 MySQL 运行测试组件,我们要创建一个名为 `rails` 的用户,并且赋予它操作测试数据库的权限: -```bash +```sh $ mysql -uroot -p mysql> CREATE USER 'rails'@'localhost'; @@ -245,47 +256,99 @@ mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.* to 'rails'@'localhost'; ``` -and create the test databases: +然后创建测试数据库: -```bash +```sh $ cd activerecord $ bundle exec rake db:mysql:build ``` -PostgreSQL's authentication works differently. A simple way to set up the development environment for example is to run with your development account -This is not needed when installed via [Homebrew](http://brew.sh). +PostgreSQL 的身份验证方式有所不同。为了使用开发账户搭建开发环境,在 Linux 或 BSD 中要这么做: -```bash +```sh $ sudo -u postgres createuser --superuser $USER ``` -And for OS X (when installed via [Homebrew](http://brew.sh)) -```bash + +在 macOS 中这么做: + +```sh $ createuser --superuser $USER ``` -and then create the test databases with +然后,执行下述命令创建测试数据库: -```bash +```sh $ cd activerecord $ bundle exec rake db:postgresql:build ``` -It is possible to build databases for both PostgreSQL and MySQL with +可以执行下述命令创建 PostgreSQL 和 MySQL 的测试数据库: -```bash +```sh $ cd activerecord $ bundle exec rake db:create ``` -You can cleanup the databases using +可以使用下述命令清理数据库: -```bash +```sh $ cd activerecord $ bundle exec rake db:drop ``` -NOTE: Using the rake task to create the test databases ensures they have the correct character set and collation. +NOTE: 使用 rake 任务创建测试数据库能保障数据库使用正确的字符集和排序规则。 + + +NOTE: 在 PostgreSQL 9.1.x 及早期版本中激活 HStore 扩展会看到这个提醒(或本地化的提醒):“WARNING: => is deprecated as an operator”。 + + +如果使用其他数据库,默认的连接信息参见 `activerecord/test/config.yml` 或 `activerecord/test/config.example.yml` 文件。如果有必要,可以在你的设备中编辑 `activerecord/test/config.yml` 文件,提供不同的凭据。不过显然,不应该把这种改动推送回 Rails 仓库。 + + + +### 为 Action Cable 做准备 + +Action Cable 默认使用 Redis 作为订阅适配器([详情](action_cable_overview.html#broadcasting)),因此为了运行 Action Cable 的测试,要安装并运行 Redis。 + + + +#### 从源码安装 Redis + +Redis 的文档不建议通过包管理器安装,因为那里的包往往是过时的。[Redis 的文档](http://redis.io/download#installation)详细说明了如何从源码安装,以及如何运行 Redis 服务器。 + + + +#### 使用包管理器安装 + +在 macOS 中可以执行下述命令: + +```sh +$ brew install redis +``` + +然后按照 Homebrew 给出的说明做。 + +在 Ubuntu 中只需运行: + +```sh +$ sudo apt-get install redis-server +``` + +在 Fedora 或 CentOS(要启用 EPEL)中运行: + +```sh +$ sudo yum install redis +``` + +如果使用 Arch Linux,运行: + +```sh +$ sudo pacman -S redis +$ sudo systemctl start redis +``` -NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator". +FreeBSD 用户要运行下述命令: -If you're using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails. +```sh +# portmaster databases/redis +``` diff --git a/source/zh-CN/documents.yaml b/source/zh-CN/documents.yaml index 82e248e..1476d06 100644 --- a/source/zh-CN/documents.yaml +++ b/source/zh-CN/documents.yaml @@ -1,188 +1,231 @@ - - name: Start Here + name: 新手入门 documents: - - name: Getting Started with Rails + name: Rails 入门 url: getting_started.html - description: Everything you need to know to install Rails and create your first application. + description: 从安装到建立第一个应用程序所需知道的一切。 - - name: Models + name: 模型 documents: - - name: Active Record Basics + name: Active Record 基础 url: active_record_basics.html - description: This guide will get you started with models, persistence to database and the Active Record pattern and library. + description: 本篇介绍 Models、数据库持久性以及 Active Record 模式。 - - name: Active Record Migrations + name: Active Record 迁移 url: active_record_migrations.html - description: This guide covers how you can use Active Record migrations to alter your database in a structured and organized manner. + description: 本篇介绍如何有条有理地使用 Active Record 来修改数据库。 - - name: Active Record Validations + name: Active Record 数据验证 url: active_record_validations.html - description: This guide covers how you can use Active Record validations + description: 本篇介绍如何使用 Active Record 验证功能。 - - name: Active Record Callbacks + name: Active Record 回调 url: active_record_callbacks.html - description: This guide covers how you can use Active Record callbacks. + description: 本篇介绍如何使用 Active Record 回调功能。 - - name: Active Record Associations + name: Active Record 关联 url: association_basics.html - description: This guide covers all the associations provided by Active Record. + description: 本篇介绍如何使用 Active Record 的关联功能。 - - name: Active Record Query Interface + name: Active Record 查询接口 url: active_record_querying.html - description: This guide covers the database query interface provided by Active Record. + description: 本篇介绍如何使用 Active Record 的数据库查询功能。 + - + name: Active Model 基础 + url: active_model_basics.html + description: 本篇介绍如何使用 Active Model。 + work_in_progress: true - - name: Views + name: 视图 documents: - - name: Action View Overview + name: Action View 概述 url: action_view_overview.html - description: This guide provides an introduction to Action View and introduces a few of the more common view helpers. + description: 本篇介绍 Action View 和常用辅助方法。 work_in_progress: true - - name: Layouts and Rendering in Rails + name: Rails 布局和视图渲染 url: layouts_and_rendering.html - description: This guide covers the basic layout features of Action Controller and Action View, including rendering and redirecting, using content_for blocks, and working with partials. + description: 本篇介绍 Action Controller 与 Action View 基本的版型功能,包含了渲染、重定向、使用 content_for 区块、以及局部模版。 - - name: Action View Form Helpers + name: Action View 表单辅助方法 url: form_helpers.html - description: Guide to using built-in Form helpers. + description: 本篇介绍 Action View 的表单辅助方法。 - - name: Controllers + name: 控制器 documents: - - name: Action Controller Overview + name: Action Controller 概览 url: action_controller_overview.html - description: This guide covers how controllers work and how they fit into the request cycle in your application. It includes sessions, filters, and cookies, data streaming, and dealing with exceptions raised by a request, among other topics. + description: 本篇介绍 Controller 的工作原理,Controller 在请求周期所扮演的角色。内容包含 Session、滤动器、Cookies、资料串流以及如何处理由请求所发起的异常。 - - name: Rails Routing from the Outside In + name: Rails 路由全解 url: routing.html - description: This guide covers the user-facing features of Rails routing. If you want to understand how to use routing in your own Rails applications, start here. + description: 本篇介绍与使用者息息相关的路由功能。想了解如何使用 Rails 的路由,从这里开始。 - - name: Digging Deeper + name: 深入探索 documents: - - name: Active Support Core Extensions + name: Active Support 核心扩展 url: active_support_core_extensions.html - description: This guide documents the Ruby core extensions defined in Active Support. + description: 本篇介绍由 Active Support 定义的核心扩展功能。 - - name: Rails Internationalization API + name: Rails 国际化 API url: i18n.html - description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on. + description: 本篇介绍如何国际化应用程序。将应用程序翻译成多种语言、更改单复数规则、对不同的国家使用正确的日期格式等。 - - name: Action Mailer Basics + name: Action Mailer 基础 url: action_mailer_basics.html - description: This guide describes how to use Action Mailer to send and receive emails. + description: 本篇介绍如何使用 Action Mailer 来收发信件。 + - + name: Active Job 基础 + url: active_job_basics.html + description: 本篇提供创建背景任务、任务排程以及执行任务的所有知识。 - - name: Testing Rails Applications + name: Rails 应用测试指南 url: testing.html - work_in_progress: true - description: This is a rather comprehensive guide to doing both unit and functional tests in Rails. It covers everything from 'What is a test?' to the testing APIs. Enjoy. + description: 这是 Rails 中测试设施的综合指南。它涵盖了从“什么是测试?”到集成测试的知识。 - - name: Securing Rails Applications + name: Ruby on Rails 安全指南 url: security.html - description: This guide describes common security problems in web applications and how to avoid them with Rails. + description: 本篇介绍网路应用程序常见的安全问题,如何在 Rails 里避免这些问题。 - - name: Debugging Rails Applications + name: 调试 Rails 应用 url: debugging_rails_applications.html - description: This guide describes how to debug Rails applications. It covers the different ways of achieving this and how to understand what is happening "behind the scenes" of your code. + description: 本篇介绍如何给 Rails 应用程式除错。包含了多种除错技巧、如何理解与了解代码背后究竟发生了什么事。 - - name: Configuring Rails Applications + name: 配置 Rails 应用 url: configuring.html - description: This guide covers the basic configuration settings for a Rails application. + description: 本篇介绍 Rails 应用程序的基本配置选项。 - - name: Rails Command Line Tools and Rake Tasks + name: Rails 命令行 url: command_line.html - description: This guide covers the command line tools and rake tasks provided by Rails. + description: 本篇介绍 Rails 提供的命令行工具。 - name: Asset Pipeline url: asset_pipeline.html - description: This guide documents the asset pipeline. + description: 本篇介绍 Asset Pipeline. - - name: Working with JavaScript in Rails + name: 在 Rails 中使用 JavaScript url: working_with_javascript_in_rails.html - description: This guide covers the built-in Ajax/JavaScript functionality of Rails. + description: 本篇介绍 Rails 内置的 Ajax 与 JavaScript 功能。 - - name: Getting Started with Engines - url: engines.html - description: This guide explains how to write a mountable engine. + name: Rails 初始化过程 work_in_progress: true + url: initialization.html + description: 本篇介绍 Rails 内部初始化过程。 + - + name: 自动加载和重新加载常量 + url: autoloading_and_reloading_constants.html + description: 本篇介绍自动加载和重新加载常量是如何工作的。 - - name: The Rails Initialization Process + name: Rails 缓存概览 + url: caching_with_rails.html + description: 本篇介绍如何通过缓存给 Rails 应用提速。 + - + name: Active Support 监测程序 work_in_progress: true - url: initialization.html - description: This guide explains the internals of the Rails initialization process as of Rails 4 + url: active_support_instrumentation.html + description: 本篇介绍如何通过 Active Support 监测 API 观察 Rails 和其他 Ruby 代码的事件。 + - + name: Rails 应用分析指南 + work_in_progress: true + url: profiling.html + description: 本篇介绍如何分析 Rails 应用并提高性能。 + - + name: 使用 Rails 开发只提供 API 的应用 + url: api_app.html + description: 本篇介绍如何将 Rails 用于只提供 API 的应用。 + - + name: Action Cable 概览 + url: action_cable_overview.html + description: 本篇介绍 Action Cable 如何工作,以及如何使用 WebSockets 创建实时功能。 + - - name: Extending Rails + name: 扩展 Rails documents: - - name: The Basics of Creating Rails Plugins + name: Rails 插件开发简介 work_in_progress: true url: plugins.html - description: This guide covers how to build a plugin to extend the functionality of Rails. + description: 本篇介绍如何开发插件扩展 Rails 的功能。 - name: Rails on Rack url: rails_on_rack.html - description: This guide covers Rails integration with Rack and interfacing with other Rack components. + description: 本篇介绍 Rails 和 Rack 的集成以及和其他 Rack 组件的交互。 - - name: Creating and Customizing Rails Generators + name: 创建及定制 Rails 生成器和模板 url: generators.html - description: This guide covers the process of adding a brand new generator to your extension or providing an alternative to an element of a built-in Rails generator (such as providing alternative test stubs for the scaffold generator). + description: 本篇介绍如何添加新的生成器,或者为 Rails 内置生成器提供替代选项(例如替换 scaffold 生成器的测试组件)。 + - + name: 引擎入门 + url: engines.html + description: 本篇介绍如何编写可挂载的引擎。 + work_in_progress: true - - name: Contributing to Ruby on Rails + name: 为 Ruby on Rails 做贡献 documents: - - name: Contributing to Ruby on Rails + name: 为 Ruby on Rails 做贡献 url: contributing_to_ruby_on_rails.html - description: Rails is not 'somebody else's framework.' This guide covers a variety of ways that you can get involved in the ongoing development of Rails. + description: Rails 不是“别人的框架”。本篇提供几条贡献 Rails 开发的路线。 - - name: API Documentation Guidelines + name: API 文档指导方针 url: api_documentation_guidelines.html - description: This guide documents the Ruby on Rails API documentation guidelines. + description: 本篇介绍 Ruby on Rails API 文档守则。 - - name: Ruby on Rails Guides Guidelines + name: Ruby on Rails 指南指导方针 url: ruby_on_rails_guides_guidelines.html - description: This guide documents the Ruby on Rails guides guidelines. + description: 本篇介绍 Ruby on Rails 指南守则。 - - name: Maintenance Policy + name: 维护方针 documents: - - name: Maintenance Policy + name: Ruby on Rails 的维护方针 url: maintenance_policy.html - description: What versions of Ruby on Rails are currently supported, and when to expect new versions. + description: Ruby on Rails 当前支持版本,和什么时候发布新版本。 - - name: Release Notes + name: 发布记 documents: - - name: Upgrading Ruby on Rails + name: Ruby on Rails 升级指南 url: upgrading_ruby_on_rails.html - description: This guide helps in upgrading applications to latest Ruby on Rails versions. + description: 本篇帮助升级到 Ruby on Rails 最新版。 + - + name: Ruby on Rails 5.0 发布记 + url: 5_0_release_notes.html + description: Rails 5.0 的发布说明。 + - + name: Ruby on Rails 4.2 发布记 + url: 4_2_release_notes.html + description: Rails 4.2 的发布说明。 - - name: Ruby on Rails 4.1 Release Notes + name: Ruby on Rails 4.1 发布记 url: 4_1_release_notes.html - description: Release notes for Rails 4.1. + description: Rails 4.1 的发布说明。 - - name: Ruby on Rails 4.0 Release Notes + name: Ruby on Rails 4.0 发布记 url: 4_0_release_notes.html - description: Release notes for Rails 4.0. + description: Rails 4.0 的发布说明 - - name: Ruby on Rails 3.2 Release Notes + name: Ruby on Rails 3.2 发布记 url: 3_2_release_notes.html - description: Release notes for Rails 3.2. + description: Rails 3.2 的发布说明 - - name: Ruby on Rails 3.1 Release Notes + name: Ruby on Rails 3.1 发布记 url: 3_1_release_notes.html - description: Release notes for Rails 3.1. + description: Rails 3.1 的发布说明 - - name: Ruby on Rails 3.0 Release Notes + name: Ruby on Rails 3.0 发布记 url: 3_0_release_notes.html - description: Release notes for Rails 3.0. + description: Rails 3.0 的发布说明 - - name: Ruby on Rails 2.3 Release Notes + name: Ruby on Rails 2.3 发布记 url: 2_3_release_notes.html - description: Release notes for Rails 2.3. + description: Rails 2.3 的发布说明 - - name: Ruby on Rails 2.2 Release Notes + name: Ruby on Rails 2.2 发布记 url: 2_2_release_notes.html - description: Release notes for Rails 2.2. + description: Rails 2.2 的发布说明 diff --git a/source/zh-CN/engines.md b/source/zh-CN/engines.md index a5f8ee2..d242f33 100644 --- a/source/zh-CN/engines.md +++ b/source/zh-CN/engines.md @@ -1,104 +1,68 @@ -Getting Started with Engines -============================ +# 引擎入门 -In this guide you will learn about engines and how they can be used to provide -additional functionality to their host applications through a clean and very -easy-to-use interface. +本文介绍引擎及其用法,即如何通过引擎这个干净、易用的接口,为宿主应用提供附加功能。 -After reading this guide, you will know: +读完本文后,您将学到: -* What makes an engine. -* How to generate an engine. -* Building features for the engine. -* Hooking the engine into an application. -* Overriding engine functionality in the application. +* 引擎由什么组成; +* 如何生成引擎; +* 如何为引擎创建特性; +* 如何把引擎挂载到应用中; +* 如何在应用中覆盖引擎的功能; +* 通过加载和配置钩子避免加载 Rails 组件。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -What are engines? ------------------ +NOTE: 本文原文尚未完工! -Engines can be considered miniature applications that provide functionality to -their host applications. A Rails application is actually just a "supercharged" -engine, with the `Rails::Application` class inheriting a lot of its behavior -from `Rails::Engine`. + -Therefore, engines and applications can be thought of almost the same thing, -just with subtle differences, as you'll see throughout this guide. Engines and -applications also share a common structure. +## 引擎是什么 -Engines are also closely related to plugins. The two share a common `lib` -directory structure, and are both generated using the `rails plugin new` -generator. The difference is that an engine is considered a "full plugin" by -Rails (as indicated by the `--full` option that's passed to the generator -command). We'll actually be using the `--mountable` option here, which includes -all the features of `--full`, and then some. This guide will refer to these -"full plugins" simply as "engines" throughout. An engine **can** be a plugin, -and a plugin **can** be an engine. +引擎可以看作为宿主应用提供附加功能的微型应用。实际上,Rails 应用只不过是“加强版”的引擎,`Rails::Application` 类从 `Rails::Engine` 类继承了大量行为。 -The engine that will be created in this guide will be called "blorgh". This -engine will provide blogging functionality to its host applications, allowing -for new articles and comments to be created. At the beginning of this guide, you -will be working solely within the engine itself, but in later sections you'll -see how to hook it into an application. +因此,引擎和应用基本上可以看作同一个事物,通过本文的介绍,我们会看到两者之间只有细微差异。引擎和应用还具有相同的结构。 -Engines can also be isolated from their host applications. This means that an -application is able to have a path provided by a routing helper such as -`articles_path` and use an engine also that provides a path also called -`articles_path`, and the two would not clash. Along with this, controllers, models -and table names are also namespaced. You'll see how to do this later in this -guide. +引擎还和插件密切相关。两者具有相同的 `lib` 目录结构,并且都使用 `rails plugin new` 生成器来生成。区别在于,引擎被 Rails 视为“完整的插件”(通过传递给生成器的 `--full` 选项可以看出这一点)。在这里我们实际使用的是 `--mountable` 选项,这个选项包含了 `--full` 选项的所有特性。本文把这类“完整的插件”简称为“引擎”。也就是说,引擎可以是插件,插件也可以是引擎。 -It's important to keep in mind at all times that the application should -**always** take precedence over its engines. An application is the object that -has final say in what goes on in its environment. The engine should -only be enhancing it, rather than changing it drastically. +本文将创建名为“blorgh”的引擎,用于为宿主应用提供博客功能,即新建文章和评论的功能。在本文的开头部分,我们将看到引擎的内部工作原理,在之后的部分中,我们将看到如何把引擎挂载到应用中。 -To see demonstrations of other engines, check out -[Devise](https://github.com/plataformatec/devise), an engine that provides -authentication for its parent applications, or -[Forem](https://github.com/radar/forem), an engine that provides forum -functionality. There's also [Spree](https://github.com/spree/spree) which -provides an e-commerce platform, and -[RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. +我们还可以把引擎和宿主应用隔离开来。也就是说,应用和引擎可以使用同名的 `articles_path` 路由辅助方法而不会发生冲突。除此之外,应用和引擎的控制器、模型和表名也具有不同的命名空间。后文将介绍这些特性是如何实现的。 -Finally, engines would not have been possible without the work of James Adam, -Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever -meet them, don't forget to say thanks! +一定要记住,在任何时候,应用的优先级都应该比引擎高。应用对其环境中发生的事情拥有最终的决定权。引擎用于增强应用的功能,而不是彻底改变应用的功能。 -Generating an engine --------------------- +引擎的例子有 [Devise](https://github.com/plataformatec/devise)(提供身份验证)、[Thredded](https://github.com/thredded/thredded)(提供论坛功能)、[Spree](https://github.com/spree/spree)(提供电子商务平台) 和 [RefineryCMS](https://github.com/refinery/refinerycms)(CMS 引擎)。 -To generate an engine, you will need to run the plugin generator and pass it -options as appropriate to the need. For the "blorgh" example, you will need to -create a "mountable" engine, running this command in a terminal: +最后,如果没有 James Adam、Piotr Sarnacki、Rails 核心开发团队和其他许多人的努力,引擎就不可能实现。如果遇见他们,请不要忘记说声谢谢! -```bash -$ bin/rails plugin new blorgh --mountable + + +## 生成引擎 + +通过运行插件生成器并传递必要的选项就可以生成引擎。在 Blorgh 引擎的例子中,我们需要创建“可挂载”的引擎,为此可以在终端中运行下面的命令: + +```sh +$ rails plugin new blorgh --mountable ``` -The full list of options for the plugin generator may be seen by typing: +通过下面的命令可以查看插件生成器选项的完整列表: -```bash -$ bin/rails plugin --help +```sh +$ rails plugin --help ``` -The `--mountable` option tells the generator that you want to create a -"mountable" and namespace-isolated engine. This generator will provide the same -skeleton structure as would the `--full` option. The `--full` option tells the -generator that you want to create an engine, including a skeleton structure -that provides the following: +通过 `--mountable` 选项,生成器会创建“可挂载”和具有独立命名空间的引擎。此选项和 `--full` 选项会为引擎生成相同的程序骨架。通过 `--full` 选项,生成器会在创建引擎的同时生成下面的程序骨架: - * An `app` directory tree - * A `config/routes.rb` file: +* `app` 目录树 +* `config/routes.rb` 文件: ```ruby Rails.application.routes.draw do end ``` - * A file at `lib/blorgh/engine.rb`, which is identical in function to a - standard Rails application's `config/application.rb` file: + +* `lib/blorgh/engine.rb` 文件,相当于 Rails 应用的 `config/application.rb` 配置文件: ```ruby module Blorgh @@ -107,20 +71,23 @@ that provides the following: end ``` -The `--mountable` option will add to the `--full` option: - * Asset manifest files (`application.js` and `application.css`) - * A namespaced `ApplicationController` stub - * A namespaced `ApplicationHelper` stub - * A layout view template for the engine - * Namespace isolation to `config/routes.rb`: + +`--mountable` 选项在 `--full` 选项的基础上增加了如下特性: + +* 静态资源文件的清单文件(`application.js` 和 `application.css`) +* 具有独立命名空间的 `ApplicationController` +* 具有独立命名空间的 `ApplicationHelper` +* 引擎的布局视图模板 +* 在 `config/routes.rb` 文件中为引擎设置独立的命名空间: ```ruby Blorgh::Engine.routes.draw do end ``` - * Namespace isolation to `lib/blorgh/engine.rb`: + +* 在 `lib/blorgh/engine.rb` 文件中为引擎设置独立的命名空间: ```ruby module Blorgh @@ -130,32 +97,29 @@ The `--mountable` option will add to the `--full` option: end ``` -Additionally, the `--mountable` option tells the generator to mount the engine -inside the dummy testing application located at `test/dummy` by adding the -following to the dummy application's routes file at -`test/dummy/config/routes.rb`: + + +此外,通过 `--mountable` 选项,生成器会在位于 `test/dummy` 的 dummy 测试应用中挂载 blorgh 引擎,具体做法是把下面这行代码添加到 dummy 应用的路由文件 `test/dummy/config/routes.rb` 中: ```ruby -mount Blorgh::Engine, at: "blorgh" +mount Blorgh::Engine => "/blorgh" ``` -### Inside an Engine + + +### 深入引擎内部 -#### Critical Files + -At the root of this brand new engine's directory lives a `blorgh.gemspec` file. -When you include the engine into an application later on, you will do so with -this line in the Rails application's `Gemfile`: +#### 关键文件 + +在新建引擎的文件夹中有一个 `blorgh.gemspec` 文件。通过在 Rails 应用的 Gemfile 文件中添加下面的代码,可以把引擎挂载到应用中: ```ruby -gem 'blorgh', path: "vendor/engines/blorgh" +gem 'blorgh', path: 'engines/blorgh' ``` -Don't forget to run `bundle install` as usual. By specifying it as a gem within -the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file -and requiring a file within the `lib` directory called `lib/blorgh.rb`. This -file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) -and defines a base module called `Blorgh`. +和往常一样,别忘了运行 `bundle install` 命令。通过在 Gemfile 中添加 `blorgh` gem,Bundler 将加载此 gem,解析其中的 `blorgh.gemspec` 文件,并加载 `lib/blorgh.rb` 文件。`lib/blorgh.rb` 文件会加载 `lib/blorgh/engine.rb` 文件,其中定义了 `Blorgh` 基础模块。 ```ruby require "blorgh/engine" @@ -164,109 +128,77 @@ module Blorgh end ``` -TIP: Some engines choose to use this file to put global configuration options -for their engine. It's a relatively good idea, so if you want to offer -configuration options, the file where your engine's `module` is defined is -perfect for that. Place the methods inside the module and you'll be good to go. +TIP: 有些引擎会通过 `lib/blorgh/engine.rb` 文件提供全局配置选项。相对而言这是个不错的主意,因此我们可以优先选择在定义引擎模块的 `lib/blorgh/engine.rb` 文件中定义全局配置选项,也就是在引擎模块中定义相关方法。 -Within `lib/blorgh/engine.rb` is the base class for the engine: +在 `lib/blorgh/engine.rb` 文件中定义引擎的基类: ```ruby module Blorgh - class Engine < Rails::Engine + class Engine < ::Rails::Engine isolate_namespace Blorgh end end ``` -By inheriting from the `Rails::Engine` class, this gem notifies Rails that -there's an engine at the specified path, and will correctly mount the engine -inside the application, performing tasks such as adding the `app` directory of -the engine to the load path for models, mailers, controllers and views. - -The `isolate_namespace` method here deserves special notice. This call is -responsible for isolating the controllers, models, routes and other things into -their own namespace, away from similar components inside the application. -Without this, there is a possibility that the engine's components could "leak" -into the application, causing unwanted disruption, or that important engine -components could be overridden by similarly named things within the application. -One of the examples of such conflicts is helpers. Without calling -`isolate_namespace`, the engine's helpers would be included in an application's -controllers. - -NOTE: It is **highly** recommended that the `isolate_namespace` line be left -within the `Engine` class definition. Without it, classes generated in an engine -**may** conflict with an application. - -What this isolation of the namespace means is that a model generated by a call -to `bin/rails g model`, such as `bin/rails g model article`, won't be called `Article`, but -instead be namespaced and called `Blorgh::Article`. In addition, the table for the -model is namespaced, becoming `blorgh_articles`, rather than simply `articles`. -Similar to the model namespacing, a controller called `ArticlesController` becomes -`Blorgh::ArticlesController` and the views for that controller will not be at -`app/views/articles`, but `app/views/blorgh/articles` instead. Mailers are namespaced -as well. - -Finally, routes will also be isolated within the engine. This is one of the most -important parts about namespacing, and is discussed later in the -[Routes](#routes) section of this guide. - -#### `app` Directory - -Inside the `app` directory are the standard `assets`, `controllers`, `helpers`, -`mailers`, `models` and `views` directories that you should be familiar with -from an application. The `helpers`, `mailers` and `models` directories are -empty, so they aren't described in this section. We'll look more into models in -a future section, when we're writing the engine. - -Within the `app/assets` directory, there are the `images`, `javascripts` and -`stylesheets` directories which, again, you should be familiar with due to their -similarity to an application. One difference here, however, is that each -directory contains a sub-directory with the engine name. Because this engine is -going to be namespaced, its assets should be too. - -Within the `app/controllers` directory there is a `blorgh` directory that -contains a file called `application_controller.rb`. This file will provide any -common functionality for the controllers of the engine. The `blorgh` directory -is where the other controllers for the engine will go. By placing them within -this namespaced directory, you prevent them from possibly clashing with -identically-named controllers within other engines or even within the -application. - -NOTE: The `ApplicationController` class inside an engine is named just like a -Rails application in order to make it easier for you to convert your -applications into engines. - -Lastly, the `app/views` directory contains a `layouts` folder, which contains a -file at `blorgh/application.html.erb`. This file allows you to specify a layout -for the engine. If this engine is to be used as a stand-alone engine, then you -would add any customization to its layout in this file, rather than the -application's `app/views/layouts/application.html.erb` file. - -If you don't want to force a layout on to users of the engine, then you can -delete this file and reference a different layout in the controllers of your -engine. - -#### `bin` Directory - -This directory contains one file, `bin/rails`, which enables you to use the -`rails` sub-commands and generators just like you would within an application. -This means that you will be able to generate new controllers and models for this -engine very easily by running commands like this: - -```bash +通过继承 `Rails::Engine` 类,`blorgh` gem 告知 Rails 在指定路径上有一个引擎,Rails 会把该引擎正确挂载到应用中,并执行相关任务,例如把 `app` 文件夹添加到模型、邮件程序、控制器和视图的加载路径中。 + +这里的 `isolate_namespace` 方法尤其需要注意。通过调用此方法,可以把引擎的控制器、模型、路由和其他组件隔离到各自的命名空间中,以便和应用中的类似组件隔离开来。要是没有这个方法,引擎的组件就可能“泄漏”到应用中,从而引起意外的混乱,引擎的重要组件也可能被应用中的同名组件覆盖。这类冲突的一个例子是辅助方法。在未调用 `isolate_namespace` 方法的情况下,引擎的辅助方法会被包含到应用的控制器中。 + +NOTE: 强烈建议在 `Engine` 类的定义中调用 `isolate_namespace` 方法。在未调用此方法的情况下,引擎中生成的类有可能和应用发生冲突。 + +命名空间隔离的意思是,通过 `bin/rails g model` 生成的模型,例如 `bin/rails g model article`,不会被命名为 `Article`,而会被命名为带有命名空间的 `Blorgh::Article`。此外,模型的表名同样带有命名空间,也就是说表名不是 `articles`,而是 `blorgh_articles`。和模型的命名规则类似,控制器不会被命名为 `ArticlesController`,而会被命名为 `Blorgh::ArticlesController`,控制器对应的视图不是 `app/views/articles`,而是 `app/views/blorgh/articles`。邮件程序的情况类似。 + +最后,路由也会被隔离在引擎中。这是命名空间最重要的内容之一,稍后将在 [路由](#engines-routes)介绍。 + + + +#### `app` 文件夹 + +和应用类似,引擎的 `app` 文件夹中包含了标准的 `assets`、`controllers`、`helpers`、`mailers`、`models` 和 `views` 文件夹。其中 `helpers`、`mailers` 和 `models` 是空文件夹,因此本节不作介绍。后文介绍引擎编写时,会详细介绍 `models` 文件夹。 + +同样,和应用类似,引擎的 `app/assets` 文件夹中包含了 `images`、`javascripts` 和 `stylesheets` 文件夹。不过两者有一个区别,引擎的这三个文件夹中还包含了和引擎同名的文件夹。因为引擎位于命名空间中,所以引擎的静态资源文件也位于命名空间中。 + +`app/controllers` 文件夹中包含 `blorgh` 文件夹,其中包含 `application_controller.rb` 文件。此文件中包含了引擎控制器的通用功能。其他控制器文件也应该放在 `blorgh` 文件夹中。通过把引擎的控制器文件放在 `blorgh` 文件夹(作为控制器的命名空间)中,就可以避免和其他引擎甚至应用中的同名控制器发生冲突。 + +NOTE: 引擎的 `ApplicationController` 类采用了和 Rails 应用相同的命名规则,这样便于把应用转换为引擎。 + +NOTE: 鉴于 Ruby 进行常量查找的方式,我们可能会遇到引擎的控制器继承自应用的 `ApplicationController`,而不是继承自引擎的 `ApplicationController` 的情况。此时 Ruby 能够解析 `ApplicationController`,因此不会触发自动加载机制。关于这个问题的更多介绍,请参阅 [常量未缺失](autoloading_and_reloading_constants.html#when-constants-aren-t-missed)。避免出现这种情况的最好办法是使用 `require_dependency` 方法,以确保加载的是引擎的 `ApplicationController`。例如: + +```ruby +# app/controllers/blorgh/articles_controller.rb: +require_dependency "blorgh/application_controller" + +module Blorgh + class ArticlesController < ApplicationController + ... + end +end +``` + + +WARNING: 不要使用 `require` 方法,否则会破坏开发环境中类的自动重新加载——使用 `require_dependency` 方法才能确保以正确的方式加载和卸载类。 + +最后,`app/views` 文件夹中包含 `layouts` 文件夹,其中包含 `blorgh/application.html.erb` 文件。此文件用于为引擎指定布局。如果此引擎要作为独立引擎使用,那么应该在此文件而不是 `app/views/layouts/application.html.erb` 文件中自定义引擎布局。 + +如果不想强制用户使用引擎布局,那么可以删除此文件,并在引擎控制器中引用不同的布局。 + + + +#### `bin` 文件夹 + +引擎的 `bin` 文件夹中包含 `bin/rails` 文件。和应用类似,此文件提供了对 `rails` 子命令和生成器的支持。也就是说,我们可以像下面这样通过命令生成引擎的控制器和模型: + +```sh $ bin/rails g model ``` -Keep in mind, of course, that anything generated with these commands inside of -an engine that has `isolate_namespace` in the `Engine` class will be namespaced. +记住,在 `Engine` 的子类中调用 `isolate_namespace` 方法后,通过这些命令生成的引擎控制器和模型都将位于命名空间中。 + + -#### `test` Directory +#### `test` 文件夹 -The `test` directory is where tests for the engine will go. To test the engine, -there is a cut-down version of a Rails application embedded within it at -`test/dummy`. This application will mount the engine in the -`test/dummy/config/routes.rb` file: +引擎的 `test` 文件夹用于储存引擎测试文件。在 `test/dummy` 文件夹中有一个内嵌于引擎中的精简版 Rails 测试应用,可用于测试引擎。此测试应用会挂载 `test/dummy/config/routes.rb` 文件中的引擎: ```ruby Rails.application.routes.draw do @@ -274,31 +206,27 @@ Rails.application.routes.draw do end ``` -This line mounts the engine at the path `/blorgh`, which will make it accessible -through the application only at that path. +上述代码会挂载 `/blorgh` 文件夹中的引擎,在应用中只能通过此路径访问该引擎。 + +`test/integration` 文件夹用于储存引擎的集成测试文件。在 `test` 文件夹中还可以创建其他文件夹。例如,我们可以为引擎的模型测试创建 `test/models` 文件夹。 -Inside the test directory there is the `test/integration` directory, where -integration tests for the engine should be placed. Other directories can be -created in the `test` directory as well. For example, you may wish to create a -`test/models` directory for your model tests. + -Providing engine functionality ------------------------------- +## 为引擎添加功能 -The engine that this guide covers provides submitting articles and commenting -functionality and follows a similar thread to the [Getting Started -Guide](getting_started.html), with some new twists. +本文创建的“blorgh”示例引擎,和[Rails 入门](getting_started.html)中的 Blog 应用类似,具有添加文章和评论的功能。 -### Generating an Article Resource + -The first thing to generate for a blog engine is the `Article` model and related -controller. To quickly generate this, you can use the Rails scaffold generator. +### 生成文章资源 -```bash +创建博客引擎的第一步是生成 `Article` 模型和相关控制器。为此,我们可以使用 Rails 的脚手架生成器: + +```sh $ bin/rails generate scaffold article title:string text:text ``` -This command will output this information: +上述命令输出的提示信息为: ``` invoke active_record @@ -322,8 +250,6 @@ invoke test_unit create test/controllers/blorgh/articles_controller_test.rb invoke helper create app/helpers/blorgh/articles_helper.rb -invoke test_unit -create test/helpers/blorgh/articles_helper_test.rb invoke assets invoke js create app/assets/javascripts/blorgh/articles.js @@ -333,22 +259,11 @@ invoke css create app/assets/stylesheets/scaffold.css ``` -The first thing that the scaffold generator does is invoke the `active_record` -generator, which generates a migration and a model for the resource. Note here, -however, that the migration is called `create_blorgh_articles` rather than the -usual `create_articles`. This is due to the `isolate_namespace` method called in -the `Blorgh::Engine` class's definition. The model here is also namespaced, -being placed at `app/models/blorgh/article.rb` rather than `app/models/article.rb` due -to the `isolate_namespace` call within the `Engine` class. +脚手架生成器完成的第一项工作是调用 `active_record` 生成器,这个生成器会为文章资源生成迁移和模型。但请注意,这里生成的迁移是 `create_blorgh_articles` 而不是通常的 `create_articles`,这是因为我们在 `Blorgh::Engine` 类的定义中调用了 `isolate_namespace` 方法。同样,这里生成的模型也带有命名空间,模型文件储存在 `app/models/blorgh/article.rb` 文件夹而不是 `app/models/article.rb` 文件夹中。 -Next, the `test_unit` generator is invoked for this model, generating a model -test at `test/models/blorgh/article_test.rb` (rather than -`test/models/article_test.rb`) and a fixture at `test/fixtures/blorgh/articles.yml` -(rather than `test/fixtures/articles.yml`). +接下来,脚手架生成器会为此模型调用 `test_unit` 生成器,这个生成器会生成模型测试 `test/models/blorgh/article_test.rb`(而不是 `test/models/article_test.rb`)和测试固件 `test/fixtures/blorgh/articles.yml`(而不是 `test/fixtures/articles.yml`)。 -After that, a line for the resource is inserted into the `config/routes.rb` file -for the engine. This line is simply `resources :articles`, turning the -`config/routes.rb` file for the engine into this: +之后,脚手架生成器会在引擎的 `config/routes.rb` 文件中为文章资源添加路由,也即 `resources :articles`,修改后的 `config/routes.rb` 文件的内容如下: ```ruby Blorgh::Engine.routes.draw do @@ -356,22 +271,11 @@ Blorgh::Engine.routes.draw do end ``` -Note here that the routes are drawn upon the `Blorgh::Engine` object rather than -the `YourApp::Application` class. This is so that the engine routes are confined -to the engine itself and can be mounted at a specific point as shown in the -[test directory](#test-directory) section. It also causes the engine's routes to -be isolated from those routes that are within the application. The -[Routes](#routes) section of this guide describes it in detail. +注意,这里的路由是通过 `Blorgh::Engine` 对象而非 `YourApp::Application` 类定义的。正如 [`test` 文件夹](#test-directory)介绍的那样,这样做的目的是把引擎路由限制在引擎中,这样就可以根据需要把引擎路由挂载到不同位置,同时也把引擎路由和应用中的其他路由隔离开来。关于这个问题的更多介绍,请参阅 [路由](#engines-routes)。 -Next, the `scaffold_controller` generator is invoked, generating a controller -called `Blorgh::ArticlesController` (at -`app/controllers/blorgh/articles_controller.rb`) and its related views at -`app/views/blorgh/articles`. This generator also generates a test for the -controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper -(`app/helpers/blorgh/articles_controller.rb`). +接下来,脚手架生成器会调用 `scaffold_controller` 生成器,以生成 `Blorgh::ArticlesController`(即 `app/controllers/blorgh/articles_controller.rb` 控制器文件)以及对应的视图(位于 `app/views/blorgh/articles` 文件夹中)、测试(即 `test/controllers/blorgh/articles_controller_test.rb` 测试文件)和辅助方法(即 `app/helpers/blorgh/articles_helper.rb` 文件)。 -Everything this generator has created is neatly namespaced. The controller's -class is defined within the `Blorgh` module: +脚手架生成器生成的上述所有组件都带有命名空间。其中控制器类在 `Blorgh` 模块中定义: ```ruby module Blorgh @@ -381,10 +285,9 @@ module Blorgh end ``` -NOTE: The `ApplicationController` class being inherited from here is the -`Blorgh::ApplicationController`, not an application's `ApplicationController`. +NOTE: 这里的 `ArticlesController` 类继承自 `Blorgh::ApplicationController` 类,而不是应用的 `ApplicationController` 类。 -The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced: +在 `app/helpers/blorgh/articles_helper.rb` 文件中定义的辅助方法也带有命名空间: ```ruby module Blorgh @@ -394,69 +297,40 @@ module Blorgh end ``` -This helps prevent conflicts with any other engine or application that may have -an article resource as well. - -Finally, the assets for this resource are generated in two files: -`app/assets/javascripts/blorgh/articles.js` and -`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little -later. +这样,即便其他引擎或应用中定义了同名的文章资源,也不会发生冲突。 -By default, the scaffold styling is not applied to the engine because the -engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't -load it. To make the scaffold styling apply, insert this line into the `` -tag of this layout: +最后,脚手架生成器会生成两个静态资源文件 `app/assets/javascripts/blorgh/articles.js` 和 `app/assets/stylesheets/blorgh/articles.css`,其用法将在后文介绍。 -```erb -<%= stylesheet_link_tag "scaffold" %> -``` - -You can see what the engine has so far by running `rake db:migrate` at the root -of our engine to run the migration generated by the scaffold generator, and then -running `rails server` in `test/dummy`. When you open -`http://localhost:3000/blorgh/articles` you will see the default scaffold that has -been generated. Click around! You've just generated your first engine's first -functions. +我们可以在引擎的根目录中通过 `bin/rails db:migrate` 命令运行前文中生成的迁移,然后在 `test/dummy` 文件夹中运行 `rails server` 命令以查看迄今为止的工作成果。打开 http://localhost:3000/blorgh/articles 页面,可以看到刚刚生成的默认脚手架。随意点击页面中的链接吧!这是我们为引擎添加的第一项功能。 -If you'd rather play around in the console, `rails console` will also work just -like a Rails application. Remember: the `Article` model is namespaced, so to -reference it you must call it as `Blorgh::Article`. +我们也可以在 Rails 控制台中对引擎的功能进行一些测试,其效果和 Rails 应用类似。注意,因为引擎的 `Article` 模型带有命名空间,所以调用时应使用 `Blorgh::Article`: -```ruby +```irb >> Blorgh::Article.find(1) => # ``` -One final thing is that the `articles` resource for this engine should be the root -of the engine. Whenever someone goes to the root path where the engine is -mounted, they should be shown a list of articles. This can be made to happen if -this line is inserted into the `config/routes.rb` file inside the engine: +最后一个需要注意的问题是,引擎的 `articles` 资源应作为引擎的根路径。当用户访问挂载引擎的根路径时,看到的应该是文章列表。具体的设置方法是在引擎的 `config/routes.rb` 文件中添加下面这行代码: ```ruby root to: "articles#index" ``` -Now people will only need to go to the root of the engine to see all the articles, -rather than visiting `/articles`. This means that instead of -`http://localhost:3000/blorgh/articles`, you only need to go to -`http://localhost:3000/blorgh` now. +这样,用户只需访问引擎的根路径,而无需访问 `/articles`,就可以看到所有文章的列表。也就是说,现在应该访问 http://localhost:3000/blorgh 页面,而不是 http://localhost:3000/blorgh/articles 页面。 + + -### Generating a Comments Resource +### 生成评论资源 -Now that the engine can create new articles, it only makes sense to add -commenting functionality as well. To do this, you'll need to generate a comment -model, a comment controller and then modify the articles scaffold to display -comments and allow people to create new ones. +到目前为止,我们的 Blorgh 引擎已经能够新建文章了,下一步应该为文章添加评论。为此,我们需要生成评论模型和评论控制器,同时修改文章脚手架,以显示文章的已有评论并提供添加评论的表单。 -From the application root, run the model generator. Tell it to generate a -`Comment` model, with the related table having two columns: a `article_id` integer -and `text` text column. +在引擎的根目录中运行模型生成器,以生成 `Comment` 模型,此模型具有 `article_id` 整型字段和 `text` 文本字段: -```bash +```sh $ bin/rails generate model Comment article_id:integer text:text ``` -This will output the following: +上述命令输出的提示信息为: ``` invoke active_record @@ -467,59 +341,46 @@ create test/models/blorgh/comment_test.rb create test/fixtures/blorgh/comments.yml ``` -This generator call will generate just the necessary model files it needs, -namespacing the files under a `blorgh` directory and creating a model class -called `Blorgh::Comment`. Now run the migration to create our blorgh_comments -table: +通过运行模型生成器,我们生成了必要的模型文件,这些文件都储存在 `blorgh` 文件夹中(用作模型的命名空间),同时创建了 `Blorgh::Comment` 模型类。接下来,在引擎的根目录中运行迁移,以创建 `blorgh_comments` 数据表: -```bash -$ rake db:migrate +```sh +$ bin/rails db:migrate ``` -To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and -add this line before the "Edit" link: +为了显示文章评论,我们需要修改 `app/views/blorgh/articles/show.html.erb` 文件,在“修改”链接之前添加下面的代码: -```html+erb +```erb

Comments

<%= render @article.comments %> ``` -This line will require there to be a `has_many` association for comments defined -on the `Blorgh::Article` model, which there isn't right now. To define one, open -`app/models/blorgh/article.rb` and add this line into the model: +上述代码要求在 `Blorgh::Article` 模型上定义到 `comments` 的 `has_many` 关联,这项工作目前还未进行。为此,我们需要打开 `app/models/blorgh/article.rb` 文件,在模型定义中添加下面这行代码: ```ruby has_many :comments ``` -Turning the model into this: +修改后的模型定义如下: ```ruby module Blorgh - class Article < ActiveRecord::Base + class Article < ApplicationRecord has_many :comments end end ``` -NOTE: Because the `has_many` is defined inside a class that is inside the -`Blorgh` module, Rails will know that you want to use the `Blorgh::Comment` -model for these objects, so there's no need to specify that using the -`:class_name` option here. +NOTE: 这里的 `has_many` 关联是在 `Blorgh` 模块内的类中定义的,因此 Rails 知道应该为关联对象使用 `Blorgh::Comment` 模型,而无需指定 `:class_name` 选项。 -Next, there needs to be a form so that comments can be created on an article. To -add this, put this line underneath the call to `render @article.comments` in -`app/views/blorgh/articles/show.html.erb`: +接下来,还需要提供添加评论的表单。为此,我们需要打开 `app/views/blorgh/articles/show.html.erb` 文件,在 `render @article.comments` 之后添加下面这行代码: ```erb <%= render "blorgh/comments/form" %> ``` -Next, the partial that this line will render needs to exist. Create a new -directory at `app/views/blorgh/comments` and in it a new file called -`_form.html.erb` which has this content to create the required partial: +接下来需要添加上述代码中使用的局部视图。新建 `app/views/blorgh/comments` 文件夹,在其中新建 `_form.html.erb` 文件并添加下面的局部视图代码: -```html+erb +```erb

New comment

<%= form_for [@article, @article.comments.build] do |f| %>

@@ -530,10 +391,7 @@ directory at `app/views/blorgh/comments` and in it a new file called <% end %> ``` -When this form is submitted, it is going to attempt to perform a `POST` request -to a route of `/articles/:article_id/comments` within the engine. This route doesn't -exist at the moment, but can be created by changing the `resources :articles` line -inside `config/routes.rb` into these lines: +此表单在提交时,会向引擎的 `/articles/:article_id/comments` 地址发起 `POST` 请求。此地址对应的路由还不存在,为此需要打开 `config/routes.rb` 文件,修改其中的 `resources :articles` 相关代码: ```ruby resources :articles do @@ -541,16 +399,15 @@ resources :articles do end ``` -This creates a nested route for the comments, which is what the form requires. +上述代码创建了表单所需的嵌套路由。 -The route now exists, but the controller that this route goes to does not. To -create it, run this command from the application root: +我们刚刚添加了路由,但路由指向的控制器还不存在。为此,需要在引擎的根目录中运行下面的命令: -```bash +```sh $ bin/rails g controller comments ``` -This will generate the following things: +上述命令输出的提示信息为: ``` create app/controllers/blorgh/comments_controller.rb @@ -560,8 +417,6 @@ invoke test_unit create test/controllers/blorgh/comments_controller_test.rb invoke helper create app/helpers/blorgh/comments_helper.rb -invoke test_unit -create test/helpers/blorgh/comments_helper_test.rb invoke assets invoke js create app/assets/javascripts/blorgh/comments.js @@ -569,10 +424,7 @@ invoke css create app/assets/stylesheets/blorgh/comments.css ``` -The form will be making a `POST` request to `/articles/:article_id/comments`, which -will correspond with the `create` action in `Blorgh::CommentsController`. This -action needs to be created, which can be done by putting the following lines -inside the class definition in `app/controllers/blorgh/comments_controller.rb`: +提交表单时向 `/articles/:article_id/comments` 地址发起的 `POST` 请求,将由 `Blorgh::CommentsController` 的 `create` 动作处理。我们需要创建此动作,为此需要打开 `app/controllers/blorgh/comments_controller.rb` 文件,并在类定义中添加下面的代码: ```ruby def create @@ -588,194 +440,140 @@ private end ``` -This is the final step required to get the new comment form working. Displaying -the comments, however, is not quite right yet. If you were to create a comment -right now, you would see this error: +这是提供评论表单的最后一步。但是仍有问题需要解决,如果我们添加一条评论,将会遇到下面的错误: ``` -Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], +Missing partial blorgh/comments/_comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in: * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * "/Users/ryan/Sites/side_projects/blorgh/app/views" ``` -The engine is unable to find the partial required for rendering the comments. -Rails looks first in the application's (`test/dummy`) `app/views` directory and -then in the engine's `app/views` directory. When it can't find it, it will throw -this error. The engine knows to look for `blorgh/comments/comment` because the -model object it is receiving is from the `Blorgh::Comment` class. +引擎无法找到渲染评论所需的局部视图。Rails 首先会在测试应用(`test/dummy`)的 `app/views` 文件夹中进行查找,然在在引擎的 `app/views` 文件夹中进行查找。如果找不到,就会抛出上述错误。因为引擎接收的模型对象来自 `Blorgh::Comment` 类,所以引擎知道应该查找 `blorgh/comments/_comment` 局部视图。 -This partial will be responsible for rendering just the comment text, for now. -Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this -line inside it: +目前,`blorgh/comments/_comment` 局部视图只需渲染评论文本。为此,我们可以新建 `app/views/blorgh/comments/_comment.html.erb` 文件,并添加下面这行代码: ```erb <%= comment_counter + 1 %>. <%= comment.text %> ``` -The `comment_counter` local variable is given to us by the `<%= render -@article.comments %>` call, which will define it automatically and increment the -counter as it iterates through each comment. It's used in this example to -display a small number next to each comment when it's created. +上述代码中的 `comment_counter` 局部变量由 `<%= render @article.comments %>` 调用提供,此调用会遍历每条评论并自动增加计数器的值。这里的 `comment_counter` 局部变量用于为每条评论添加序号。 + +到此为止,我们完成了博客引擎的评论功能。接下来我们就可以在应用中使用这项功能了。 + + -That completes the comment function of the blogging engine. Now it's time to use -it within an application. +## 把引擎挂载到应用中 -Hooking Into an Application ---------------------------- +要想在应用中使用引擎非常容易。本节介绍如何把引擎挂载到应用中并完成必要的初始化设置,以及如何把引擎连接到应用中的 `User` 类上,以便使应用中的用户拥有引擎中的文章及其评论。 -Using an engine within an application is very easy. This section covers how to -mount the engine into an application and the initial setup required, as well as -linking the engine to a `User` class provided by the application to provide -ownership for articles and comments within the engine. + -### Mounting the Engine +### 挂载引擎 -First, the engine needs to be specified inside the application's `Gemfile`. If -there isn't an application handy to test this out in, generate one using the -`rails new` command outside of the engine directory like this: +首先,需要在应用的 Gemfile 中指定引擎。我们需要新建一个应用用于测试,为此可以在引擎文件夹之外执行 `rails new` 命令: -```bash +```sh $ rails new unicorn ``` -Usually, specifying the engine inside the Gemfile would be done by specifying it -as a normal, everyday gem. +通常,只需在 Gemfile 中以普通 gem 的方式指定引擎。 ```ruby gem 'devise' ``` -However, because you are developing the `blorgh` engine on your local machine, -you will need to specify the `:path` option in your `Gemfile`: +由于我们是在本地开发 `blorgh` 引擎,因此需要在 Gemfile 中指定 `:path` 选项: ```ruby -gem 'blorgh', path: "/path/to/blorgh" +gem 'blorgh', path: 'engines/blorgh' ``` -Then run `bundle` to install the gem. +然后通过 `bundle` 命令安装 gem。 -As described earlier, by placing the gem in the `Gemfile` it will be loaded when -Rails is loaded. It will first require `lib/blorgh.rb` from the engine, then -`lib/blorgh/engine.rb`, which is the file that defines the major pieces of -functionality for the engine. +如前文所述,Gemfile 中的 gem 将在 Rails 启动时加载。上述代码首先加载引擎中的 `lib/blorgh.rb` 文件,然后加载 `lib/blorgh/engine.rb` 文件,后者定义了引擎的主要功能。 -To make the engine's functionality accessible from within an application, it -needs to be mounted in that application's `config/routes.rb` file: +要想在应用中访问引擎的功能,我们需要在应用的 `config/routes.rb` 文件中挂载该引擎: ```ruby mount Blorgh::Engine, at: "/blog" ``` -This line will mount the engine at `/blog` in the application. Making it -accessible at `http://localhost:3000/blog` when the application runs with `rails -server`. +上述代码会在应用的 `/blog` 路径上挂载引擎。通过 `rails server` 命令运行应用后,我们就可以通过 http://localhost:3000/blog 访问引擎了。 -NOTE: Other engines, such as Devise, handle this a little differently by making -you specify custom helpers (such as `devise_for`) in the routes. These helpers -do exactly the same thing, mounting pieces of the engines's functionality at a -pre-defined path which may be customizable. +NOTE: 其他一些引擎,例如 Devise,工作原理略有不同,这些引擎会在路由中自定义辅助方法(例如 `devise_for`)。这些辅助方法的作用都是在预定义路径(可以自定义)上挂载引擎的功能。 -### Engine setup + -The engine contains migrations for the `blorgh_articles` and `blorgh_comments` -table which need to be created in the application's database so that the -engine's models can query them correctly. To copy these migrations into the -application use this command: +### 引擎设置 -```bash -$ rake blorgh:install:migrations +引擎中包含了 `blorgh_articles` 和 `blorgh_comments` 数据表的迁移。通过这些迁移在应用的数据库中创建数据表之后,引擎模型才能正确查询对应的数据表。在引擎的 `test/dummy` 文件夹中运行下面的命令,可以把这些迁移复制到应用中: + +```sh +$ bin/rails blorgh:install:migrations ``` -If you have multiple engines that need migrations copied over, use -`railties:install:migrations` instead: +如果需要从多个引擎中复制迁移,可以使用 `railties:install:migrations`: -```bash -$ rake railties:install:migrations +```sh +$ bin/rails railties:install:migrations ``` -This command, when run for the first time, will copy over all the migrations -from the engine. When run the next time, it will only copy over migrations that -haven't been copied over already. The first run for this command will output -something such as this: +第一次运行上述命令时,Rails 会从所有引擎中复制迁移。再次运行时,只会复制尚未复制的迁移。第一次运行上述命令时输出的提示信息为: -```bash -Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh -Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh +``` +Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh +Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh ``` -The first timestamp (`[timestamp_1]`) will be the current time, and the second -timestamp (`[timestamp_2]`) will be the current time plus a second. The reason -for this is so that the migrations for the engine are run after any existing -migrations in the application. +其中第一个时间戳(`[timestamp_1]`)是当前时间,第二个时间戳(`[timestamp_2]`)是当前时间加上 1 秒。这样就能确保引擎的迁移总是在应用的现有迁移之后运行。 -To run these migrations within the context of the application, simply run `rake -db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the -articles will be empty. This is because the table created inside the application is -different from the one created within the engine. Go ahead, play around with the -newly mounted engine. You'll find that it's the same as when it was only an -engine. +通过 `bin/rails db:migrate` 命令即可在应用的上下文中运行引擎的迁移。此时访问 http://localhost:3000/blog 会看到文章列表是空的,这是因为在应用中和在引擎中创建的数据表有所不同。继续浏览刚刚挂载的这个引擎的其他页面,我们会发现引擎和应用看起来并没有什么区别。 -If you would like to run migrations only from one engine, you can do it by -specifying `SCOPE`: +通过指定 `SCOPE` 选项,我们可以只运行指定引擎的迁移: -```bash -rake db:migrate SCOPE=blorgh +```sh +$ bin/rails db:migrate SCOPE=blorgh ``` -This may be useful if you want to revert engine's migrations before removing it. -To revert all migrations from blorgh engine you can run code such as: +在需要还原并删除引擎的迁移时常常采取这种做法。通过下面的命令可以还原 `blorgh` 引擎的所有迁移: -```bash -rake db:migrate SCOPE=blorgh VERSION=0 +```sh +$ bin/rails db:migrate SCOPE=blorgh VERSION=0 ``` -### Using a Class Provided by the Application + + +### 使用应用提供的类 + + -#### Using a Model Provided by the Application +#### 使用应用提供的模型 -When an engine is created, it may want to use specific classes from an -application to provide links between the pieces of the engine and the pieces of -the application. In the case of the `blorgh` engine, making articles and comments -have authors would make a lot of sense. +在创建引擎时,有时需要通过应用提供的类把引擎和应用连接起来。在 `blorgh` 引擎的例子中,我们需要把文章及其评论和作者关联起来。 -A typical application might have a `User` class that would be used to represent -authors for an article or a comment. But there could be a case where the -application calls this class something different, such as `Person`. For this -reason, the engine should not hardcode associations specifically for a `User` -class. +一个典型的应用可能包含 `User` 类,可用于表示文章和评论的作者。但有的应用包含的可能是 `Person` 类而不是 `User` 类。因此,我们不能通过硬编码直接在引擎中建立和 `User` 类的关联。 -To keep it simple in this case, the application will have a class called `User` -that represents the users of the application (we'll get into making this -configurable further on). It can be generated using this command inside the -application: +为了避免例子变得复杂,我们假设应用包含的是 `User` 类(后文将对这个类进行配置)。通过下面的命令可以在应用中生成这个 `User` 类: -```bash -rails g model user name:string +```sh +$ bin/rails g model user name:string ``` -The `rake db:migrate` command needs to be run here to ensure that our -application has the `users` table for future use. +然后执行 `bin/rails db:migrate` 命令以创建 `users` 数据表。 -Also, to keep it simple, the articles form will have a new text field called -`author_name`, where users can elect to put their name. The engine will then -take this name and either create a new `User` object from it, or find one that -already has that name. The engine will then associate the article with the found or -created `User` object. +同样,为了避免例子变得复杂,我们会在文章表单中添加 `author_name` 文本字段,用于输入作者名称。引擎会根据作者名称新建或查找已有的 `User` 对象,然后建立此 `User` 对象和其文章的关联。 -First, the `author_name` text field needs to be added to the -`app/views/blorgh/articles/_form.html.erb` partial inside the engine. This can be -added above the `title` field with this code: +具体操作的第一步是在引擎的 `app/views/blorgh/articles/_form.html.erb` 局部视图中添加 `author_name` 文本字段,添加的位置是在 `title` 字段之前: -```html+erb +```erb

<%= f.label :author_name %>
<%= f.text_field :author_name %>
``` -Next, we need to update our `Blorgh::ArticleController#article_params` method to -permit the new form parameter: +接下来,需要更新 `Blorgh::ArticleController#article_params` 方法,以便使用新增的表单参数: ```ruby def article_params @@ -783,21 +581,15 @@ def article_params end ``` -The `Blorgh::Article` model should then have some code to convert the `author_name` -field into an actual `User` object and associate it as that article's `author` -before the article is saved. It will also need to have an `attr_accessor` set up -for this field, so that the setter and getter methods are defined for it. +然后还要在 `Blorgh::Article` 模型中添加相关代码,以便把 `author_name` 字段转换为实际的 `User` 对象,并在保存文章之前把 `User` 对象和其文章关联起来。为此,需要为 `author_name` 字段设置 `attr_accessor`,也就是为其定义设值方法(setter)和读值方法(getter)。 -To do all this, you'll need to add the `attr_accessor` for `author_name`, the -association for the author and the `before_save` call into -`app/models/blorgh/article.rb`. The `author` association will be hard-coded to the -`User` class for the time being. +为此,我们不仅需要为 `author_name` 添加 `attr_accessor`,还需要为 `author` 建立关联,并在 `app/models/blorgh/article.rb` 文件中添加 `before_validation` 调用。这里,我们暂时通过硬编码直接把 `author` 关联到 `User` 类上。 ```ruby attr_accessor :author_name belongs_to :author, class_name: "User" -before_save :set_author +before_validation :set_author private def set_author @@ -805,148 +597,97 @@ private end ``` -By representing the `author` association's object with the `User` class, a link -is established between the engine and the application. There needs to be a way -of associating the records in the `blorgh_articles` table with the records in the -`users` table. Because the association is called `author`, there should be an -`author_id` column added to the `blorgh_articles` table. +通过把 `author` 对象关联到 `User` 类上,我们成功地把引擎和应用连接起来。接下来还需要通过某种方式把 `blorgh_articles` 和 `users` 数据表中的记录关联起来。由于关联的名称是 `author`,我们应该为 `blorgh_articles` 数据表添加 `author_id` 字段。 -To generate this new column, run this command within the engine: +在引擎中运行下面的命令可以生成 `author_id` 字段: -```bash +```sh $ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer ``` -NOTE: Due to the migration's name and the column specification after it, Rails -will automatically know that you want to add a column to a specific table and -write that into the migration for you. You don't need to tell it any more than -this. +NOTE: 通过迁移名称和所提供的字段信息,Rails 知道需要向数据表中添加哪些字段,并会将相关代码写入迁移中,因此无需手动编写迁移代码。 -This migration will need to be run on the application. To do that, it must first -be copied using this command: +我们应该在应用中运行迁移,因此需要通过下面的命令把引擎的迁移复制到应用中: -```bash -$ rake blorgh:install:migrations +```sh +$ bin/rails blorgh:install:migrations ``` -Notice that only _one_ migration was copied over here. This is because the first -two migrations were copied over the first time this command was run. +注意,上述命令实际只复制了一个迁移,因为之前的两个迁移在上一次执行此命令时已经复制过了。 ``` -NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been -skipped. Migration with the same name already exists. NOTE Migration -[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration -with the same name already exists. Copied migration -[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh +NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists. +NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists. +Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh ``` -Run the migration using: +然后通过下面的命令运行迁移: -```bash -$ rake db:migrate +```sh +$ bin/rails db:migrate ``` -Now with all the pieces in place, an action will take place that will associate -an author - represented by a record in the `users` table - with an article, -represented by the `blorgh_articles` table from the engine. +现在,一切都已各就各位,我们完成了作者(用应用的 `users` 数据表中的记录表示)和文章(用引擎的 `blorgh_articles` 数据表中的记录表示)的关联。 -Finally, the author's name should be displayed on the article's page. Add this code -above the "Title" output inside `app/views/blorgh/articles/show.html.erb`: +最后,还需要把作者名称显示在文章页面上。为此,需要在 `app/views/blorgh/articles/show.html.erb` 文件中把下面的代码添加到“Title”之前: -```html+erb +```erb

Author: - <%= @article.author %> + <%= @article.author.name %>

``` -By outputting `@article.author` using the `<%=` tag, the `to_s` method will be -called on the object. By default, this will look quite ugly: + -``` -# -``` +#### 使用应用提供的控制器 -This is undesirable. It would be much better to have the user's name there. To -do this, add a `to_s` method to the `User` class within the application: +默认情况下,Rails 控制器通常会通过继承 `ApplicationController` 类实现功能共享,例如身份验证和会话变量的访问。而引擎的作用域是和宿主应用隔离开的,因此其 `ApplicationController` 类具有独立的命名空间。独立的命名空间避免了代码冲突,但是引擎的控制器常常需要访问宿主应用的 `ApplicationController` 类中的方法,为此我们可以让引擎的 `ApplicationController` 类继承自宿主应用的 `ApplicationController` 类。在 Blorgh 引擎的例子中,我们可以对 `app/controllers/blorgh/application_controller.rb` 文件进行如下修改: ```ruby -def to_s - name +module Blorgh + class ApplicationController < ::ApplicationController + end end ``` -Now instead of the ugly Ruby object output, the author's name will be displayed. - -#### Using a Controller Provided by the Application +默认情况下,引擎的控制器继承自 `Blorgh::ApplicationController` 类,因此通过上述修改,这些控制器将能够访问宿主应用的 `ApplicationController` 类中的方法,就好像它们是宿主应用的一部分一样。 -Because Rails controllers generally share code for things like authentication -and accessing session variables, they inherit from `ApplicationController` by -default. Rails engines, however are scoped to run independently from the main -application, so each engine gets a scoped `ApplicationController`. This -namespace prevents code collisions, but often engine controllers need to access -methods in the main application's `ApplicationController`. An easy way to -provide this access is to change the engine's scoped `ApplicationController` to -inherit from the main application's `ApplicationController`. For our Blorgh -engine this would be done by changing -`app/controllers/blorgh/application_controller.rb` to look like: +当然,进行上述修改的前提是,宿主应用必须是具有 `ApplicationController` 类的应用。 -```ruby -class Blorgh::ApplicationController < ApplicationController -end -``` + -By default, the engine's controllers inherit from -`Blorgh::ApplicationController`. So, after making this change they will have -access to the main application's `ApplicationController`, as though they were -part of the main application. +### 配置引擎 -This change does require that the engine is run from a Rails application that -has an `ApplicationController`. +本节介绍如何使 `User` 类成为可配置的,然后介绍引擎的基本配置中的注意事项。 -### Configuring an Engine + -This section covers how to make the `User` class configurable, followed by -general configuration tips for the engine. +#### 在引擎中配置所使用的应用中的类 -#### Setting Configuration Settings in the Application +接下来我们需要想办法在引擎中配置所使用的应用中的用户类。如前文所述,应用中的用户类有可能是 `User`,也有可能是 `Person` 或其他类,因此这个用户类必须是可配置的。为此,我们需要在引擎中通过 `author_class` 选项指定所使用的应用中的用户类。 -The next step is to make the class that represents a `User` in the application -customizable for the engine. This is because that class may not always be -`User`, as previously explained. To make this setting customizable, the engine -will have a configuration setting called `author_class` that will be used to -specify which class represents users inside the application. - -To define this configuration setting, you should use a `mattr_accessor` inside -the `Blorgh` module for the engine. Add this line to `lib/blorgh.rb` inside the -engine: +具体操作是在引擎的 `Blorgh` 模块中使用 `mattr_accessor` 方法,也就是把下面这行代码添加到引擎的 `lib/blorgh.rb` 文件中: ```ruby mattr_accessor :author_class ``` -This method works like its brothers, `attr_accessor` and `cattr_accessor`, but -provides a setter and getter method on the module with the specified name. To -use it, it must be referenced using `Blorgh.author_class`. +`mattr_accessor` 方法的工作原理与 `attr_accessor` 和 `cattr_accessor` 方法类似,其作用是根据指定名称为模块提供设值方法和读值方法。使用时直接调用 `Blorgh.author_class` 方法即可。 -The next step is to switch the `Blorgh::Article` model over to this new setting. -Change the `belongs_to` association inside this model -(`app/models/blorgh/article.rb`) to this: +接下来需要把 `Blorgh::Article` 模型切换到新配置,具体操作是在 `app/models/blorgh/article.rb` 中修改模型的 `belongs_to` 关联: ```ruby belongs_to :author, class_name: Blorgh.author_class ``` -The `set_author` method in the `Blorgh::Article` model should also use this class: +`Blorgh::Article` 模型的 `set_author` 方法的定义也调用了 `Blorgh.author_class` 方法: ```ruby self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name) ``` -To save having to call `constantize` on the `author_class` result all the time, -you could instead just override the `author_class` getter method inside the -`Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the -saved value before returning the result: +为了避免在每次调用 `Blorgh.author_class` 方法时调用 `constantize` 方法,我们可以在 `lib/blorgh.rb` 文件中覆盖 `Blorgh` 模块的 `author_class` 读值方法,在返回 `author_class` 前调用 `constantize` 方法: ```ruby def self.author_class @@ -954,142 +695,115 @@ def self.author_class end ``` -This would then turn the above code for `set_author` into this: +这时上述 `set_author` 方法的定义将变为: ```ruby self.author = Blorgh.author_class.find_or_create_by(name: author_name) ``` -Resulting in something a little shorter, and more implicit in its behavior. The -`author_class` method should always return a `Class` object. +修改后的代码更短,意义更明确。`author_class` 方法本来就应该返回 `Class` 对象。 -Since we changed the `author_class` method to return a `Class` instead of a -`String`, we must also modify our `belongs_to` definition in the `Blorgh::Article` -model: +因为修改后的 `author_class` 方法返回的是 `Class`,而不是原来的 `String`,我们还需要修改 `Blorgh::Article` 模型中 `belongs_to` 关联的定义: ```ruby belongs_to :author, class_name: Blorgh.author_class.to_s ``` -To set this configuration setting within the application, an initializer should -be used. By using an initializer, the configuration will be set up before the -application starts and calls the engine's models, which may depend on this -configuration setting existing. +为了配置引擎所使用的应用中的类,我们需要使用初始化脚本。只有通过初始化脚本,我们才能在应用启动并调用引擎模型前完成相关配置。 -Create a new initializer at `config/initializers/blorgh.rb` inside the -application where the `blorgh` engine is installed and put this content in it: +在安装 `blorgh` 引擎的应用中,打开 `config/initializers/blorgh.rb` 文件,创建新的初始化脚本并添加如下代码: ```ruby Blorgh.author_class = "User" ``` -WARNING: It's very important here to use the `String` version of the class, -rather than the class itself. If you were to use the class, Rails would attempt -to load that class and then reference the related table. This could lead to -problems if the table wasn't already existing. Therefore, a `String` should be -used and then converted to a class using `constantize` in the engine later on. +WARNING: 注意这里使用的是类的字符串版本,而非类本身。如果我们使用了类本身,Rails 就会尝试加载该类并引用对应的数据表。如果对应的数据表还未创建,就会抛出错误。因此,这里只能使用类的字符串版本,然后在引擎中通过 `constantize` 方法把类的字符串版本转换为类本身。 -Go ahead and try to create a new article. You will see that it works exactly in the -same way as before, except this time the engine is using the configuration -setting in `config/initializers/blorgh.rb` to learn what the class is. +接下来我们试着添加一篇文章,整个过程和之前并无差别,只不过这次引擎使用的是我们在 `config/initializers/blorgh.rb` 文件中配置的类。 -There are now no strict dependencies on what the class is, only what the API for -the class must be. The engine simply requires this class to define a -`find_or_create_by` method which returns an object of that class, to be -associated with an article when it's created. This object, of course, should have -some sort of identifier by which it can be referenced. +这样,我们再也不必关心应用中的用户类到底是什么,而只需关心该用户类是否实现了我们所需要的 API。`blorgh` 引擎只要求应用中的用户类实现了 `find_or_create_by` 方法,此方法需返回该用户类的对象,以便和对应的文章关联起来。当然,用户类的对象必须具有某种标识符,以便引用。 -#### General Engine Configuration + -Within an engine, there may come a time where you wish to use things such as -initializers, internationalization or other configuration options. The great -news is that these things are entirely possible, because a Rails engine shares -much the same functionality as a Rails application. In fact, a Rails -application's functionality is actually a superset of what is provided by -engines! +#### 引擎的基本配置 -If you wish to use an initializer - code that should run before the engine is -loaded - the place for it is the `config/initializers` folder. This directory's -functionality is explained in the [Initializers -section](configuring.html#initializers) of the Configuring guide, and works -precisely the same way as the `config/initializers` directory inside an -application. The same thing goes if you want to use a standard initializer. +有时我们需要在引擎中使用初始化脚本、国际化和其他配置选项。一般来说这些都可以实现,因为 Rails 引擎和 Rails 应用共享了相当多的功能。事实上,Rails 应用的功能就是 Rails 引擎的功能的超集。 -For locales, simply place the locale files in the `config/locales` directory, -just like you would in an application. +引擎的初始化脚本包含了需要在加载引擎之前运行的代码,其存储位置是引擎的 `config/initializers` 文件夹。[初始化脚本](configuring.html#initializers)介绍过应用的 `config/initializers` 文件夹的功能,而引擎和应用的 `config/initializers` 文件夹的功能完全相同。对于标准的初始化脚本,需要完成的工作都是一样的。 -Testing an engine ------------------ +引擎的区域设置也和应用相同,只需把区域设置文件放在引擎的 `config/locales` 文件夹中即可。 -When an engine is generated, there is a smaller dummy application created inside -it at `test/dummy`. This application is used as a mounting point for the engine, -to make testing the engine extremely simple. You may extend this application by -generating controllers, models or views from within the directory, and then use -those to test your engine. + -The `test` directory should be treated like a typical Rails testing environment, -allowing for unit, functional and integration tests. +## 测试引擎 -### Functional Tests +在使用生成器创建引擎时,Rails 会在引擎的 `test/dummy` 文件夹中创建一个小型的虚拟应用,作为测试引擎时的挂载点。通过在 `test/dummy` 文件夹中生成控制器、模型和视图,我们可以扩展这个应用,以更好地满足测试需求。 -A matter worth taking into consideration when writing functional tests is that -the tests are going to be running on an application - the `test/dummy` -application - rather than your engine. This is due to the setup of the testing -environment; an engine needs an application as a host for testing its main -functionality, especially controllers. This means that if you were to make a -typical `GET` to a controller in a controller's functional test like this: +`test` 文件夹和典型的 Rails 测试环境一样,支持单元测试、功能测试和集成测试。 -```ruby -get :index -``` + -It may not function correctly. This is because the application doesn't know how -to route these requests to the engine unless you explicitly tell it **how**. To -do this, you must also pass the `:use_route` option as a parameter on these -requests: +### 功能测试 + +在编写功能测试时,我们需要思考如何在 `test/dummy` 应用上运行测试,而不是在引擎上运行测试。这是由测试环境的设置决定的,只有通过引擎的宿主应用我们才能测试引擎的功能(尤其是引擎控制器)。也就是说,在编写引擎控制器的功能测试时,我们应该像下面这样处理典型的 `GET` 请求: ```ruby -get :index, use_route: :blorgh -``` +module Blorgh + class FooControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers -This tells the application that you still want to perform a `GET` request to the -`index` action of this controller, but you want to use the engine's route to get -there, rather than the application's one. + def test_index + get foos_url + ... + end + end +end +``` -Another way to do this is to assign the `@routes` instance variable to `Engine.routes` in your test setup: +上述代码还无法正常工作,这是因为宿主应用不知道如何处理引擎的路由,因此我们需要手动指定路由。具体操作是把 `@routes` 实例变量的值设置为引擎的路由: ```ruby -setup do - @routes = Engine.routes +module Blorgh + class FooControllerTest < ActionDispatch::IntegrationTest + include Engine.routes.url_helpers + + setup do + @routes = Engine.routes + end + + def test_index + get foos_url + ... + end + end end ``` -This will also ensure url helpers for the engine will work as expected in your tests. +上述代码告诉应用,用户对 `Foo` 控制器的 `index` 动作发起的 `GET` 请求应该由引擎的路由来处理,而不是由应用的路由来处理。 + +`include Engine.routes.url_helpers` 这行代码可以确保引擎的 URL 辅助方法能够在测试中正常工作。 + + + +## 改进引擎的功能 -Improving engine functionality ------------------------------- +本节介绍如何在宿主应用中添加或覆盖引擎的 MVC 功能。 -This section explains how to add and/or override engine MVC functionality in the -main Rails application. + -### Overriding Models and Controllers +### 覆盖模型和控制器 -Engine model and controller classes can be extended by open classing them in the -main Rails application (since model and controller classes are just Ruby classes -that inherit Rails specific functionality). Open classing an Engine class -redefines it for use in the main application. This is usually implemented by -using the decorator pattern. +要想扩展引擎的模型类和控制器类,我们可以在宿主应用中直接打开它们(因为模型类和控制器类只不过是继承了特定 Rails 功能的 Ruby 类)。通过打开类的技术,我们可以根据宿主应用的需求对引擎的类进行自定义,实际操作中通常会使用装饰器模式。 -For simple class modifications, use `Class#class_eval`. For complex class -modifications, consider using `ActiveSupport::Concern`. +通过 `Class#class_eval` 方法可以对类进行简单修改,通过 `ActiveSupport::Concern` 模块可以完成对类的复杂修改。 -#### A note on Decorators and Loading Code + -Because these decorators are not referenced by your Rails application itself, -Rails' autoloading system will not kick in and load your decorators. This means -that you need to require them yourself. +#### 使用装饰器以及加载代码时的注意事项 -Here is some sample code to do this: +打开类时使用的装饰器并未在 Rails 应用中引用,因此 Rails 的自动加载系统不会加载这些装饰器。换句话说,我们需要手动加载这些装饰器。 + +下面是一些示例代码: ```ruby # lib/blorgh/engine.rb @@ -1106,12 +820,13 @@ module Blorgh end ``` -This doesn't apply to just Decorators, but anything that you add in an engine -that isn't referenced by your main application. +不光是装饰器,对于添加到引擎中但没有在宿主应用中引用的任何东西,都需要进行这样的处理。 + + -#### Implementing Decorator Pattern Using Class#class_eval +#### 通过 `Class#class_eval` 实现装饰器模式 -**Adding** `Article#time_since_created`: +添加 `Article#time_since_created` 方法: ```ruby # MyApp/app/decorators/models/blorgh/article_decorator.rb @@ -1126,13 +841,12 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments end ``` - -**Overriding** `Article#summary`: +覆盖 `Article#summary` 方法: ```ruby # MyApp/app/decorators/models/blorgh/article_decorator.rb @@ -1147,7 +861,7 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments def summary "#{title}" @@ -1155,20 +869,18 @@ class Article < ActiveRecord::Base end ``` -#### Implementing Decorator Pattern Using ActiveSupport::Concern + -Using `Class#class_eval` is great for simple adjustments, but for more complex -class modifications, you might want to consider using [`ActiveSupport::Concern`] -(http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). -ActiveSupport::Concern manages load order of interlinked dependent modules and -classes at run time allowing you to significantly modularize your code. +#### 通过 `ActiveSupport::Concern` 模块实现装饰器模式 -**Adding** `Article#time_since_created` and **Overriding** `Article#summary`: +对类进行简单修改时,使用 `Class#class_eval` 方法很方便,但对于复杂的修改,就应该考虑使用 [`ActiveSupport::Concern` 模块](http://api.rubyonrails.org/classes/ActiveSupport/Concern.html)了。`ActiveSupport::Concern` 模块能够管理互相关联、依赖的模块和类运行时的加载顺序,这样我们就可以放心地实现代码的模块化。 + +添加 `Article#time_since_created` 方法并覆盖 `Article#summary` 方法: ```ruby # MyApp/app/models/blorgh/article.rb -class Blorgh::Article < ActiveRecord::Base +class Blorgh::Article < ApplicationRecord include Blorgh::Concerns::Models::Article def time_since_created @@ -1184,25 +896,24 @@ end ```ruby # Blorgh/app/models/article.rb -class Article < ActiveRecord::Base +class Article < ApplicationRecord include Blorgh::Concerns::Models::Article end ``` ```ruby -# Blorgh/lib/concerns/models/article +# Blorgh/lib/concerns/models/article.rb module Blorgh::Concerns::Models::Article extend ActiveSupport::Concern - # 'included do' causes the included code to be evaluated in the - # context where it is included (article.rb), rather than being - # executed in the module's context (blorgh/concerns/models/article). + # `included do` 中的代码可以在代码所在位置(article.rb)的上下文中执行, + # 而不是在模块的上下文中执行(blorgh/concerns/models/article)。 included do attr_accessor :author_name belongs_to :author, class_name: "User" - before_save :set_author + before_validation :set_author private def set_author @@ -1222,25 +933,19 @@ module Blorgh::Concerns::Models::Article end ``` -### Overriding Views + + +### 覆盖视图 -When Rails looks for a view to render, it will first look in the `app/views` -directory of the application. If it cannot find the view there, it will check in -the `app/views` directories of all engines that have this directory. +Rails 在查找需要渲染的视图时,首先会在应用的 `app/views` 文件夹中查找。如果找不到,就会接着在所有引擎的 `app/views` 文件夹中查找。 -When the application is asked to render the view for `Blorgh::ArticlesController`'s -index action, it will first look for the path -`app/views/blorgh/articles/index.html.erb` within the application. If it cannot -find it, it will look inside the engine. +在渲染 `Blorgh::ArticlesController` 的 `index` 动作的视图时,Rails 首先在应用中查找 `app/views/blorgh/articles/index.html.erb` 文件。如果找不到,就会接着在引擎中查找。 -You can override this view in the application by simply creating a new file at -`app/views/blorgh/articles/index.html.erb`. Then you can completely change what -this view would normally output. +只要在应用中新建 `app/views/blorgh/articles/index.html.erb` 视图,就可覆盖引擎中的对应视图,这样我们就可以根据需要自定义视图的内容。 -Try this now by creating a new file at `app/views/blorgh/articles/index.html.erb` -and put this content in it: +马上动手试一下,新建 `app/views/blorgh/articles/index.html.erb` 文件并添加下面的内容: -```html+erb +```erb

Articles

<%= link_to "New Article", new_article_path %> <% @articles.each do |article| %> @@ -1251,15 +956,13 @@ and put this content in it: <% end %> ``` -### Routes + + +### 路由 -Routes inside an engine are isolated from the application by default. This is -done by the `isolate_namespace` call inside the `Engine` class. This essentially -means that the application and its engines can have identically named routes and -they will not clash. +默认情况下,引擎和应用的路由是隔离开的。这种隔离是通过在 `Engine` 类中调用 `isolate_namespace` 方法实现的。这样,应用和引擎中的同名路由就不会发生冲突。 -Routes inside an engine are drawn on the `Engine` class within -`config/routes.rb`, like this: +在 `config/routes.rb` 文件中,我们可以在 `Engine` 类上定义引擎的路由,例如: ```ruby Blorgh::Engine.routes.draw do @@ -1267,132 +970,91 @@ Blorgh::Engine.routes.draw do end ``` -By having isolated routes such as this, if you wish to link to an area of an -engine from within an application, you will need to use the engine's routing -proxy method. Calls to normal routing methods such as `articles_path` may end up -going to undesired locations if both the application and the engine have such a -helper defined. +正因为引擎和应用的路由是隔离开的,当我们想要在应用中链接到引擎的某个位置时,就必须使用引擎的路由代理方法。如果像使用普通路由辅助方法那样直接使用 `articles_path` 辅助方法,将无法确定实际生成的链接,因为引擎和应用有可能都定义了这个辅助方法。 -For instance, the following example would go to the application's `articles_path` -if that template was rendered from the application, or the engine's `articles_path` -if it was rendered from the engine: +例如,对于下面的例子,如果是在应用中渲染模板,就会调用应用的 `articles_path` 辅助方法,如果是在引擎中渲染模板,就会调用引擎的 `articles_path` 辅助方法: ```erb <%= link_to "Blog articles", articles_path %> ``` -To make this route always use the engine's `articles_path` routing helper method, -we must call the method on the routing proxy method that shares the same name as -the engine. +要想确保使用的是引擎的 `articles_path` 辅助方法,我们必须通过路由代理方法来调用这个辅助方法: ```erb <%= link_to "Blog articles", blorgh.articles_path %> ``` -If you wish to reference the application inside the engine in a similar way, use -the `main_app` helper: +要想确保使用的是应用的 `articles_path` 辅助方法,我们可以使用 `main_app` 路由代理方法: ```erb <%= link_to "Home", main_app.root_path %> ``` -If you were to use this inside an engine, it would **always** go to the -application's root. If you were to leave off the `main_app` "routing proxy" -method call, it could potentially go to the engine's or application's root, -depending on where it was called from. +这样,当我们在引擎中渲染模板时,上述代码生成的链接将总是指向应用的根路径。要是不使用 `main_app` 路由代理方法,在不同位置渲染模板时,上述代码生成的链接就既有可能指向引擎的根路径,也有可能指向应用的根路径。 -If a template rendered from within an engine attempts to use one of the -application's routing helper methods, it may result in an undefined method call. -If you encounter such an issue, ensure that you're not attempting to call the -application's routing methods without the `main_app` prefix from within the -engine. +当我们在引擎中渲染模板时,如果在模板中调用了应用的路由辅助方法,Rails 就有可能抛出未定义方法错误。如果遇到此类问题,请检查代码中是否存在未通过 `main_app` 路由代理方法直接调用应用的路由辅助方法的情况。 -### Assets + -Assets within an engine work in an identical way to a full application. Because -the engine class inherits from `Rails::Engine`, the application will know to -look up assets in the engine's 'app/assets' and 'lib/assets' directories. +### 静态资源文件 -Like all of the other components of an engine, the assets should be namespaced. -This means that if you have an asset called `style.css`, it should be placed at -`app/assets/stylesheets/[engine name]/style.css`, rather than -`app/assets/stylesheets/style.css`. If this asset isn't namespaced, there is a -possibility that the host application could have an asset named identically, in -which case the application's asset would take precedence and the engine's one -would be ignored. +引擎和应用的静态资源文件的工作原理完全相同。由于引擎类继承自 `Rails::Engine` 类,应用知道应该在引擎的 `app/assets` 和 `lib/assets` 文件夹中查找静态资源文件。 -Imagine that you did have an asset located at -`app/assets/stylesheets/blorgh/style.css` To include this asset inside an -application, just use `stylesheet_link_tag` and reference the asset as if it -were inside the engine: +和引擎的所有其他组件一样,引擎的静态资源文件应该具有独立的命名空间。也就是说,引擎的静态资源文件 `style.css` 的路径应该是 `app/assets/stylesheets/[engine name]/style.css`,而不是 `app/assets/stylesheets/style.css`。如果引擎的静态资源文件不具有独立的命名空间,那么就有可能和宿主应用中的同名静态资源文件发生冲突,而一旦发生冲突,宿主应用中的静态资源文件将具有更高的优先级,引擎的静态资源文件将被忽略。 + +假设引擎有 `app/assets/stylesheets/blorgh/style.css` 这么一个静态资源文件,要想在宿主应用中包含此文件,直接使用 `stylesheet_link_tag` 辅助方法即可: ```erb <%= stylesheet_link_tag "blorgh/style.css" %> ``` -You can also specify these assets as dependencies of other assets using Asset -Pipeline require statements in processed files: +同样,我们也可以使用 Asset Pipeline 的 `require` 语句加载引擎中的静态资源文件: -``` +```css /* *= require blorgh/style */ ``` -INFO. Remember that in order to use languages like Sass or CoffeeScript, you -should add the relevant library to your engine's `.gemspec`. +TIP: 记住,若想使用 Sass 和 CoffeeScript 等语言,要把相关的 gem 添加到引擎的 `.gemspec` 文件中。 -### Separate Assets & Precompiling + -There are some situations where your engine's assets are not required by the -host application. For example, say that you've created an admin functionality -that only exists for your engine. In this case, the host application doesn't -need to require `admin.css` or `admin.js`. Only the gem's admin layout needs -these assets. It doesn't make sense for the host app to include -`"blorgh/admin.css"` in its stylesheets. In this situation, you should -explicitly define these assets for precompilation. This tells sprockets to add -your engine assets when `rake assets:precompile` is triggered. +### 独立的静态资源文件和预编译 -You can define assets for precompilation in `engine.rb`: +有时,宿主应用并不需要加载引擎的静态资源文件。例如,假设我们创建了一个仅适用于某个引擎的管理后台,这时宿主应用就不需要加载引擎的 `admin.css` 和 `admin.js` 文件,因为只有引擎的管理后台才需要这些文件。也就是说,在宿主应用的样式表中包含 `blorgh/admin.css` 文件没有任何意义。对于这种情况,我们应该显式定义那些需要预编译的静态资源文件,这样在执行 `bin/rails assets:precompile` 命令时,Sprockets 就会预编译所指定的引擎的静态资源文件。 + +我们可以在引擎的 `engine.rb` 文件中定义需要预编译的静态资源文件: ```ruby initializer "blorgh.assets.precompile" do |app| - app.config.assets.precompile += %w(admin.css admin.js) + app.config.assets.precompile += %w( admin.js admin.css ) end ``` -For more information, read the [Asset Pipeline guide](asset_pipeline.html). +关于这个问题的更多介绍,请参阅[Asset Pipeline](asset_pipeline.html)。 + + -### Other Gem Dependencies +### 其他 gem 依赖 -Gem dependencies inside an engine should be specified inside the `.gemspec` file -at the root of the engine. The reason is that the engine may be installed as a -gem. If dependencies were to be specified inside the `Gemfile`, these would not -be recognized by a traditional gem install and so they would not be installed, -causing the engine to malfunction. +我们应该在引擎根目录中的 `.gemspec` 文件中声明引擎的 gem 依赖,因为我们可能会以 gem 的方式安装引擎。如果在引擎的 `Gemfile` 文件中声明 gem 依赖,在通过 `gem install` 命令安装引擎时,就无法识别并安装这些依赖,这样引擎安装后将无法正常工作。 -To specify a dependency that should be installed with the engine during a -traditional `gem install`, specify it inside the `Gem::Specification` block -inside the `.gemspec` file in the engine: +要想让 `gem install` 命令能够识别引擎的 gem 依赖,只需在引擎的 `.gemspec` 文件的 `Gem::Specification` 代码块中进行声明: ```ruby s.add_dependency "moo" ``` -To specify a dependency that should only be installed as a development -dependency of the application, specify it like this: +还可以像下面这样声明用于开发环境的依赖: ```ruby s.add_development_dependency "moo" ``` -Both kinds of dependencies will be installed when `bundle install` is run inside -of the application. The development dependencies for the gem will only be used -when the tests for the engine are running. +不管是用于所有环境的依赖,还是用于开发环境的依赖,在执行 `bundle install` 命令时都会被安装,只不过用于开发环境的依赖只会在运行引擎测试时用到。 -Note that if you want to immediately require dependencies when the engine is -required, you should require them before the engine's initialization. For -example: +注意,如果有些依赖在加载引擎时就必须加载,那么应该在引擎初始化之前就加载它们,例如: ```ruby require 'other_engine/engine' @@ -1403,3 +1065,120 @@ module MyEngine end end ``` + + + +## Active Support `on_load` 钩子 + +由于 Ruby 是动态语言,所有有些代码会导致加载相关的 Rails 组件。以下述代码片段为例: + +```ruby +ActiveRecord::Base.include(MyActiveRecordHelper) +``` + +加载这段代码时发现有 `ActiveRecord::Base`,因此 Ruby 会查找这个常量的定义,然后引入它。这就导致整个 Active Record 组件在启动时加载。 + +`ActiveSupport.on_load` 可以延迟加载代码,在真正需要时才加载。上述代码可以修改为: + +```ruby +ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } +``` + +这样修改之后,加载 `ActiveRecord::Base` 时才会引入 `MyActiveRecordHelper`。 + + + +### 运作方式 + +在 Rails 框架中,加载相应的库时会调用这些钩子。例如,加载 `ActionController::Base` 时,调用 `:action_controller_base` 钩子。也就是说,`ActiveSupport.on_load` 调用设定的 `:action_controller_base` 钩子在 `ActionController::Base` 的上下文中调用(因此 `self` 是 `ActionController::Base` 的实例)。 + + + +### 修改代码,使用 `on_load` 钩子 + +修改代码的方式很简单。如果代码引用了某个 Rails 组件,如 `ActiveRecord::Base`,只需把代码放在 `on_load` 钩子中。 + +**示例 1** + +```ruby +ActiveRecord::Base.include(MyActiveRecordHelper) +``` + +改为: + +```ruby +ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } +# self 在这里指代 ActiveRecord::Base 实例,因此可以直接调用 #include +``` + +* 示例 2** + +```ruby +ActionController::Base.prepend(MyActionControllerHelper) +``` + +改为: + +```ruby +ActiveSupport.on_load(:action_controller_base) { prepend MyActionControllerHelper } +# self 在这里指代 ActionController::Base 实例,因此可以直接调用 #prepend +``` + +**示例 3** + +```ruby +ActiveRecord::Base.include_root_in_json = true +``` + +改为: + +```ruby +ActiveSupport.on_load(:active_record) { self.include_root_in_json = true } +# self 在这里指代 ActiveRecord::Base 实例 +``` + + + +### 可用的钩子 + +下面是可在代码中使用的钩子。 + +若想勾入下述某个类的初始化过程,使用相应的钩子。 + +| 类 | 可用的钩子 | +|---|---| +| `ActionCable` | `action_cable` | +| `ActionController::API` | `action_controller_api` | +| `ActionController::API` | `action_controller` | +| `ActionController::Base` | `action_controller_base` | +| `ActionController::Base` | `action_controller` | +| `ActionController::TestCase` | `action_controller_test_case` | +| `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` | +| `ActionMailer::Base` | `action_mailer` | +| `ActionMailer::TestCase` | `action_mailer_test_case` | +| `ActionView::Base` | `action_view` | +| `ActionView::TestCase` | `action_view_test_case` | +| `ActiveJob::Base` | `active_job` | +| `ActiveJob::TestCase` | `active_job_test_case` | +| `ActiveRecord::Base` | `active_record` | +| `ActiveSupport::TestCase` | `active_support_test_case` | +| `i18n` | `i18n` | + + + +## 配置钩子 + +下面是可用的配置钩子。这些钩子不勾入具体的组件,而是在整个应用的上下文中运行。 + +| 钩子 | 使用场景 | +|---|---| +| `before_configuration` | 第一运行,在所有初始化脚本运行之前调用。 | +| `before_initialize` | 第二运行,在初始化各组件之前运行。 | +| `before_eager_load` | 第三运行。`config.cache_classes` 设为 `false` 时不运行。 | +| `after_initialize` | 最后运行,各组件初始化完成之后调用。 | + +**示例** + +```ruby +config.before_configuration { puts 'I am called before any initializers' } +``` diff --git a/source/zh-CN/form_helpers.md b/source/zh-CN/form_helpers.md index ccefb91..092b3a2 100644 --- a/source/zh-CN/form_helpers.md +++ b/source/zh-CN/form_helpers.md @@ -1,26 +1,26 @@ -表单帮助方法 -=========== +# 表单辅助方法 -表单是网页程序的基本组成部分,用于接收用户的输入。然而,由于表单中控件的名称和各种属性,使用标记语言难以编写和维护。Rails 提供了很多视图帮助方法简化表单的创建过程。因为各帮助方法的用途不一样,所以开发者在使用之前必须要知道相似帮助方法的差异。 +表单是 Web 应用中用户输入的基本界面。尽管如此,由于需要处理表单控件的名称和众多属性,编写和维护表单标记可能很快就会变得单调乏味。Rails 提供用于生成表单标记的视图辅助方法来消除这种复杂性。然而,由于这些辅助方法具有不同的用途和用法,开发者在使用之前需要知道它们之间的差异。 -读完本文,你将学到: +读完本文后,您将学到: -* 如何创建搜索表单等不需要操作模型的普通表单; -* 如何使用针对模型的表单创建和编辑数据库中的记录; -* 如何使用各种类型的数据生成选择列表; -* 如何使用 Rails 提供用于处理日期和时间的帮助方法; -* 上传文件的表单有什么特殊之处; -* 创建操作外部资源的案例; -* 如何编写复杂的表单; +* 如何在 Rails 应用中创建搜索表单和类似的不针对特定模型的通用表单; +* 如何使用针对特定模型的表单来创建和修改对应的数据库记录; +* 如何使用多种类型的数据生成选择列表; +* Rails 提供了哪些日期和时间辅助方法; +* 上传文件的表单有什么特殊之处; +* 如何用 `post` 方法把表单提交到外部资源并设置真伪令牌; +* 如何创建复杂表单。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -NOTE: 本文的目的不是全面解说每个表单方法和其参数,完整的说明请阅读 [Rails API 文档](http://api.rubyonrails.org/)。 +NOTE: 本文不是所有可用表单辅助方法及其参数的完整文档。关于表单辅助方法的完整介绍,请参阅 [Rails API 文档](http://api.rubyonrails.org/)。 -编写简单的表单 ------------- + -最基本的表单帮助方法是 `form_tag`。 +## 处理基本表单 + +`form_tag` 方法是最基本的表单辅助方法。 ```erb <%= form_tag do %> @@ -28,32 +28,32 @@ NOTE: 本文的目的不是全面解说每个表单方法和其参数,完整 <% end %> ``` -像上面这样不传入参数时,`form_tag` 会创建一个 `
` 标签,提交表单后,向当前页面发起 POST 请求。假设当前页面是 `/home/index`,生成的 HTML 如下(为了提升可读性,添加了一些换行): +无参数调用 `form_tag` 方法会创建 `` 标签,在提交表单时会向当前页面发起 POST 请求。例如,假设当前页面是 `/home/index`,上面的代码会生成下面的 HTML(为了提高可读性,添加了一些换行): ```html - -
- - -
+ + + Form contents
``` -你会发现 HTML 中多了一个 `div` 元素,其中有两个隐藏的 `input` 元素。这个 `div` 元素很重要,没有就无法提交表单。第一个 `input` 元素的 `name` 属性值为 `utf8`,其作用是强制浏览器使用指定的编码处理表单,不管是 GET 还是 POST。第二个 `input` 元素的 `name` 属性值为 `authenticity_token`,这是 Rails 的一项安全措施,称为“跨站请求伪造保护”。`form_tag` 帮助方法会为每个非 POST 表单生成这个元素(表明启用了这项安全保护措施)。详情参阅“[Rails 安全指南](security.html#cross-site-request-forgery-csrf)”。 +我们注意到,上面的 HTML 的第二行是一个 `hidden` 类型的 `input` 元素。这个 `input` 元素很重要,一旦缺少,表单就不能成功提交。这个 `input` 元素的 `name` 属性的值是 `utf8`,用于说明浏览器处理表单时使用的字符编码方式。对于所有表单,不管表单动作是“GET”还是“POST”,都会生成这个 `input` 元素。 + +上面的 HTML 的第三行也是一个 `input` 元素,元素的 `name` 属性的值是 `authenticity_token`。这个 `input` 元素是 Rails 的一个名为跨站请求伪造保护的安全特性。在启用跨站请求伪造保护的情况下,表单辅助方法会为所有非 GET 表单生成这个 `input` 元素。关于跨站请求伪造保护的更多介绍,请参阅 [跨站请求伪造(CSRF)](security.html#cross-site-request-forgery-csrf)。 -NOTE: 为了行文简洁,后续代码没有包含这个 `div` 元素。 + -### 普通的搜索表单 +### 通用搜索表单 -在网上见到最多的表单是搜索表单,搜索表单包含以下元素: +搜索表单是网上最常见的基本表单,包含: -* `form` 元素,`action` 属性值为 `GET`; -* 输入框的 `label` 元素; -* 文本输入框 ; -* 提交按钮; +* 具有“GET”方法的表单元素 +* 文本框的 `label` 标签 +* 文本框 +* 提交按钮 -创建这样一个表单要分别使用帮助方法 `form_tag`、`label_tag`、`text_field_tag` 和 `submit_tag`,如下所示: +我们可以分别使用 `form_tag`、`label_tag`、`text_field_tag`、`submit_tag` 标签来创建搜索表单,就像下面这样: ```erb <%= form_tag("/search", method: "get") do %> @@ -63,52 +63,58 @@ NOTE: 为了行文简洁,后续代码没有包含这个 `div` 元素。 <% end %> ``` -生成的 HTML 如下: +上面的代码会生成下面的 HTML: ```html
-
+
``` -TIP: 表单中的每个 `input` 元素都有 ID 属性,其值和 `name` 属性的值一样(上例中是 `q`)。ID 可用于 CSS 样式或使用 JavaScript 处理表单控件。 +NOTE: 表单中的文本框会根据 `name` 属性(在上面的例子中值为 `q`)生成 `id` 属性。`id` 属性在应用 CSS 样式或使用 JavaScript 操作表单控件时非常有用。 -除了 `text_field_tag` 和 `submit_tag` 之外,每个 HTML 表单控件都有对应的帮助方法。 +除 `text_field_tag` 和 `submit_tag` 方法之外,每个 HTML 表单控件都有对应的辅助方法。 -NOTE: 搜索表单的请求类型一定要用 GET,这样用户才能把某个搜索结果页面加入收藏夹,以便后续访问。一般来说,Rails 建议使用合适的请求方法处理表单。 +WARNING: 搜索表单的方法都应该设置为“GET”,这样用户就可以把搜索结果添加为书签。一般来说,Rails 推荐为表单动作使用正确的 HTTP 动词。 -### 调用 `form_tag` 时使用多个 Hash 参数 + -`form_tag` 方法可接受两个参数:表单提交地址和一个 Hash 选项。Hash 选项指定提交表单使用的请求方法和 HTML 选项,例如 `form` 元素的 `class` 属性。 +### 在调用表单辅助方法时使用多个散列 -和 `link_to` 方法一样,提交地址不一定非得使用字符串,也可使用一个由 URL 参数组成的 Hash,这个 Hash 经 Rails 路由转换成 URL 地址。这种情况下,`form_tag` 方法的两个参数都是 Hash,同时指定两个参数时很容易产生问题。假设写成下面这样: +`form_tag` 辅助方法接受两个参数:提交表单的地址和选项散列。选项散列用于指明提交表单的方法,以及 HTML 选项,例如表单的 `class` 属性。 + +和 `link_to` 辅助方法一样,提交表单的地址可以是字符串,也可以是散列形式的 URL 参数。Rails 路由能够识别这个散列,将其转换为有效的 URL 地址。尽管如此,由于 `form_tag` 方法的两个参数都是散列,如果我们想同时指定两个参数,就很容易遇到问题。假如有下面的代码: ```ruby form_tag(controller: "people", action: "search", method: "get", class: "nifty_form") # => '
' ``` -在这段代码中,`method` 和 `class` 会作为生成 URL 的请求参数,虽然你想传入两个 Hash,但实际上只传入了一个。所以,你要把第一个 Hash(或两个 Hash)放在一对花括号中,告诉 Ruby 哪个是哪个,写成这样: +在上面的代码中,`method` 和 `class` 选项的值会被添加到生成的 URL 地址的查询字符串中,不管我们是不是想要使用两个散列作为参数,Rails 都会把这些选项当作一个散列。为了告诉 Rails 我们想要使用两个散列作为参数,我们可以把第一个散列放在大括号中,或者把两个散列都放在大括号中。这样就可以生成我们想要的 HTML 了: ```ruby form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form") # => '' ``` -### 生成表单中控件的帮助方法 + + +### 用于生成表单元素的辅助方法 -Rails 提供了很多用来生成表单中控件的帮助方法,例如复选框,文本输入框和单选框。这些基本的帮助方法都以 `_tag` 结尾,例如 `text_field_tag` 和 `check_box_tag`,生成单个 `input` 元素。这些帮助方法的第一个参数都是 `input` 元素的 `name` 属性值。提交表单后,`name` 属性的值会随表单中的数据一起传入控制器,在控制器中可通过 `params` 这个 Hash 获取各输入框中的值。例如,如果表单中包含 `<%= text_field_tag(:query) %>`,就可以在控制器中使用 `params[:query]` 获取这个输入框中的值。 +Rails 提供了一系列用于生成表单元素(如复选框、文本字段和单选按钮)的辅助方法。这些名称以 `_tag` 结尾的基本辅助方法(如 `text_field_tag` 和 `check_box_tag`)只生成单个 `input` 元素,并且第一个参数都是 `input` 元素的 `name` 属性的值。在提交表单时,`name` 属性的值会和表单数据一起传递,这样在控制器中就可以通过 `params` 来获得各个 `input` 元素的值。例如,如果表单包含 `<%= text_field_tag(:query) %>`,我们就可以通过 `params[:query]` 来获得这个文本字段的值。 -Rails 使用特定的规则生成 `input` 的 `name` 属性值,便于提交非标量值,例如数组和 Hash,这些值也可通过 `params` 获取。 +在给 `input` 元素命名时,Rails 有一些命名约定,使我们可以提交非标量值(如数组或散列),这些值同样可以通过 `params` 来获得。关于这些命名约定的更多介绍,请参阅 [理解参数命名约定](#understanding-parameter-naming-conventions)。 -各帮助方法的详细用法请查阅 [API 文档](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html)。 +关于这些辅助方法的用法的详细介绍,请参阅 [API 文档](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html)。 + + #### 复选框 -复选框是一种表单控件,给用户一些选项,可用于启用或禁用某项功能。 +复选框表单控件为用户提供一组可以启用或禁用的选项: ```erb <%= check_box_tag(:pet_dog) %> @@ -117,7 +123,7 @@ Rails 使用特定的规则生成 `input` 的 `name` 属性值,便于提交非 <%= label_tag(:pet_cat, "I own a cat") %> ``` -生成的 HTML 如下: +上面的代码会生成下面的 HTML: ```html @@ -126,11 +132,13 @@ Rails 使用特定的规则生成 `input` 的 `name` 属性值,便于提交非 ``` -`check_box_tag` 方法的第一个参数是 `name` 属性的值。第二个参数是 `value` 属性的值。选中复选框后,`value` 属性的值会包含在提交的表单数据中,因此可以通过 `params` 获取。 +`check_box_tag` 辅助方法的第一个参数是生成的 `input` 元素的 `name` 属性的值。可选的第二个参数是 `input` 元素的值,当对应复选框被选中时,这个值会包含在表单数据中,并可以通过 `params` 来获得。 + + -#### 单选框 +#### 单选按钮 -单选框有点类似复选框,但是各单选框之间是互斥的,只能选择一组中的一个: +和复选框类似,单选按钮表单控件为用户提供一组选项,区别在于这些选项是互斥的,用户只能从中选择一个: ```erb <%= radio_button_tag(:age, "child") %> @@ -139,7 +147,7 @@ Rails 使用特定的规则生成 `input` 的 `name` 属性值,便于提交非 <%= label_tag(:age_adult, "I'm over 21") %> ``` -生成的 HTML 如下: +上面的代码会生成下面的 HTML: ```html @@ -148,13 +156,15 @@ Rails 使用特定的规则生成 `input` 的 `name` 属性值,便于提交非 ``` -和 `check_box_tag` 方法一样,`radio_button_tag` 方法的第二个参数也是 `value` 属性的值。因为两个单选框的 `name` 属性值一样(都是 `age`),所以用户只能选择其中一个单选框,`params[:age]` 的值不是 `"child"` 就是 `"adult"`。 +和 `check_box_tag` 一样,`radio_button_tag` 辅助方法的第二个参数是生成的 `input` 元素的值。因为两个单选按钮的 `name` 属性的值相同(都是 `age`),所以用户只能从中选择一个,`params[:age]` 的值要么是 `"child"` 要么是 `"adult"`。 + +NOTE: 在使用复选框和单选按钮时一定要指定 `label` 标签。`label` 标签为对应选项提供说明文字,并扩大可点击区域,使用户更容易选中想要的选项。 -NOTE: 复选框和单选框一定要指定 `label` 标签。`label` 标签可以为指定的选项框附加文字说明,还能增加选项框的点选范围,让用户更容易选中。 + -### 其他帮助方法 +### 其他你可能感兴趣的辅助方法 -其他值得说明的表单控件包括:多行文本输入框,密码输入框,隐藏输入框,搜索关键字输入框,电话号码输入框,日期输入框,时间输入框,颜色输入框,日期时间输入框,本地日期时间输入框,月份输入框,星期输入框,URL 地址输入框,Email 地址输入框,数字输入框和范围输入框: +其他值得一提的表单控件包括文本区域、密码框、隐藏输入字段、搜索字段、电话号码字段、日期字段、时间字段、颜色字段、本地日期时间字段、月份字段、星期字段、URL 地址字段、电子邮件地址字段、数字字段和范围字段: ```erb <%= text_area_tag(:message, "Hi, nice site", size: "24x6") %> @@ -163,7 +173,6 @@ NOTE: 复选框和单选框一定要指定 `label` 标签。`label` 标签可以 <%= search_field(:user, :name) %> <%= telephone_field(:user, :phone) %> <%= date_field(:user, :born_on) %> -<%= datetime_field(:user, :meeting_time) %> <%= datetime_local_field(:user, :graduation_day) %> <%= month_field(:user, :birthday_month) %> <%= week_field(:user, :birthday_week) %> @@ -175,7 +184,7 @@ NOTE: 复选框和单选框一定要指定 `label` 标签。`label` 标签可以 <%= range_field(:product, :discount, in: 1..100) %> ``` -生成的 HTML 如下: +上面的代码会生成下面的 HTML: ```html @@ -184,7 +193,6 @@ NOTE: 复选框和单选框一定要指定 `label` 标签。`label` 标签可以 - @@ -196,42 +204,47 @@ NOTE: 复选框和单选框一定要指定 `label` 标签。`label` 标签可以 ``` -用户看不到隐藏输入框,但却和其他文本类输入框一样,能保存数据。隐藏输入框中的值可以通过 JavaScript 修改。 +隐藏输入字段不显示给用户,但和其他 `input` 元素一样可以保存数据。我们可以使用 JavaScript 来修改隐藏输入字段的值。 -NOTE: 搜索关键字输入框,电话号码输入框,日期输入框,时间输入框,颜色输入框,日期时间输入框,本地日期时间输入框,月份输入框,星期输入框,URL 地址输入框,Email 地址输入框,数字输入框和范围输入框是 HTML5 提供的控件。如果想在旧版本的浏览器中保持体验一致,需要使用 HTML5 polyfill(使用 CSS 或 JavaScript 编写)。polyfill 虽[无不足之处](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills),但现今比较流行的工具是 [Modernizr](http://www.modernizr.com/) 和 [yepnope](http://yepnopejs.com/),根据检测到的 HTML5 特性添加相应的功能。 +WARNING: 搜索字段、电话号码字段、日期字段、时间字段、颜色字段、日期时间字段、本地日期时间字段、月份字段、星期字段、URL 地址字段、电子邮件地址字段、数字字段和范围字段都是 HTML5 控件。要想在旧版本浏览器中拥有一致的体验,我们需要使用 HTML5 polyfill(针对 CSS 或 JavaScript 代码)。[HTML5 Cross Browser Polyfills](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills) 提供了 HTML5 polyfill 的完整列表,目前最流行的工具是 [Modernizr](https://modernizr.com/),通过检测 HTML5 特性是否存在来添加缺失的功能。 -TIP: 如果使用密码输入框,或许还不想把其中的值写入日志。具体做法参见“[Rails 安全指南](security.html#logging)”。 +TIP: 使用密码框时可以配置 Rails 应用,不把密码框的值写入日志,详情参阅 [日志](security.html#logging)。 -处理模型对象 ------------ + -### 模型对象帮助方法 +## 处理模型对象 -表单的一个特别常见的用途是编辑或创建模型对象。这时可以使用 `*_tag` 帮助方法,但是太麻烦了,每个元素都要设置正确的参数名称和默认值。Rails 提供了很多帮助方法可以简化这一过程,这些帮助方法没有 `_tag` 后缀,例如 `text_field` 和 `text_area`。 + -这些帮助方法的第一个参数是实例变量的名字,第二个参数是在对象上调用的方法名(一般都是模型的属性)。Rails 会把在对象上调用方法得到的值设为控件的 `value` 属性值,并且设置相应的 `name` 属性值。如果在控制器中定义了 `@person` 实例变量,其名字为“Henry”,在表单中有以下代码: +### 模型对象辅助方法 + +表单经常用于修改或创建模型对象。这种情况下当然可以使用 `*_tag` 辅助方法,但使用起来却有些麻烦,因为我们需要确保每个标记都使用了正确的参数名称并设置了合适的默认值。为此,Rails 提供了量身定制的辅助方法。这些辅助方法的名称不使用 `_tag` 后缀,例如 `text_field` 和 `text_area`。 + +这些辅助方法的第一个参数是实例变量,第二个参数是在这个实例变量对象上调用的方法(通常是模型属性)的名称。 Rails 会把 `input` 控件的值设置为所调用方法的返回值,并为 `input` 控件的 `name` 属性设置合适的值。假设我们在控制器中定义了 `@person` 实例变量,这个人的名字是 Henry,那么表单中的下述代码: ```erb <%= text_field(:person, :name) %> ``` -生成的结果如下: +会生成下面的 HTML: -```erb +```html ``` -提交表单后,用户输入的值存储在 `params[:person][:name]` 中。`params[:person]` 这个 Hash 可以传递给 `Person.new` 方法;如果 `@person` 是 `Person` 的实例,还可传递给 `@person.update`。一般来说,这些帮助方法的第二个参数是对象属性的名字,但 Rails 并不对此做强制要求,只要对象能响应 `name` 和 `name=` 方法即可。 +提交表单时,用户输入的值储存在 `params[:person][:name]` 中。`params[:person]` 这个散列可以传递给 `Person.new` 方法作为参数,而如果 `@person` 是 `Person` 模型的实例,这个散列还可以传递给 `@person.update` 方法作为参数。尽管这些辅助方法的第二个参数通常都是模型属性的名称,但不是必须这样做。在上面的例子中,只要 `@person` 对象拥有 `name` 和 `name=` 方法即可省略第二个参数。 + +WARNING: 传入的参数必须是实例变量的名称,如 `:person` 或 `"person"`,而不是模型实例本身。 -WARNING: 传入的参数必须是实例变量的名字,例如 `:person` 或 `"person"`,而不是模型对象的实例本身。 +Rails 还提供了用于显示模型对象数据验证错误的辅助方法,详情参阅 [在视图中显示验证错误](active_record_validations.html#displaying-validation-errors-in-views)。 -Rails 还提供了用于显示模型对象数据验证错误的帮助方法,详情参阅“[Active Record 数据验证](active_record_validations.html#displaying-validation-errors-in-views)”一文。 + ### 把表单绑定到对象上 -虽然上述用法很方便,但却不是最好的使用方式。如果 `Person` 有很多要编辑的属性,我们就得不断重复编写要编辑对象的名字。我们想要的是能把表单绑定到对象上的方法,`form_for` 帮助方法就是为此而生。 +上一节介绍的辅助方法使用起来虽然很方便,但远非完美的解决方案。如果 `Person` 模型有很多属性需要修改,那么实例变量对象的名称就需要重复写很多遍。更好的解决方案是把表单绑定到模型对象上,为此我们可以使用 `form_for` 辅助方法。 -假设有个用来处理文章的控制器 `app/controllers/articles_controller.rb`: +假设有一个用于处理文章的控制器 `app/controllers/articles_controller.rb`: ```ruby def new @@ -239,7 +252,7 @@ def new end ``` -在 `new` 动作对应的视图 `app/views/articles/new.html.erb` 中可以像下面这样使用 `form_for` 方法: +在对应的 `app/views/articles/new.html.erb` 视图中,可以像下面这样使用 `form_for` 辅助方法: ```erb <%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %> @@ -249,127 +262,133 @@ end <% end %> ``` -有几点要注意: +这里有几点需要注意: -* `@article` 是要编辑的对象; -* `form_for` 方法的参数中只有一个 Hash。路由选项传入嵌套 Hash `:url` 中,HTML 选项传入嵌套 Hash `:html` 中。还可指定 `:namespace` 选项为 `form` 元素生成一个唯一的 ID 属性值。`:namespace` 选项的值会作为自动生成的 ID 的前缀。 -* `form_for` 方法会拽入一个**表单构造器**对象(`f` 变量); -* 生成表单控件的帮助方法在表单构造器对象 `f` 上调用; +* 实际需要修改的对象是 `@article`。 +* `form_for` 辅助方法的选项是一个散列,其中 `:url` 键对应的值是路由选项,`:html` 键对应的值是 HTML 选项,这两个选项本身也是散列。还可以提供 `:namespace` 选项来确保表单元素具有唯一的 ID 属性,自动生成的 ID 会以 `:namespace` 选项的值和下划线作为前缀。 +* `form_for` 辅助方法会产出一个表单生成器对象,即变量 `f`。 +* 用于生成表单控件的辅助方法都在表单生成器对象 `f` 上调用。 -上述代码生成的 HTML 如下: +上面的代码会生成下面的 HTML: ```html - +
``` -`form_for` 方法的第一个参数指明通过 `params` 的哪个键获取表单中的数据。在上面的例子中,第一个参数名为 `article`,因此所有控件的 `name` 属性都是 `article[attribute_name]` 这种形式。所以,在 `create` 动作中,`params[:article]` 这个 Hash 有两个键:`:title` 和 `:body`。`name` 属性的重要性参阅“[理解参数命名约定](#understanding-parameter-naming-conventions)”一节。 +`form_for` 辅助方法的第一个参数决定了 `params` 使用哪个键来访问表单数据。在上面的例子中,这个参数为 `@article`,因此所有 `input` 控件的 `name` 属性都是 `article[attribute_name]` 这种形式,而在 `create` 动作中 `params[:article]` 是一个拥有 `:title` 和 `:body` 键的散列。关于 `input` 控件 `name` 属性重要性的更多介绍,请参阅 [理解参数命名约定](#understanding-parameter-naming-conventions)。 -在表单构造器对象上调用帮助方法和在模型对象上调用的效果一样,唯有一点区别,无法指定编辑哪个模型对象,因为这由表单构造器负责。 +在表单生成器上调用的辅助方法和模型对象辅助方法几乎完全相同,区别在于前者无需指定需要修改的对象,因为表单生成器已经指定了需要修改的对象。 -使用 `fields_for` 帮助方法也可创建类似的绑定,但不会生成 `
` 标签。在同一表单中编辑多个模型对象时经常使用 `fields_for` 方法。例如,有个 `Person` 模型,和 `ContactDetail` 模型关联,编写如下的表单可以同时创建两个模型的对象: +使用 `fields_for` 辅助方法也可以把表单绑定到对象上,但不会创建 `` 标签。需要在同一个表单中修改多个模型对象时可以使用 `fields_for` 方法。例如,假设 `Person` 模型和 `ContactDetail` 模型关联,我们可以在下面这个表单中同时创建这两个模型的对象: ```erb <%= form_for @person, url: {action: "create"} do |person_form| %> <%= person_form.text_field :name %> - <%= fields_for @person.contact_detail do |contact_details_form| %> - <%= contact_details_form.text_field :phone_number %> + <%= fields_for @person.contact_detail do |contact_detail_form| %> + <%= contact_detail_form.text_field :phone_number %> <% end %> <% end %> ``` -生成的 HTML 如下: +上面的代码会生成下面的 HTML: ```html - +
``` -`fields_for` 方法拽入的对象和 `form_for` 方法一样,都是表单构造器(其实在代码内部 `form_for` 会调用 `fields_for` 方法)。 +和 `form_for` 辅助方法一样, `fields_for` 方法产出的对象是一个表单生成器(实际上 `form_for` 方法在内部调用了 `fields_for` 方法)。 + + -### 记录辨别技术 +### 使用记录识别技术 -用户可以直接处理程序中的 `Article` 模型,根据开发 Rails 的最佳实践,应该将其视为一个资源: +`Article` 模型对我们来说是直接可用的,因此根据 Rails 开发的最佳实践,我们应该把这个模型声明为资源: ```ruby resources :articles ``` -TIP: 声明资源有很多附属作用。资源的创建与使用请阅读“[Rails 路由全解](routing.html#resource-routing-the-rails-default)”一文。 +NOTE: 资源的声明有许多副作用。关于设置和使用资源的更多介绍,请参阅 [资源路由:Rails 的默认风格](routing.html#resource-routing-the-rails-default)。 -处理 REST 资源时,使用“记录辨别”技术可以简化 `form_for` 方法的调用。简单来说,你可以只把模型实例传给 `form_for`,让 Rails 查找模型名等其他信息: +在处理 REST 架构的资源时,使用记录识别技术可以大大简化 `form_for` 辅助方法的调用。简而言之,使用记录识别技术后,我们只需把模型实例传递给 `form_for` 方法作为参数,Rails 会找出模型名称和其他信息: ```ruby -## Creating a new article -# long-style: +## 创建一篇新文章 +# 冗长风格: form_for(@article, url: articles_path) -# same thing, short-style (record identification gets used): +# 简短风格,效果一样(用到了记录识别技术): form_for(@article) -## Editing an existing article -# long-style: +## 编辑一篇现有文章 +# 冗长风格: form_for(@article, url: article_path(@article), html: {method: "patch"}) -# short-style: +# 简短风格: form_for(@article) ``` -注意,不管记录是否存在,使用简短形式的 `form_for` 调用都很方便。记录辨别技术很智能,会调用 `record.new_record?` 方法检查是否为新记录;而且还能自动选择正确的提交地址,根据对象所属的类生成 `name` 属性的值。 +注意,不管是新建记录还是修改已有记录,`form_for` 方法调用的短格式都是相同的,很方便。记录识别技术很智能,能够通过调用 `record.new_record?` 方法来判断记录是否为新记录,同时还能选择正确的提交地址,并根据对象的类设置 `name` 属性的值。 -Rails 还会自动设置 `class` 和 `id` 属性。在新建文章的表单中,`id` 和 `class` 属性的值都是 `new_article`。如果编辑 ID 为 23 的文章,表单的 `class` 为 `edit_article`,`id` 为 `edit_article_23`。为了行文简洁,后文会省略这些属性。 +Rails 还会自动为表单的 `class` 和 `id` 属性设置合适的值,例如,用于创建文章的表单,其 `id` 和 `class` 属性的值都会被设置为 `new_article`。用于修改 ID 为 23 的文章的表单,其 `class` 属性会被设置为 `edit_article`,其 `id` 属性会被设置为 `edit_article_23`。为了行文简洁,后文会省略这些属性。 -WARNING: 如果在模型中使用单表继承(single-table inheritance,简称 STI),且只有父类声明为资源,子类就不能依赖记录辨别技术,必须指定模型名,`:url` 和 `:method` 选项。 +WARNING: 在模型中使用单表继承(single-table inheritance,STI)时,如果只有父类声明为资源,在子类上就不能使用记录识别技术。这时,必须显式说明模型名称、`:url` 和 `:method`。 + + #### 处理命名空间 -如果在路由中使用了命名空间,`form_for` 方法也有相应的简写形式。如果程序中有个 `admin` 命名空间,表单可以写成: +如果在路由中使用了命名空间,我们同样可以使用 `form_for` 方法调用的短格式。例如,假设有 `admin` 命名空间,那么 `form_for` 方法调用的短格式可以写成: ```ruby form_for [:admin, @article] ``` -这个表单会提交到命名空间 `admin` 中的 `ArticlesController`(更新文章时提交到 `admin_article_path(@article)`)。如果命名空间有很多层,句法类似: +上面的代码会创建提交到 `admin` 命名空间中 `ArticlesController` 控制器的表单(在更新文章时会提交到 `admin_article_path(@article)` 这个地址)。对于多层命名空间的情况,语法也类似: ```ruby form_for [:admin, :management, @article] ``` -关于 Rails 路由的详细信息以及相关的约定,请阅读“[Rails 路由全解](routing.html)”一文。 +关于 Rails 路由及其相关约定的更多介绍,请参阅[Rails 路由全解](routing.html)。 + + -### 表单如何处理 PATCH,PUT 或 DELETE 请求? +### 表单如何处理 PATCH、PUT 或 DELETE 请求方法? -Rails 框架建议使用 REST 架构设计程序,因此除了 GET 和 POST 请求之外,还要处理 PATCH 和 DELETE 请求。但是大多数浏览器不支持从表单中提交 GET 和 POST 之外的请求。 +Rails 框架鼓励应用使用 REST 架构的设计,这意味着除了 GET 和 POST 请求,应用还要处理许多 PATCH 和 DELETE 请求。不过,大多数浏览器只支持表单的 GET 和 POST 方法,而不支持其他方法。 -为了解决这个问题,Rails 使用 POST 请求进行模拟,并在表单中加入一个名为 `_method` 的隐藏字段,其值表示真正希望使用的请求方法: +为了解决这个问题,Rails 使用 `name` 属性的值为 `_method` 的隐藏的 `input` 标签和 POST 方法来模拟其他方法,从而实现相同的效果: ```ruby form_tag(search_path, method: "patch") ``` -生成的 HTML 为: +上面的代码会生成下面的 HTML: ```html
-
- - - -
+ + + ... +
``` -处理提交的数据时,Rails 以 `_method` 的值为准,发起相应类型的请求(在这个例子中是 PATCH 请求)。 +在处理提交的数据时,Rails 会考虑 `_method` 这个特殊参数的值,并按照指定的 HTTP 方法处理请求(在本例中为 PATCH)。 + + -快速创建选择列表 --------------- +## 快速创建选择列表 -HTML 中的选择列表往往需要编写很多标记语言(每个选项都要创建一个 `option` 元素),因此最适合自动生成。 +选择列表由大量 HTML 标签组成(需要为每个选项分别创建 `option` 标签),因此最适合动态生成。 -选择列表的标记语言如下所示: +下面是选择列表的一个例子: ```html ``` -这个列表列出了一组城市名。在程序内部只需要处理各选项的 ID,因此把各选项的 `value` 属性设为 ID。下面来看一下 Rails 为我们提供了哪些帮助方法。 +这个选择列表显示了一组城市的列表,用户看到的是城市的名称,应用处理的是城市的 ID。每个 `option` 标签的 `value` 属性的值就是城市的 ID。下面我们会看到 Rails 为生成选择列表提供了哪些辅助方法。 + + ### `select` 和 `option` 标签 -最常见的帮助方法是 `select_tag`,如其名所示,其作用是生成 `select` 标签,其中可以包含一个由选项组成的字符串: +最通用的辅助方法是 `select_tag`,故名思义,这个辅助方法用于生成 `select` 标签,并在这个 `select` 标签中封装选项字符串: ```erb <%= select_tag(:city_id, '...') %> ``` -这只是个开始,还无法动态生成 `option` 标签。`option` 标签可以使用帮助方法 `options_for_select` 生成: +使用 `select_tag` 辅助方法只是第一步,仅靠它我们还无法动态生成 `option` 标签。接下来,我们可以使用 `options_for_select` 辅助方法生成 `option` 标签: ```erb <%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %> ``` -生成的 HTML 为: +输出: ```html @@ -404,21 +425,21 @@ HTML 中的选择列表往往需要编写很多标记语言(每个选项都要 ... ``` -`options_for_select` 方法的第一个参数是一个嵌套数组,每个元素都有两个子元素:选项的文本(城市名)和选项的 `value` 属性值(城市 ID)。选项的 `value` 属性值会提交到控制器中。ID 的值经常表示数据库对象,但这个例子除外。 +`options_for_select` 辅助方法的第一个参数是嵌套数组,其中每个子数组都有两个元素:选项文本(城市名称)和选项值(城市 ID)。选项值会提交给控制器。选项值通常是对应的数据库对象的 ID,但并不一定是这样。 -知道上述用法后,就可以结合 `select_tag` 和 `options_for_select` 两个方法生成所需的完整 HTML 标记: +掌握了上述知识,我们就可以联合使用 `select_tag` 和 `options_for_select` 辅助方法来动态生成选择列表了: ```erb <%= select_tag(:city_id, options_for_select(...)) %> ``` -`options_for_select` 方法还可预先选中一个选项,通过第二个参数指定: +`options_for_select` 辅助方法允许我们传递第二个参数来设置默认选项: ```erb <%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %> ``` -生成的 HTML 如下: +输出: ```html @@ -426,17 +447,22 @@ HTML 中的选择列表往往需要编写很多标记语言(每个选项都要 ... ``` -当 Rails 发现生成的选项 `value` 属性值和指定的值一样时,就会在这个选项中加上 `selected` 属性。 +当 Rails 发现生成的选项值和第二个参数指定的值一样时,就会为这个选项添加 `selected` 属性。 -TIP: `options_for_select` 方法的第二个参数必须完全和需要选中的选项 `value` 属性值相等。如果 `value` 的值是整数 2,就不能传入字符串 `"2"`,必须传入数字 `2`。注意,从 `params` 中获取的值都是字符串。 +WARNING: 如果 `select` 标签的 `required` 属性的值为 `true`,`size` 属性的值为 1,`multiple` 属性未设置为 `true`,并且未设置 `:include_blank` 或 `:prompt` 选项时,`:include_blank` 选项的值会被强制设置为 `true`。 -使用 Hash 可以为选项指定任意属性: +我们可以通过散列为选项添加任意属性: ```erb -<%= options_for_select([['Lisbon', 1, {'data-size' => '2.8 million'}], ['Madrid', 2, {'data-size' => '3.2 million'}]], 2) %> +<%= options_for_select( + [ + ['Lisbon', 1, { 'data-size' => '2.8 million' }], + ['Madrid', 2, { 'data-size' => '3.2 million' }] + ], 2 +) %> ``` -生成的 HTML 如下: +输出: ```html @@ -444,9 +470,11 @@ TIP: `options_for_select` 方法的第二个参数必须完全和需要选中的 ... ``` -### 处理模型的选择列表 + + +### 用于处理模型的选择列表 -大多数情况下,表单的控件用于处理指定的数据库模型,正如你所期望的,Rails 为此提供了很多用于生成选择列表的帮助方法。和其他表单帮助方法一样,处理模型时要去掉 `select_tag` 中的 `_tag`: +在大多数情况下,表单控件会绑定到特定的数据库模型,和我们期望的一样,Rails 为此提供了辅助方法。与其他表单辅助方法一致,在处理模型时,需要从 `select_tag` 中删除 `_tag` 后缀: ```ruby # controller: @@ -458,16 +486,16 @@ TIP: `options_for_select` 方法的第二个参数必须完全和需要选中的 <%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %> ``` -注意,第三个参数,选项数组,和传入 `options_for_select` 方法的参数一样。这种帮助方法的一个好处是,无需关心如何预先选中正确的城市,只要用户设置了所在城市,Rails 就会读取 `@person.city_id` 的值,为你代劳。 +需要注意的是,`select` 辅助方法的第三个参数,即选项数组,和传递给 `options_for_select` 辅助方法作为参数的选项数组是一样的。如果用户已经设置了默认城市,Rails 会从 `@person.city_id` 属性中读取这一设置,一切都是自动的,十分方便。 -和其他帮助方法一样,如果要在绑定到 `@person` 对象上的表单构造器上使用 `select` 方法,相应的句法为: +和其他辅助方法一样,如果要在绑定到 `@person` 对象的表单生成器上使用 `select` 辅助方法,相关句法如下: ```erb # select on a form builder <%= f.select(:city_id, ...) %> ``` -`select` 帮助方法还可接受一个代码块: +我们还可以把块传递给 `select` 辅助方法: ```erb <%= f.select(:city_id) do %> @@ -477,64 +505,77 @@ TIP: `options_for_select` 方法的第二个参数必须完全和需要选中的 <% end %> ``` -WARNING: 如果使用 `select` 方法(或类似的帮助方法,例如 `collection_select` 和 `select_tag`)处理 `belongs_to` 关联,必须传入外键名(在上例中是 `city_id`),而不是关联名。如果传入的是 `city` 而不是 `city_id`,把 `params` 传给 `Person.new` 或 `update` 方法时,会抛出异常:` ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)`。这个要求还可以这么理解,表单帮助方法只能编辑模型的属性。此外还要知道,允许用户直接编辑外键具有潜在地安全隐患。 +WARNING: 如果我们使用 `select` 辅助方法(或类似的辅助方法,如 `collection_select`、`select_tag`)来设置 `belongs_to` 关联,就必须传入外键的名称(在上面的例子中是 `city_id`),而不是关联的名称。在上面的例子中,如果传入的是 `city` 而不是 `city_id`,在把 `params` 传递给 `Person.new` 或 `update` 方法时,Active Record 会抛出 `ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)` 错误。换一个角度看,这说明表单辅助方法只能修改模型属性。我们还应该注意到允许用户直接修改外键的潜在安全后果。 + + -### 根据任意对象组成的集合创建 `option` 标签 +### 从任意对象组成的集合创建 `option` 标签 -使用 `options_for_select` 方法生成 `option` 标签必须使用数组指定各选项的文本和值。如果有个 `City` 模型,想根据模型实例组成的集合生成 `option` 标签应该怎么做呢?一种方法是遍历集合,创建一个嵌套数组: +使用 `options_for_select` 辅助方法生成 `option` 标签需要创建包含各个选项的文本和值的数组。但如果我们已经拥有 `City` 模型(可能是 Active Record 模型),并且想要从这些对象的集合生成 `option` 标签,那么应该怎么做呢?一个解决方案是创建并遍历嵌套数组: ```erb <% cities_array = City.all.map { |city| [city.name, city.id] } %> <%= options_for_select(cities_array) %> ``` -这种方法完全可行,但 Rails 提供了一个更简洁的帮助方法:`options_from_collection_for_select`。这个方法接受一个由任意对象组成的集合,以及另外两个参数:获取选项文本和值使用的方法。 +这是一个完全有效的解决方案,但 Rails 提供了一个更简洁的替代方案:`options_from_collection_for_select` 辅助方法。这个辅助方法接受一个任意对象组成的集合作为参数,以及两个附加参数,分别用于读取选项值和选项文本的方法的名称: ```erb <%= options_from_collection_for_select(City.all, :id, :name) %> ``` -从这个帮助方法的名字中可以看出,它只生成 `option` 标签。如果想生成可使用的选择列表,和 `options_for_select` 方法一样要结合 `select_tag` 方法一起使用。`select` 方法集成了 `select_tag` 和 `options_for_select` 两个方法,类似地,处理集合时,可以使用 `collection_select` 方法,它集成了 `select_tag` 和 `options_from_collection_for_select` 两个方法。 +顾名思义,`options_from_collection_for_select` 辅助方法只生成 `option` 标签。和 `options_for_select` 辅助方法一样,要想生成可用的选择列表,我们需要联合使用 `options_from_collection_for_select` 和 `select_tag` 辅助方法。在处理模型对象时,`select` 辅助方法联合使用了 `select_tag` 和 `options_for_select` 辅助方法,同样,`collection_select` 辅助方法联合使用了 `select_tag` 和 `options_from_collection_for_select` 辅助方法。 ```erb <%= collection_select(:person, :city_id, City.all, :id, :name) %> ``` -`options_from_collection_for_select` 对 `collection_select` 来说,就像 `options_for_select` 与 `select` 的关系一样。 +和其他辅助方法一样,如果要在绑定到 `@person` 对象的表单生成器上使用 `collection_select` 辅助方法,相关句法如下: -NOTE: 传入 `options_for_select` 方法的子数组第一个元素是选项文本,第二个元素是选项的值,但传入 `options_from_collection_for_select` 方法的第一个参数是获取选项值的方法,第二个才是获取选项文本的方法。 +```erb +<%= f.collection_select(:city_id, City.all, :id, :name) %> +``` + +总结一下,`options_from_collection_for_select` 对于 `collection_select` 辅助方法,就如同 `options_for_select` 对于 `select` 辅助方法。 + +NOTE: 传递给 `options_for_select` 辅助方法作为参数的嵌套数组,子数组的第一个元素是选项文本,第二个元素是选项值,然而传递给 `options_from_collection_for_select` 辅助方法作为参数的嵌套数组,子数组的第一个元素是读取选项值的方法的名称,第二个元素是读取选项文本的方法的名称。 + + ### 时区和国家选择列表 -要想在 Rails 程序中实现时区相关的功能,就得询问用户其所在的时区。设定时区时可以使用 `collection_select` 方法根据预先定义的时区对象生成一个选择列表,也可以直接使用 `time_zone_select` 帮助方法: +要想利用 Rails 提供的时区相关功能,首先需要设置用户所在的时区。为此,我们可以使用 `collection_select` 辅助方法从预定义时区对象生成选择列表,我们也可以使用更简单的 `time_zone_select` 辅助方法: ```erb <%= time_zone_select(:person, :time_zone) %> ``` -如果想定制时区列表,可使用 `time_zone_options_for_select` 帮助方法。这两个方法可接受的参数请查阅 API 文档。 +Rails 还提供了 `time_zone_options_for_select` 辅助方法用于手动生成定制的时区选择列表。关于 `time_zone_select` 和 `time_zone_options_for_select` 辅助方法的更多介绍,请参阅 [API 文档](http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-time_zone_options_for_select)。 + +Rails 的早期版本提供了用于生成国家选择列表的 `country_select` 辅助方法,现在这一功能被放入独立的 [country_select 插件](https://github.com/stefanpenner/country_select)。需要注意的是,在使用这个插件生成国家选择列表时,一些特定地区是否应该被当作国家还存在争议,这也是 Rails 不再内置这一功能的原因。 -以前 Rails 还内置了 `country_select` 帮助方法,用于创建国家选择列表,但现在已经被提取出来做成了 [country_select](https://github.com/stefanpenner/country_select) gem。使用这个 gem 时要注意,是否包含某个国家还存在争议(正因为此,Rails 才不想内置)。 + -使用日期和时间表单帮助方法 ----------------------- +## 使用日期和时间的表单辅助方法 -你可以选择不使用生成 HTML5 日期和时间输入框的帮助方法,而使用生成日期和时间选择列表的帮助方法。生成日期和时间选择列表的帮助方法和其他表单帮助方法有两个重要的不同点: +我们可以选择不使用生成 HTML5 日期和时间输入字段的表单辅助方法,而使用替代的日期和时间辅助方法。这些日期和时间辅助方法与所有其他表单辅助方法主要有两点不同: -* 日期和时间不在单个 `input` 元素中输入,而是每个时间单位都有各自的元素,因此在 `params` 中就没有单个值能表示完整的日期和时间; -* 其他帮助方法通过 `_tag` 后缀区分是独立的帮助方法还是操作模型对象的帮助方法。对日期和时间帮助方法来说,`select_date`、`select_time` 和 `select_datetime` 是独立的帮助方法,`date_select`、`time_select` 和 `datetime_select` 是相应的操作模型对象的帮助方法。 +* 日期和时间不是在单个 `input` 元素中输入,而是每个时间单位(年、月、日等)都有各自的 `input` 元素。因此在 `params` 散列中没有表示日期和时间的单个值。 +* 其他表单辅助方法使用 `_tag` 后缀区分独立的辅助方法和处理模型对象的辅助方法。对于日期和时间辅助方法,`select_date`、`select_time` 和 `select_datetime` 是独立的辅助方法,`date_select`、`time_select` 和 `datetime_select` 是对应的处理模型对象的辅助方法。 -这两类帮助方法都会为每个时间单位(年,月,日等)生成各自的选择列表。 +这两类辅助方法都会为每个时间单位(年、月、日等)生成各自的选择列表。 -### 独立的帮助方法 + -`select_*` 这类帮助方法的第一个参数是 `Date`、`Time` 或 `DateTime` 类的实例,并选中指定的日期时间。如果不指定,就使用当前日期时间。例如: +### 独立的辅助方法 + +`select_*` 这类辅助方法的第一个参数是 `Date`、`Time` 或 `DateTime` 类的实例,用于指明选中的日期时间。如果省略这个参数,选中当前的日期时间。例如: ```erb <%= select_date Date.today, prefix: :start_date %> ``` -生成的 HTML 如下(为了行为简便,省略了各选项): +上面的代码会生成下面的 HTML(为了行文简洁,省略了实际选项值): ```html @@ -542,23 +583,25 @@ NOTE: 传入 `options_for_select` 方法的子数组第一个元素是选项文 ``` -上面各控件会组成 `params[:start_date]`,其中包含名为 `:year`、`:month` 和 `:day` 的键。如果想获取 `Time` 或 `Date` 对象,要读取各时间单位的值,然后传入适当的构造方法中,例如: +上面的代码会使 `params[:start_date]` 成为拥有 `:year`、`:month` 和 `:day` 键的散列。要想得到实际的 `Date`、`Time` 或 `DateTime` 对象,我们需要提取 `params[:start_date]` 中的信息并传递给适当的构造方法,例如: ```ruby Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i) ``` -`:prefix` 选项的作用是指定从 `params` 中获取各时间组成部分的键名。在上例中,`:prefix` 选项的值是 `start_date`。如果不指定这个选项,就是用默认值 `date`。 +`:prefix` 选项用于说明从 `params` 散列中取回时间信息的键名。这个选项的默认值是 `date`,在上面的例子中被设置为 `start_date`。 + + -### 处理模型对象的帮助方法 +### 处理模型对象的辅助方法 -`select_date` 方法在更新或创建 Active Record 对象的表单中有点力不从心,因为 Active Record 期望 `params` 中的每个元素都对应一个属性。用于处理模型对象的日期和时间帮助方法会提交一个名字特殊的参数,Active Record 看到这个参数时就知道必须和其他参数结合起来传递给字段类型对应的构造方法。例如: +在更新或创建 Active Record 对象的表单中,`select_date` 辅助方法不能很好地工作,因为 Active Record 期望 `params` 散列的每个元素都对应一个模型属性。处理模型对象的日期和时间辅助方法使用特殊名称提交参数,Active Record 一看到这些参数就知道必须把这些参数和其他参数一起传递给对应字段类型的构造方法。例如: ```erb <%= date_select :person, :birth_date %> ``` -生成的 HTML 如下(为了行为简介,省略了各选项): +上面的代码会生成下面的 HTML(为了行文简洁,省略了实际选项值): ```html @@ -566,41 +609,46 @@ Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, pa ``` -创建的 `params` Hash 如下: +上面的代码会生成下面的 `params` 散列: ```ruby {'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}} ``` -传递给 `Person.new`(或 `update`)方法时,Active Record 知道这些参数应该结合在一起组成 `birth_date` 属性,使用括号中的信息决定传给 `Date.civil` 等方法的顺序。 +当把这个 `params` 散列传递给 `Person.new` 或 `update` 方法时,Active Record 会发现应该把这些参数都用于构造 `birth_date` 属性,并且会使用附加信息来确定把这些参数传递给构造方法(如 `Date.civil` 方法)的顺序。 + + ### 通用选项 -这两种帮助方法都使用同一组核心函数生成各选择列表,因此使用的选项基本一样。默认情况下,Rails 生成的年份列表包含本年前后五年。如果这个范围不能满足需求,可以使用 `:start_year` 和 `:end_year` 选项指定。更详细的可用选项列表请参阅 [API 文档](http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html)。 +这两类辅助方法使用一组相同的核心函数来生成选择列表,因此使用的选项也大体相同。特别是默认情况下,Rails 生成的年份选项会包含当前年份的前后 5 年。如果这个范围不能满足使用需求,可以使用 `:start_year` 和 `:end_year` 选项覆盖这一默认设置。关于这两类辅助方法的可用选项的更多介绍,请参阅 [API 文档](http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html)。 + +根据经验,在处理模型对象时应该使用 `date_select` 辅助方法,在其他情况下应该使用 `select_date` 辅助方法。例如在根据日期过滤搜索结果时就应该使用 `select_date` 辅助方法。 -基本原则是,使用 `date_select` 方法处理模型对象,其他情况都使用 `select_date` 方法,例如在搜索表单中根据日期过滤搜索结果。 +NOTE: 在许多情况下,内置的日期选择器显得笨手笨脚,不能帮助用户正确计算出日期和星期几之间的关系。 -NOTE: 很多时候内置的日期选择列表不太智能,不能协助用户处理日期和星期几之间的对应关系。 + -### 单个时间单位选择列表 +### 独立组件 -有时只需显示日期中的一部分,例如年份或月份。为此,Rails 提供了一系列帮助方法,分别用于创建各时间单位的选择列表:`select_year`,`select_month`,`select_day`,`select_hour`,`select_minute`,`select_second`。各帮助方法的作用一目了然。默认情况下,这些帮助方法创建的选择列表 `name` 属性都跟时间单位的名称一样,例如,`select_year` 方法创建的 `select` 元素 `name` 属性值为 `year`,`select_month` 方法创建的 `select` 元素 `name` 属性值为 `month`,不过也可使用 `:field_name` 选项指定其他值。`:prefix` 选项的作用与在 `select_date` 和 `select_time` 方法中一样,且默认值也一样。 +偶尔我们需要显示单个日期组件,例如年份或月份。为此,Rails 提供了一系列辅助方法,每个时间单位对应一个辅助方法,即 `select_year`、`select_month`、`select_day`、`select_hour`、`select_minute` 和 `select_second` 辅助方法。这些辅助方法的用法非常简单。默认情况下,它们会生成以时间单位命名的输入字段(例如,`select_year` 辅助方法生成名为“year”的输入字段,`select_month` 辅助方法生成名为“month”的输入字段),我们可以使用 `:field_name` 选项指定输入字段的名称。`:prefix` 选项的用法和在 `select_date` 和 `select_time` 辅助方法中一样,默认值也一样。 -这些帮助方法的第一个参数指定选中哪个值,可以是 `Date`、`Time` 或 `DateTime` 类的实例(会从实例中获取对应的值),也可以是数字。例如: +这些辅助方法的第一个参数可以是 `Date`、`Time` 或 `DateTime` 类的实例(会从实例中取出对应的值)或数值,用于指明选中的日期时间。例如: ```erb <%= select_year(2009) %> <%= select_year(Time.now) %> ``` -如果今年是 2009 年,那么上述两种用法生成的 HTML 是一样的。用户选择的值可以通过 `params[:date][:year]` 获取。 +如果当前年份是 2009 年,上面的代码会成生相同的 HTML。用户选择的年份可以通过 `params[:date][:year]` 取回。 -上传文件 --------- + -程序中一个常见的任务是上传某种文件,可以是用户的照片,或者 CSV 文件包含要处理的数据。处理文件上传功能时有一点要特别注意,表单的编码必须设为 `"multipart/form-data"`。如果使用 `form_for` 生成上传文件的表单,Rails 会自动加入这个编码。如果使用 `form_tag` 就得自己设置,如下例所示。 +## 上传文件 -下面这两个表单都能用于上传文件: +上传某种类型的文件是常见任务,例如上传某人的照片或包含待处理数据的 CSV 文件。在上传文件时特别需要注意的是,表单的编码必须设置为 `multipart/form-data`。使用 `form_for` 辅助方法时会自动完成这一设置。如果使用 `form_tag` 辅助方法,就必须手动完成这一设置,具体操作可以参考下面的例子。 + +下面这两个表单都用于上传文件。 ```erb <%= form_tag({action: :upload}, multipart: true) do %> @@ -612,11 +660,13 @@ NOTE: 很多时候内置的日期选择列表不太智能,不能协助用户 <% end %> ``` -像往常一样,Rails 提供了两种帮助方法:独立的 `file_field_tag` 方法和处理模型的 `file_field` 方法。这两个方法和其他帮助方法唯一的区别是不能为文件选择框指定默认值,因为这样做没有意义。正如你所期望的,`file_field_tag` 方法上传的文件在 `params[:picture]` 中,`file_field` 方法上传的文件在 `params[:person][:picture]` 中。 +Rails 同样为上传文件提供了一对辅助方法:独立的辅助方法 `file_field_tag` 和处理模型的辅助方法 `file_field`。这两个辅助方法和其他辅助方法的唯一区别是,我们无法为文件上传控件设置默认值,因为这样做没有意义。和我们期望的一样,在上述例子的第一个表单中上传的文件通过 `params[:picture]` 取回,在第二个表单中通过 `params[:person][:picture]` 取回。 + + -### 上传了什么 +### 上传的内容 -存在 `params` Hash 中的对象其实是 `IO` 的子类,根据文件大小,可能是 `StringIO` 或者是存储在临时文件中的 `File` 实例。不管是哪个类,这个对象都有 `original_filename` 属性,其值为文件在用户电脑中的文件名;还有个 `content_type` 属性,其值为上传文件的 MIME 类型。下面这段代码把上传的文件保存在 `#{Rails.root}/public/uploads` 文件夹中,文件名和原始文件名一样(假设使用前面的表单上传)。 +在上传文件时,`params` 散列中保存的文件对象实际上是 `IO` 类的子类的实例。根据上传文件大小的不同,这个实例有可能是 `StringIO` 类的实例,也可能是临时文件的 `File` 类的实例。在这两种情况下,文件对象具有 `original_filename` 属性,其值为上传的文件在用户计算机上的文件名,也具有 `content_type` 属性,其值为上传的文件的 MIME 类型。下面这段代码把上传的文件保存在 `#{Rails.root}/public/uploads` 文件夹中,文件名不变(假设使用上一节例子中的表单来上传文件)。 ```ruby def upload @@ -627,18 +677,21 @@ def upload end ``` -文件上传完毕后可以做很多操作,例如把文件存储在某个地方(服务器的硬盘,Amazon S3 等);把文件和模型关联起来;缩放图片,生成缩略图。这些复杂的操作已经超出了本文范畴。有很多代码库可以协助完成这些操作,其中两个广为人知的是 [CarrierWave](https://github.com/jnicklas/carrierwave) 和 [Paperclip](http://www.thoughtbot.com/projects/paperclip)。 +一旦文件上传完毕,就可以执行很多后续操作,例如把文件储存到磁盘、Amazon S3 等位置并和模型关联起来,缩放图片并生成缩略图等。这些复杂的操作已经超出本文的范畴,不过有一些 Ruby 库可以帮助我们完成这些操作,其中两个众所周知的是 [CarrierWave](https://github.com/jnicklas/carrierwave) 和 [Paperclip](https://github.com/thoughtbot/paperclip)。 + +NOTE: 如果用户没有选择要上传的文件,对应参数会是空字符串。 -NOTE: 如果用户没有选择文件,相应的参数为空字符串。 + -### 使用 Ajax 上传文件 +### 处理 Ajax -异步上传文件和其他类型的表单不一样,仅在 `form_for` 方法中加入 `remote: true` 选项是不够的。在 Ajax 表单中,使用浏览器中的 JavaScript 进行序列化,但是 JavaScript 无法读取硬盘中的文件,因此文件无法上传。常见的解决方法是使用一个隐藏的 `iframe` 作为表单提交的目标。 +和其他表单不同,异步上传文件的表单可不是为 `form_for` 辅助方法设置 `remote: true` 选项这么简单。在这个 Ajax 表单中,上传文件的序列化是通过浏览器端的 JavaScript 完成的,而 JavaScript 无法读取硬盘上的文件,因此文件无法上传。最常见的解决方案是使用不可见的 iframe 作为表单提交的目标。 -定制表单构造器 -------------- + -前面说过,`form_for` 和 `fields_for` 方法拽入的对象是 `FormBuilder` 或其子类的实例。表单构造器中封装了用于显示单个对象表单元素的信息。你可以使用常规的方式使用各帮助方法,也可以继承 `FormBuilder` 类,添加其他的帮助方法。例如: +## 定制表单生成器 + +前面说过,`form_for` 和 `fields_for` 辅助方法产出的对象是 `FormBuilder` 类或其子类的实例,即表单生成器。表单生成器为单个对象封装了显示表单所需的功能。我们可以用常规的方式使用表单辅助方法,也可以继承 `FormBuilder` 类并添加其他辅助方法。例如: ```erb <%= form_for @person do |f| %> @@ -654,7 +707,7 @@ NOTE: 如果用户没有选择文件,相应的参数为空字符串。 <% end %> ``` -在此之前需要定义 `LabellingFormBuilder` 类,如下所示: +在使用前需要定义 `LabellingFormBuilder` 类: ```ruby class LabellingFormBuilder < ActionView::Helpers::FormBuilder @@ -664,59 +717,62 @@ class LabellingFormBuilder < ActionView::Helpers::FormBuilder end ``` -如果经常这么使用,可以定义 `labeled_form_for` 帮助方法,自动启用 `builder: LabellingFormBuilder` 选项。 +如果经常这样使用,我们可以定义 `labeled_form_for` 辅助方法,自动应用 `builder: LabellingFormBuilder` 选项。 + +```ruby +def labeled_form_for(record, options = {}, &block) + options.merge! builder: LabellingFormBuilder + form_for record, options, &block +end +``` -所用的表单构造器还会决定执行下面这个渲染操作时会发生什么: +表单生成器还会确定进行下面的渲染时应该执行的操作: ```erb <%= render partial: f %> ``` -如果 `f` 是 `FormBuilder` 类的实例,上述代码会渲染局部视图 `form`,并把传入局部视图的对象设为表单构造器。如果表单构造器是 `LabellingFormBuilder` 类的实例,则会渲染局部视图 `labelling_form`。 +如果表单生成器 `f` 是 `FormBuilder` 类的实例,那么上面的代码会渲染局部视图 `form`,并把传入局部视图的对象设置为表单生成器。如果表单生成器 `f` 是 `LabellingFormBuilder` 类的实例,那么上面的代码会渲染局部视图 `labelling_form`。 -理解参数命名约定 --------------- + -从前几节可以看出,表单提交的数据可以直接保存在 `params` Hash 中,或者嵌套在子 Hash 中。例如,在 `Person` 模型对应控制器的 `create` 动作中,`params[:person]` 一般是一个 Hash,保存创建 `Person` 实例的所有属性。`params` Hash 中也可以保存数组,或由 Hash 组成的数组,等等。 +## 理解参数命名约定 -HTML 表单基本上不能处理任何结构化数据,提交的只是由普通的字符串组成的键值对。在程序中使用的数组参数和 Hash 参数是通过 Rails 的参数命名约定生成的。 +从前面几节我们可以看到,表单提交的数据可以保存在 `params` 散列或嵌套的子散列中。例如,在 `Person` 模型的标准 `create` 动作中,`params[:person]` 通常是储存了创建 `Person` 实例所需的所有属性的散列。`params` 散列也可以包含数组、散列构成的数组等等。 -TIP: 如果想快速试验本节中的示例,可以在控制台中直接调用 Rack 的参数解析器。例如: -T> -```ruby -TIP: Rack::Utils.parse_query "name=fred&phone=0123456789" -TIP: # => {"name"=>"fred", "phone"=>"0123456789"} -TIP: ``` +从根本上说,HTML 表单并不理解任何类型的结构化数据,表单提交的数据都是普通字符串组成的键值对。我们在应用中看到的数组和散列都是 Rails 根据参数命名约定生成的。 + + ### 基本结构 -数组和 Hash 是两种基本结构。获取 Hash 中值的方法和 `params` 一样。如果表单中包含以下控件: +数组和散列是两种基本数据结构。散列句法用于访问 `params` 中的值。例如,如果表单包含: ```html ``` -得到的 `params` 值为: +`params` 散列会包含: -```erb +```ruby {'person' => {'name' => 'Henry'}} ``` -在控制器中可以使用 `params[:person][:name]` 获取提交的值。 +在控制器中可以使用 `params[:person][:name]` 取回表单提交的值。 -Hash 可以随意嵌套,不限制层级,例如: +散列可以根据需要嵌套,不限制层级,例如: ```html ``` -得到的 `params` 值为: +`params` 散列会包含: ```ruby {'person' => {'address' => {'city' => 'New York'}}} ``` -一般情况下 Rails 会忽略重复的参数名。如果参数名中包含空的方括号(`[]`),Rails 会将其组建成一个数组。如果想让用户输入多个电话号码,在表单中可以这么做: +通常 Rails 会忽略重复的参数名。如果参数名包含一组空的方括号 `[]`,Rails 就会用这些参数的值生成一个数组。例如,要想让用户输入多个电话号码,我们可以在表单中添加: ```html @@ -724,11 +780,13 @@ Hash 可以随意嵌套,不限制层级,例如: ``` -得到的 `params[:person][:phone_number]` 就是一个数组。 +得到的 `params[:person][:phone_number]` 是包含用户输入的电话号码的数组。 -### 结合在一起使用 + -上述命名约定可以结合起来使用,让 `params` 的某个元素值为数组(如前例),或者由 Hash 组成的数组。例如,使用下面的表单控件可以填写多个地址: +### 联合使用 + +我们可以联合使用数组和散列。散列的元素可以是前面例子中那样的数组,也可以是散列构成的数组。例如,通过重复使用下面的表单控件我们可以添加任意长度的多行地址: ```html @@ -736,17 +794,19 @@ Hash 可以随意嵌套,不限制层级,例如: ``` -得到的 `params[:addresses]` 值是一个由 Hash 组成的数组,Hash 中的键包括 `line1`、`line2` 和 `city`。如果 Rails 发现输入框的 `name` 属性值已经存在于当前 Hash 中,就会新建一个 Hash。 +得到的 `params[:addresses]` 是散列构成的数组,散列的键包括 `line1`、`line2` 和 `city`。如果 Rails 发现输入控件的名称已经存在于当前散列的键中,就会新建一个散列。 + +不过还有一个限制,尽管散列可以任意嵌套,但数组只能有一层。数组通常可以用散列替换。例如,模型对象的数组可以用以模型对象 ID 、数组索引或其他参数为键的散列替换。 -不过有个限制,虽然 Hash 可以嵌套任意层级,但数组只能嵌套一层。如果需要嵌套多层数组,可以使用 Hash 实现。例如,如果想创建一个包含模型对象的数组,可以创建一个 Hash,以模型对象的 ID、数组索引或其他参数为键。 +WARNING: 数组参数在 `check_box` 辅助方法中不能很好地工作。根据 HTML 规范,未选中的复选框不提交任何值。然而,未选中的复选框也提交值往往会更容易处理。为此,`check_box` 辅助方法通过创建辅助的同名隐藏 `input` 元素来模拟这一行为。如果复选框未选中,只有隐藏的 `input` 元素的值会被提交;如果复选框被选中,复选框本身的值和隐藏的 `input` 元素的值都会被提交,但复选框本身的值优先级更高。在处理数组参数时,这样的重复提交会把 Rails 搞糊涂,因为 Rails 无法确定什么时候创建新的数组元素。这种情况下,我们可以使用 `check_box_tag` 辅助方法,或者用散列代替数组。 -WARNING: 数组类型参数不能很好的在 `check_box` 帮助方法中使用。根据 HTML 规范,未选中的复选框不应该提交值。但是不管是否选中都提交值往往更便于处理。为此 `check_box` 方法额外创建了一个同名的隐藏 `input` 元素。如果没有选中复选框,只会提交隐藏 `input` 元素的值,如果选中则同时提交两个值,但复选框的值优先级更高。处理数组参数时重复提交相同的参数会让 Rails 迷惑,因为对 Rails 来说,见到重复的 `input` 值,就会创建一个新数组元素。所以更推荐使用 `check_box_tag` 方法,或者用 Hash 代替数组。 + -### 使用表单帮助方法 +### 使用表单辅助方法 -前面几节并没有使用 Rails 提供的表单帮助方法。你可以自己创建 `input` 元素的 `name` 属性,然后直接将其传递给 `text_field_tag` 等帮助方法。但是 Rails 提供了更高级的支持。本节介绍 `form_for` 和 `fields_for` 方法的 `name` 参数以及 `:index` 选项。 +在前面两节中我们没有使用 Rails 表单辅助方法。尽管我们可以手动为 `input` 元素命名,然后直接把它们传递给 `text_field_tag` 这类辅助方法,但 Rails 支持更高级的功能。我们可以使用 `form_for` 和 `fields_for` 辅助方法的 `name` 参数以及 `:index` 选项。 -你可能会想编写一个表单,其中有很多字段,用于编辑某人的所有地址。例如: +假设我们想要渲染一个表单,用于修改某人地址的各个字段。例如: ```erb <%= form_for @person do |person_form| %> @@ -759,7 +819,7 @@ WARNING: 数组类型参数不能很好的在 `check_box` 帮助方法中使用 <% end %> ``` -假设这个人有两个地址,ID 分别为 23 和 45。那么上述代码生成的 HTML 如下: +如果某人有两个地址,ID 分别为 23 和 45,那么上面的代码会生成下面的 HTML: ```html
@@ -769,15 +829,15 @@ WARNING: 数组类型参数不能很好的在 `check_box` 帮助方法中使用
``` -得到的 `params` Hash 如下: +得到的 `params` 散列会包含: ```ruby {'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}} ``` -Rails 之所以知道这些输入框中的值是 `person` Hash 的一部分,是因为我们在第一个表单构造器上调用了 `fields_for` 方法。指定 `:index` 选项的目的是告诉 Rails,其中的输入框 `name` 属性值不是 `person[address][city]`,而要在 `address` 和 `city` 索引之间插入 `:index` 选项对应的值(放入方括号中)。这么做很有用,因为便于分辨要修改的 `Address` 记录是哪个。`:index` 选项的值可以是具有其他意义的数字、字符串,甚至是 `nil`(此时会新建一个数组参数)。 +Rails 之所以知道这些输入控件的值是 `person` 散列的一部分,是因为我们在第一个表单生成器上调用了 `fields_for` 辅助方法。指定 `:index` 选项是为了告诉 Rails,不要把输入控件命名为 `person[address][city]`,而要在 `address` 和 `city` 之间插入索引(放在 `[]` 中)。这样要想确定需要修改的 `Address` 记录就变得很容易,因此往往也很有用。`:index` 选项的值还可以是其他重要数字、字符串甚至 `nil`(使用 `nil` 时会创建数组参数)。 -如果想创建更复杂的嵌套,可以指定 `name` 属性的第一部分(前例中的 `person[address]`): +要想创建更复杂的嵌套,我们可以显式指定输入控件名称的 `name` 参数(在上面的例子中是 `person[address]`): ```erb <%= fields_for 'person[address][primary]', address, index: address do |address_form| %> @@ -785,15 +845,15 @@ Rails 之所以知道这些输入框中的值是 `person` Hash 的一部分, <% end %> ``` -生成的 HTML 如下: +上面的代码会生成下面的 HTML: ```html ``` -一般来说,最终得到的 `name` 属性值是 `fields_for` 或 `form_for` 方法的第一个参数加 `:index` 选项的值再加属性名。`:index` 选项也可直接传给 `text_field` 等帮助方法,但在表单构造器中指定可以避免代码重复。 +一般来说,输入控件的最终名称是 `fields_for` 或 `form_for` 辅助方法的 `name` 参数,加上 `:index` 选项的值,再加上属性名。我们也可以直接把 `:index` 选项传递给 `text_field` 这样的辅助方法作为参数,但在表单生成器中指定这个选项比在输入控件中分别指定这个选项要更为简洁。 -为了简化句法,还可以不使用 `:index` 选项,直接在第一个参数后面加上 `[]`。这么做和指定 `index: address` 选项的作用一样,因此下面这段代码 +还有一种简易写法,可以在 `name` 参数后加上 `[]` 并省略 `:index` 选项。这种简易写法和指定 `index: address` 选项的效果是一样的: ```erb <%= fields_for 'person[address][primary][]', address do |address_form| %> @@ -801,29 +861,29 @@ Rails 之所以知道这些输入框中的值是 `person` Hash 的一部分, <% end %> ``` -生成的 HTML 和前面一样。 +上面的代码生成的 HTML 和前一个例子完全相同。 + -处理外部资源的表单 ----------------- +## 处理外部资源的表单 -如果想把数据提交到外部资源,还是可以使用 Rails 提供的表单帮助方法。但有时需要为这些资源创建 `authenticity_token`。做法是把 `authenticity_token: 'your_external_token'` 作为选项传递给 `form_tag` 方法: +Rails 表单辅助方法也可用于创建向外部资源提交数据的表单。不过,有时我们需要为这些外部资源设置 `authenticity_token`,具体操作是为 `form_tag` 辅助方法设置 `authenticity_token: 'your_external_token'` 选项: ```erb -<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token') do %> +<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %> Form contents <% end %> ``` -提交到外部资源的表单,其中可包含的字段有时受 API 的限制,例如支付网关。所有可能不用生成隐藏的 `authenticity_token` 字段,此时把 `:authenticity_token` 选项设为 `false` 即可: +在向外部资源(例如支付网关)提交数据时,有时表单中可用的字段会受到外部 API 的限制,并且不需要生成 `authenticity_token`。通过设置 `authenticity_token: false` 选项即可禁用 `authenticity_token`。 ```erb -<%= form_tag 'http://farfar.away/form', authenticity_token: false) do %> +<%= form_tag 'http://farfar.away/form', authenticity_token: false do %> Form contents <% end %> ``` -以上技术也可用在 `form_for` 方法中: +相同的技术也可用于 `form_for` 辅助方法: ```erb <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %> @@ -831,7 +891,7 @@ Rails 之所以知道这些输入框中的值是 `person` Hash 的一部分, <% end %> ``` -如果不想生成 `authenticity_token` 字段,可以这么做: +或者,如果想要禁用 `authenticity_token`: ```erb <%= form_for @invoice, url: external_url, authenticity_token: false do |f| %> @@ -839,31 +899,36 @@ Rails 之所以知道这些输入框中的值是 `person` Hash 的一部分, <% end %> ``` -编写复杂的表单 ------------- + + +## 创建复杂表单 -很多程序已经复杂到在一个表单中编辑一个对象已经无法满足需求了。例如,创建 `Person` 对象时还想让用户在同一个表单中创建多个地址(家庭地址,工作地址,等等)。以后编辑这个 `Person` 时,还想让用户根据需要添加、删除或修改地址。 +许多应用可不只是在表单中修改单个对象这样简单。例如,在创建 `Person` 模型的实例时,我们可能还想让用户在同一个表单中创建多条地址记录(如家庭地址、单位地址等)。之后在修改 `Person` 模型的实例时,用户应该能够根据需要添加、删除或修改地址。 -### 设置模型 + -Active Record 为此种需求在模型中提供了支持,通过 `accepts_nested_attributes_for` 方法实现: +### 配置模型 + +为此,Active Record 通过 `accepts_nested_attributes_for` 方法在模型层面提供支持: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses end -class Address < ActiveRecord::Base +class Address < ApplicationRecord belongs_to :person end ``` -这段代码会在 `Person` 对象上创建 `addresses_attributes=` 方法,用于创建、更新和删除地址(可选操作)。 +上面的代码会在 `Person` 模型上创建 `addresses_attributes=` 方法,用于创建、更新或删除地址。 + + ### 嵌套表单 -使用下面的表单可以创建 `Person` 对象及其地址: +通过下面的表单我们可以创建 `Person` 模型的实例及其关联的地址: ```erb <%= form_for @person do |f| %> @@ -883,7 +948,7 @@ end <% end %> ``` -如果关联支持嵌套属性,`fields_for` 方法会为关联中的每个元素执行一遍代码块。如果没有地址,就不执行代码块。一般的作法是在控制器中构建一个或多个空的子属性,这样至少会有一组字段显示出来。下面的例子会在新建 `Person` 对象的表单中显示两组地址字段。 +如果关联支持嵌套属性,`fields_for` 方法会为关联中的每个元素执行块。如果 `Person` 模型的实例没有关联地址,就不会显示地址字段。一般的做法是构建一个或多个空的子属性,这样至少会显示一组字段。下面的例子会在新建 `Person` 模型实例的表单中显示两组地址字段。 ```ruby def new @@ -892,7 +957,7 @@ def new end ``` -`fields_for` 方法拽入一个表单构造器,参数的名字就是 `accepts_nested_attributes_for` 方法期望的。例如,如果用户填写了两个地址,提交的参数如下: +`fields_for` 辅助方法会产出表单生成器,而 `accepts_nested_attributes_for` 方法需要参数名。例如,当创建具有两个地址的 `Person` 模型的实例时,表单提交的参数如下: ```ruby { @@ -912,13 +977,15 @@ end } ``` -`:addresses_attributes` Hash 的键是什么不重要,但至少不能相同。 +`:addresses_attributes` 散列的键是什么并不重要,只要每个地址的键互不相同即可。 + +如果关联对象在数据库中已存在,`fields_for` 方法会使用这个对象的 ID 自动生成隐藏输入字段。通过设置 `include_id: false` 选项可以禁止自动生成隐藏输入字段。如果自动生成的隐藏输入字段位置不对,导致 HTML 无效,或者 ORM 中子对象不存在 ID,那么我们就应该禁止自动生成隐藏输入字段。 -如果关联的对象已经存在于数据库中,`fields_for` 方法会自动生成一个隐藏字段,`value` 属性的值为记录的 `id`。把 `include_id: false` 选项传递给 `fields_for` 方法可以禁止生成这个隐藏字段。如果自动生成的字段位置不对,导致 HTML 无法通过验证,或者在 ORM 关系中子对象不存在 `id` 字段,就可以禁止自动生成这个隐藏字段。 + -### 控制器端 +### 控制器 -像往常一样,参数传递给模型之前,在控制器中要[过滤参数](action_controller_overview.html#strong-parameters): +照例,我们需要在控制器中[把参数列入白名单](action_controller_overview.html#strong-parameters),然后再把参数传递给模型: ```ruby def create @@ -932,18 +999,20 @@ private end ``` + + ### 删除对象 -如果允许用户删除关联的对象,可以把 `allow_destroy: true` 选项传递给 `accepts_nested_attributes_for` 方法: +通过为 `accepts_nested_attributes_for` 方法设置 `allow_destroy: true` 选项,用户就可以删除关联对象。 ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses, allow_destroy: true end ``` -如果属性组成的 Hash 中包含 `_destroy` 键,且其值为 `1` 或 `true`,就会删除对象。下面这个表单允许用户删除地址: +如果对象属性散列包含 `_destroy` 键并且值为 1,这个对象就会被删除。下面的表单允许用户删除地址: ```erb <%= form_for @person do |f| %> @@ -951,7 +1020,7 @@ end
    <%= f.fields_for :addresses do |addresses_form| %>
  • - <%= addresses_form.check_box :_destroy%> + <%= addresses_form.check_box :_destroy %> <%= addresses_form.label :kind %> <%= addresses_form.text_field :kind %> ... @@ -961,7 +1030,7 @@ end <% end %> ``` -别忘了修改控制器中的参数白名单,允许使用 `_destroy`: +别忘了在控制器中更新参数白名单,添加 `_destroy` 字段。 ```ruby def person_params @@ -970,19 +1039,23 @@ def person_params end ``` -### 避免创建空记录 + + +### 防止创建空记录 -如果用户没有填写某些字段,最好将其忽略。此功能可以通过 `accepts_nested_attributes_for` 方法的 `:reject_if` 选项实现,其值为 Proc 对象。这个 Proc 对象会在通过表单提交的每一个属性 Hash 上调用。如果返回值为 `false`,Active Record 就不会为这个 Hash 构建关联对象。下面的示例代码只有当 `kind` 属性存在时才尝试构建地址对象: +通常我们需要忽略用户没有填写的字段。要实现这个功能,我们可以为 `accepts_nested_attributes_for` 方法设置 `:reject_if` 选项,这个选项的值是一个 Proc 对象。在表单提交每个属性散列时都会调用这个 Proc 对象。当 Proc 对象的返回值为 `true` 时,Active Record 不会为这个属性 Hash 创建关联对象。在下面的例子中,当设置了 `kind` 属性时,Active Record 才会创建地址: ```ruby -class Person < ActiveRecord::Base +class Person < ApplicationRecord has_many :addresses accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?} end ``` -为了方便,可以把 `reject_if` 选项的值设为 `:all_blank`,此时创建的 Proc 会拒绝为 `_destroy` 之外其他属性都为空的 Hash 构建对象。 +方便起见,我们可以把 `:reject_if` 选项的值设为 `:all_blank`,此时创建的 Proc 对象会拒绝为除 `_destroy` 之外的其他属性都为空的属性散列创建关联对象。 + + ### 按需添加字段 -我们往往不想事先显示多组字段,而是当用户点击“添加新地址”按钮后再显示。Rails 并没有内建这种功能。生成新的字段时要确保关联数组的键是唯一的,一般可在 JavaScript 中使用当前时间。 +有时,与其提前显示多组字段,倒不如等用户点击“添加新地址”按钮后再添加。Rails 没有内置这种功能。在生成这些字段时,我们必须保证关联数组的键是唯一的,这种情况下通常会使用 JavaScript 的当前时间(从 1970 年 1 月 1 日午夜开始经过的毫秒数)。 diff --git a/source/zh-CN/generators.md b/source/zh-CN/generators.md index 93fb5ee..5ce9468 100644 --- a/source/zh-CN/generators.md +++ b/source/zh-CN/generators.md @@ -1,69 +1,72 @@ -Creating and Customizing Rails Generators & Templates -===================================================== +# 创建及定制 Rails 生成器和模板 -Rails generators are an essential tool if you plan to improve your workflow. With this guide you will learn how to create generators and customize existing ones. +如果你打算改进自己的工作流程,Rails 生成器是必备工具。本文教你创建及定制生成器的方式。 -After reading this guide, you will know: +读完本文后,您将学到: -* How to see which generators are available in your application. -* How to create a generator using templates. -* How Rails searches for generators before invoking them. -* How to customize your scaffold by creating new generators. -* How to customize your scaffold by changing generator templates. -* How to use fallbacks to avoid overwriting a huge set of generators. -* How to create an application template. +* 如何查看应用中有哪些生成器可用; +* 如何使用模板创建生成器; +* 在调用生成器之前,Rails 如何搜索生成器; +* Rails 内部如何使用模板生成 Rails 代码; +* 如何通过创建新生成器定制脚手架; +* 如何通过修改生成器模板定制脚手架; +* 如何使用后备机制防范覆盖大量生成器; +* 如何创建应用模板。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -First Contact -------------- + -When you create an application using the `rails` command, you are in fact using a Rails generator. After that, you can get a list of all available generators by just invoking `rails generate`: +## 第一次接触 -```bash +使用 `rails` 命令创建应用时,使用的其实就是一个 Rails 生成器。创建应用之后,可以使用 `rails generator` 命令列出全部可用的生成器: + +```sh $ rails new myapp $ cd myapp $ bin/rails generate ``` -You will get a list of all generators that comes with Rails. If you need a detailed description of the helper generator, for example, you can simply do: +你会看到 Rails 自带的全部生成器。如果想查看生成器的详细描述,比如说 `helper` 生成器,可以这么做: -```bash +```sh $ bin/rails generate helper --help ``` -Creating Your First Generator ------------------------------ + + +## 创建首个生成器 -Since Rails 3.0, generators are built on top of [Thor](https://github.com/erikhuda/thor). Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`. +自 Rails 3.0 起,生成器使用 [Thor](https://github.com/erikhuda/thor) 构建。Thor 提供了强大的解析选项和处理文件的丰富 API。举个例子。我们来构建一个生成器,在 `config/initializers` 目录中创建一个名为 `initializer.rb` 的初始化脚本。 -The first step is to create a file at `lib/generators/initializer_generator.rb` with the following content: +第一步是创建 `lib/generators/initializer_generator.rb` 文件,写入下述内容: ```ruby class InitializerGenerator < Rails::Generators::Base def create_initializer_file - create_file "config/initializers/initializer.rb", "# Add initialization content here" + create_file "config/initializers/initializer.rb", "# 这里是初始化文件的内容" end end ``` -NOTE: `create_file` is a method provided by `Thor::Actions`. Documentation for `create_file` and other Thor methods can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html) +NOTE: `create_file` 是 `Thor::Actions` 提供的一个方法。`create_file` 即其他 Thor 方法的文档参见 [Thor 的文档](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html)。 -Our new generator is quite simple: it inherits from `Rails::Generators::Base` and has one method definition. When a generator is invoked, each public method in the generator is executed sequentially in the order that it is defined. Finally, we invoke the `create_file` method that will create a file at the given destination with the given content. If you are familiar with the Rails Application Templates API, you'll feel right at home with the new generators API. -To invoke our new generator, we just need to do: +这个生成器相当简单:继承自 `Rails::Generators::Base`,定义了一个方法。调用生成器时,生成器中的公开方法按照定义的顺序依次执行。最后,我们调用 `create_file` 方法在指定的位置创建一个文件,写入指定的内容。如果你熟悉 Rails Application Templates API,对这个生成器 API 就不会感到陌生。 -```bash +若想调用这个生成器,只需这么做: + +```sh $ bin/rails generate initializer ``` -Before we go on, let's see our brand new generator description: +在继续之前,先看一下这个生成器的描述: -```bash +```sh $ bin/rails generate initializer --help ``` -Rails is usually able to generate good descriptions if a generator is namespaced, as `ActiveRecord::Generators::ModelGenerator`, but not in this particular case. We can solve this problem in two ways. The first one is calling `desc` inside our generator: +如果把生成器放在命名空间里(如 `ActiveRecord::Generators::ModelGenerator`),Rails 通常能生成好的描述,但这里没有。这一问题有两个解决方法。第一个是,在生成器中调用 `desc`: ```ruby class InitializerGenerator < Rails::Generators::Base @@ -74,14 +77,15 @@ class InitializerGenerator < Rails::Generators::Base end ``` -Now we can see the new description by invoking `--help` on the new generator. The second way to add a description is by creating a file named `USAGE` in the same directory as our generator. We are going to do that in the next step. +现在,调用生成器时指定 `--help` 选项便能看到刚添加的描述。添加描述的第二个方法是,在生成器所在的目录中创建一个名为 `USAGE` 的文件。下一节将这么做。 + + -Creating Generators with Generators ------------------------------------ +## 使用生成器创建生成器 -Generators themselves have a generator: +生成器本身也有一个生成器: -```bash +```sh $ bin/rails generate generator initializer create lib/generators/initializer create lib/generators/initializer/initializer_generator.rb @@ -89,7 +93,7 @@ $ bin/rails generate generator initializer create lib/generators/initializer/templates ``` -This is the generator just created: +下述代码是这个生成器生成的: ```ruby class InitializerGenerator < Rails::Generators::NamedBase @@ -97,25 +101,25 @@ class InitializerGenerator < Rails::Generators::NamedBase end ``` -First, notice that we are inheriting from `Rails::Generators::NamedBase` instead of `Rails::Generators::Base`. This means that our generator expects at least one argument, which will be the name of the initializer, and will be available in our code in the variable `name`. +首先注意,我们继承的是 `Rails::Generators::NamedBase`,而不是 `Rails::Generators::Base`。这表明,我们的生成器至少需要一个参数,即初始化脚本的名称,在代码中通过 `name` 变量获取。 -We can see that by invoking the description of this new generator (don't forget to delete the old generator file): +查看这个生成器的描述可以证实这一点(别忘了删除旧的生成器文件): -```bash +```sh $ bin/rails generate initializer --help Usage: rails generate initializer NAME [options] ``` -We can also see that our new generator has a class method called `source_root`. This method points to where our generator templates will be placed, if any, and by default it points to the created directory `lib/generators/initializer/templates`. +还能看到,这个生成器有个名为 `source_root` 的类方法。这个方法指向生成器模板(如果有的话)所在的位置,默认是生成的 `lib/generators/initializer/templates` 目录。 -In order to understand what a generator template means, let's create the file `lib/generators/initializer/templates/initializer.rb` with the following content: +为了弄清生成器模板的作用,下面创建 `lib/generators/initializer/templates/initializer.rb` 文件,写入下述内容: ```ruby # Add initialization content here ``` -And now let's change the generator to copy this template when invoked: +然后修改生成器,调用时复制这个模板: ```ruby class InitializerGenerator < Rails::Generators::NamedBase @@ -127,36 +131,39 @@ class InitializerGenerator < Rails::Generators::NamedBase end ``` -And let's execute our generator: +下面执行这个生成器: -```bash +```sh $ bin/rails generate initializer core_extensions ``` -We can see that now an initializer named core_extensions was created at `config/initializers/core_extensions.rb` with the contents of our template. That means that `copy_file` copied a file in our source root to the destination path we gave. The method `file_name` is automatically created when we inherit from `Rails::Generators::NamedBase`. +可以看到,这个命令生成了 `config/initializers/core_extensions.rb` 文件,里面的内容与模板中一样。这表明,`copy_file` 方法的作用是把源根目录中的文件复制到指定的目标路径。`file_name` 方法是继承自 `Rails::Generators::NamedBase` 之后自动创建的。 -The methods that are available for generators are covered in the [final section](#generator-methods) of this guide. +生成器中可用的方法在本章[最后一节](#generator-methods)说明。 -Generators Lookup ------------------ + -When you run `rails generate initializer core_extensions` Rails requires these files in turn until one is found: +## 查找生成器 -```bash +执行 `rails generate initializer core_extensions` 命令时,Rails 按照下述顺序引入文件,直到找到所需的生成器为止: + +``` rails/generators/initializer/initializer_generator.rb generators/initializer/initializer_generator.rb rails/generators/initializer_generator.rb generators/initializer_generator.rb ``` -If none is found you get an error message. +如果最后找不到,显示一个错误消息。 + +TIP: 上述示例把文件放在应用的 `lib` 目录中,因为这个目录在 `$LOAD_PATH` 中。 + -INFO: The examples above put files under the application's `lib` because said directory belongs to `$LOAD_PATH`. + -Customizing Your Workflow -------------------------- +## 定制工作流程 -Rails own generators are flexible enough to let you customize scaffolding. They can be configured in `config/application.rb`, these are some defaults: +Rails 自带的生成器十分灵活,可以定制脚手架。生成器在 `config/application.rb` 文件中配置,下面是一些默认值: ```ruby config.generators do |g| @@ -166,9 +173,9 @@ config.generators do |g| end ``` -Before we customize our workflow, let's first see what our scaffold looks like: +在定制工作流程之前,先看看脚手架是什么: -```bash +```sh $ bin/rails generate scaffold User name:string invoke active_record create db/migrate/20130924151154_create_users.rb @@ -191,23 +198,29 @@ $ bin/rails generate scaffold User name:string create test/controllers/users_controller_test.rb invoke helper create app/helpers/users_helper.rb - invoke test_unit - create test/helpers/users_helper_test.rb invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/users.js.coffee + create app/assets/javascripts/users.coffee invoke scss - create app/assets/stylesheets/users.css.scss + create app/assets/stylesheets/users.scss invoke scss - create app/assets/stylesheets/scaffolds.css.scss + create app/assets/stylesheets/scaffolds.scss ``` -Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. +通过上述输出不难看出 Rails 3.0 及以上版本中生成器的工作方式。脚手架生成器其实什么也不生成,只是调用其他生成器。因此,我们可以添加、替换和删除任何生成器。例如,脚手架生成器调用了 scaffold_controller 生成器,而它调用了 erb、test_unit 和 helper 生成器。因为各个生成器的职责单一,所以可以轻易复用,从而避免代码重复。 -Our first customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds. We can achieve that by changing our configuration to the following: +使用脚手架生成资源时,如果不想生成默认的 `app/assets/stylesheets/scaffolds.scss` 文件,可以禁用 `scaffold_stylesheet`: + +```ruby + config.generators do |g| + g.scaffold_stylesheet false + end +``` + +其次,我们可以不让脚手架生成样式表、JavaScript 和测试固件文件。为此,我们要像下面这样修改配置: ```ruby config.generators do |g| @@ -219,11 +232,11 @@ config.generators do |g| end ``` -If we generate another resource with the scaffold generator, we can see that stylesheet, JavaScript and fixture files are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators. +如果再使用脚手架生成器生成一个资源,你会看到,它不再创建样式表、JavaScript 和固件文件了。如果想进一步定制,例如使用 DataMapper 和 RSpec 替换 Active Record 和 TestUnit,只需添加相应的 gem,然后配置生成器。 -To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator within the rails namespace, as this is where rails searches for generators used as hooks: +下面举个例子。我们将创建一个辅助方法生成器,添加一些实例变量读值方法。首先,在 rails 命名空间(Rails 在这里搜索作为钩子的生成器)中创建一个生成器: -```bash +```sh $ bin/rails generate generator rails/my_helper create lib/generators/rails/my_helper create lib/generators/rails/my_helper/my_helper_generator.rb @@ -231,9 +244,7 @@ $ bin/rails generate generator rails/my_helper create lib/generators/rails/my_helper/templates ``` -After that, we can delete both the `templates` directory and the `source_root` -class method call from our new generator, because we are not going to need them. -Add the method below, so our generator looks like the following: +然后,把 `templates` 目录和 `source_root` 类方法删除,因为用不到。然后添加下述方法,此时生成器如下所示: ```ruby # lib/generators/rails/my_helper/my_helper_generator.rb @@ -248,14 +259,14 @@ end end ``` -We can try out our new generator by creating a helper for products: +下面为 products 创建一个辅助方法,试试这个新生成器: -```bash +```sh $ bin/rails generate my_helper products create app/helpers/products_helper.rb ``` -And it will generate the following helper file in `app/helpers`: +上述命令会在 `app/helpers` 目录中生成下述辅助方法文件: ```ruby module ProductsHelper @@ -263,7 +274,7 @@ module ProductsHelper end ``` -Which is what we expected. We can now tell scaffold to use our new helper generator by editing `config/application.rb` once again: +这正是我们预期的。接下来再次编辑 `config/application.rb`,告诉脚手架使用这个新辅助方法生成器: ```ruby config.generators do |g| @@ -276,20 +287,20 @@ config.generators do |g| end ``` -and see it in action when invoking the generator: +然后调用这个生成器,实测一下: -```bash +```sh $ bin/rails generate scaffold Article body:text [...] invoke my_helper create app/helpers/articles_helper.rb ``` -We can notice on the output that our new helper was invoked instead of the Rails default. However one thing is missing, which is tests for our new generator and to do that, we are going to reuse old helpers test generators. +从输出中可以看出,Rails 调用了这个新辅助方法生成器,而不是默认的那个。不过,少了点什么:没有生成测试。我们将复用旧的辅助方法生成器测试。 -Since Rails 3.0, this is easy to do due to the hooks concept. Our new helper does not need to be focused in one specific test framework, it can simply provide a hook and a test framework just needs to implement this hook in order to be compatible. +自 Rails 3.0 起,测试很容易,因为有了钩子。辅助方法无需限定于特定的测试框架,只需提供一个钩子,让测试框架实现钩子即可。 -To do that, we can change the generator this way: +为此,我们可以按照下述方式修改生成器: ```ruby # lib/generators/rails/my_helper/my_helper_generator.rb @@ -306,29 +317,30 @@ end end ``` -Now, when the helper generator is invoked and TestUnit is configured as the test framework, it will try to invoke both `Rails::TestUnitGenerator` and `TestUnit::MyHelperGenerator`. Since none of those are defined, we can tell our generator to invoke `TestUnit::Generators::HelperGenerator` instead, which is defined since it's a Rails generator. To do that, we just need to add: +现在,如果再调用这个辅助方法生成器,而且配置的测试框架是 TestUnit,它会调用 `Rails::TestUnitGenerator` 和 `TestUnit::MyHelperGenerator`。这两个生成器都没定义,我们可以告诉生成器去调用 `TestUnit::Generators::HelperGenerator`。这个生成器是 Rails 自带的。为此,我们只需添加: ```ruby -# Search for :helper instead of :my_helper +# 搜索 :helper,而不是 :my_helper hook_for :test_framework, as: :helper ``` -And now you can re-run scaffold for another resource and see it generating tests as well! +现在,你可以使用脚手架再生成一个资源,你会发现它生成了测试。 -Customizing Your Workflow by Changing Generators Templates ----------------------------------------------------------- + -In the step above we simply wanted to add a line to the generated helper, without adding any extra functionality. There is a simpler way to do that, and it's by replacing the templates of already existing generators, in that case `Rails::Generators::HelperGenerator`. +## 通过修改生成器模板定制工作流程 -In Rails 3.0 and above, generators don't just look in the source root for templates, they also search for templates in other paths. And one of them is `lib/templates`. Since we want to customize `Rails::Generators::HelperGenerator`, we can do that by simply making a template copy inside `lib/templates/rails/helper` with the name `helper.rb`. So let's create that file with the following content: +前面我们只想在生成的辅助方法中添加一行代码,而不增加额外的功能。为此有种更为简单的方式:替换现有生成器的模板。这里要替换的是 `Rails::Generators::HelperGenerator` 的模板。 -```erb +在 Rails 3.0 及以上版本中,生成器搜索模板时不仅查看源根目录,还会在其他路径中搜索模板。其中一个是 `lib/templates`。我们要定制的是 `Rails::Generators::HelperGenerator`,因此可以在 `lib/templates/rails/helper` 目录中放一个模板副本,名为 `helper.rb`。创建这个文件,写入下述内容: + +```ruby module <%= class_name %>Helper attr_reader :<%= plural_name %>, :<%= plural_name.singularize %> end ``` -and revert the last change in `config/application.rb`: +然后撤销之前对 `config/application.rb` 文件的修改: ```ruby config.generators do |g| @@ -340,14 +352,29 @@ config.generators do |g| end ``` -If you generate another resource, you can see that we get exactly the same result! This is useful if you want to customize your scaffold templates and/or layout by just creating `edit.html.erb`, `index.html.erb` and so on inside `lib/templates/erb/scaffold`. +再生成一个资源,你将看到,得到的结果完全一样。如果你想定制脚手架模板和(或)布局,只需在 `lib/templates/erb/scaffold` 目录中创建 `edit.html.erb`、`index.html.erb`,等等。 -Adding Generators Fallbacks ---------------------------- +Rails 的脚手架模板经常使用 ERB 标签,这些标签要转义,这样生成的才是有效的 ERB 代码。 -One last feature about generators which is quite useful for plugin generators is fallbacks. For example, imagine that you want to add a feature on top of TestUnit like [shoulda](https://github.com/thoughtbot/shoulda) does. Since TestUnit already implements all generators required by Rails and shoulda just wants to overwrite part of it, there is no need for shoulda to reimplement some generators again, it can simply tell Rails to use a `TestUnit` generator if none was found under the `Shoulda` namespace. +例如,在模板中要像下面这样转义 ERB 标签(注意多了个 %): -We can easily simulate this behavior by changing our `config/application.rb` once again: +```erb +<%%= stylesheet_include_tag :application %> +``` + +生成的内容如下: + +```erb +<%= stylesheet_include_tag :application %> +``` + + + +## 为生成器添加后备机制 + +生成器最后一个相当有用的功能是插件生成器的后备机制。比如说我们想在 TestUnit 的基础上添加类似 [shoulda](https://github.com/thoughtbot/shoulda) 的功能。因为 TestUnit 已经实现了 Rails 所需的全部生成器,而 shoulda 只是覆盖其中部分,所以 shoulda 没必要重新实现某些生成器。相反,shoulda 可以告诉 Rails,在 `Shoulda` 命名空间中找不到某个生成器时,使用 `TestUnit` 中的生成器。 + +我们可以再次修改 `config/application.rb` 文件,模拟这种行为: ```ruby config.generators do |g| @@ -357,14 +384,14 @@ config.generators do |g| g.stylesheets false g.javascripts false - # Add a fallback! + # 添加后备机制 g.fallbacks[:shoulda] = :test_unit end ``` -Now, if you create a Comment scaffold, you will see that the shoulda generators are being invoked, and at the end, they are just falling back to TestUnit generators: +现在,使用脚手架生成 Comment 资源时,你会看到调用了 shoulda 生成器,而它调用的其实是 TestUnit 生成器: -```bash +```sh $ bin/rails generate scaffold Comment body:text invoke active_record create db/migrate/20130924143118_create_comments.rb @@ -387,23 +414,22 @@ $ bin/rails generate scaffold Comment body:text create test/controllers/comments_controller_test.rb invoke my_helper create app/helpers/comments_helper.rb - invoke shoulda - create test/helpers/comments_helper_test.rb invoke jbuilder create app/views/comments/index.json.jbuilder create app/views/comments/show.json.jbuilder invoke assets invoke coffee - create app/assets/javascripts/comments.js.coffee + create app/assets/javascripts/comments.coffee invoke scss ``` -Fallbacks allow your generators to have a single responsibility, increasing code reuse and reducing the amount of duplication. +后备机制能让生成器专注于实现单一职责,尽量复用代码,减少重复代码量。 + + -Application Templates ---------------------- +## 应用模板 -Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred as a "template". This is a brief overview of the Templates API. For detailed documentation see the [Rails Application Templates guide](rails_application_templates.html). +至此,我们知道生成器可以在应用内部使用,但是你知道吗,生成器也可用于生成应用?这种生成器叫“模板”(template)。本节简介 Templates API,详情参阅[Rails 应用模板](rails_application_templates.html)。 ```ruby gem "rspec-rails", group: "test" @@ -418,61 +444,85 @@ if yes?("Would you like to install Devise?") end ``` -In the above template we specify that the application relies on the `rspec-rails` and `cucumber-rails` gem so these two will be added to the `test` group in the `Gemfile`. Then we pose a question to the user about whether or not they would like to install Devise. If the user replies "y" or "yes" to this question, then the template will add Devise to the `Gemfile` outside of any group and then runs the `devise:install` generator. This template then takes the users input and runs the `devise` generator, with the user's answer from the last question being passed to this generator. +在上述模板中,我们指定应用要使用 `rspec-rails` 和 `cucumber-rails` 两个 gem,因此把它们添加到 `Gemfile` 的 `test` 组。然后,我们询问用户是否想安装 Devise。如果用户回答“y”或“yes”,这个模板会将其添加到 `Gemfile` 中,而且不放在任何分组中,然后运行 `devise:install` 生成器。然后,这个模板获取用户的输入,运行 `devise` 生成器,并传入用户对前一个问题的回答。 -Imagine that this template was in a file called `template.rb`. We can use it to modify the outcome of the `rails new` command by using the `-m` option and passing in the filename: +假如这个模板保存在名为 `template.rb` 的文件中。我们可以使用它修改 `rails new` 命令的输出,方法是把文件名传给 `-m` 选项: -```bash +```sh $ rails new thud -m template.rb ``` -This command will generate the `Thud` application, and then apply the template to the generated output. +上述命令会生成 Thud 应用,然后把模板应用到生成的输出上。 -Templates don't have to be stored on the local system, the `-m` option also supports online templates: +模板不一定非得存储在本地系统中,`-m` 选项也支持在线模板: -```bash +```sh $ rails new thud -m https://gist.github.com/radar/722911/raw/ ``` -Whilst the final section of this guide doesn't cover how to generate the most awesome template known to man, it will take you through the methods available at your disposal so that you can develop it yourself. These same methods are also available for generators. +本章最后一节虽然不说明如何生成大多数已知的优秀模板,但是会详细说明可用的方法,供你自己开发模板。那些方法也可以在生成器中使用。 + + + +## 添加命令行参数 -Generator methods ------------------ +Rails 的生成器可以轻易修改,接受自定义的命令行参数。这个功能源自 [Thor](http://www.rubydoc.info/github/erikhuda/thor/master/Thor/Base/ClassMethods#class_option-instance_method): -The following are methods available for both generators and templates for Rails. +```ruby +class_option :scope, type: :string, default: 'read_products' +``` -NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html) +现在,生成器可以这样调用: + +```sh +$ rails generate initializer --scope write_products +``` + +在生成器类内部,命令行参数通过 `options` 方法访问。 + + + +## 生成器方法 + +下面是可供 Rails 生成器和模板使用的方法。 + +NOTE: 本文不涵盖 Thor 提供的方法。如果想了解,参阅 [Thor 的文档](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html)。 + + + ### `gem` -Specifies a gem dependency of the application. +指定应用的一个 gem 依赖。 ```ruby gem "rspec", group: "test", version: "2.1.0" gem "devise", "1.1.5" ``` -Available options are: +可用的选项: -* `:group` - The group in the `Gemfile` where this gem should go. -* `:version` - The version string of the gem you want to use. Can also be specified as the second argument to the method. -* `:git` - The URL to the git repository for this gem. +* `:group`:把 gem 添加到 `Gemfile` 中的哪个分组里。 +* `:version`:要使用的 gem 版本号,字符串。也可以在 `gem` 方法的第二个参数中指定。 +* `:git`:gem 的 Git 仓库的 URL。 -Any additional options passed to this method are put on the end of the line: +传给这个方法的其他选项放在行尾: ```ruby gem "devise", git: "git://github.com/plataformatec/devise", branch: "master" ``` -The above code will put the following line into `Gemfile`: +上述代码在 `Gemfile` 中写入下面这行代码: ```ruby gem "devise", git: "git://github.com/plataformatec/devise", branch: "master" ``` + + ### `gem_group` -Wraps gem entries inside a group: +把 gem 放在一个分组里: ```ruby gem_group :development, :test do @@ -480,17 +530,29 @@ gem_group :development, :test do end ``` + + ### `add_source` -Adds a specified source to `Gemfile`: +在 `Gemfile` 中添加指定的源: ```ruby add_source "http://gems.github.com/" ``` +这个方法也接受块: + +```ruby +add_source "http://gems.github.com/" do + gem "rspec-rails" +end +``` + + + ### `inject_into_file` -Injects a block of code into a defined position in your file. +在文件中的指定位置插入一段代码: ```ruby inject_into_file 'name_of_file.rb', after: "#The code goes below this line. Don't forget the Line break at the end\n" do <<-'RUBY' @@ -499,25 +561,29 @@ RUBY end ``` + + ### `gsub_file` -Replaces text inside a file. +替换文件中的文本: ```ruby gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code' ``` -Regular Expressions can be used to make this method more precise. You can also use `append_file` and `prepend_file` in the same way to place code at the beginning and end of a file respectively. +使用正则表达式替换的效果更精准。可以使用类似的方式调用 `append_file` 和 `prepend_file`,分别在文件的末尾和开头添加代码。 + + ### `application` -Adds a line to `config/application.rb` directly after the application class definition. +在 `config/application.rb` 文件中应用类定义后面直接添加内容: ```ruby application "config.asset_host = 'http://example.com/'" ``` -This method can also take a block: +这个方法也接受块: ```ruby application do @@ -525,19 +591,23 @@ application do end ``` -Available options are: +可用的选项: -* `:env` - Specify an environment for this configuration option. If you wish to use this option with the block syntax the recommended syntax is as follows: +* `:env`:指定配置选项所属的环境。如果想在块中使用这个选项,建议使用下述句法: -```ruby -application(nil, env: "development") do - "config.asset_host = 'http://localhost:3000/'" -end -``` + ```ruby + application(nil, env: "development") do + "config.asset_host = 'http://localhost:3000/'" + end + ``` + + + + ### `git` -Runs the specified git command: +运行指定的 Git 命令: ```ruby git :init @@ -546,17 +616,19 @@ git commit: "-m First commit!" git add: "onefile.rb", rm: "badfile.cxx" ``` -The values of the hash here being the arguments or options passed to the specific git command. As per the final example shown here, multiple git commands can be specified at a time, but the order of their running is not guaranteed to be the same as the order that they were specified in. +这里的散列是传给指定 Git 命令的参数或选项。如最后一行所示,一次可以指定多个 Git 命令,但是命令的运行顺序不一定与指定的顺序一样。 + + ### `vendor` -Places a file into `vendor` which contains the specified code. +在 `vendor` 目录中放一个文件,内有指定的代码: ```ruby vendor "sekrit.rb", '#top secret stuff' ``` -This method also takes a block: +这个方法也接受块: ```ruby vendor "seeds.rb" do @@ -564,15 +636,17 @@ vendor "seeds.rb" do end ``` + + ### `lib` -Places a file into `lib` which contains the specified code. +在 `lib` 目录中放一个文件,内有指定的代码: ```ruby lib "special.rb", "p Rails.root" ``` -This method also takes a block: +这个方法也接受块 ```ruby lib "super_special.rb" do @@ -580,15 +654,17 @@ lib "super_special.rb" do end ``` + + ### `rakefile` -Creates a Rake file in the `lib/tasks` directory of the application. +在应用的 `lib/tasks` 目录中创建一个 Rake 文件: ```ruby rakefile "test.rake", "hello there" ``` -This method also takes a block: +这个方法也接受块: ```ruby rakefile "test.rake" do @@ -600,15 +676,17 @@ rakefile "test.rake" do end ``` + + ### `initializer` -Creates an initializer in the `config/initializers` directory of the application: +在应用的 `config/initializers` 目录中创建一个初始化脚本: ```ruby initializer "begin.rb", "puts 'this is the beginning'" ``` -This method also takes a block, expected to return a string: +这个方法也接受块,期待返回一个字符串: ```ruby initializer "begin.rb" do @@ -616,47 +694,56 @@ initializer "begin.rb" do end ``` + + ### `generate` -Runs the specified generator where the first argument is the generator name and the remaining arguments are passed directly to the generator. +运行指定的生成器,第一个参数是生成器的名称,后续参数直接传给生成器: ```ruby generate "scaffold", "forums title:string description:text" ``` + ### `rake` -Runs the specified Rake task. +运行指定的 Rake 任务: ```ruby rake "db:migrate" ``` -Available options are: +可用的选项: + +* `:env`:指定在哪个环境中运行 Rake 任务。 +* `:sudo`:是否使用 `sudo` 运行任务。默认为 `false`。 -* `:env` - Specifies the environment in which to run this rake task. -* `:sudo` - Whether or not to run this task using `sudo`. Defaults to `false`. + ### `capify!` -Runs the `capify` command from Capistrano at the root of the application which generates Capistrano configuration. +在应用的根目录中运行 Capistrano 提供的 `capify` 命令,生成 Capistrano 配置。 ```ruby capify! ``` + + ### `route` -Adds text to the `config/routes.rb` file: +在 `config/routes.rb` 文件中添加文本: ```ruby route "resources :people" ``` + + ### `readme` -Output the contents of a file in the template's `source_path`, usually a README. +输出模板的 `source_path` 中某个文件的内容,通常是 README 文件: ```ruby readme "README" diff --git a/source/zh-CN/getting_started.md b/source/zh-CN/getting_started.md index c11c987..f4fb83a 100644 --- a/source/zh-CN/getting_started.md +++ b/source/zh-CN/getting_started.md @@ -1,168 +1,181 @@ -Rails 入门 -========== +# Rails 入门 本文介绍如何开始使用 Ruby on Rails。 -读完本文,你将学到: +读完本文后,您将学到: -* 如何安装 Rails,新建 Rails 程序,如何连接数据库; -* Rails 程序的基本文件结构; -* MVC(模型,视图,控制器)和 REST 架构的基本原理; -* 如何快速生成 Rails 程序骨架; +* 如何安装 Rails、创建 Rails 应用,如何连接数据库; +* Rails 应用的基本文件结构; +* MVC(模型、视图、控制器)和 REST 架构的基本原理; +* 如何快速生成 Rails 应用骨架。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -前提条件 -------- + -本文针对想从零开始开发 Rails 程序的初学者,不需要预先具备任何的 Rails 使用经验。不过,为了能顺利阅读,还是需要事先安装好一些软件: +## 前提条件 -* [Ruby](https://www.ruby-lang.org/en/downloads) 1.9.3 及以上版本 -* 包管理工具 [RubyGems](https://rubygems.org),随 Ruby 1.9+ 安装。想深入了解 RubyGems,请阅读 [RubyGems 指南](http://guides.rubygems.org) -* [SQLite3](https://www.sqlite.org) 数据库 +本文针对想从零开始开发 Rails 应用的初学者,不要求 Rails 使用经验。不过,为了能顺利阅读,还是需要事先安装好一些软件: -Rails 是使用 Ruby 语言开发的网页程序框架。如果之前没接触过 Ruby,学习 Rails 可要深下一番功夫。网上有很多资源可以学习 Ruby: +* [Ruby](https://www.ruby-lang.org/en/downloads) 2.2.2 及以上版本 +* [开发工具包](http://rubyinstaller.org/downloads/)的正确版本(针对 Windows 用户) +* 包管理工具 [RubyGems](https://rubygems.org/),随 Ruby 预装。若想深入了解 RubyGems,请参阅 [RubyGems 指南](http://guides.rubygems.org/) +* [SQLite3 数据库](https://www.sqlite.org/) -* [Ruby 语言官方网站](https://www.ruby-lang.org/zh_cn/documentation/) -* [reSRC 列出的免费编程书籍](http://resrc.io/list/10/list-of-free-programming-books/#ruby) +Rails 是使用 Ruby 语言开发的 Web 应用框架。如果之前没接触过 Ruby,会感到直接学习 Rails 的学习曲线很陡。这里提供几个学习 Ruby 的在线资源: -记住,某些资源虽然很好,但是针对 Ruby 1.8,甚至 1.6 编写的,所以没有介绍一些 Rails 日常开发会用到的句法。 +* [Ruby 语言官方网站](https://www.ruby-lang.org/en/documentation/) +* [免费编程图书列表](https://github.com/vhf/free-programming-books/blob/master/free-programming-books.md#ruby) -Rails 是什么? -------------- +需要注意的是,有些资源虽然很好,但针对的是 Ruby 1.8 甚至 1.6 这些老版本,因此不涉及一些 Rails 日常开发的常见句法。 -Rails 是使用 Ruby 语言编写的网页程序开发框架,目的是为开发者提供常用组件,简化网页程序的开发。只需编写较少的代码,就能实现其他编程语言或框架难以企及的功能。经验丰富的 Rails 程序员会发现,Rails 让程序开发变得更有乐趣。 + -Rails 有自己的一套规则,认为问题总有最好的解决方法,而且建议使用最好的方法,有些情况下甚至不推荐使用其他替代方案。学会如何按照 Rails 的思维开发,能极大提高开发效率。如果坚持在 Rails 开发中使用其他语言中的旧思想,尝试使用别处学来的编程模式,开发过程就不那么有趣了。 +## Rails 是什么? + +Rails 是使用 Ruby 语言编写的 Web 应用开发框架,目的是通过解决快速开发中的共通问题,简化 Web 应用的开发。与其他编程语言和框架相比,使用 Rails 只需编写更少代码就能实现更多功能。有经验的 Rails 程序员常说,Rails 让 Web 应用开发变得更有趣。 + +Rails 有自己的设计原则,认为问题总有最好的解决方法,并且有意识地通过设计来鼓励用户使用最好的解决方法,而不是其他替代方案。一旦掌握了“Rails 之道”,就可能获得生产力的巨大提升。在 Rails 开发中,如果不改变使用其他编程语言时养成的习惯,总想使用原有的设计模式,开发体验可能就不那么让人愉快了。 Rails 哲学包含两大指导思想: -* **不要自我重复(DRY):** DRY 是软件开发中的一个原则,“系统中的每个功能都要具有单一、准确、可信的实现。”。不重复表述同一件事,写出的代码才能更易维护,更具扩展性,也更不容易出问题。 -* **多约定,少配置:** Rails 为网页程序的大多数需求都提供了最好的解决方法,而且默认使用这些约定,不用在长长的配置文件中设置每个细节。 +* 不要自我重复(DRY): DRY 是软件开发中的一个原则,意思是“系统中的每个功能都要具有单一、准确、可信的实现。”。不重复表述同一件事,写出的代码才更易维护、更具扩展性,也更不容易出问题。 +* 多约定,少配置: Rails 为 Web 应用的大多数需求都提供了最好的解决方法,并且默认使用这些约定,而不是在长长的配置文件中设置每个细节。 -新建 Rails 程序 --------------- + -阅读本文时,最好跟着一步一步操作,如果错过某段代码或某个步骤,程序就可能出错,所以请一步一步跟着做。完整的源码可以在[这里](https://github.com/rails/docrails/tree/master/guides/code/getting_started)获取。 +## 创建 Rails 项目 -本文会新建一个名为 `blog` 的 Rails 程序,这是一个非常简单的博客。在开始开发程序之前,要确保已经安装了 Rails。 +阅读本文的最佳方法是一步步跟着操作。所有这些步骤对于运行示例应用都是必不可少的,同时也不需要更多的代码或步骤。 -TIP: 文中的示例代码使用 `$` 表示命令行提示符,你的提示符可能修改过,所以会不一样。在 Windows 中,提示符可能是 `c:\source_code>`。 +通过学习本文,你将学会如何创建一个名为 Blog 的 Rails 项目,这是一个非常简单的博客。在动手开发之前,请确保已经安装了 Rails。 -### 安装 Rails +TIP: 文中的示例代码使用 UNIX 风格的命令行提示符 $,如果你的命令行提示符是自定义的,看起来可能会不一样。在 Windows 中,命令行提示符可能类似 `c:\source_code>`。 -打开命令行:在 Mac OS X 中打开 Terminal.app,在 Windows 中选择“运行”,然后输入“cmd.exe”。下文中所有以 `$` 开头的代码,都要在命令行中运行。先确认是否安装了 Ruby 最新版: + -TIP: 有很多工具可以帮助你快速在系统中安装 Ruby 和 Ruby on Rails。Windows 用户可以使用 [Rails Installer](http://railsinstaller.org),Mac OS X 用户可以使用 [Tokaido](https://github.com/tokaido/tokaidoapp)。 +### 安装 Rails + +打开命令行:在 macOS 中打开 Terminal.app,在 Windows 中要在开始菜单中选择“运行”,然后输入“cmd.exe”。本文中所有以 $ 开头的代码,都应该在命令行中执行。首先确认是否安装了 Ruby 的最新版本: -```bash +```sh $ ruby -v -ruby 2.1.2p95 +ruby 2.3.1p112 ``` -如果你还没安装 Ruby,请访问 [ruby-lang.org](https://www.ruby-lang.org/en/downloads/),找到针对所用系统的安装方法。 +TIP: 有很多工具可以帮助你快速地在系统中安装 Ruby 和 Ruby on Rails。Windows 用户可以使用 [Rails Installer](http://railsinstaller.org/),macOS 用户可以使用 [Tokaido](https://github.com/tokaido/tokaidoapp)。更多操作系统中的安装方法请访问 [ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/)。 -很多类 Unix 系统都自带了版本尚新的 SQLite3。Windows 等其他操作系统的用户可以在 [SQLite3 的网站](https://www.sqlite.org)上找到安装说明。然后,确认是否在 PATH 中: +很多类 UNIX 系统都预装了版本较新的 SQLite3。在 Windows 中,通过 Rails Installer 安装 Rails 会同时安装 SQLite3。其他操作系统中 SQLite3 的安装方法请参阅 [SQLite3 官网](https://www.sqlite.org/)。接下来,确认 SQLite3 是否在 PATH 中: -```bash +```sh $ sqlite3 --version ``` -命令行应该回显版本才对。 +执行结果应该显示 SQLite3 的版本号。 安装 Rails,请使用 RubyGems 提供的 `gem install` 命令: -```bash +```sh $ gem install rails ``` -要检查所有软件是否都正确安装了,可以执行下面的命令: +执行下面的命令来确认所有软件是否都已正确安装: -```bash +```sh $ rails --version ``` -如果显示的结果类似“Rails 4.2.0”,那么就可以继续往下读了。 +如果执行结果类似 `Rails 5.1.0`,那么就可以继续往下读了。 + + -### 创建 Blog 程序 +### 创建 Blog 应用 -Rails 提供了多个被称为“生成器”的脚本,可以简化开发,生成某项操作需要的所有文件。其中一个是新程序生成器,生成一个 Rails 程序骨架,不用自己一个一个新建文件。 +Rails 提供了许多名为生成器(generator)的脚本,这些脚本可以为特定任务生成所需的全部文件,从而简化开发。其中包括新应用生成器,这个脚本用于创建 Rails 应用骨架,避免了手动编写基础代码。 -打开终端,进入有写权限的文件夹,执行以下命令生成一个新程序: +要使用新应用生成器,请打开终端,进入具有写权限的文件夹,输入: -```bash +```sh $ rails new blog ``` -这个命令会在文件夹 `blog` 中新建一个 Rails 程序,然后执行 `bundle install` 命令安装 `Gemfile` 中列出的 gem。 +这个命令会在文件夹 `blog` 中创建名为 Blog 的 Rails 应用,然后执行 `bundle install` 命令安装 Gemfile 中列出的 gem 及其依赖。 -TIP: 执行 `rails new -h` 可以查看新程序生成器的所有命令行选项。 +TIP: 执行 `rails new -h` 命令可以查看新应用生成器的所有命令行选项。 -生成 `blog` 程序后,进入该文件夹: +创建 blog 应用后,进入该文件夹: -```bash +```sh $ cd blog ``` -`blog` 文件夹中有很多自动生成的文件和文件夹,组成一个 Rails 程序。本文大部分时间都花在 `app` 文件夹上。下面简单介绍默认生成的文件和文件夹的作用: +`blog` 文件夹中有许多自动生成的文件和文件夹,这些文件和文件夹组成了 Rails 应用的结构。本文涉及的大部分工作都在 app 文件夹中完成。下面简单介绍一下这些用新应用生成器默认选项生成的文件和文件夹的功能: -| 文件/文件夹 | 作用 | -| ----------- | ------- | -|app/|存放程序的控制器、模型、视图、帮助方法、邮件和静态资源文件。本文主要关注的是这个文件夹。| -|bin/|存放运行程序的 `rails` 脚本,以及其他用来部署或运行程序的脚本。| -|config/|设置程序的路由,数据库等。详情参阅“[设置 Rails 程序](/configuring.html)”一文。| -|config.ru|基于 Rack 服务器的程序设置,用来启动程序。| -|db/|存放当前数据库的模式,以及数据库迁移文件。| -|Gemfile, Gemfile.lock|这两个文件用来指定程序所需的 gem 依赖件,用于 Bundler gem。关于 Bundler 的详细介绍,请访问 [Bundler 官网](http://bundler.io)。| -|lib/|程序的扩展模块。| -|log/|程序的日志文件。| -|public/|唯一对外开放的文件夹,存放静态文件和编译后的资源文件。| -|Rakefile|保存并加载可在命令行中执行的任务。任务在 Rails 的各组件中定义。如果想添加自己的任务,不要修改这个文件,把任务保存在 `lib/tasks` 文件夹中。| -|README.rdoc|程序的简单说明。你应该修改这个文件,告诉其他人这个程序的作用,如何安装等。| -|test/|单元测试,固件等测试用文件。详情参阅“[测试 Rails 程序](/testing.html)”一文。| -|tmp/|临时文件,例如缓存,PID,会话文件。| -|vendor/|存放第三方代码。经常用来放第三方 gem。| +| 文件/文件夹 | 作用 | +|---|---| +| `app/` | 包含应用的控制器、模型、视图、辅助方法、邮件程序、频道、作业和静态资源文件。这个文件夹是本文剩余内容关注的重点。 | +| `bin/` | 包含用于启动应用的 rails 脚本,以及用于安装、更新、部署或运行应用的其他脚本。 | +| `config/` | 配置应用的路由、数据库等。详情请参阅[配置 Rails 应用](configuring.html)。 | +| `config.ru` | 基于 Rack 的服务器所需的 Rack 配置,用于启动应用。 | +| `db/` | 包含当前数据库的模式,以及数据库迁移文件。 | +| `Gemfile`, `Gemfile.lock` | 这两个文件用于指定 Rails 应用所需的 gem 依赖。Bundler gem 需要用到这两个文件。关于 Bundler 的更多介绍,请访问 [Bundler 官网](http://bundler.io/)。 | +| `lib/` | 应用的扩展模块。 | +| `log/` | 应用日志文件。 | +| `public/` | 仅有的可以直接从外部访问的文件夹,包含静态文件和编译后的静态资源文件。 | +| `Rakefile` | 定位并加载可在命令行中执行的任务。这些任务在 Rails 的各个组件中定义。如果要添加自定义任务,请不要修改 Rakefile,直接把自定义任务保存在 `lib/tasks` 文件夹中即可。 | +| `README.md` | 应用的自述文件,说明应用的用途、安装方法等。 | +| `test/` | 单元测试、固件和其他测试装置。详情请参阅[Rails 应用测试指南](testing.html)。 | +| `tmp/` | 临时文件(如缓存和 PID 文件)。 | +| `vendor/` | 包含第三方代码,如第三方 gem。 | +| `.gitignore` | 告诉 Git 要忽略的文件(或模式)。详情参见 [GitHub 帮助文档](https://help.github.com/articles/ignoring-files)。 | -Hello, Rails! -------------- + -首先,我们来添加一些文字,在页面中显示。为了能访问网页,要启动程序服务器。 +## Hello, Rails! -### 启动服务器 +首先,让我们快速地在页面中添加一些文字。为了访问页面,需要运行 Rails 应用服务器(即 Web 服务器)。 -现在,新建的 Rails 程序已经可以正常运行。要访问网站,需要在开发电脑上启动服务器。请在 `blog` 文件夹中执行下面的命令: + -```bash -$ rails server +### 启动 Web 服务器 + +实际上这个 Rails 应用已经可以正常运行了。要访问应用,需要在开发设备中启动 Web 服务器。请在 `blog` 文件夹中执行下面的命令: + +```sh +$ bin/rails server ``` -TIP: 把 CoffeeScript 编译成 JavaScript 需要 JavaScript 运行时,如果没有运行时,会报错,提示没有 `execjs`。Mac OS X 和 Windows 一般都提供了 JavaScript 运行时。Rails 生成的 `Gemfile` 中,安装 `therubyracer` gem 的代码被注释掉了,如果需要使用这个 gem,请把前面的注释去掉。在 JRuby 中推荐使用 `therubyracer`。在 JRuby 中生成的 `Gemfile` 已经包含了这个 gem。所有支持的运行时参见 [ExecJS](https://github.com/sstephenson/execjs#readme)。 +TIP: Windows 用户需要把 `bin` 文件夹下的脚本文件直接传递给 Ruby 解析器,例如 `ruby bin\rails server`。 + +TIP: 编译 CoffeeScript 和压缩 JavaScript 静态资源文件需要 JavaScript 运行时,如果没有运行时,在压缩静态资源文件时会报错,提示没有 `execjs`。macOS 和 Windows 一般都提供了 JavaScript 运行时。在 Rails 应用的 Gemfile 中,`therubyracer` gem 被注释掉了,如果需要使用这个 gem,请去掉注释。对于 JRuby 用户,推荐使用 `therubyrhino` 这个运行时,在 JRuby 中创建 Rails 应用的 Gemfile 中默认包含了这个 gem。要查看 Rails 支持的所有运行时,请参阅 [ExecJS](https://github.com/rails/execjs#readme)。 -上述命令会启动 WEBrick,这是 Ruby 内置的服务器。要查看程序,请打开一个浏览器窗口,访问 。应该会看到默认的 Rails 信息页面: +上述命令会启动 Puma,这是 Rails 默认使用的 Web 服务器。要查看运行中的应用,请打开浏览器窗口,访问 。这时应该看到默认的 Rails 欢迎页面: -![欢迎使用页面](images/getting_started/rails_welcome.png) +![默认的 Rails 欢迎页面](images/getting_started/rails_welcome.png) -TIP: 要想停止服务器,请在命令行中按 Ctrl+C 键。服务器成功停止后回重新看到命令行提示符。在大多数类 Unix 系统中,包括 Mac OS X,命令行提示符是 `$` 符号。在开发模式中,一般情况下无需重启服务器,修改文件后,服务器会自动重新加载。 +TIP: 要停止 Web 服务器,请在终端中按 Ctrl+C 键。服务器停止后命令行提示符会重新出现。在大多数类 Unix 系统中,包括 macOS,命令行提示符是 $ 符号。在开发模式中,一般情况下无需重启服务器,服务器会自动加载修改后的文件。 -“欢迎使用”页面是新建 Rails 程序后的“冒烟测试”:确保程序设置正确,能顺利运行。你可以点击“About your application's environment”链接查看程序所处环境的信息。 +欢迎页面是创建 Rails 应用的冒烟测试,看到这个页面就表示应用已经正确配置,能够正常工作了。 + + ### 显示“Hello, Rails!” -要在 Rails 中显示“Hello, Rails!”,需要新建一个控制器和视图。 +要让 Rails 显示“Hello, Rails!”,需要创建控制器和视图。 -控制器用来接受向程序发起的请求。路由决定哪个控制器会接受到这个请求。一般情况下,每个控制器都有多个路由,对应不同的动作。动作用来提供视图中需要的数据。 +控制器接受向应用发起的特定访问请求。路由决定哪些访问请求被哪些控制器接收。一般情况下,一个控制器会对应多个路由,不同路由对应不同动作。动作搜集数据并把数据提供给视图。 -视图的作用是,以人类能看懂的格式显示数据。有一点要特别注意,数据是在控制器中获取的,而不是在视图中。视图只是把数据显示出来。默认情况下,视图使用 eRuby(嵌入式 Ruby)语言编写,经由 Rails 解析后,再发送给用户。 +视图以人类能看懂的格式显示数据。有一点要特别注意,数据是在控制器而不是视图中获取的,视图只是显示数据。默认情况下,视图模板使用 eRuby(嵌入式 Ruby)语言编写,经由 Rails 解析后,再发送给用户。 -控制器可用控制器生成器创建,你要告诉生成器,我想要个名为“welcome”的控制器和一个名为“index”的动作,如下所示: +可以用控制器生成器来创建控制器。下面的命令告诉控制器生成器创建一个包含“index”动作的“Welcome”控制器: -```bash -$ rails generate controller welcome index +```sh +$ bin/rails generate controller Welcome index ``` -运行上述命令后,Rails 会生成很多文件,以及一个路由。 +上述命令让 Rails 生成了多个文件和一个路由: -```bash +``` create app/controllers/welcome_controller.rb route get 'welcome/index' invoke erb @@ -172,26 +185,29 @@ invoke test_unit create test/controllers/welcome_controller_test.rb invoke helper create app/helpers/welcome_helper.rb +invoke test_unit invoke assets invoke coffee -create app/assets/javascripts/welcome.js.coffee +create app/assets/javascripts/welcome.coffee invoke scss -create app/assets/stylesheets/welcome.css.scss +create app/assets/stylesheets/welcome.scss ``` -在这些文件中,最重要的当然是控制器,位于 `app/controllers/welcome_controller.rb`,以及视图,位于 `app/views/welcome/index.html.erb`。 +其中最重要的文件是控制器和视图,控制器位于 `app/controllers/welcome_controller.rb` 文件 ,视图位于 `app/views/welcome/index.html.erb` 文件 。 -使用文本编辑器打开 `app/views/welcome/index.html.erb` 文件,删除全部内容,写入下面这行代码: +在文本编辑器中打开 `app/views/welcome/index.html.erb` 文件,删除所有代码,然后添加下面的代码: ```html

    Hello, Rails!

    ``` -### 设置程序的首页 + + +### 设置应用主页 -我们已经创建了控制器和视图,现在要告诉 Rails 在哪个地址上显示“Hello, Rails!”。这里,我们希望访问根地址 时显示。但是现在显示的还是欢迎页面。 +现在我们已经创建了控制器和视图,还需要告诉 Rails 何时显示“Hello, Rails!”,我们希望在访问根地址 时显示。目前根地址显示的还是默认的 Rails 欢迎页面。 -我们要告诉 Rails 真正的首页是什么。 +接下来需要告诉 Rails 真正的主页在哪里。 在编辑器中打开 `config/routes.rb` 文件。 @@ -199,40 +215,39 @@ create app/assets/stylesheets/welcome.css.scss Rails.application.routes.draw do get 'welcome/index' - # The priority is based upon order of creation: - # first created -> highest priority. - # - # You can have the root of your site routed with "root" - # root 'welcome#index' - # - # ... + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html +end ``` -这是程序的路由文件,使用特殊的 DSL(domain-specific language,领域专属语言)编写,告知 Rails 请求应该发往哪个控制器和动作。文件中有很多注释,举例说明如何定义路由。其中有一行说明了如何指定控制器和动作设置网站的根路由。找到以 `root` 开头的代码行,去掉注释,变成这样: +这是应用的路由文件,使用特殊的 DSL(Domain-Specific Language,领域专属语言)编写,告诉 Rails 把访问请求发往哪个控制器和动作。编辑这个文件,添加一行代码 `root 'welcome#index'`,此时文件内容应该变成下面这样: ```ruby -root 'welcome#index' +Rails.application.routes.draw do + get 'welcome/index' + + root 'welcome#index' +end ``` -`root 'welcome#index'` 告知 Rails,访问程序的根路径时,交给 `welcome` 控制器中的 `index` 动作处理。`get 'welcome/index'` 告知 Rails,访问 时,交给 `welcome` 控制器中的 `index` 动作处理。`get 'welcome/index'` 是运行 `rails generate controller welcome index` 时生成的。 +`root 'welcome#index'` 告诉 Rails 对根路径的访问请求应该发往 welcome 控制器的 index 动作,`get 'welcome/index'` 告诉 Rails 对 的访问请求应该发往 welcome 控制器的 index 动作。后者是之前使用控制器生成器创建控制器(`bin/rails generate controller Welcome index`)时自动生成的。 -如果生成控制器时停止了服务器,请再次启动(`rails server`),然后在浏览器中访问 。你会看到之前写入 `app/views/welcome/index.html.erb` 文件的“Hello, Rails!”,说明新定义的路由把根目录交给 `WelcomeController` 的 `index` 动作处理了,而且也正确的渲染了视图。 +如果在生成控制器时停止了服务器,请再次启动服务器(`bin/rails server`),然后在浏览器中访问 。我们会看到之前添加到 `app/views/welcome/index.html.erb` 文件 的“Hello, Rails!”信息,这说明新定义的路由确实把访问请求发往了 `WelcomeController` 的 `index` 动作,并正确渲染了视图。 -TIP: 关于路由的详细介绍,请阅读“[Rails 路由全解](/routing.html)”一文。 +TIP: 关于路由的更多介绍,请参阅[Rails 路由全解](routing.html)。 -开始使用 -------- + -前文已经介绍如何创建控制器、动作和视图,下面我们来创建一些更实质的功能。 +## 启动并运行起来 -在博客程序中,我们要创建一个新“资源”。资源是指一系列类似的对象,比如文章,人和动物。 +前文已经介绍了如何创建控制器、动作和视图,接下来我们要创建一些更具实用价值的东西。 -资源可以被创建、读取、更新和删除,这些操作简称 CRUD。 +在 Blog 应用中创建一个资源(resource)。资源是一个术语,表示一系列类似对象的集合,如文章、人或动物。资源中的项目可以被创建、读取、更新和删除,这些操作简称 CRUD(Create, Read, Update, Delete)。 -Rails 提供了一个 `resources` 方法,可以声明一个符合 REST 架构的资源。创建文章资源后,`config/routes.rb` 文件的内容如下: +Rails 提供了 `resources` 方法,用于声明标准的 REST 资源。把 articles 资源添加到 `config/routes.rb` 文件,此时文件内容应该变成下面这样: ```ruby Rails.application.routes.draw do + get 'welcome/index' resources :articles @@ -240,10 +255,10 @@ Rails.application.routes.draw do end ``` -执行 `rake routes` 任务,会看到定义了所有标准的 REST 动作。输出结果中各列的意义稍后会说明,现在只要留意 `article` 的单复数形式,这在 Rails 中有特殊的含义。 +执行 `bin/rails routes` 命令,可以看到所有标准 REST 动作都具有对应的路由。输出结果中各列的意义稍后会作说明,现在只需注意 Rails 从 article 的单数形式推导出了它的复数形式,并进行了合理使用。 -```bash -$ bin/rake routes +```sh +$ bin/rails routes Prefix Verb URI Pattern Controller#Action articles GET /articles(.:format) articles#index POST /articles(.:format) articles#create @@ -256,42 +271,44 @@ edit_article GET /articles/:id/edit(.:format) articles#edit root GET / welcome#index ``` -下一节,我们会加入新建文章和查看文章的功能。这两个操作分别对应于 CRUD 的 C 和 R,即创建和读取。新建文章的表单如下所示: +下一节,我们将为应用添加新建文章和查看文章的功能。这两个操作分别对应于 CRUD 的“C”和“R”:创建和读取。下面是用于新建文章的表单: + +![用于新建文章的表单](images/getting_started/new_article.png) -![新建文章表单](images/getting_started/new_article.png) +表单看起来很简陋,不过没关系,之后我们再来美化。 -表单看起来很简陋,不过没关系,后文会加入更多的样式。 + -### 挖地基 +### 打地基 -首先,程序中要有个页面用来新建文章。一个比较好的选择是 `/articles/new`。这个路由前面已经定义了,可以访问。打开 ,会看到如下的路由错误: +首先,应用需要一个页面用于新建文章,`/articles/new` 是个不错的选择。相关路由之前已经定义过了,可以直接访问。打开 ,会看到下面的路由错误: ![路由错误,常量 ArticlesController 未初始化](images/getting_started/routing_error_no_controller.png) -产生这个错误的原因是,没有定义用来处理该请求的控制器。解决这个问题的方法很简单:创建名为 `ArticlesController` 的控制器。执行下面的命令即可: +产生错误的原因是,用于处理该请求的控制器还没有定义。解决问题的方法很简单:创建 `Articles` 控制器。执行下面的命令: -```bash -$ bin/rails g controller articles +```sh +$ bin/rails generate controller Articles ``` -打开刚生成的 `app/controllers/articles_controller.rb` 文件,会看到一个几乎没什么内容的控制器: +打开刚刚生成的 `app/controllers/articles_controller.rb` 文件,会看到一个空的控制器: ```ruby class ArticlesController < ApplicationController end ``` -控制器就是一个类,继承自 `ApplicationController`。在这个类中定义的方法就是控制器的动作。动作的作用是处理文章的 CRUD 操作。 +控制器实际上只是一个继承自 `ApplicationController` 的类。接在来要在这个类中定义的方法也就是控制器的动作。这些动作对文章执行 CRUD 操作。 -NOTE: 在 Ruby 中,方法分为 `public`、`private` 和 `protected` 三种,只有 `public` 方法才能作为控制器的动作。详情参阅 [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/) 一书。 +NOTE: 在 Ruby 中,有 `public`、`private` 和 `protected` 三种方法,其中只有 `public` 方法才能作为控制器的动作。详情请参阅 [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/) 一书。 现在刷新 ,会看到一个新错误: -![ArticlesController 控制器不知如何处理 new 动作](images/getting_started/unknown_action_new_for_articles.png) +![未知动作,在 ArticlesController 中找不到 new 动作](images/getting_started/unknown_action_new_for_articles.png) -这个错误的意思是,在刚生成的 `ArticlesController` 控制器中找不到 `new` 动作。因为在生成控制器时,除非指定要哪些动作,否则不会生成,控制器是空的。 +这个错误的意思是,Rails 在刚刚生成的 `ArticlesController` 中找不到 new 动作。这是因为在 Rails 中生成控制器时,如果不指定想要的动作,生成的控制器就会是空的。 -手动创建动作只需在控制器中定义一个新方法。打开 `app/controllers/articles_controller.rb` 文件,在 `ArticlesController` 类中,定义 `new` 方法,如下所示: +在控制器中手动定义动作,只需要定义一个新方法。打开 `app/controllers/articles_controller.rb` 文件,在 `ArticlesController` 类中定义 `new` 方法,此时控制器应该变成下面这样: ```ruby class ArticlesController < ApplicationController @@ -300,39 +317,39 @@ class ArticlesController < ApplicationController end ``` -在 `ArticlesController` 中定义 `new` 方法后,再刷新 ,看到的还是个错误: +在 `ArticlesController` 中定义 `new` 方法后,再次刷新 ,会看到另一个错误: -![找不到 articles/new 所用模板](images/getting_started/template_is_missing_articles_new.png) +![未知格式,缺少对应模板](images/getting_started/template_is_missing_articles_new.png) -产生这个错误的原因是,Rails 希望这样的常规动作有对应的视图,用来显示内容。没有视图可用,Rails 就报错了。 +产生错误的原因是,Rails 要求这样的常规动作有用于显示数据的对应视图。如果没有视图可用,Rails 就会抛出异常。 -在上图中,最后一行被截断了,我们来看一下完整的信息: +上图中下面的几行都被截断了,下面是完整信息: -``` -Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" -``` +> ArticlesController#new is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you’re loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we’re showing an error to be extra-clear. If you expect 204 No Content, carry on. That’s what you’ll get from an XHR or API request. Give it a shot. -这行信息还挺长,我们来看一下到底是什么意思。 +内容还真不少!让我们快速浏览一下,看看各部分是什么意思。 -第一部分说明找不到哪个模板,这里,丢失的是 `articles/new` 模板。Rails 首先会寻找这个模板,如果找不到,再找名为 `application/new` 的模板。之所以这么找,是因为 `ArticlesController` 继承自 `ApplicationController`。 +第一部分说明缺少哪个模板,这里缺少的是 `articles/new` 模板。Rails 首先查找这个模板,如果找不到再查找 `application/new` 模板。之所以会查找后面这个模板,是因为 `ArticlesController` 继承自 `ApplicationController`。 -后面一部分是个 Hash。`:locale` 表示要找哪国语言模板,默认是英语(`"en"`)。`:format` 表示响应使用的模板格式,默认为 `:html`,所以 Rails 要寻找一个 HTML 模板。`:handlers` 表示用来处理模板的程序,HTML 模板一般使用 `:erb`,XML 模板使用 `:builder`,`:coffee` 用来把 CoffeeScript 转换成 JavaScript。 +下一部分是 `request.formats`,说明响应使用的模板格式。当我们在浏览器中请求页面时,`request.formats` 的值是 `text/html`,因此 Rails 会查找 HTML 模板。`request.variant` 指明伺服的是何种物理设备,帮助 Rails 判断该使用哪个模板渲染响应。它的值是空的,因为没有为其提供信息。 -最后一部分说明 Rails 在哪里寻找模板。在这个简单的程序里,模板都存放在一个地方,复杂的程序可能存放在多个位置。 +在本例中,能够工作的最简单的模板位于 `app/views/articles/new.html.erb` 文件中。文件的扩展名很重要:第一个扩展名是模板格式,第二个扩展名是模板处理器。Rails 会尝试在 `app/views` 文件夹中查找 `articles/new` 模板。这个模板的格式只能是 `html`,模板处理器只能是 `erb`、`builder` 和 `coffee` 中的一个。`:erb` 是最常用的 HTML 模板处理器,`:builder` 是 XML 模板处理器,`:coffee` 模板处理器用 CoffeeScript 创建 JavaScript 模板。因为我们要创建 HTML 表单,所以应该使用能够在 HTML 中嵌入 Ruby 的 `ERB` 语言。 -让这个程序正常运行,最简单的一种模板是 `app/views/articles/new.html.erb`。模板文件的扩展名是关键所在:第一个扩展名是模板的类型,第二个扩展名是模板的处理程序。Rails 会尝试在 `app/views` 文件夹中寻找名为 `articles/new` 的模板。这个模板的类型只能是 `html`,处理程序可以是 `erb`、`builder` 或 `coffee`。因为我们要编写一个 HTML 表单,所以使用 `erb`。所以这个模板文件应该命名为 `articles/new.html.erb`,还要放在 `app/views` 文件夹中。 +所以我们需要创建 `articles/new.html.erb` 文件,并把它放在应用的 `app/views` 文件夹中。 -新建文件 `app/views/articles/new.html.erb`,写入如下代码: +现在让我们继续前进。新建 `app/views/articles/new.html.erb` 文件,添加下面的代码: -```html +```erb

    New Article

    ``` -再次刷新 ,可以看到页面中显示了一个标头。现在路由、控制器、动作和视图都能正常运行了。接下来要编写新建文章的表单了。 +刷新 ,会看到页面有了标题。现在路由、控制器、动作和视图都可以协调地工作了!是时候创建用于新建文章的表单了。 + + -### 首个表单 +### 第一个表单 -要在模板中编写表单,可以使用“表单构造器”。Rails 中常用的表单构造器是 `form_for`。在 `app/views/articles/new.html.erb` 文件中加入以下代码: +在模板中创建表单,可以使用表单构建器。Rails 中最常用的表单构建器是 `form_for` 辅助方法。让我们使用这个方法,在 `app/views/articles/new.html.erb` 文件中添加下面的代码: ```erb <%= form_for :article do |f| %> @@ -352,24 +369,24 @@ Missing template articles/new, application/new with {locale:[:en], formats:[:htm <% end %> ``` -现在刷新页面,会看到上述代码生成的表单。在 Rails 中编写表单就是这么简单! +现在刷新页面,会看到和前文截图一样的表单。在 Rails 中创建表单就是这么简单! -调用 `form_for` 方法时,要指定一个对象。在上面的表单中,指定的是 `:article`。这个对象告诉 `form_for`,这个表单是用来处理哪个资源的。在 `form_for` 方法的块中,`FormBuilder` 对象(用 `f` 表示)创建了两个标签和两个文本字段,一个用于文章标题,一个用于文章内容。最后,在 `f` 对象上调用 `submit` 方法,创建一个提交按钮。 +调用 `form_for` 辅助方法时,需要为表单传递一个标识对象作为参数,这里是 `:article` 符号。这个符号告诉 `form_for` 辅助方法表单用于处理哪个对象。在 `form_for` 辅助方法的块中,`f` 表示 `FormBuilder` 对象,用于创建两个标签和两个文本字段,分别用于添加文章的标题和正文。最后在 `f` 对象上调用 `submit` 方法来为表单创建提交按钮。 -不过这个表单还有个问题。如果查看这个页面的源码,会发现表单 `action` 属性的值是 `/articles/new`。这就是问题所在,因为其指向的地址就是现在这个页面,而这个页面是用来显示新建文章表单的。 +不过这个表单还有一个问题,查看 HTML 源代码会看到表单 `action` 属性的值是 `/articles/new`,指向的是当前页面,而当前页面只是用于显示新建文章的表单。 -要想转到其他地址,就要使用其他的地址。这个问题可使用 `form_for` 方法的 `:url` 选项解决。在 Rails 中,用来处理新建资源表单提交数据的动作是 `create`,所以表单应该转向这个动作。 +应该把表单指向其他 URL,为此可以使用 `form_for` 辅助方法的 `:url` 选项。在 Rails 中习惯用 `create` 动作来处理提交的表单,因此应该把表单指向这个动作。 -修改 `app/views/articles/new.html.erb` 文件中的 `form_for`,改成这样: +修改 `app/views/articles/new.html.erb` 文件的 `form_for` 这一行,改为: ```erb <%= form_for :article, url: articles_path do |f| %> ``` -这里,我们把 `:url` 选项的值设为 `articles_path` 帮助方法。要想知道这个方法有什么作用,我们要回过头再看一下 `rake routes` 的输出: +这里我们把 `articles_path` 辅助方法传递给 `:url` 选项。要想知道这个方法有什么用,我们可以回过头看一下 `bin/rails routes` 的输出结果: -```bash -$ bin/rake routes +```sh +$ bin/rails routes Prefix Verb URI Pattern Controller#Action articles GET /articles(.:format) articles#index POST /articles(.:format) articles#create @@ -382,17 +399,19 @@ edit_article GET /articles/:id/edit(.:format) articles#edit root GET / welcome#index ``` -`articles_path` 帮助方法告诉 Rails,对应的地址是 `/articels`,默认情况下,这个表单会向这个路由发起 `POST` 请求。这个路由对应于 `ArticlesController` 控制器的 `create` 动作。 +`articles_path` 辅助方法告诉 Rails 把表单指向和 `articles` 前缀相关联的 URI 模式。默认情况下,表单会向这个路由发起 `POST` 请求。这个路由和当前控制器 `ArticlesController` 的 `create` 动作相关联。 + +有了表单和与之相关联的路由,我们现在可以填写表单,然后点击提交按钮来新建文章了,请实际操作一下。提交表单后,会看到一个熟悉的错误: -表单写好了,路由也定义了,现在可以填写表单,然后点击提交按钮新建文章了。请实际操作一下。提交表单后,会看到一个熟悉的错误: +![未知动作,在 `ArticlesController` 中找不到 `create` 动作](images/getting_started/unknown_action_create_for_articles.png) -![ArticlesController 控制器不知如何处理 create 动作](images/getting_started/unknown_action_create_for_articles.png) +解决问题的方法是在 `ArticlesController` 中创建 `create` 动作。 -解决这个错误,要在 `ArticlesController` 控制器中定义 `create` 动作。 + ### 创建文章 -要解决前一节出现的错误,可以在 `ArticlesController` 类中定义 `create` 方法。在 `app/controllers/articles_controller.rb` 文件中 `new` 方法后面添加以下代码: +要消除“未知动作”错误,我们需要修改 `app/controllers/articles_controller.rb` 文件,在 `ArticlesController` 类的 `new` 动作之后添加 `create` 动作,就像下面这样: ```ruby class ArticlesController < ApplicationController @@ -404,9 +423,9 @@ class ArticlesController < ApplicationController end ``` -然后再次提交表单,会看到另一个熟悉的错误:找不到模板。现在暂且不管这个错误。`create` 动作的作用是把新文章保存到数据库中。 +现在重新提交表单,会看到什么都没有改变。别着急!这是因为当我们没有说明动作的响应是什么时,Rails 默认返回 `204 No Content response`。我们刚刚添加了 `create` 动作,但没有说明响应是什么。这里,`create` 动作应该把新建文章保存到数据库中。 -提交表单后,其中的字段以参数的形式传递给 Rails。这些参数可以在控制器的动作中使用,完成指定的操作。要想查看这些参数的内容,可以把 `create` 动作改成: +表单提交后,其字段以参数形式传递给 Rails,然后就可以在控制器动作中引用这些参数,以执行特定任务。要想查看这些参数的内容,可以把 `create` 动作的代码修改成下面这样: ```ruby def create @@ -414,38 +433,44 @@ def create end ``` -`render` 方法接受一个简单的 Hash 为参数,这个 Hash 的键是 `plain`,对应的值为 `params[:article].inspect`。`params` 方法表示通过表单提交的参数,返回 `ActiveSupport::HashWithIndifferentAccess` 对象,可以使用字符串或者 Symbol 获取键对应的值。现在,我们只关注通过表单提交的参数。 +这里 `render` 方法接受了一个简单的散列(hash)作为参数,`:plain` 键的值是 `params[:article].inspect`。`params` 方法是代表表单提交的参数(或字段)的对象。`params` 方法返回 `ActionController::Parameters` 对象,这个对象允许使用字符串或符号访问散列的键。这里我们只关注通过表单提交的参数。 -如果现在再次提交表单,不会再看到找不到模板错误,而是会看到类似下面的文字: +TIP: 请确保牢固掌握 `params` 方法,这个方法很常用。让我们看一个示例 URL:`http://www.example.com/?username=dhh&email=dhh@email.com`。在这个 URL 中,`params[:username]` 的值是“dhh”,`params[:email]` 的值是“`dhh@email.com`”。 -```ruby -{"title"=>"First article!", "text"=>"This is my first article."} +如果再次提交表单,会看到下面这些内容: + +``` +"First Article!", "text"=>"This is my first article."} permitted: false> ``` -`create` 动作把表单提交的参数显示出来了。不过这么做没什么用,看到了参数又怎样,什么都没发生。 +`create` 动作把表单提交的参数都显示出来了,但这并没有什么用,只是看到了参数实际上却什么也没做。 + + ### 创建 Article 模型 -在 Rails 中,模型的名字使用单数,对应的数据表名使用复数。Rails 提供了一个生成器用来创建模型,大多数 Rails 开发者创建模型时都会使用。创建模型,请在终端里执行下面的命令: +在 Rails 中,模型使用单数名称,对应的数据库表使用复数名称。Rails 提供了用于创建模型的生成器,大多数 Rails 开发者在新建模型时倾向于使用这个生成器。要想新建模型,请执行下面的命令: -```bash +``` $ bin/rails generate model Article title:string text:text ``` -这个命令告知 Rails,我们要创建 `Article` 模型,以及一个字符串属性 `title` 和文本属性 `text`。这两个属性会自动添加到 `articles` 数据表中,映射到 `Article` 模型。 +上面的命令告诉 Rails 创建 `Article` 模型,并使模型具有字符串类型的 `title` 属性和文本类型的 `text` 属性。这两个属性会自动添加到数据库的 `articles` 表中,并映射到 `Article` 模型上。 + +为此 Rails 会创建一堆文件。这里我们只关注 `app/models/article.rb` 和 `db/migrate/20140120191729_create_articles.rb` 这两个文件 (后面这个文件名和你看到的可能会有点不一样)。后者负责创建数据库结构,下一节会详细说明。 -执行这个命令后,Rails 会生成一堆文件。现在我们只关注 `app/models/article.rb` 和 `db/migrate/20140120191729_create_articles.rb`(你得到的文件名可能有点不一样)这两个文件。后者用来创建数据库结构,下一节会详细说明。 +TIP: Active Record 很智能,能自动把数据表的字段名映射到模型属性上,因此无需在 Rails 模型中声明属性,让 Active Record 自动完成即可。 -TIP: Active Record 很智能,能自动把数据表中的字段映射到模型的属性上。所以无需在 Rails 的模型中声明属性,因为 Active Record 会自动映射。 + ### 运行迁移 -如前文所述,`rails generate model` 命令会在 `db/migrate` 文件夹中生成一个数据库迁移文件。迁移是一个 Ruby 类,能简化创建和修改数据库结构的操作。Rails 使用 rake 任务运行迁移,修改数据库结构后还能撤销操作。迁移的文件名中有个时间戳,这样能保证迁移按照创建的时间顺序运行。 +如前文所述,`bin/rails generate model` 命令会在 `db/migrate` 文件夹中生成数据库迁移文件。迁移是用于简化创建和修改数据库表操作的 Ruby 类。Rails 使用 rake 命令运行迁移,并且在迁移作用于数据库之后还可以撤销迁移操作。迁移的文件名包含了时间戳,以确保迁移按照创建时间顺序运行。 -`db/migrate/20140120191729_create_articles.rb`(还记得吗,你的迁移文件名可能有点不一样)文件的内容如下所示: +让我们看一下 `db/migrate/YYYYMMDDHHMMSS_create_articles.rb` 文件(记住,你的文件名可能会有点不一样),会看到下面的内容: ```ruby -class CreateArticles < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration[5.0] def change create_table :articles do |t| t.string :title @@ -457,30 +482,32 @@ class CreateArticles < ActiveRecord::Migration end ``` -在这个迁移中定义了一个名为 `change` 的方法,在运行迁移时执行。`change` 方法中定义的操作都是可逆的,Rails 知道如何撤销这次迁移操作。运行迁移后,会创建 `articles` 表,以及一个字符串字段和文本字段。同时还会创建两个时间戳字段,用来跟踪记录的创建时间和更新时间。 +上面的迁移创建了 `change` 方法,在运行迁移时会调用这个方法。在 `change` 方法中定义的操作都是可逆的,在需要时 Rails 知道如何撤销这些操作。运行迁移后会创建 `articles` 表,这个表包括一个字符串字段和一个文本字段,以及两个用于跟踪文章创建和更新时间的时间戳字段。 -TIP: 关于迁移的详细说明,请参阅“[Active Record 数据库迁移](/migrations.html)”一文。 +TIP: 关于迁移的更多介绍,请参阅[Active Record 迁移](active_record_migrations.html)。 -然后,使用 rake 命令运行迁移: +现在可以使用 `bin/rails` 命令运行迁移了: -```bash -$ bin/rake db:migrate +```sh +$ bin/rails db:migrate ``` -Rails 会执行迁移操作,告诉你创建了 `articles` 表。 +Rails 会执行迁移命令并告诉我们它创建了 Articles 表。 -```bash +``` == CreateArticles: migrating ================================================== -- create_table(:articles) -> 0.0019s == CreateArticles: migrated (0.0020s) ========================================= ``` -NOTE: 因为默认情况下,程序运行在开发环境中,所以相关的操作应用于 `config/database.yml` 文件中 `development` 区域设置的数据库上。如果想在其他环境中运行迁移,必须在命令中指明:`rake db:migrate RAILS_ENV=production`。 +NOTE: 因为默认情况下我们是在开发环境中工作,所以上述命令应用于 `config/database.yml` 文件中 `development` 部分定义的的数据库。要想在其他环境中执行迁移,例如生产环境,就必须在调用命令时显式传递环境变量:`bin/rails db:migrate RAILS_ENV=production`。 + + ### 在控制器中保存数据 -再回到 `ArticlesController` 控制器,我们要修改 `create` 动作,使用 `Article` 模型把数据保存到数据库中。打开 `app/controllers/articles_controller.rb` 文件,把 `create` 动作修改成这样: +回到 `ArticlesController`,修改 `create` 动作,使用新建的 `Article` 模型把数据保存到数据库。打开 `app/controllers/articles_controller.rb` 文件,像下面这样修改 `create` 动作: ```ruby def create @@ -491,15 +518,27 @@ def create end ``` -在 Rails 中,每个模型可以使用各自的属性初始化,自动映射到数据库字段上。`create` 动作中的第一行就是这个目的(还记得吗,`params[:article]` 就是我们要获取的属性)。`@article.save` 的作用是把模型保存到数据库中。保存完后转向 `show` 动作。稍后再编写 `show` 动作。 +让我们看一下上面的代码都做了什么:Rails 模型可以用相应的属性初始化,它们会自动映射到对应的数据库字段。`create` 动作中的第一行代码完成的就是这个操作(记住,`params[:article]` 包含了我们想要的属性)。接下来 `@article.save` 负责把模型保存到数据库。最后把页面重定向到 `show` 动作,这个 `show` 动作我们稍后再定义。 -TIP: 后文会看到,`@article.save` 返回一个布尔值,表示保存是否成功。 +TIP: 你可能想知道,为什么在上面的代码中 `Article.new` 的 `A` 是大写的,而在本文的其他地方引用 articles 时大都是小写的。因为这里我们引用的是在 `app/models/article.rb` 文件中定义的 `Article` 类,而在 Ruby 中类名必须以大写字母开头。 -再次访问 ,填写表单,还差一步就能创建文章了,会看到一个错误页面: +TIP: 之后我们会看到,`@article.save` 返回布尔值,以表明文章是否保存成功。 -![新建文章时禁止使用属性](images/getting_started/forbidden_attributes_for_new_article.png) +现在访问 ,我们就快要能够创建文章了,但我们还会看到下面的错误: -Rails 提供了很多安全防范措施保证程序的安全,你所看到的错误就是因为违反了其中一个措施。这个防范措施叫做“健壮参数”,我们要明确地告知 Rails 哪些参数可在控制器中使用。这里,我们想使用 `title` 和 `text` 参数。请把 `create` 动作修成成: +![禁用属性错误](images/getting_started/forbidden_attributes_for_new_article.png) + +Rails 提供了多种安全特性来帮助我们编写安全的应用,上面看到的就是一种安全特性。这个安全特性叫做 [健壮参数](action_controller_overview.html#strong-parameters)(strong parameter),要求我们明确地告诉 Rails 哪些参数允许在控制器动作中使用。 + +为什么我们要这样自找麻烦呢?一次性获取所有控制器参数并自动赋值给模型显然更简单,但这样做会造成恶意使用的风险。设想一下,如果有人对服务器发起了一个精心设计的请求,看起来就像提交了一篇新文章,但同时包含了能够破坏应用完整性的额外字段和值,会怎么样?这些恶意数据会批量赋值给模型,然后和正常数据一起进入数据库,这样就有可能破坏我们的应用或者造成更大损失。 + +所以我们只能为控制器参数设置白名单,以避免错误地批量赋值。这里,我们想在 `create` 动作中合法使用 `title` 和 `text` 参数,为此需要使用 `require` 和 `permit` 方法。像下面这样修改 `create` 动作中的一行代码: + +```ruby +@article = Article.new(params.require(:article).permit(:title, :text)) +``` + +上述代码通常被抽象为控制器类的一个方法,以便在控制器的多个动作中重用,例如在 `create` 和 `update` 动作中都会用到。除了批量赋值问题,为了禁止从外部调用这个方法,通常还要把它设置为 `private`。最后的代码像下面这样: ```ruby def create @@ -515,33 +554,43 @@ private end ``` -看到 `permit` 方法了吗?这个方法允许在动作中使用 `title` 和 `text` 属性。 +TIP: 关于键壮参数的更多介绍,请参阅上面提供的参考资料和[这篇博客](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/)。 -TIP: 注意,`article_params` 是私有方法。这种用法可以防止攻击者把修改后的属性传递给模型。关于健壮参数的更多介绍,请阅读[这篇文章](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/)。 + ### 显示文章 -现在再次提交表单,Rails 会提示找不到 `show` 动作。这个提示没多大用,我们还是先添加 `show` 动作吧。 +现在再次提交表单,Rails 会提示找不到 `show` 动作。尽管这个提示没有多大用处,但在继续前进之前我们还是先添加 `show` 动作吧。 -我们在 `rake routes` 的输出中看到,`show` 动作的路由是: +之前我们在 `bin/rails routes` 命令的输出结果中看到,`show` 动作对应的路由是: ``` article GET /articles/:id(.:format) articles#show ``` -`:id` 的意思是,路由期望接收一个名为 `id` 的参数,在这个例子中,就是文章的 ID。 +特殊句法 `:id` 告诉 Rails 这个路由期望接受 `:id` 参数,在这里也就是文章的 ID。 + +和前面一样,我们需要在 `app/controllers/articles_controller.rb` 文件中添加 `show` 动作,并创建对应的视图文件。 + +NOTE: 常见的做法是按照以下顺序在控制器中放置标准的 CRUD 动作:`index`,`show`,`new`,`edit`,`create`,`update` 和 `destroy`。你也可以按照自己的顺序放置这些动作,但要记住它们都是公开方法,如前文所述,必须放在私有方法之前才能正常工作。 -和前面一样,我们要在 `app/controllers/articles_controller.rb` 文件中添加 `show` 动作,以及相应的视图文件。 +有鉴于此,让我们像下面这样添加 `show` 动作: ```ruby -def show - @article = Article.find(params[:id]) -end +class ArticlesController < ApplicationController + def show + @article = Article.find(params[:id]) + end + + def new + end + + # 为了行文简洁,省略以下内容 ``` -有几点要注意。我们调用 `Article.find` 方法查找想查看的文章,传入的参数 `params[:id]` 会从请求中获取 `:id` 参数。我们还把文章对象存储在一个实例变量中(以 `@` 开头的变量),只有这样,变量才能在视图中使用。 +上面的代码中有几个问题需要注意。我们使用 `Article.find` 来查找文章,并传入 `params[:id]` 以便从请求中获得 `:id` 参数。我们还使用实例变量(前缀为 `@`)保存对文章对象的引用。这样做是因为 Rails 会把所有实例变量传递给视图。 -然后,新建 `app/views/articles/show.html.erb` 文件,写入下面的代码: +现在新建 `app/views/articles/show.html.erb` 文件,添加下面的代码: ```erb

    @@ -555,27 +604,39 @@ end

    ``` -做了以上修改后,就能真正的新建文章了。访问 ,自己试试。 +通过上面的修改,我们终于能够新建文章了。访问 ,自己试一试吧! ![显示文章](images/getting_started/show_action_for_articles.png) + + ### 列出所有文章 -我们还要列出所有文章,对应的路由是: +我们还需要列出所有文章,下面就来完成这个功能。在 `bin/rails routes` 命令的输出结果中,和列出文章对应的路由是: ``` articles GET /articles(.:format) articles#index ``` -在 `app/controllers/articles_controller.rb` 文件中,为 `ArticlesController` 控制器添加 `index` 动作: +在 `app/controllers/articles_controller.rb` 文件的 `ArticlesController` 中为上述路由添加对应的 `index` 动作。在编写 `index` 动作时,常见的做法是把它作为控制器的第一个方法,就像下面这样: ```ruby -def index - @articles = Article.all -end +class ArticlesController < ApplicationController + def index + @articles = Article.all + end + + def show + @article = Article.find(params[:id]) + end + + def new + end + + # 为了行文简洁,省略以下内容 ``` -然后编写这个动作的视图,保存为 `app/views/articles/index.html.erb`: +最后,在 `app/views/articles/index.html.erb` 文件中为 `index` 动作添加视图: ```erb

    Listing articles

    @@ -590,45 +651,48 @@ end <%= article.title %> <%= article.text %> + <%= link_to 'Show', article_path(article) %> <% end %> ``` -现在访问 ,会看到已经发布的文章列表。 +现在访问 ,会看到已创建的所有文章的列表。 + + ### 添加链接 -至此,我们可以新建、显示、列出文章了。下面我们添加一些链接,指向这些页面。 +至此,我们可以创建、显示、列出文章了。下面我们添加一些指向这些页面的链接。 -打开 `app/views/welcome/index.html.erb` 文件,改成这样: +打开 `app/views/welcome/index.html.erb` 文件,修改成下面这样: ```erb

    Hello, Rails!

    <%= link_to 'My Blog', controller: 'articles' %> ``` -`link_to` 是 Rails 内置的视图帮助方法之一,根据提供的文本和地址创建超链接。这上面这段代码中,地址是文章列表页面。 +`link_to` 方法是 Rails 内置的视图辅助方法之一,用于创建基于链接文本和地址的超链接。在这里地址指的是文章列表页面的路径。 -接下来添加到其他页面的链接。先在 `app/views/articles/index.html.erb` 中添加“New Article”链接,放在 `` 标签之前: +接下来添加指向其他视图的链接。首先在 `app/views/articles/index.html.erb` 文件中添加“New Article”链接,把这个链接放在 `
    ` 标签之前: ```erb <%= link_to 'New article', new_article_path %> ``` -点击这个链接后,会转向新建文章的表单页面。 +点击这个链接会打开用于新建文章的表单。 -然后在 `app/views/articles/new.html.erb` 中添加一个链接,位于表单下面,返回到 `index` 动作: +接下来在 `app/views/articles/new.html.erb` 文件中添加返回 `index` 动作的链接,把这个链接放在表单之后: ```erb -<%= form_for :article do |f| %> +<%= form_for :article, url: articles_path do |f| %> ... <% end %> <%= link_to 'Back', articles_path %> ``` -最后,在 `app/views/articles/show.html.erb` 模板中添加一个链接,返回 `index` 动作,这样用户查看某篇文章后就可以返回文章列表页面了: +最后,在 `app/views/articles/show.html.erb` 模板中添加返回 `index` 动作的链接,这样用户看完一篇文章后就可以返回文章列表页面了: ```erb

    @@ -644,33 +708,35 @@ end <%= link_to 'Back', articles_path %> ``` -TIP: 如果要链接到同一个控制器中的动作,不用指定 `:controller` 选项,因为默认情况下使用的就是当前控制器。 +TIP: 链接到当前控制器的动作时不需要指定 `:controller` 选项,因为 Rails 默认使用当前控制器。 + +TIP: 在开发环境中(默认情况下我们是在开发环境中工作),Rails 针对每个浏览器请求都会重新加载应用,因此对应用进行修改之后不需要重启服务器。 -TIP: 在开发模式下(默认),每次请求 Rails 都会重新加载程序,因此修改之后无需重启服务器。 + -### 添加数据验证 +### 添加验证 -模型文件,比如 `app/models/article.rb`,可以简单到只有这两行代码: +`app/models/article.rb` 模型文件简单到只有两行代码: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord end ``` -文件中没有多少代码,不过请注意,`Article` 类继承自 `ActiveRecord::Base`。Active Record 提供了很多功能,包括:基本的数据库 CRUD 操作,数据验证,复杂的搜索功能,以及多个模型之间的关联。 +虽然这个文件中代码很少,但请注意 `Article` 类继承自 `ApplicationRecord` 类,而 `ApplicationRecord` 类继承自 `ActiveRecord::Base` 类。正是 `ActiveRecord::Base` 类为 Rails 模型提供了大量功能,包括基本的数据库 CRUD 操作(创建、读取、更新、删除)、数据验证,以及对复杂搜索的支持和关联多个模型的能力。 -Rails 为模型提供了很多方法,用来验证传入的数据。打开 `app/models/article.rb` 文件,修改成: +Rails 提供了许多方法用于验证传入模型的数据。打开 `app/models/article.rb` 文件,像下面这样修改: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord validates :title, presence: true, length: { minimum: 5 } end ``` -添加的这段代码可以确保每篇文章都有一个标题,而且至少有五个字符。在模型中可以验证数据是否满足多种条件,包括:字段是否存在、是否唯一,数据类型,以及关联对象是否存在。“[Active Record 数据验证](/active_record_validations.html)”一文会详细介绍数据验证。 +添加的代码用于确保每篇文章都有标题,并且标题长度不少于 5 个字符。在 Rails 模型中可以验证多种条件,包括字段是否存在、字段是否唯一、字段的格式、关联对象是否存在,等等。关于验证的更多介绍,请参阅[Active Record 数据验证](active_record_validations.html)。 -添加数据验证后,如果把不满足验证条件的文章传递给 `@article.save`,会返回 `false`。打开 `app/controllers/articles_controller.rb` 文件,会发现,我们还没在 `create` 动作中检查 `@article.save` 的返回结果。如果保存失败,应该再次显示表单。为了实现这种功能,请打开 `app/controllers/articles_controller.rb` 文件,把 `new` 和 `create` 动作改成: +现在验证已经添加完毕,如果我们在调用 `@article.save` 时传递了无效的文章数据,验证就会返回 `false`。再次打开 `app/controllers/articles_controller.rb` 文件,会看到我们并没有在 `create` 动作中检查 `@article.save` 的调用结果。在这里如果 `@article.save` 失败了,就需要把表单再次显示给用户。为此,需要像下面这样修改 `app/controllers/articles_controller.rb` 文件中的 `new` 和 `create` 动作: ```ruby def new @@ -693,25 +759,29 @@ private end ``` -在 `new` 动作中添加了一个实例变量 `@article`。稍后你会知道为什么要这么做。 +在上面的代码中,我们在 `new` 动作中创建了新的实例变量 `@article`,稍后你就会知道为什么要这样做。 -注意,在 `create` 动作中,如果保存失败,调用的是 `render` 方法而不是 `redirect_to` 方法。用 `render` 方法才能在保存失败后把 `@article` 对象传给 `new` 动作的视图。渲染操作和表单提交在同一次请求中完成;而 `redirect_to` 会让浏览器发起一次新请求。 +注意在 `create` 动作中,当 `save` 返回 `false` 时,我们用 `render` 代替了 `redirect_to`。使用 `render` 方法是为了把 `@article` 对象回传给 `new` 模板。这里渲染操作是在提交表单的这个请求中完成的,而 `redirect_to` 会告诉浏览器发起另一个请求。 -刷新 ,提交一个没有标题的文章,Rails 会退回这个页面,但这种处理方法没多少用,你要告诉用户哪儿出错了。为了实现这种功能,请在 `app/views/articles/new.html.erb` 文件中检测错误消息: +刷新 ,试着提交一篇没有标题的文章,Rails 会返回这个表单,但这种处理方式没有多大用处,更好的做法是告诉用户哪里出错了。为此需要修改 `app/views/articles/new.html.erb` 文件,添加显示错误信息的代码: ```erb <%= form_for :article, url: articles_path do |f| %> + <% if @article.errors.any? %> -

    -

    <%= pluralize(@article.errors.count, "error") %> prohibited - this article from being saved:

    -
      - <% @article.errors.full_messages.each do |msg| %> -
    • <%= msg %>
    • - <% end %> -
    -
    +
    +

    + <%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved: +

    +
      + <% @article.errors.full_messages.each do |msg| %> +
    • <%= msg %>
    • + <% end %> +
    +
    <% end %> +

    <%= f.label :title %>
    <%= f.text_field :title %> @@ -725,52 +795,73 @@ private

    <%= f.submit %>

    + <% end %> <%= link_to 'Back', articles_path %> ``` -我们添加了很多代码,使用 `@article.errors.any?` 检查是否有错误,如果有错误,使用 `@article.errors.full_messages` 显示错误。 +上面我们添加了一些代码。我们使用 `@article.errors.any?` 检查是否有错误,如果有错误就使用 `@article.errors.full_messages` 列出所有错误信息。 -`pluralize` 是 Rails 提供的帮助方法,接受一个数字和字符串作为参数。如果数字比 1 大,字符串会被转换成复数形式。 +`pluralize` 是 Rails 提供的辅助方法,接受一个数字和一个字符串作为参数。如果数字比 1 大,字符串会被自动转换为复数形式。 -在 `new` 动作中加入 `@article = Article.new` 的原因是,如果不这么做,在视图中 `@article` 的值就是 `nil`,调用 `@article.errors.any?` 时会发生错误。 +在 `ArticlesController` 中添加 `@article = Article.new` 是因为如果不这样做,在视图中 `@article` 的值就会是 `nil`,这样在调用 `@article.errors.any?` 时就会抛出错误。 -TIP: Rails 会自动把出错的表单字段包含在一个 `div` 中,并为其添加了一个 class:`field_with_errors`。我们可以定义一些样式,凸显出错的字段。 +TIP: Rails 会自动用 div 包围含有错误信息的字段,并为这些 div 添加 `field_with_errors` 类。我们可以定义 CSS 规则突出显示错误信息。 -再次访问 ,尝试发布一篇没有标题的文章,会看到一个很有用的错误提示。 +当我们再次访问 ,试着提交一篇没有标题的文章,就会看到友好的错误信息。 ![出错的表单](images/getting_started/form_with_errors.png) + + ### 更新文章 -我们已经说明了 CRUD 中的 CR 两种操作。下面进入 U 部分,更新文章。 +我们已经介绍了 CRUD 操作中的“CR”两种操作,下面让我们看一下“U”操作,也就是更新文章。 -首先,要在 `ArticlesController` 中添加 `edit` 动作: +第一步要在 `ArticlesController` 中添加 `edit` 动作,通常把这个动作放在 `new` 动作和 `create` 动作之间,就像下面这样: ```ruby +def new + @article = Article.new +end + def edit @article = Article.find(params[:id]) end + +def create + @article = Article.new(article_params) + + if @article.save + redirect_to @article + else + render 'new' + end +end ``` -视图中要添加一个类似新建文章的表单。新建 `app/views/articles/edit.html.erb` 文件,写入下面的代码: +接下来在视图中添加一个表单,这个表单类似于前文用于新建文章的表单。创建 `app/views/articles/edit.html.erb` 文件,添加下面的代码: ```erb -

    Editing article

    +

    Edit article

    + +<%= form_for(@article) do |f| %> -<%= form_for :article, url: article_path(@article), method: :patch do |f| %> <% if @article.errors.any? %> -
    -

    <%= pluralize(@article.errors.count, "error") %> prohibited - this article from being saved:

    -
      - <% @article.errors.full_messages.each do |msg| %> -
    • <%= msg %>
    • - <% end %> -
    -
    +
    +

    + <%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved: +

    +
      + <% @article.errors.full_messages.each do |msg| %> +
    • <%= msg %>
    • + <% end %> +
    +
    <% end %> +

    <%= f.label :title %>
    <%= f.text_field :title %> @@ -784,20 +875,33 @@ end

    <%= f.submit %>

    + <% end %> <%= link_to 'Back', articles_path %> ``` -这里的表单指向 `update` 动作,现在还没定义,稍后会添加。 +上面的代码把表单指向了 `update` 动作,这个动作稍后我们再来定义。 -`method: :patch` 选项告诉 Rails,提交这个表单时使用 `PATCH` 方法发送请求。根据 REST 架构,更新资源时要使用 HTTP `PATCH` 方法。 +传入 `@article` 对象后,会自动为表单创建 URL,用于提交编辑后的文章。 -`form_for` 的第一个参数可以是对象,例如 `@article`,把对象中的字段填入表单。如果传入一个和实例变量(`@article`)同名的 Symbol(`:article`),效果也是一样。上面的代码使用的就是 Symbol。详情参见 [form_for 的文档](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for)。 +`method: :patch` 选项告诉 Rails 使用 `PATCH` 方法提交表单。根据 REST 协议,`PATCH` 方法是**更新**资源时使用的 HTTP 方法。 -然后,要在 `app/controllers/articles_controller.rb` 中添加 `update` 动作: +`form_for` 辅助方法的第一个参数可以是对象,例如 `@article`,`form_for` 辅助方法会用这个对象的字段来填充表单。如果传入和实例变量(`@article`)同名的符号(`:article`),也会自动产生相同效果,上面的代码使用的就是符号。关于 `form_for` 辅助方法参数的更多介绍,请参阅 [`form_for` 的文档](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for)。 + +接下来在 `app/controllers/articles_controller.rb` 文件中创建 `update` 动作,把这个动作放在 `create` 动作和 `private` 方法之间: ```ruby +def create + @article = Article.new(article_params) + + if @article.save + redirect_to @article + else + render 'new' + end +end + def update @article = Article.find(params[:id]) @@ -814,13 +918,13 @@ private end ``` -新定义的 `update` 方法用来处理对现有文章的更新操作,接收一个 Hash,包含想要修改的属性。和之前一样,如果更新文章出错了,要再次显示表单。 +`update` 动作用于更新已有记录,它接受一个散列作为参数,散列中包含想要更新的属性。和之前一样,如果更新文章时发生错误,就需要把表单再次显示给用户。 -上面的代码再次使用了前面为 `create` 动作定义的 `article_params` 方法。 +上面的代码重用了之前为 `create` 动作定义的 `article_params` 方法。 -TIP: 不用把所有的属性都提供给 `update` 动作。例如,如果使用 `@article.update(title: 'A new title')`,Rails 只会更新 `title` 属性,不修改其他属性。 +TIP: 不用把所有属性都传递给 `update` 方法。例如,调用 `@article.update(title: 'A new title')` 时,Rails 只更新 `title` 属性而不修改其他属性。 -最后,我们想在文章列表页面,在每篇文章后面都加上一个链接,指向 `edit` 动作。打开 `app/views/articles/index.html.erb` 文件,在“Show”链接后面添加“Edit”链接: +最后,我们想在文章列表中显示指向 `edit` 动作的链接。打开 `app/views/articles/index.html.erb` 文件,在 `Show` 链接后面添加 `Edit` 链接: ```erb
    @@ -830,51 +934,57 @@ TIP: 不用把所有的属性都提供给 `update` 动作。例如,如果使 -<% @articles.each do |article| %> - - - - - - -<% end %> + <% @articles.each do |article| %> + + + + + + + <% end %>
    <%= article.title %><%= article.text %><%= link_to 'Show', article_path(article) %><%= link_to 'Edit', edit_article_path(article) %>
    <%= article.title %><%= article.text %><%= link_to 'Show', article_path(article) %><%= link_to 'Edit', edit_article_path(article) %>
    ``` -我们还要在 `app/views/articles/show.html.erb` 模板的底部加上“Edit”链接: +接着在 `app/views/articles/show.html.erb` 模板中添加 `Edit` 链接,这样文章页面也有 `Edit` 链接了。把这个链接添加到模板底部: ```erb ... +<%= link_to 'Edit', edit_article_path(@article) %> | <%= link_to 'Back', articles_path %> -| <%= link_to 'Edit', edit_article_path(@article) %> ``` -下图是文章列表页面现在的样子: +下面是文章列表现在的样子: + +![文章列表](images/getting_started/index_action_with_edit_link.png) -![在文章列表页面显示了编辑链接](images/getting_started/index_action_with_edit_link.png) + ### 使用局部视图去掉视图中的重复代码 -编辑文章页面和新建文章页面很相似,显示表单的代码是相同的。下面使用局部视图去掉两个视图中的重复代码。按照约定,局部视图的文件名以下划线开头。 +编辑文章页面和新建文章页面看起来很相似,实际上这两个页面用于显示表单的代码是相同的。现在我们要用局部视图来去掉这些重复代码。按照约定,局部视图的文件名以下划线开头。 -TIP: 关于局部视图的详细介绍参阅“[Layouts and Rendering in Rails](/layouts_and_rendering.html)”一文。 +TIP: 关于局部视图的更多介绍,请参阅[Rails 布局和视图渲染](layouts_and_rendering.html)。 -新建 `app/views/articles/_form.html.erb` 文件,写入以下代码: +新建 `app/views/articles/_form.html.erb` 文件,添加下面的代码: ```erb <%= form_for @article do |f| %> + <% if @article.errors.any? %> -
    -

    <%= pluralize(@article.errors.count, "error") %> prohibited - this article from being saved:

    -
      - <% @article.errors.full_messages.each do |msg| %> -
    • <%= msg %>
    • - <% end %> -
    -
    +
    +

    + <%= pluralize(@article.errors.count, "error") %> prohibited + this article from being saved: +

    +
      + <% @article.errors.full_messages.each do |msg| %> +
    • <%= msg %>
    • + <% end %> +
    +
    <% end %> +

    <%= f.label :title %>
    <%= f.text_field :title %> @@ -888,14 +998,13 @@ TIP: 关于局部视图的详细介绍参阅“[Layouts and Rendering in Rails](

    <%= f.submit %>

    + <% end %> ``` -除了第一行 `form_for` 的用法变了之外,其他代码都和之前一样。之所以能在两个动作中共用一个 `form_for`,是因为 `@article` 是一个资源,对应于符合 REST 架构的路由,Rails 能自动分辨使用哪个地址和请求方法。 - -关于这种 `form_for` 用法的详细说明,请查阅 [API 文档](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style)。 +除了第一行 `form_for` 的用法变了之外,其他代码都和之前一样。之所以能用这个更短、更简单的 `form_for` 声明来代替新建文章页面和编辑文章页面的两个表单,是因为 `@article` 是一个资源,对应于一套 REST 式路由,Rails 能够推断出应该使用哪个地址和方法。关于 `form_for` 用法的更多介绍,请参阅“[面向资源的风格](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style)”。 -下面来修改 `app/views/articles/new.html.erb` 视图,使用新建的局部视图,把其中的代码全删掉,替换成: +现在更新 `app/views/articles/new.html.erb` 视图,以使用新建的局部视图。把文件内容替换为下面的代码: ```erb

    New article

    @@ -905,7 +1014,7 @@ TIP: 关于局部视图的详细介绍参阅“[Layouts and Rendering in Rails]( <%= link_to 'Back', articles_path %> ``` -然后按照同样地方法修改 `app/views/articles/edit.html.erb` 视图: +然后按照同样的方法修改 `app/views/articles/edit.html.erb` 视图: ```erb

    Edit article

    @@ -915,21 +1024,23 @@ TIP: 关于局部视图的详细介绍参阅“[Layouts and Rendering in Rails]( <%= link_to 'Back', articles_path %> ``` + + ### 删除文章 -现在介绍 CRUD 中的 D,从数据库中删除文章。按照 REST 架构的约定,删除文章的路由是: +现在该介绍 CRUD 中的“D”操作了,也就是从数据库删除文章。按照 REST 架构的约定,在 `bin/rails routes` 命令的输出结果中删除文章的路由是: -```ruby +``` DELETE /articles/:id(.:format) articles#destroy ``` -删除资源时使用 DELETE 请求。如果还使用 GET 请求,可以构建如下所示的恶意地址: +删除资源的路由应该使用 `delete` 路由方法。如果在删除资源时仍然使用 `get` 路由,就可能给那些设计恶意地址的人提供可乘之机: ```html look at this cat! ``` -删除资源使用 DELETE 方法,路由会把请求发往 `app/controllers/articles_controller.rb` 中的 `destroy` 动作。`destroy` 动作现在还不存在,下面来添加: +我们用 `delete` 方法来删除资源,对应的路由会映射到 `app/controllers/articles_controller.rb` 文件中的 `destroy` 动作,稍后我们要创建这个动作。`destroy` 动作是控制器中的最后一个 CRUD 动作,和其他公共 CRUD 动作一样,这个动作应该放在 `private` 或 `protected` 方法之前。打开 `app/controllers/articles_controller.rb` 文件,添加下面的代码: ```ruby def destroy @@ -940,9 +1051,63 @@ def destroy end ``` -想把记录从数据库删除,可以在 Active Record 对象上调用 `destroy` 方法。注意,我们无需为这个动作编写视图,因为它会转向 `index` 动作。 +在 `app/controllers/articles_controller.rb` 文件中,`ArticlesController` 的完整代码应该像下面这样: + +```ruby +class ArticlesController < ApplicationController + def index + @articles = Article.all + end + + def show + @article = Article.find(params[:id]) + end + + def new + @article = Article.new + end -最后,在 `index` 动作的模板(`app/views/articles/index.html.erb`)中加上“Destroy”链接: + def edit + @article = Article.find(params[:id]) + end + + def create + @article = Article.new(article_params) + + if @article.save + redirect_to @article + else + render 'new' + end + end + + def update + @article = Article.find(params[:id]) + + if @article.update(article_params) + redirect_to @article + else + render 'edit' + end + end + + def destroy + @article = Article.find(params[:id]) + @article.destroy + + redirect_to articles_path + end + + private + def article_params + params.require(:article).permit(:title, :text) + end +end +``` + +在 Active Record 对象上调用 `destroy` 方法,就可从数据库中删除它们。注意,我们不需要为 `destroy` 动作添加视图,因为完成操作后它会重定向到 `index` 动作。 + +最后,在 `index` 动作的模板(`app/views/articles/index.html.erb`)中加上“Destroy”链接,这样就大功告成了: ```erb

    Listing Articles

    @@ -954,70 +1119,76 @@ end -<% @articles.each do |article| %> - - <%= article.title %> - <%= article.text %> - <%= link_to 'Show', article_path(article) %> - <%= link_to 'Edit', edit_article_path(article) %> - <%= link_to 'Destroy', article_path(article), - method: :delete, data: { confirm: 'Are you sure?' } %> - -<% end %> + <% @articles.each do |article| %> + + <%= article.title %> + <%= article.text %> + <%= link_to 'Show', article_path(article) %> + <%= link_to 'Edit', edit_article_path(article) %> + <%= link_to 'Destroy', article_path(article), + method: :delete, + data: { confirm: 'Are you sure?' } %> + + <% end %> ``` -生成“Destroy”链接的 `link_to` 用法有点不一样,第二个参数是具名路由,随后还传入了几个参数。`:method` 和 `:'data-confirm'` 选项设置链接的 HTML5 属性,点击链接后,首先会显示一个对话框,然后发起 DELETE 请求。这两个操作通过 `jquery_ujs` 这个 JavaScript 脚本实现。生成程序骨架时,会自动把 `jquery_ujs` 加入程序的布局中(`app/views/layouts/application.html.erb`)。没有这个脚本,就不会显示确认对话框。 +在上面的代码中,`link_to` 辅助方法生成“Destroy”链接的用法有点不同,其中第二个参数是具名路由(named route),还有一些选项作为其他参数。`method: :delete` 和 `data: { confirm: 'Are you sure?' }` 选项用于设置链接的 HTML5 属性,这样点击链接后 Rails 会先向用户显示一个确认对话框,然后用 `delete` 方法发起请求。这些操作是通过 JavaScript 脚本 `rails-ujs` 实现的,这个脚本在生成应用骨架时已经被自动包含在了应用的布局中(`app/views/layouts/application.html.erb`)。如果没有这个脚本,确认对话框就无法显示。 ![确认对话框](images/getting_started/confirm_dialog.png) -恭喜,现在你可以新建、显示、列出、更新、删除文章了。 +TIP: 关于非侵入式 JavaScript 的更多介绍,请参阅[在 Rails 中使用 JavaScript](working_with_javascript_in_rails.html)。 + +恭喜你!现在你已经可以创建、显示、列出、更新和删除文章了! -TIP: 一般情况下,Rails 建议使用资源对象,而不手动设置路由。关于路由的详细介绍参阅“[Rails 路由全解](/routing.html)”一文。 +TIP: 通常 Rails 鼓励用资源对象来代替手动声明路由。关于路由的更多介绍,请参阅[Rails 路由全解](routing.html)。 -添加第二个模型 ------------- + -接下来要在程序中添加第二个模型,用来处理文章的评论。 +## 添加第二个模型 + +现在是为应用添加第二个模型的时候了。这个模型用于处理文章评论。 + + ### 生成模型 -下面要用到的生成器,和之前生成 `Article` 模型的一样。我们要创建一个 `Comment` 模型,表示文章的评论。在终端执行下面的命令: +接下来将要使用的生成器,和之前用于创建 `Article` 模型的一样。这次我们要创建 `Comment` 模型,用于保存文章评论。在终端中执行下面的命令: -```bash -$ rails generate model Comment commenter:string body:text article:references +```sh +$ bin/rails generate model Comment commenter:string body:text article:references ``` -这个命令生成四个文件: +上面的命令会生成 4 个文件: -| 文件 | 作用 | -|----------------------------------------------|----------------------------------------------------| -| db/migrate/20140120201010_create_comments.rb | 生成 comments 表所用的迁移文件(你得到的文件名稍有不同) | -| app/models/comment.rb | Comment 模型文件 | -| test/models/comment_test.rb | Comment 模型的测试文件 | -| test/fixtures/comments.yml | 测试时使用的固件 | +| 文件 | 用途 | +|---|---| +| `db/migrate/20140120201010_create_comments.rb` | 用于在数据库中创建 comments 表的迁移文件(你的文件名会包含不同的时间戳) | +| `app/models/comment.rb` | `Comment` 模型文件 | +| `test/models/comment_test.rb` | `Comment` 模型的测试文件 | +| `test/fixtures/comments.yml` | 用于测试的示例评论 | -首先来看一下 `app/models/comment.rb` 文件: +首先看一下 `app/models/comment.rb` 文件: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :article end ``` -文件的内容和前面的 `Article` 模型差不多,不过多了一行代码:`belongs_to :article`。这行代码用来建立 Active Record 关联。下文会简单介绍关联。 +可以看到,`Comment` 模型文件的内容和之前的 `Article` 模型差不多,仅仅多了一行 `belongs_to :article`,这行代码用于建立 Active Record 关联。下一节会简单介绍关联。 + +在上面的 Bash 命令中使用的 `:references` 关键字是一种特殊的模型数据类型,用于在数据表中新建字段。这个字段以提供的模型名加上 `_id` 后缀作为字段名,保存整数值。之后通过分析 `db/schema.rb` 文件可以更好地理解这些内容。 -除了模型文件,Rails 还生成了一个迁移文件,用来创建对应的数据表: +除了模型文件,Rails 还生成了迁移文件,用于创建对应的数据表: ```ruby -class CreateComments < ActiveRecord::Migration +class CreateComments < ActiveRecord::Migration[5.0] def change create_table :comments do |t| t.string :commenter t.text :body - - # this line adds an integer column called `article_id`. - t.references :article, index: true + t.references :article, foreign_key: true t.timestamps end @@ -1025,53 +1196,57 @@ class CreateComments < ActiveRecord::Migration end ``` -`t.references` 这行代码为两个模型的关联创建一个外键字段,同时还为这个字段创建了索引。下面运行这个迁移: +`t.references` 这行代码创建 `article_id` 整数字段,为这个字段建立索引,并建立指向 `articles` 表的 `id` 字段的外键约束。下面运行这个迁移: -```bash -$ rake db:migrate +```sh +$ bin/rails db:migrate ``` -Rails 相当智能,只会执行还没有运行的迁移,在命令行中会看到以下输出: +Rails 很智能,只会运行针对当前数据库还没有运行过的迁移,运行结果像下面这样: -```bash +``` == CreateComments: migrating ================================================= -- create_table(:comments) -> 0.0115s == CreateComments: migrated (0.0119s) ======================================== ``` + + ### 模型关联 -使用 Active Record 关联可以轻易的建立两个模型之间的关系。评论和文章之间的关联是这样的: +Active Record 关联让我们可以轻易地声明两个模型之间的关系。对于评论和文章,我们可以像下面这样声明: -* 评论属于一篇文章 -* 一篇文章有多个评论 +* 每一条评论都属于某一篇文章 +* 一篇文章可以有多条评论 -这种关系和 Rails 用来声明关联的句法具有相同的逻辑。我们已经看过 `Comment` 模型中那行代码,声明评论属于文章: +实际上,这种表达方式和 Rails 用于声明模型关联的句法非常接近。前文我们已经看过 `Comment` 模型中用于声明模型关联的代码,这行代码用于声明每一条评论都属于某一篇文章: ```ruby -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :article end ``` -我们要编辑 `app/models/article.rb` 文件,加入这层关系的另一端: +现在修改 `app/models/article.rb` 文件来添加模型关联的另一端: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments validates :title, presence: true, length: { minimum: 5 } end ``` -这两行声明能自动完成很多操作。例如,如果实例变量 `@article` 是一个文章对象,可以使用 `@article.comments` 取回一个数组,其元素是这篇文章的评论。 +这两行声明能够启用一些自动行为。例如,如果 `@article` 实例变量表示一篇文章,就可以使用 `@article.comments` 以数组形式取回这篇文章的所有评论。 -TIP: 关于 Active Record 关联的详细介绍,参阅“[Active Record 关联](/association_basics.html)”一文。 +TIP: 关于模型关联的更多介绍,请参阅[Active Record 关联](association_basics.html)。 -### 添加评论的路由 + -和 `article` 控制器一样,添加路由后 Rails 才知道在哪个地址上查看评论。打开 `config/routes.rb` 文件,按照下面的方式修改: +### 为评论添加路由 + +和 `welcome` 控制器一样,在添加路由之后 Rails 才知道在哪个地址上查看评论。再次打开 `config/routes.rb` 文件,像下面这样进行修改: ```ruby resources :articles do @@ -1079,33 +1254,34 @@ resources :articles do end ``` -我们把 `comments` 放在 `articles` 中,这叫做嵌套资源,表明了文章和评论间的层级关系。 +上面的代码在 `articles` 资源中创建 `comments` 资源,这种方式被称为嵌套资源。这是表明文章和评论之间层级关系的另一种方式。 + +TIP: 关于路由的更多介绍,请参阅[Rails 路由全解](routing.html)。 -TIP: 关于路由的详细介绍,参阅“[Rails 路由全解](/routing.html)”一文。 + ### 生成控制器 -有了模型,下面要创建控制器了,还是使用前面用过的生成器: +有了模型,下面应该创建对应的控制器了。还是使用前面用过的生成器: -```bash -$ rails generate controller Comments +```sh +$ bin/rails generate controller Comments ``` -这个命令生成六个文件和一个空文件夹: +上面的命令会创建 5 个文件和一个空文件夹: -| 文件/文件夹 | 作用 | -| -------------------------------------------- | ---------------------------------------- | -| app/controllers/comments_controller.rb | Comments 控制器文件 | -| app/views/comments/ | 控制器的视图存放在这个文件夹里 | -| test/controllers/comments_controller_test.rb | 控制器测试文件 | -| app/helpers/comments_helper.rb | 视图帮助方法文件 | -| test/helpers/comments_helper_test.rb | 帮助方法测试文件 | -| app/assets/javascripts/comment.js.coffee | 控制器的 CoffeeScript 文件 | -| app/assets/stylesheets/comment.css.scss | 控制器的样式表文件 | +| 文件/文件夹 | 用途 | +|---|---| +| `app/controllers/comments_controller.rb` | Comments 控制器文件 | +| `app/views/comments/` | 控制器的视图保存在这里 | +| `test/controllers/comments_controller_test.rb` | 控制器的测试文件 | +| `app/helpers/comments_helper.rb` | 视图辅助方法文件 | +| `app/assets/javascripts/comments.coffee` | 控制器的 CoffeeScript 文件 | +| `app/assets/stylesheets/comments.scss` | 控制器的样式表文件 | -在任何一个博客中,读者读完文章后就可以发布评论。评论发布后,会转向文章显示页面,查看自己的评论是否显示出来了。所以,`CommentsController` 中要定义新建评论的和删除垃圾评论的方法。 +在博客中,读者看完文章后可以直接发表评论,并且马上可以看到这些评论是否在页面上显示出来了。我们的博客采取同样的设计。这里 `CommentsController` 需要提供创建评论和删除垃圾评论的方法。 -首先,修改显示文章的模板(`app/views/articles/show.html.erb`),允许读者发布评论: +首先修改显示文章的模板(`app/views/articles/show.html.erb`),添加发表评论的功能: ```erb

    @@ -1133,13 +1309,13 @@ $ rails generate controller Comments

    <% end %> +<%= link_to 'Edit', edit_article_path(@article) %> | <%= link_to 'Back', articles_path %> -| <%= link_to 'Edit', edit_article_path(@article) %> ``` -上面的代码在显示文章的页面添加了一个表单,调用 `CommentsController` 控制器的 `create` 动作发布评论。`form_for` 的参数是个数组,构建嵌套路由,例如 `/articles/1/comments`。 +上面的代码在显示文章的页面中添加了用于新建评论的表单,通过调用 `CommentsController` 的 `create` 动作来发表评论。这里 `form_for` 辅助方法以数组为参数,会创建嵌套路由,例如 `/articles/1/comments`。 -下面在 `app/controllers/comments_controller.rb` 文件中定义 `create` 方法: +接下来在 `app/controllers/comments_controller.rb` 文件中添加 `create` 动作: ```ruby class CommentsController < ApplicationController @@ -1156,11 +1332,11 @@ class CommentsController < ApplicationController end ``` -这里使用的代码要比文章的控制器复杂得多,因为设置了嵌套关系,必须这么做评论功能才能使用。发布评论时要知道这个评论属于哪篇文章,所以要在 `Article` 模型上调用 `find` 方法查找文章对象。 +上面的代码比 `Articles` 控制器的代码复杂得多,这是嵌套带来的副作用。对于每一个发表评论的请求,都必须记录这条评论属于哪篇文章,因此需要在 `Article` 模型上调用 `find` 方法来获取文章对象。 -而且,这段代码还充分利用了关联关系生成的方法。我们在 `@article.comments` 上调用 `create` 方法,创建并保存评论。这么做能自动把评论和文章联系起来,让这个评论属于这篇文章。 +此外,上面的代码还利用了关联特有的方法,在 `@article.comments` 上调用 `create` 方法来创建和保存评论,同时自动把评论和对应的文章关联起来。 -添加评论后,调用 `article_path(@article)` 帮助方法,转向原来的文章页面。前面说过,这个帮助函数调用 `ArticlesController` 的 `show` 动作,渲染 `show.html.erb` 模板。我们要在这个模板中显示评论,所以要修改一下 `app/views/articles/show.html.erb`: +添加评论后,我们使用 `article_path(@article)` 辅助方法把用户带回原来的文章页面。如前文所述,这里调用了 `ArticlesController` 的 `show` 动作来渲染 `show.html.erb` 模板,因此需要修改 `app/views/articles/show.html.erb` 文件来显示评论: ```erb

    @@ -1201,22 +1377,25 @@ end

    <% end %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` -现在,可以为文章添加评论了,成功添加后,评论会在正确的位置显示。 +现在可以在我们的博客中为文章添加评论了,评论添加后就会显示在正确的位置上。 + +![带有评论的文章](images/getting_started/article_with_comments.png) + + -![文章的评论](images/getting_started/article_with_comments.png) +## 重构 -重构 ----- +现在博客的文章和评论都已经正常工作,打开 `app/views/articles/show.html.erb` 文件,会看到文件代码变得又长又不美观。因此下面我们要用局部视图来重构代码。 -现在博客的文章和评论都能正常使用了。看一下 `app/views/articles/show.html.erb` 模板,内容太多。下面使用局部视图重构。 + -### 渲染局部视图中的集合 +### 渲染局部视图集合 -首先,把显示文章评论的代码抽出来,写入局部视图中。新建 `app/views/comments/_comment.html.erb` 文件,写入下面的代码: +首先创建评论的局部视图,把显示文章评论的代码抽出来。创建 `app/views/comments/_comment.html.erb` 文件,添加下面的代码: ```erb

    @@ -1230,7 +1409,7 @@ end

    ``` -然后把 `app/views/articles/show.html.erb` 修改成: +然后像下面这样修改 `app/views/articles/show.html.erb` 文件: ```erb

    @@ -1261,15 +1440,17 @@ end

    <% end %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` -这个视图会使用局部视图 `app/views/comments/_comment.html.erb` 渲染 `@article.comments` 集合中的每个评论。`render` 方法会遍历 `@article.comments` 集合,把每个评论赋值给一个和局部视图同名的本地变量,在这个例子中本地变量是 `comment`,这个本地变量可以在局部视图中使用。 +这样对于 `@article.comments` 集合中的每条评论,都会渲染 `app/views/comments/_comment.html.erb` 文件中的局部视图。`render` 方法会遍历 `@article.comments` 集合,把每条评论赋值给局部视图中的同名局部变量,也就是这里的 `comment` 变量。 + + -### 渲染局部视图中的表单 +### 渲染局部视图表单 -我们把添加评论的代码也移到局部视图中。新建 `app/views/comments/_form.html.erb` 文件,写入: +我们把添加评论的代码也移到局部视图中。创建 `app/views/comments/_form.html.erb` 文件,添加下面的代码: ```erb <%= form_for([@article, @article.comments.build]) do |f| %> @@ -1287,7 +1468,7 @@ end <% end %> ``` -然后把 `app/views/articles/show.html.erb` 改成: +然后像下面这样修改 `app/views/articles/show.html.erb` 文件: ```erb

    @@ -1304,22 +1485,23 @@ end <%= render @article.comments %>

    Add a comment:

    -<%= render "comments/form" %> +<%= render 'comments/form' %> -<%= link_to 'Edit Article', edit_article_path(@article) %> | -<%= link_to 'Back to Articles', articles_path %> +<%= link_to 'Edit', edit_article_path(@article) %> | +<%= link_to 'Back', articles_path %> ``` -第二个 `render` 方法的参数就是要渲染的局部视图,即 `comments/form`。Rails 很智能,能解析其中的斜线,知道要渲染 `app/views/comments` 文件夹中的 `_form.html.erb` 模板。 +上面的代码中第二个 `render` 方法的参数就是我们刚刚定义的 `comments/form` 局部视图。Rails 很智能,能够发现字符串中的斜线,并意识到我们想渲染 `app/views/comments` 文件夹中的 `_form.html.erb` 文件。 -`@article` 变量在所有局部视图中都可使用,因为它是实例变量。 +`@article` 是实例变量,因此在所有局部视图中都可以使用。 -删除评论 -------- + -博客还有一个重要的功能是删除垃圾评论。为了实现这个功能,要在视图中添加一个连接,并在 `CommentsController` 中定义 `destroy` 动作。 +## 删除评论 -先在 `app/views/comments/_comment.html.erb` 局部视图中加入删除评论的链接: +博客还有一个重要功能是删除垃圾评论。为了实现这个功能,我们需要在视图中添加一个链接,并在 `CommentsController` 中添加 `destroy` 动作。 + +首先在 `app/views/comments/_comment.html.erb` 局部视图中添加删除评论的链接: ```erb

    @@ -1339,7 +1521,7 @@ end

    ``` -点击“Destroy Comment”链接后,会向 `CommentsController` 控制器发起 `DELETE /articles/:article_id/comments/:id` 请求。我们可以从这个请求中找到要删除的评论。下面在控制器中加入 `destroy` 动作(`app/controllers/comments_controller.rb`): +点击“Destroy Comment”链接后,会向 `CommentsController` 发起 `DELETE /articles/:article_id/comments/:id` 请求,这个请求将用于删除指定评论。下面在控制器(`app/controllers/comments_controller.rb`)中添加 `destroy` 动作: ```ruby class CommentsController < ApplicationController @@ -1363,32 +1545,37 @@ class CommentsController < ApplicationController end ``` -`destroy` 动作先查找当前文章,然后在 `@article.comments` 集合中找到对应的评论,将其从数据库中删掉,最后转向显示文章的页面。 +`destroy` 动作首先找到指定文章,然后在 `@article.comments` 集合中找到指定评论,接着从数据库删除这条评论,最后重定向到显示文章的页面。 + + ### 删除关联对象 -如果删除一篇文章,也要删除文章中的评论,不然这些评论会占用数据库空间。在 Rails 中可以在关联中指定 `dependent` 选项达到这一目的。把 `Article` 模型(`app/models/article.rb`)修改成: +如果要删除一篇文章,文章的相关评论也需要删除,否则这些评论还会占用数据库空间。在 Rails 中可以使用关联的 `dependent` 选项来完成这一工作。像下面这样修改 `app/models/article.rb` 文件中的 `Article` 模型: ```ruby -class Article < ActiveRecord::Base +class Article < ApplicationRecord has_many :comments, dependent: :destroy validates :title, presence: true, length: { minimum: 5 } end ``` -安全 ----- + + +## 安全 -### 基本认证 + -如果把这个博客程序放在网上,所有人都能添加、编辑、删除文章和评论。 +### 基本身份验证 -Rails 提供了一种简单的 HTTP 身份认证机制可以避免出现这种情况。 +现在如果我们把博客放在网上,任何人都能够添加、修改、删除文章或删除评论。 -在 `ArticlesController` 中,我们要用一种方法禁止未通过认证的用户访问其中几个动作。我们需要的是 `http_basic_authenticate_with` 方法,通过这个方法的认证后才能访问所请求的动作。 +Rails 提供了一个非常简单的 HTTP 身份验证系统,可以很好地解决这个问题。 -要使用这个身份认证机制,需要在 `ArticlesController` 控制器的顶部调用 `http_basic_authenticate_with` 方法。除了 `index` 和 `show` 动作,访问其他动作都要通过认证,所以在 `app/controllers/articles_controller.rb` 中,要这么做: +我们需要一种方法来禁止未认证用户访问 `ArticlesController` 的动作。这里我们可以使用 Rails 的 `http_basic_authenticate_with` 方法,通过这个方法的认证后才能访问所请求的动作。 + +要使用这个身份验证系统,可以在 `app/controllers/articles_controller` 文件中的 `ArticlesController` 的顶部进行指定。这里除了 `index` 和 `show` 动作,其他动作都要通过身份验证才能访问,为此要像下面这样添加代码: ```ruby class ArticlesController < ApplicationController @@ -1399,10 +1586,10 @@ class ArticlesController < ApplicationController @articles = Article.all end - # snipped for brevity + # 为了行文简洁,省略以下内容 ``` -同时,我们还希望只有通过认证的用户才能删除评论。修改 `CommentsController` 控制器(`app/controllers/comments_controller.rb`): +同时只有通过身份验证的用户才能删除评论,为此要在 `CommentsController`(`app/controllers/comments_controller.rb`)中像下面这样添加代码: ```ruby class CommentsController < ApplicationController @@ -1411,47 +1598,46 @@ class CommentsController < ApplicationController def create @article = Article.find(params[:article_id]) - ... + # ... end - # snipped for brevity + # 为了行文简洁,省略以下内容 ``` -现在,如果想新建文章,会看到一个 HTTP 基本认证对话框。 +现在如果我们试着新建文章,就会看到 HTTP 基本身份验证对话框: + +![HTTP 基本身份验证对话框](images/getting_started/challenge.png) -![HTTP 基本认证对话框](images/getting_started/challenge.png) +此外,还可以在 Rails 中使用其他身份验证方法。在众多选择中,[Devise](https://github.com/plataformatec/devise) 和 [Authlogic](https://github.com/binarylogic/authlogic) 是两个流行的 Rails 身份验证扩展。 -其他的身份认证方法也可以在 Rails 程序中使用。其中两个比较流行的是 [Devise](https://github.com/plataformatec/devise) 引擎和 [Authlogic](https://github.com/binarylogic/authlogic) gem。 + ### 其他安全注意事项 -安全,尤其是在网页程序中,是个很宽泛和值得深入研究的领域。Rails 程序的安全措施,在“[Ruby on Rails 安全指南](/security.html)”中有更深入的说明。 +安全,尤其是 Web 应用的安全,是一个广泛和值得深入研究的领域。关于 Rails 应用安全的更多介绍,请参阅[Ruby on Rails 安全指南](security.html)。 -接下来做什么 ------------ + -至此,我们开发了第一个 Rails 程序,请尽情的修改、试验。在开发过程中难免会需要帮助,如果使用 Rails 时需要协助,可以使用这些资源: +## 接下来做什么? -* [Ruby on Rails 指南]({{ site.baseurl}}/index.html) -* [Ruby on Rails 教程](http://railstutorial-china.org/) -* [Ruby on Rails 邮件列表](http://groups.google.com/group/rubyonrails-talk) -* irc.freenode.net 上的 [#rubyonrails](irc://irc.freenode.net/#rubyonrails) 频道 +至此,我们已经完成了第一个 Rails 应用,请在此基础上尽情修改、试验。 -Rails 本身也提供了帮助文档,可以使用下面的 rake 任务生成: +记住你不需要独自完成一切,在安装和运行 Rails 时如果需要帮助,请随时使用下面的资源: -* 运行 `rake doc:guides`,会在程序的 `doc/guides` 文件夹中生成一份 Rails 指南。在浏览器中打开 `doc/guides/index.html` 可以查看这份指南。 -* 运行 `rake doc:rails`,会在程序的 `doc/api` 文件夹中生成一份完整的 API 文档。在浏览器中打开 `doc/api/index.html` 可以查看 API 文档。 +* [Ruby on Rails 指南](http://rails.guide) +* [Ruby on Rails 教程](http://railstutorial-china.org) +* [Ruby on Rails 邮件列表](http://groups.google.com/group/rubyonrails-talk) +* irc.freenode.net 中的 [#rubyonrails](irc://irc.freenode.net/#rubyonrails) 频道 -TIP: 使用 `doc:guides` 任务在本地生成 Rails 指南,要安装 RedCloth gem。在 `Gemfile` 中加入这个 gem,然后执行 `bundle install` 命令即可。 + -常见问题 -------- +## 配置问题 -使用 Rails 时,最好使用 UTF-8 编码存储所有外部数据。如果没使用 UTF-8 编码,Ruby 的代码库和 Rails 一般都能将其转换成 UTF-8,但不一定总能成功,所以最好还是确保所有的外部数据都使用 UTF-8 编码。 +在 Rails 中,储存外部数据最好都使用 UTF-8 编码。虽然 Ruby 库和 Rails 通常都能将使用其他编码的外部数据转换为 UTF-8 编码,但并非总是能可靠地工作,所以最好还是确保所有的外部数据都使用 UTF-8 编码。 -如果编码出错,常见的征兆是浏览器中显示很多黑色方块和问号。还有一种常见的符号是“ü”,包含在“ü”中。Rails 内部采用很多方法尽量避免出现这种问题。如果你使用的外部数据编码不是 UTF-8,有时会出现这些问题,Rails 无法自动纠正。 +编码出错的最常见症状是在浏览器中出现带有问号的黑色菱形块,另一个常见症状是本该出现“ü”字符的地方出现了“ü”字符。Rails 内部采取了许多步骤来解决常见的可以自动检测和纠正的编码问题。尽管如此,如果不使用 UTF-8 编码来储存外部数据,偶尔还是会出现无法自动检测和纠正的编码问题。 -非 UTF-8 编码的数据经常来源于: +下面是非 UTF-8 编码数据的两种常见来源: -* 你的文本编辑器:大多数文本编辑器(例如 TextMate)默认使用 UTF-8 编码保存文件。如果你的编辑器没使用 UTF-8 编码,有可能是你在模板中输入了特殊字符(例如 é),在浏览器中显示为方块和问号。这种问题也会出现在国际化文件中。默认不使用 UTF-8 保存文件的编辑器(例如 Dreamweaver 的某些版本)都会提供一种方法,把默认编码设为 UTF-8。记得要修改。 -* 你的数据库:默认情况下,Rails 会把从数据库中取出的数据转换成 UTF-8 格式。如果数据库内部不使用 UTF-8 编码,就无法保存用户输入的所有字符。例如,数据库内部使用 Latin-1 编码,用户输入俄语、希伯来语或日语字符时,存进数据库时就会永远丢失。如果可能,在数据库中尽量使用 UTF-8 编码。 +* 文本编辑器:大多数文本编辑器(例如 TextMate)默认使用 UTF-8 编码保存文件。如果你的文本编辑器未使用 UTF-8 编码,就可能导致在模板中输入的特殊字符(例如 é)在浏览器中显示为带有问号的黑色菱形块。这个问题也会出现在 i18n 翻译文件中。大多数未默认使用 UTF-8 编码的文本编辑器(例如 Dreamweaver 的某些版本)提供了将默认编码修改为 UTF-8 的方法,别忘了进行修改。 +* 数据库:默认情况下,Rails 会把从数据库中取出的数据转换成 UTF-8 格式。尽管如此,如果数据库内部不使用 UTF-8 编码,就有可能无法保存用户输入的所有字符。例如,如果数据库内部使用 Latin-1 编码,而用户输入了俄语、希伯来语或日语字符,那么在把数据保存到数据库时就会造成数据永久丢失。因此,只要可能,就请在数据库内部使用 UTF-8 编码。 diff --git a/source/zh-CN/i18n.md b/source/zh-CN/i18n.md index 70a9732..e4f2fd3 100644 --- a/source/zh-CN/i18n.md +++ b/source/zh-CN/i18n.md @@ -1,145 +1,152 @@ -Rails 国际化 API -================ +# Rails 国际化 API -Rails 内建支持 Ruby I18n(internationalization 的简写)。Ruby I18n 这个 gem 用法简单,是个扩展性强的框架,可以把程序翻译成英语之外的其他语言,或者为程序提供多种语言支持。 +Rails(Rails 2.2 及以上版本)自带的 Ruby I18n(internationalization 的简写)gem,提供了易用、可扩展的框架,用于把应用翻译成英语之外的语言,或为应用提供多语言支持。 -“国际化”是指把程序中的字符串或者其他需要本地化的内容(例如,日期和货币的格式)提取出来的过程。“本地化”是指把提取出来的内容翻译成本地语言或者本地所用格式的过程。 +“国际化”(internationalization)过程通常是指,把所有字符串及本地化相关信息(例如日期或货币格式)从应用中抽取出来。“本地化”(localization)过程通常是指,翻译这些字符串并提供相关信息的本地格式。 -所以在 Rails 程序国际化的过程中要做这些事情: +因此,在国际化 Rails 应用的过程中,我们需要: -* 确保程序支持 i18n; -* 告诉 Rails 在哪里寻找本地语言包; -* 告诉 Rails 怎么设置、保存和切换本地语言包; +* 确保 Rails 提供了 I18n 支持; +* 把区域设置字典(locale dictionary)的位置告诉 Rails; +* 告诉 Rails 如何设置、保存和切换区域(locale)。 -在本地化程序的过程中或许要做以下三件事: +在本地化 Rails 应用的过程中,我们可能需要完成下面三项工作: -* 修改或提供 Rails 默认使用的本地语言包,例如,日期和时间的格式、月份名、Active Record 模型名等; -* 把程序中的字符串提取出来,保存到键值对中,例如 Flash 消息、视图中的纯文本等; -* 在某个地方保存语言包; +* 替换或补充 Rails 的默认区域设置,例如日期和时间格式、月份名称、Active Record 模型名等; +* 从应用中抽取字符串,并放入字典,例如视图中的闪现信息(flash message)、静态文本等; +* 把生成的字典储存在某个地方。 -本文会详细介绍 I18n API,并提供一个教程,演示如何从头开始国际化一个 Rails 程序。 +本文介绍 Rails I18n API,并提供国际化 Rails 应用的入门教程。 -读完本文,你将学到: +读完本文后,您将学到: -* Ruby on Rails 是如何处理 i18n 的; -* 如何在 REST 架构的程序中正确使用 i18n; -* 如何使用 i18n 翻译 ActiveRecord 错误和 ActionMailer E-mail; -* 协助翻译程序的工具; +* Rails 中 I18n 的工作原理; +* 在 REST 式应用中正确使用 I18n 的几种方式; +* 如何使用 I18n 翻译 Active Record 错误或 Action Mailer 电子邮件主题; +* 用于进一步翻译应用的其他工具。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -NOTE: Ruby I18n 框架提供了 Rails 程序国际化和本地化所需的各种功能。不过,还可以使用各种插件和扩展,添加额外的功能。详情参见 [Ruby I18n 的维基](http://ruby-i18n.org/wiki)。 +NOTE: Ruby I18n 框架提供了 Rails 应用国际化/本地化所需的全部必要支持。我们还可以使用各种 gem 来添加附加功能或特性。更多介绍请参阅 [rails-18n gem](https://github.com/svenfuchs/rails-i18n)。 -Ruby on Rails 是如何处理 i18n 的 ------------------------------- + -国际化是个很复杂的问题。自然语言千差万别(例如复数变形规则),很难提供一种工具解决所有问题。因此,Rails I18n API 只关注: +## Rails 中 I18n 的工作原理 -* 默认支持和英语类似的语言; -* 让支持其他语言变得简单; +国际化是一个复杂的问题。自然语言在很多方面(例如复数规则)有所不同,要想一次性提供解决所有问题的工具很难。因此,Rails I18n API 专注于: -Rails 框架中的每个静态字符串(例如,Active Record 数据验证消息,日期和时间的格式)都支持国际化,因此本地化时只要重写默认值即可。 +* 支持英语及类似语言 +* 易于定制和扩展,以支持其他语言 -### Ruby I18n 的整体架构 +作为这个解决方案的一部分,Rails 框架中的每个静态字符串(例如,Active Record 数据验证信息、时间和日期格式)都已国际化。Rails 应用的本地化意味着把这些静态字符串翻译为所需语言。 -Ruby I18n 分成两部分: + -* 公开的 API:这是一个 Ruby 模块,定义了库中可用的公开方法; -* 一个默认的后台(特意取名为“Simple”),实现这些方法; +### I18n 库的总体架构 -普通用户只要使用这些公开方法即可,但了解后台的功能也有助于使用 i18n API。 +因此,Ruby I18n gem 分为两部分: -NOTE: 默认提供的“Simple”后台可以用其他强大的后台替代(推荐这么做),例如把翻译后的数据存储在关系型数据库中,或 GetText 语言包中。详情参见下文的“[使用其他后台](#using-different-backends)”一节。 +* I18n 框架的公开 API——包含公开方法的 Ruby 模块,定义 I18n 库的工作方式 +* 实现这些方法的默认后端(称为简单后端) -### 公开 API +作为用户,我们应该始终只访问 I18n 模块的公开方法,但了解后端的功能也很有帮助。 -I18n API 最重要的方法是: +NOTE: 我们可以把默认的简单后端替换为其他功能更强的后端,这时翻译数据可能会储存在关系数据库、GetText 字典或类似解决方案中。更多介绍请参阅 [使用不同的后端](#using-different-backends)。 + + + +### I18n 公开 API + +I18n API 中最重要的两个方法是: ```ruby -translate # Lookup text translations -localize # Localize Date and Time objects to local formats +translate # 查找文本翻译 +localize # 把日期和时间对象转换为本地格式(本地化) ``` -这两个方法都有别名,分别为 `#t` 和 `#l`。因此可以这么用: +这两个方法的别名分别为 `#t` 和 `#l`,用法如下: ```ruby I18n.t 'store.title' I18n.l Time.now ``` -I18n API 同时还提供了针对下述属性的读取和设值方法: +对于下列属性,I18n API 还提供了属性读值方法和设值方法: ```ruby -load_path # Announce your custom translation files -locale # Get and set the current locale -default_locale # Get and set the default locale -exception_handler # Use a different exception_handler -backend # Use a different backend +load_path # 自定义翻译文件的路径 +locale # 获取或设置当前区域 +default_locale # 获取或设置默认区域 +available_locales # 应用可用的区域设置白名单 +enforce_available_locales # 强制使用白名单(true 或 false) +exception_handler # 使用其他异常处理程序 +backend # 使用其他后端 ``` -下一节起,我们要从零开始国际化一个简单的 Rails 程序。 +现在,我们已经掌握了 Rails I18n API 的基本用法,从下一节开始,我们将从头开始国际化一个简单的 Rails 应用。 + + -为国际化做准备 ------------- +## Rails 应用的国际化设置 -为程序提供 I18n 支持只需简单几步。 +本节介绍为 Rails 应用提供 I18n 支持的几个步骤。 + + ### 配置 I18n 模块 -按照“多约定,少配置”原则,Rails 会为程序提供一些合理的默认值。如果想使用其他设置,可以很容易的改写默认值。 +根据“多约定,少配置”原则,Rails I18n 库提供了默认翻译字符串。如果需要不同的翻译字符串,可以直接覆盖默认值。 -Rails 会自动把 `config/locales` 文件夹中所有 `.rb` 和 `.yml` 文件加入**译文加载路径**。 +Rails 会把 `config/locales` 文件夹中的 `.rb` 和 `.yml` 文件自动添加到翻译文件加载路径中。 -默认提供的 `en.yml` 文件中包含一些简单的翻译文本: +这个文件夹中的 `en.yml` 区域设置文件包含了一个翻译字符串示例: -```ruby +```yml en: hello: "Hello world" ``` -上面这段代码的意思是,在 `:en` 语言中,`hello` 键映射到字符串 `"Hello world"` 上。Rails 中的每个字符串的国际化都使用这种方式,比如说 Active Model [数据验证消息](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml)以及[日期和时间格式](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml)。在默认的后台中,可以使用 YAML 或标准的 Ruby Hash 存储翻译数据。 +上面的代码表示,在 `:en` 区域设置中,键 `hello` 会映射到 `Hello world` 字符串上。在 Rails 中,字符串都以这种方式进行国际化,例如,Active Model 的数据验证信息位于 [activemodel/lib/active_model/locale/en.yml](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml) 文件中,时间和日期格式位于 [activesupport/lib/active_support/locale/en.yml](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) 文件中。我们可以使用 YAML 或标准 Ruby 散列,把翻译信息储存在默认的简单后端中。 -I18n 库使用的默认语言是**英语**,所以如果没设为其他语言,就会用 `:en` 查找翻译数据。 +I18n 库使用英语作为默认的区域设置,例如,如果未设置为其他区域,那就使用 `:en` 区域来查找翻译。 -NOTE: 经过[讨论](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)之后,i18n 库决定为语言名称使用一种**务实的方案**,只说明所用语言(例如,`:en`,`:pl`),不区分地区(例如,`:en-US`,`:en-GB`)。地区经常用来区分同一语言在不同地区的分支或者方言。很多国际化程序只使用语言名称,例如 `:cs`、`:th` 和 `:es`(分别为捷克语,泰语和西班牙语)。不过,同一语种在不同地区可能有重要差别。例如,在 `:en-US` 中,货币符号是“$”,但在 `:en-GB` 中是“£”。在 Rails 中使用区分地区的语言设置也是可行的,只要在 `:en-GB` 中使用完整的“English - United Kingdom”即可。很多 [Rails I18n 插件](http://rails-i18n.org/wiki),例如 [Globalize3](https://github.com/globalize/globalize),都可以实现。 +NOTE: 经过[讨论](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en),I18n 库在选取区域设置的键时最终采取了务实的方式,也就是仅包含语言部分,例如 `:en`、`:pl`,而不是传统上使用的语言和区域两部分,例如 `:en-US` 、 `:en-GB`。很多国际化的应用都是这样做的,例如把 `:cs`、`:th` 和 `:es` 分别用于捷克语、泰语和西班牙语。尽管如此,在同一语系中也可能存在重要的区域差异,例如,`:en-US` 使用 `$` 作为货币符号,而 `:en-GB` 使用 `£` 作为货币符号。因此,如果需要,我们也可以使用传统方式,例如,在 `:en-GB` 字典中提供完整的 `"English - United Kingdom"` 区域。像 [Globalize3](https://github.com/globalize/globalize) 这样的 gem 可以实现这一功能。 -**译文加载路径**(`I18n.load_path`)是一个 Ruby 数组,由译文文件的路径组成,Rails 程序会自动加载这些文件。你可以使用任何一个文件夹,任何一种文件命名方式。 +Rails 会自动加载翻译文件加载路径(`I18n.load_path`),这是一个保存有翻译文件路径的数组。通过配置翻译文件加载路径,我们可以自定义翻译文件的目录结构和文件命名规则。 -NOTE: 首次加载查找译文时,后台会惰性加载这些译文。这么做即使已经声明过,也可以更换所用后台。 +NOTE: I18n 库的后端采用了延迟加载技术,相关翻译信息仅在第一次查找时加载。我们可以根据需要,随时替换默认后端。 -`application.rb` 文件中的默认内容有介绍如何从其他文件夹中添加本地数据,以及如何设置默认使用的语言。去掉相关代码行前面的注释,修改即可。 +默认的区域设置和翻译的加载路径可以在 `config/application.rb` 文件中配置,如下所示: ```ruby -# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. -# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] -# config.i18n.default_locale = :de +config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] +config.i18n.default_locale = :de ``` -### (选做)更改 I18n 库的设置 - -如果基于某些原因不想使用 `application.rb` 文件中的设置,我们来介绍一下手动设置的方法。 - -告知 I18n 库在哪里寻找译文文件,可以在程序的任何地方指定加载路径。但要保证这个设置要在加载译文之前执行。我们可能还要修改默认使用的语言。要完成这两个设置,最简单的方法是把下面的代码放到一个初始化脚本中: +在查找翻译文件之前,必须先指定翻译文件加载路径。应该通过初始化脚本修改默认区域设置,而不是 `config/application.rb` 文件: ```ruby -# in config/initializers/locale.rb +# config/initializers/locale.rb -# tell the I18n library where to find your translations +# 指定 I18n 库搜索翻译文件的路径 I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] -# set default locale to something other than :en +# 应用可用的区域设置白名单 +I18n.available_locales = [:en, :pt] + +# 修改默认区域设置(默认是 :en) I18n.default_locale = :pt ``` -### 设置并传入所用语言 + -如果想把 Rails 程序翻译成英语(默认语言)之外的其他语言,可以在 `application.rb` 文件或初始化脚本中设置 `I18n.default_locale` 选项。这个设置对所有请求都有效。 +### 跨请求管理区域设置 -不过,或许你希望为程序提供多种语言支持。此时,需要在请求中指定所用语言。 +除非显式设置了 `I18n.locale`,默认区域设置将会应用于所有翻译文件。 -WARNING: 你可能想过把用户选择适用的语言保存在会话或 cookie 中,请**千万别**这么做。所用语言应该是透明的,写在 URL 中。这样做不会破坏用户对网页内容的预想,如果把 URL 发给一个朋友,他应该看到和我相同的内容。这种方式在 [REST 架构](http://en.wikipedia.org/wiki/Representational_State_Transfer)中很容易实现。不过也有不适用 REST 架构的情况,后文会说明。 +本地化应用有时需要支持多区域设置。此时,需要在每个请求之前设置区域,这样在请求的整个生命周期中,都会根据指定区域,对所有字符串进行翻译。 -设置所用语言的方法很简单,只需在 `ApplicationController` 的 `before_action` 中作如下设定即可: +我们可以在 `ApplicationController` 中使用 `before_action` 方法设置区域: ```ruby before_action :set_locale @@ -149,20 +156,22 @@ def set_locale end ``` -使用这种方法要把语言作为 URL 查询参数传入,例如 `http://example.com/books?locale=pt`(这是 Google 使用的方法)。`http://localhost:3000?locale=pt` 会加载葡萄牙语,`http://localhost:3000?locale=de` 会加载德语,以此类推。如果想手动在 URL 中指定语言再刷新页面,可以跳过后面几小节,直接阅读“[国际化程序](#internationalizing-your-application)”一节。 +上面的例子说明了如何使用 URL 查询参数来设置区域。例如,对于 http://example.com/books?locale=pt 会使用葡萄牙语进行本地化,对于 http://localhost:3000?locale=de 会使用德语进行本地化。 + +接下来介绍区域设置的几种不同方式。 -当然了,你可能并不想手动在每个 URL 中指定语言,或者想使用其他形式的 URL,例如 `http://example.com/pt/books` 或 `http://example.com/en/books`。下面分别介绍其他各种设置语言的方法。 + -### 使用不同的域名加载不同的语言 +#### 根据域名设置区域 -设置所用语言可以通过不同的域名实现。例如,`www.example.com` 加载英语内容,`www.example.es` 加载西班牙语内容。这里使用的是不同的顶级域名。这么做有多个好处: +第一种方式是,根据应用的域名设置区域。例如,通过 `www.example.com` 加载英语(或默认)区域设置,通过 `www.example.es` 加载西班牙语区域设置。也就是根据顶级域名设置区域。这种方式有下列优点: -* 所用语言在 URL 中很明显; -* 用户很容易得知所查看内容使用的语言; -* 在 Rails 中可以轻松实现; -* 搜索引擎似乎喜欢把不同语言的内容放在不同但相互关联的域名上; +* 区域设置成为 URL 地址显而易见的一部分 +* 用户可以直观地判断出页面所使用的语言 +* 在 Rails 中非常容易实现 +* 搜索引擎偏爱这种把不同语言内容放在不同域名上的做法 -在 `ApplicationController` 中加入如下代码可以实现这种处理方式: +在 `ApplicationController` 中,我们可以进行如下配置: ```ruby before_action :set_locale @@ -171,67 +180,65 @@ def set_locale I18n.locale = extract_locale_from_tld || I18n.default_locale end -# Get locale from top-level domain or return nil if such locale is not available -# You have to put something like: +# 从顶级域名中获取区域设置,如果获取失败会返回 nil +# 需要在 /etc/hosts 文件中添加如下设置: # 127.0.0.1 application.com # 127.0.0.1 application.it # 127.0.0.1 application.pl -# in your /etc/hosts file to try this out locally def extract_locale_from_tld parsed_locale = request.host.split('.').last - I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil + I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil end ``` -类似地,还可以使用不同的二级域名提供不同语言的内容: +我们还可以通过类似方式,根据子域名设置区域: ```ruby -# Get locale code from request subdomain (like http://it.application.local:3000) -# You have to put something like: +# 从子域名中获取区域设置(例如 http://it.application.local:3000) +# 需要在 /etc/hosts 文件中添加如下设置: # 127.0.0.1 gr.application.local -# in your /etc/hosts file to try this out locally def extract_locale_from_subdomain parsed_locale = request.subdomains.first - I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil + I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil end ``` -如果程序中需要切换语言的连接,可以这么写: +要想为应用添加区域设置切换菜单,可以使用如下代码: ```ruby -link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}") +link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}") ``` -上述代码假设 `APP_CONFIG[:deutsch_website_url]` 的值为 `http://www.application.de`。 +其中 `APP_CONFIG[:deutsch_website_url]` 的值类似 `http://www.application.de`。 -这种方法虽有种种好处,但你或许不想在不同的域名上提供不同语言的内容。最好的实现方式肯定是在 URL 的参数中加上语言代码。 +尽管这个解决方案具有上面提到的各种优点,但通过不同域名来提供不同的本地化版本(“语言版本”)有时并非我们的首选。在其他各种可选方案中,在 URL 参数(或请求路径)中包含区域设置是最常见的。 -### 在 URL 参数中设置所用语言 + +#### 根据 URL 参数设置区域 -The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case. +区域设置(和传递)的最常见方式,是将其包含在 URL 参数中,例如,在前文第一个示例中,`before_action` 方法调用中的 `I18n.locale = params[:locale]`。此时,我们会使用 `www.example.com/books?locale=ja` 或 `www.example.com/ja/books` 这样的网址。 -This approach has almost the same set of advantages as setting the locale from the domain name: namely that it's RESTful and in accord with the rest of the World Wide Web. It does require a little bit more work to implement, though. +和根据域名设置区域类似,这种方式具有不少优点,尤其是 REST 式的命名风格,顺应了当前的互联网潮流。不过采用这种方式所需的工作量要大一些。 -Getting the locale from `params` and setting it accordingly is not hard; including it in every URL and thus **passing it through the requests** is. To include an explicit option in every URL (e.g. `link_to( books_url(/service/locale: I18n.locale))`) would be tedious and probably impossible, of course. +从 URL 参数获取并设置区域并不难,只要把区域设置包含在 URL 中并通过请求传递即可。当然,没有人愿意在生成每个 URL 地址时显式添加区域设置,例如 `link_to(books_url(/service/locale: I18n.locale))`。 -Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionController/Base.html#M000515), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionController/Base.html#M000503) and helper methods dependent on it (by implementing/overriding this method). +Rails 的 `ApplicationController#default_url_options` 方法提供的“集中修改 URL 动态生成规则”的功能,正好可以解决这个问题:我们可以设置 `url_for` 及相关辅助方法的默认行为(通过覆盖 `default_url_options` 方法)。 -We can include something like this in our `ApplicationController` then: +我们可以在 `ApplicationController` 中添加下面的代码: ```ruby # app/controllers/application_controller.rb -def default_url_options(options={}) - logger.debug "default_url_options is passed options: #{options.inspect}\n" +def default_url_options { locale: I18n.locale } end ``` -Every helper method dependent on `url_for` (e.g. helpers for named routes like `root_path` or `root_url`, resource routes like `books_path` or `books_url`, etc.) will now **automatically include the locale in the query string**, like this: `http://localhost:3001/?locale=ja`. +这样,所有依赖于 `url_for` 的辅助方法(例如,具名路由辅助方法 `root_path` 和 `root_url`,资源路由辅助方法 `books_path` 和 `books_url` 等等)都会自动在查询字符串中添加区域设置,例如:`http://localhost:3001/?locale=ja`。 -You may be satisfied with this. It does impact the readability of URLs, though, when the locale "hangs" at the end of every URL in your application. Moreover, from the architectural standpoint, locale is usually hierarchically above the other parts of the application domain: and URLs should reflect this. +至此,我们也许已经很满意了。但是,在应用的每个 URL 地址的末尾添加区域设置,会影响 URL 地址的可读性。此外,从架构的角度看,区域设置的层级应该高于 URL 地址中除域名之外的其他组成部分,这一点也应该通过 URL 地址自身体现出来。 -You probably want URLs to look like this: `www.example.com/en/books` (which loads the English locale) and `www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scoping`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html) option in this way: +要想使用 `http://www.example.com/en/books`(加载英语区域设置)和 `http://www.example.com/nl/books`(加载荷兰语区域设置)这样的 URL 地址,我们可以使用前文提到的覆盖 `default_url_options` 方法的方式,通过 `scope` 方法设置路由: ```ruby # config/routes.rb @@ -240,9 +247,12 @@ scope "/:locale" do end ``` -Now, when you call the `books_path` method you should get `"/en/books"` (for the default locale). An URL like `http://localhost:3001/nl/books` should load the Dutch locale, then, and following calls to `books_path` should return `"/nl/books"` (because the locale changed). +现在,当我们调用 `books_path` 方法时,就会得到 `"/en/books"`(对于默认区域设置)。像 `http://localhost:3001/nl/books` 这样的 URL 地址会加载荷兰语区域设置,之后调用 `books_path` 方法时会返回 `"/nl/books"`(因为区域设置发生了变化)。 -If you don't want to force the use of a locale in your routes you can use an optional path scope (denoted by the parentheses) like so: +WARNING: 由于 `default_url_options` 方法的返回值是根据请求分别缓存的,因此无法通过循环调用辅助方法来生成 URL 地址中的区域设置, +也就是说,无法在每次迭代中设置相应的 `I18n.locale`。正确的做法是,保持 `I18n.locale` 不变,向辅助方法显式传递 `:locale` 选项,或者编辑 `request.original_fullpath`。 + +如果不想在路由中强制使用区域设置,我们可以使用可选的路径作用域(用括号表示),就像下面这样: ```ruby # config/routes.rb @@ -251,30 +261,46 @@ scope "(:locale)", locale: /en|nl/ do end ``` -With this approach you will not get a `Routing Error` when accessing your resources such as `http://localhost:3001/books` without a locale. This is useful for when you want to use the default locale when one is not specified. +通过这种方式,访问不带区域设置的 `http://localhost:3001/books` URL 地址时就不会抛出 `Routing Error` 错误了。这样,我们就可以在不指定区域设置时,使用默认的区域设置。 -Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. An URL like `http://localhost:3001/nl` will not work automatically, because the `root to: "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.) +当然,我们需要特别注意应用的根地址﹝通常是“主页(homepage)”或“仪表盘(dashboard)”﹞。像 `root to: "books#index"` 这样的不考虑区域设置的路由声明,会导致 `http://localhost:3001/nl` 无法正常访问。(尽管“只有一个根地址”看起来并没有错) -You would probably need to map URLs like these: +因此,我们可以像下面这样映射 URL 地址: ```ruby # config/routes.rb get '/:locale' => 'dashboard#index' ``` -Do take special care about the **order of your routes**, so this route declaration does not "eat" other ones. (You may want to add it directly before the `root :to` declaration.) +需要特别注意路由的声明顺序,以避免这条路由覆盖其他路由。(我们可以把这条路由添加到 `root :to` 路由声明之前) -NOTE: Have a look at two plugins which simplify work with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master). +NOTE: 有一些 gem 可以简化路由设置,如 [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master)、[rails-translate-routes](https://github.com/francesc/rails-translate-routes) 和 [route_translator](https://github.com/enriclluelles/route_translator)。 -### Setting the Locale from the Client Supplied Information + -In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites - see the box about _sessions_, _cookies_ and RESTful architecture above. +#### 根据用户偏好设置进行区域设置 -#### Using `Accept-Language` +支持用户身份验证的应用,可能会允许用户在界面中选择区域偏好设置。通过这种方式,用户选择的区域偏好设置会储存在数据库中,并用于处理该用户发起的请求。 -One source of client supplied information would be an `Accept-Language` HTTP header. People may [set this in their browser](http://www.w3.org/International/questions/qa-lang-priorities) or other clients (such as _curl_). +```ruby +def set_locale + I18n.locale = current_user.try(:locale) || I18n.default_locale +end +``` + + + +#### 使用隐式区域设置 + +如果没有显式地为请求设置区域(例如,通过上面提到的各种方式),应用就会尝试推断出所需区域。 + + -A trivial implementation of using an `Accept-Language` header would be: +##### 根据 HTTP 首部推断区域设置 + +`Accept-Language` HTTP 首部指明响应请求时使用的首选语言。浏览器[根据用户的语言偏好设置设定这个 HTTP 首部](http://www.w3.org/International/questions/qa-lang-priorities),这是推断区域设置的首选方案。 + +下面是使用 `Accept-Language` HTTP 首部的一个简单实现: ```ruby def set_locale @@ -289,28 +315,35 @@ private end ``` -Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb). +实际上,我们通常会使用更可靠的代码。Iain Hecker 开发的 [http_accept_language](https://github.com/iain/http_accept_language/tree/master) 或 Ryan Tomayko 开发的 [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb) Rack 中间件就提供了更好的解决方案。 + + + +##### 根据 IP 地理位置推断区域设置 + +我们可以通过客户端请求的 IP 地址来推断客户端所处的地理位置,进而推断其区域设置。[GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry) 这样的服务或 [geocoder](https://github.com/alexreisner/geocoder) 这样的 gem 就可以实现这一功能。 + +一般来说,这种方式远不如使用 HTTP 首部可靠,因此并不适用于大多数 Web 应用。 -#### Using GeoIP (or Similar) Database + -Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above - you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned. +#### 在会话或 Cookie 中储存区域设置 -#### User Profile +WARNING: 我们可能会认为,可以把区域设置储存在会话或 Cookie 中。但是,我们不能这样做。区域设置应该是透明的,并作为 URL 地址的一部分。这样,我们就不会打破用户的正常预期:如果我们发送一个 URL 地址给朋友,他们应该看到和我们一样的页面和内容。这就是所谓的 REST 规则。关于 REST 规则的更多介绍,请参阅 [Stefan Tilkov 写的系列文章](http://www.infoq.com/articles/rest-introduction)。后文将讨论这个规则的一些例外情况。 -You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above - you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value. + -Internationalizing your Application ------------------------------------ +## 国际化和本地化 -OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. With that in place, you're now ready for the really interesting stuff. +现在,我们已经完成了对 Rails 应用 I18n 支持的初始化,进行了区域设置,并在不同请求中应用了区域设置。 -Let's _internationalize_ our application, i.e. abstract every locale-specific parts, and then _localize_ it, i.e. provide necessary translations for these abstracts. +接下来,我们要通过抽象本地化相关元素,完成应用的国际化。最后,通过为这些抽象元素提供必要翻译,完成应用的本地化。 -You most probably have something like this in one of your applications: +下面给出一个例子: ```ruby # config/routes.rb -Yourapp::Application.routes.draw do +Rails.application.routes.draw do root to: "home#index" end ``` @@ -341,11 +374,13 @@ end

    <%= flash[:notice] %>

    ``` -![rails i18n demo untranslated](images/i18n/demo_untranslated.png) +![demo untranslated](images/i18n/demo_untranslated.png) -### Adding Translations + -Obviously there are **two strings that are localized to English**. In order to internationalize this code, **replace these strings** with calls to Rails' `#t` helper with a key that makes sense for the translation: +### 抽象本地化代码 + +在我们的代码中有两个英文字符串(`"Hello Flash"` 和 `"Hello World"`),它们在响应用户请求时显示。为了国际化这部分代码,需要用 Rails 提供的 `#t` 辅助方法来代替这两个字符串,同时为每个字符串选择合适的键: ```ruby # app/controllers/home_controller.rb @@ -358,19 +393,23 @@ end ```erb # app/views/home/index.html.erb -

    <%=t :hello_world %>

    +

    <%= t :hello_world %>

    <%= flash[:notice] %>

    ``` -When you now render this view, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing. +现在,Rails 在渲染 `index` 视图时会显示错误信息,告诉我们缺少 `:hello_world` 和 `:hello_flash` 这两个键的翻译。 -![rails i18n demo translation missing](images/i18n/demo_translation_missing.png) +![demo translation missing](images/i18n/demo_translation_missing.png) -NOTE: Rails adds a `t` (`translate`) helper method to your views so that you do not need to spell out `I18n.t` all the time. Additionally this helper will catch missing translations and wrap the resulting error message into a ``. +NOTE: Rails 为视图添加了 `t`(`translate`)辅助方法,从而避免了反复使用 `I18n.t` 这么长的写法。此外,`t` 辅助方法还能捕获缺少翻译的错误,把生成的错误信息放在 `` 元素里。 -So let's add the missing translations into the dictionary files (i.e. do the "localization" part): + -```ruby +### 为国际化字符串提供翻译 + +下面,我们把缺少的翻译添加到翻译字典文件中: + +```yml # config/locales/en.yml en: hello_world: Hello world! @@ -382,47 +421,111 @@ pirate: hello_flash: Ahoy Flash ``` -There you go. Because you haven't changed the default_locale, I18n will use English. Your application now shows: +因为我们没有修改 `default_locale`,翻译会使用 `:en` 区域设置,响应请求时生成的视图会显示英文字符串: + +![demo translated en](images/i18n/demo_translated_en.png) + +如果我们通过 URL 地址(`http://localhost:3000?locale=pirate`)把区域设置为 `pirate`,响应请求时生成的视图就会显示海盗黑话: + +![demo translated pirate](images/i18n/demo_translated_pirate.png) + +NOTE: 添加新的区域设置文件后,需要重启服务器。 + +要想把翻译储存在 SimpleStore 中,我们可以使用 YAML(`.yml`)或纯 Ruby(`.rb`)文件。大多数 Rails 开发者会优先选择 YAML。不过 YAML 有一个很大的缺点,它对空格和特殊字符非常敏感,因此有可能出现应用无法正确加载字典的情况。而 Ruby 文件如果有错误,在第一次加载时应用就会崩溃,因此我们很容易就能找出问题。(如果在使用 YAML 字典时遇到了“奇怪的问题”,可以尝试把字典的相关部分放入 Ruby 文件中。) + +如果翻译存储在 YAML 文件中,有些键必须转义: + +* true, on, yes +* false, off, no -![rails i18n demo translated to English](images/i18n/demo_translated_en.png) +例如: -And when you change the URL to pass the pirate locale (`http://localhost:3000?locale=pirate`), you'll get: +```yml +# config/locales/en.yml +en: + success: + 'true': 'True!' + 'on': 'On!' + 'false': 'False!' + failure: + true: 'True!' + off: 'Off!' + false: 'False!' +``` -![rails i18n demo translated to pirate](images/i18n/demo_translated_pirate.png) +```ruby +I18n.t 'success.true' # => 'True!' +I18n.t 'success.on' # => 'On!' +I18n.t 'success.false' # => 'False!' +I18n.t 'failure.false' # => Translation Missing +I18n.t 'failure.off' # => Translation Missing +I18n.t 'failure.true' # => Translation Missing +``` -NOTE: You need to restart the server when you add new locale files. + -You may use YAML (`.yml`) or plain Ruby (`.rb`) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.) +### 把变量传递给翻译 -### Passing variables to translations +成功完成应用国际化的一个关键因素是,避免在抽象本地化代码时,对语法规则做出不正确的假设。某个区域设置的基本语法规则,在另一个区域设置中可能不成立。 -You can use variables in the translation messages and pass their values from the view. +下面给出一个不正确抽象的例子,其中对翻译的不同组成部分的排序进行了假设。注意,为了处理这个例子中出现的情况,Rails 提供了 `number_to_currency` 辅助方法。 ```erb -# app/views/home/index.html.erb -<%=t 'greet_username', user: "Bill", message: "Goodbye" %> +# app/views/products/show.html.erb +<%= "#{t('currency')}#{@product.price}" %> +``` + +```yml +# config/locales/en.yml +en: + currency: "$" + +# config/locales/es.yml +es: + currency: "€" ``` -```yaml +如果产品价格是 10,那么西班牙语的正确翻译是“10 €”而不是“€10”,但上面的抽象并不能正确处理这种情况。 + +为了创建正确的抽象,I18n gem 提供了变量插值(variable interpolation)功能,它允许我们在翻译定义(translation definition)中使用变量,并把这些变量的值传递给翻译方法。 + +下面给出一个正确抽象的例子: + +```erb +# app/views/products/show.html.erb +<%= t('product_price', price: @product.price) %> +``` + +```yml # config/locales/en.yml en: - greet_username: "%{message}, %{user}!" + product_price: "$%{price}" + +# config/locales/es.yml +es: + product_price: "%{price} €" ``` -### Adding Date/Time Formats +所有的语法和标点都由翻译定义自己决定,所以抽象可以给出正确的翻译。 + +NOTE: `default` 和 `scope` 是保留关键字,不能用作变量名。如果误用,Rails 会抛出 `I18n::ReservedInterpolationKey` 异常。如果没有把翻译所需的插值变量传递给 `#translate` 方法,Rails 会抛出 `I18n::MissingInterpolationArgument` 异常。 + + -OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option - by default the `:default` format is used. +### 添加日期/时间格式 + +现在,我们要给视图添加时间戳,以便演示日期/时间的本地化功能。要想本地化时间格式,可以把时间对象传递给 `I18n.l` 方法或者(最好)使用 `#l` 辅助方法。可以通过 `:format` 选项指定时间格式(默认情况下使用 `:default` 格式)。 ```erb # app/views/home/index.html.erb

    <%=t :hello_world %>

    -

    <%= flash[:notice] %>

    <%= flash[:notice] %>

    <%= l Time.now, format: :short %>

    ``` -And in our pirate translations file let's add a time format (it's already there in Rails' defaults for English): +然后在 `pirate` 翻译文件中添加时间格式(Rails 默认使用的英文翻译文件已经包含了时间格式): -```ruby +```yml # config/locales/pirate.yml pirate: time: @@ -430,27 +533,33 @@ pirate: short: "arrrround %H'ish" ``` -So that would give you: +得到的结果如下: -![rails i18n demo localized time to pirate](images/i18n/demo_localized_pirate.png) +![demo localized pirate](images/i18n/demo_localized_pirate.png) -TIP: Right now you might need to add some more date/time formats in order to make the I18n backend work as expected (at least for the 'pirate' locale). Of course, there's a great chance that somebody already did all the work by **translating Rails' defaults for your locale**. See the [rails-i18n repository at GitHub](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for an archive of various locale files. When you put such file(s) in `config/locales/` directory, they will automatically be ready for use. +TIP: 现在,我们可能需要添加一些日期/时间格式,这样 I18n 后端才能按照预期工作(至少应该为 `pirate` 区域设置添加日期/时间格式)。当然,很可能已经有人通过翻译 Rails 相关区域设置的默认值,完成了这些工作。[GitHub 上的 rails-i18n 仓库](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale)提供了各种本地化文件的存档。把这些本地化文件放在 `config/locales/` 文件夹中即可正常使用。 -### Inflection Rules For Other Locales + -Rails allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit. +### 其他区域的变形规则 -### Localized Views +Rails 允许我们为英语之外的区域定义变形规则(例如单复数转换规则)。在 `config/initializers/inflections.rb` 文件中,我们可以为多个区域定义规则。这个初始化脚本包含了为英语指定附加规则的例子,我们可以参考这些例子的格式为其他区域定义规则。 -Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.) + -You can make use of this feature, e.g. when working with a large amount of static content, which would be clumsy to put inside YAML or Ruby dictionaries. Bear in mind, though, that any change you would like to do later to the template must be propagated to all of them. +### 本地化视图 -### Organization of Locale Files +假设应用中包含 `BooksController`,`index` 动作默认会渲染 `app/views/books/index.html.erb` 模板。如果我们在同一个文件夹中创建了包含本地化变量的 `index.es.html.erb` 模板,当区域设置为 `:es` 时,`index` 动作就会渲染这个模板,而当区域设置为默认区域时, `index` 动作会渲染通用的 `index.html.erb` 模板。(在 Rails 的未来版本中,本地化的这种自动化魔术,有可能被应用于 `public` 文件夹中的资源) -When you are using the default SimpleStore shipped with the i18n library, dictionaries are stored in plain-text files on the disc. Putting translations for all parts of your application in one file per locale could be hard to manage. You can store these files in a hierarchy which makes sense to you. +本地化视图功能很有用,例如,如果我们有大量静态内容,就可以使用本地化视图,从而避免把所有东西都放进 YAML 或 Ruby 字典里的麻烦。但要记住,一旦我们需要修改模板,就必须对每个模板文件逐一进行修改。 -For example, your `config/locales` directory could look like this: + + +### 区域设置文件的组织 + +当我们使用 I18n 库自带的 SimpleStore 时,字典储存在磁盘上的纯文本文件中。对于每个区域,把应用的各部分翻译都放在一个文件中,可能会带来管理上的困难。因此,把每个区域的翻译放在多个文件中,分层进行管理是更好的选择。 + +例如,我们可以像下面这样组织 `config/locales` 文件夹: ``` |-defaults @@ -475,142 +584,160 @@ For example, your `config/locales` directory could look like this: |-----en.rb ``` -This way, you can separate model and model attribute names from text inside views, and all of this from the "defaults" (e.g. date and time formats). Other stores for the i18n library could provide different means of such separation. +这样,我们就可以把模型和属性名同视图中的文本分离,同时还能使用“默认值”(例如日期和时间格式)。I18n 库的不同后端可以提供不同的分离方式。 -NOTE: The default locale loading mechanism in Rails does not load locale files in nested dictionaries, like we have here. So, for this to work, we must explicitly tell Rails to look further: +NOTE: Rails 默认的区域设置加载机制,无法自动加载上面例子中位于嵌套文件夹中的区域设置文件。因此,我们还需要进行显式设置: ```ruby - # config/application.rb - config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] - +# config/application.rb +config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] ``` -Do check the [Rails i18n Wiki](http://rails-i18n.org/wiki) for list of tools available for managing translations. -Overview of the I18n API Features ---------------------------------- + + +## I18n API 功能概述 + +现在我们已经对 I18n 库有了较好的了解,知道了如何国际化简单的 Rails 应用。在下面几个小节中,我们将更深入地了解相关功能。 -You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth. +这几个小节将展示使用 `I18n.translate` 方法以及 `translate` 视图辅助方法的示例(注意视图辅助方法提供的附加功能)。 -These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method). +所涉及的功能如下: -Covered are features like these: +* 查找翻译 +* 把数据插入翻译中 +* 复数的翻译 +* 使用安全 HTML 翻译(只针对视图辅助方法) +* 本地化日期、数字、货币等 -* looking up translations -* interpolating data into translations -* pluralizing translations -* using safe HTML translations (view helper method only) -* localizing dates, numbers, currency, etc. + -### Looking up Translations +### 查找翻译 -#### Basic Lookup, Scopes and Nested Keys + -Translations are looked up by keys which can be both Symbols or Strings, so these calls are equivalent: +#### 基本查找、作用域和嵌套键 + +Rails 通过键来查找翻译,其中键可以是符号或字符串。这两种键是等价的,例如: ```ruby I18n.t :message I18n.t 'message' ``` -The `translate` method also takes a `:scope` option which can contain one or more additional keys that will be used to specify a "namespace" or scope for a translation key: +`translate` 方法接受 `:scope` 选项,选项的值可以包含一个或多个附加键,用于指定翻译键(translation key)的“命名空间”或作用域: ```ruby I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] ``` -This looks up the `:record_invalid` message in the Active Record error messages. +上述代码会在 Active Record 错误信息中查找 `:record_invalid` 信息。 -Additionally, both the key and scopes can be specified as dot-separated keys as in: +此外,我们还可以用点号分隔的键来指定翻译键和作用域: ```ruby I18n.translate "activerecord.errors.messages.record_invalid" ``` -Thus the following calls are equivalent: +因此,下列调用是等效的: ```ruby I18n.t 'activerecord.errors.messages.record_invalid' -I18n.t 'errors.messages.record_invalid', scope: :active_record +I18n.t 'errors.messages.record_invalid', scope: :activerecord I18n.t :record_invalid, scope: 'activerecord.errors.messages' I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] ``` -#### Defaults + + +#### 默认值 -When a `:default` option is given, its value will be returned if the translation is missing: +如果指定了 `:default` 选项,在缺少翻译的情况下,就会返回该选项的值: ```ruby I18n.t :missing, default: 'Not here' # => 'Not here' ``` -If the `:default` value is a Symbol, it will be used as a key and translated. One can provide multiple values as default. The first one that results in a value will be returned. +如果 `:default` 选项的值是符号,这个值会被当作键并被翻译。我们可以为 `:default` 选项指定多个值,第一个被成功翻译的键或遇到的字符串将被作为返回值。 -E.g., the following first tries to translate the key `:missing` and then the key `:also_missing.` As both do not yield a result, the string "Not here" will be returned: +例如,下面的代码首先尝试翻译 `:missing` 键,然后是 `:also_missing` 键。由于两次翻译都不能得到结果,最后会返回 `"Not here"` 字符串。 ```ruby I18n.t :missing, default: [:also_missing, 'Not here'] # => 'Not here' ``` -#### Bulk and Namespace Lookup + + +#### 批量查找和命名空间查找 -To look up multiple translations at once, an array of keys can be passed: +要想一次查找多个翻译,我们可以传递键的数组作为参数: ```ruby I18n.t [:odd, :even], scope: 'errors.messages' # => ["must be odd", "must be even"] ``` -Also, a key can translate to a (potentially nested) hash of grouped translations. E.g., one can receive _all_ Active Record error messages as a Hash with: +此外,键可以转换为一组翻译的(可能是嵌套的)散列。例如,下面的代码可以生成所有 Active Record 错误信息的散列: ```ruby I18n.t 'activerecord.errors.messages' # => {:inclusion=>"is not included in the list", :exclusion=> ... } ``` -#### "Lazy" Lookup + -Rails implements a convenient way to look up the locale inside _views_. When you have the following dictionary: +#### 惰性查找 -```yaml +Rails 实现了一种在视图中查找区域设置的便捷方法。如果有下述字典: + +```yml es: books: index: title: "Título" ``` -you can look up the `books.index.title` value **inside** `app/views/books/index.html.erb` template like this (note the dot): +我们就可以像下面这样在 `app/views/books/index.html.erb` 模板中查找 `books.index.title` 的值(注意点号): ```erb <%= t '.title' %> ``` -NOTE: Automatic translation scoping by partial is only available from the `translate` view helper method. +NOTE: 只有 `translate` 视图辅助方法支持根据片段自动补全翻译作用域的功能。 -### Interpolation +我们还可以在控制器中使用惰性查找(lazy lookup): -In many cases you want to abstract your translations so that **variables can be interpolated into the translation**. For this reason the I18n API provides an interpolation feature. +```yml +en: + books: + create: + success: Book created! +``` -All options besides `:default` and `:scope` that are passed to `#translate` will be interpolated to the translation: +用于设置闪现信息: ```ruby -I18n.backend.store_translations :en, thanks: 'Thanks %{name}!' -I18n.translate :thanks, name: 'Jeremy' -# => 'Thanks Jeremy!' +class BooksController < ApplicationController + def create + # ... + redirect_to books_url, notice: t('.success') + end +end ``` -If a translation uses `:default` or `:scope` as an interpolation variable, an `I18n::ReservedInterpolationKey` exception is raised. If a translation expects an interpolation variable, but this has not been passed to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. + -### Pluralization +### 复数转换 -In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ar), [Japanese](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ja), [Russian](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Thus, the I18n API provides a flexible pluralization feature. +在英语中,一个字符串只有一种单数形式和一种复数形式,例如,“1 message”和“2 messages”。其他语言([阿拉伯语](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ar)、[日语](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ja)、[俄语](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ru)等)则具有不同的语法,有更多或更少的[复数形式](http://cldr.unicode.org/index/cldr-spec/plural-rules)。因此,I18n API 提供了灵活的复数转换功能。 -The `:count` interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by CLDR: +`:count` 插值变量具有特殊作用,既可以把它插入翻译,又可以用于从翻译中选择复数形式(根据 CLDR 定义的复数转换规则): ```ruby I18n.backend.store_translations :en, inbox: { + zero: 'no messages', # 可选 one: 'one message', other: '%{count} messages' } @@ -619,23 +746,30 @@ I18n.translate :inbox, count: 2 I18n.translate :inbox, count: 1 # => 'one message' + +I18n.translate :inbox, count: 0 +# => 'no messages' ``` -The algorithm for pluralizations in `:en` is as simple as: +`:en` 区域设置的复数转换算法非常简单: ```ruby -entry[count == 1 ? 0 : 1] +lookup_key = :zero if count == 0 && entry.has_key?(:zero) +lookup_key ||= count == 1 ? :one : :other +entry[lookup_key] ``` -I.e. the translation denoted as `:one` is regarded as singular, the other is used as plural (including the count being zero). +也就是说,`:one` 标记的是单数,`:other` 标记的是复数。如果数量为零,而且有 `:zero` 元素,用它的值,而不用 `:other` 的值。 + +如果查找键没能返回可转换为复数形式的散列,就会引发 `I18n::InvalidPluralizationData` 异常。 -If the lookup for the key does not return a Hash suitable for pluralization, an `18n::InvalidPluralizationData` exception is raised. + -### Setting and Passing a Locale +### 区域的设置和传递 -The locale can be either set pseudo-globally to `I18n.locale` (which uses `Thread.current` like, e.g., `Time.zone`) or can be passed as an option to `#translate` and `#localize`. +区域设置可以伪全局地设置为 `I18n.locale`(使用 `Thread.current`,例如 `Time.zone`),也可以作为选项传递给 `#translate` 和 `#localize` 方法。 -If no locale is passed, `I18n.locale` is used: +如果我们没有传递区域设置,Rails 就会使用 `I18n.locale`: ```ruby I18n.locale = :de @@ -643,24 +777,26 @@ I18n.t :foo I18n.l Time.now ``` -Explicitly passing a locale: +显式传递区域设置: ```ruby I18n.t :foo, locale: :de I18n.l Time.now, locale: :de ``` -The `I18n.locale` defaults to `I18n.default_locale` which defaults to :`en`. The default locale can be set like this: +`I18n.locale` 的默认值是 `I18n.default_locale` ,而 `I18n.default_locale` 的默认值是 `:en`。可以像下面这样设置默认区域: ```ruby I18n.default_locale = :de ``` -### Using Safe HTML Translations + + +### 使用安全 HTML 翻译 -Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. When you use them in views the HTML will not be escaped. +带有 `'_html'` 后缀的键和名为 `'html'` 的键被认为是 HTML 安全的。当我们在视图中使用这些键时,HTML 不会被转义。 -```yaml +```yml # config/locales/en.yml en: welcome: welcome! @@ -677,66 +813,35 @@ en:
    <%= t('title.html') %>
    ``` -NOTE: Automatic conversion to HTML safe translate text is only available from the `translate` view helper method. +不过插值是会被转义的。例如,对于: -![i18n demo html safe](images/i18n/demo_html_safe.png) - -How to Store your Custom Translations -------------------------------------- - -The Simple backend shipped with Active Support allows you to store translations in both plain Ruby and YAML format.[^2] - -For example a Ruby Hash providing translations can look like this: - -```ruby -{ - pt: { - foo: { - bar: "baz" - } - } -} +```yml +en: + welcome_html: "Welcome %{username}!" ``` -The equivalent YAML file would look like this: +我们可以安全地传递用户设置的用户名: -```ruby -pt: - foo: - bar: baz +```erb +<%# This is safe, it is going to be escaped if needed. %> +<%= t('welcome_html', username: @current_user.username) %> ``` -As you see, in both cases the top level key is the locale. `:foo` is a namespace key and `:bar` is the key for the translation "baz". +另一方面,安全字符串是逐字插入的。 -Here is a "real" example from the Active Support `en.yml` translations YAML file: +NOTE: 只有 `translate` 视图辅助方法支持 HTML 安全翻译文本的自动转换。 -```ruby -en: - date: - formats: - default: "%Y-%m-%d" - short: "%b %d" - long: "%B %d, %Y" -``` +![demo html safe](images/i18n/demo_html_safe.png) -So, all of the following equivalent lookups will return the `:short` date format `"%b %d"`: + -```ruby -I18n.t 'date.formats.short' -I18n.t 'formats.short', scope: :date -I18n.t :short, scope: 'date.formats' -I18n.t :short, scope: [:date, :formats] -``` - -Generally we recommend using YAML as a format for storing translations. There are cases, though, where you want to store Ruby lambdas as part of your locale data, e.g. for special date formats. - -### Translations for Active Record Models +### Active Record 模型的翻译 -You can use the methods `Model.model_name.human` and `Model.human_attribute_name(attribute)` to transparently look up translations for your model and attribute names. +我们可以使用 `Model.model_name.human` 和 `Model.human_attribute_name(attribute)` 方法,来透明地查找模型名和属性名的翻译。 -For example when you add the following translations: +例如,当我们添加了下述翻译: -```ruby +```yml en: activerecord: models: @@ -744,14 +849,14 @@ en: attributes: user: login: "Handle" - # will translate User attribute "login" as "Handle" + # 会把 User 的属性 "login" 翻译为 "Handle" ``` -Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle". +`User.model_name.human` 会返回 `"Dude"`,而 `User.human_attribute_name("login")` 会返回 `"Handle"`。 -You can also set a plural form for model names, adding as following: +我们还可以像下面这样为模型名添加复数形式: -```ruby +```yml en: activerecord: models: @@ -760,25 +865,42 @@ en: other: Dudes ``` -Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude". +这时 `User.model_name.human(count: 2)` 会返回 `"Dudes"`,而 `User.model_name.human(count: 1)` 或 `User.model_name.human` 会返回 `"Dude"`。 + +要想访问模型的嵌套属性,我们可以在翻译文件的模型层级中嵌套使用“模型/属性”: + +```yml +en: + activerecord: + attributes: + user/gender: + female: "Female" + male: "Male" +``` + +这时 `User.human_attribute_name("gender.female")` 会返回 `"Female"`。 -#### Error Message Scopes +NOTE: 如果我们使用的类包含了 `ActiveModel`,而没有继承自 `ActiveRecord::Base`,我们就应该用 `activemodel` 替换上述例子中键路径中的 `activerecord`。 -Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account. + -This gives you quite powerful means to flexibly adjust your messages to your application's needs. +#### 错误消息的作用域 -Consider a User model with a validation for the name attribute like this: +Active Record 验证的错误消息翻译起来很容易。Active Record 提供了一些用于放置消息翻译的命名空间,以便为不同的模型、属性和验证提供不同的消息和翻译。当然 Active Record 也考虑到了单表继承问题。 + +这就为根据应用需求灵活调整信息,提供了非常强大的工具。 + +假设 `User` 模型对 `name` 属性进行了验证: ```ruby -class User < ActiveRecord::Base +class User < ApplicationRecord validates :name, presence: true end ``` -The key for the error message in this case is `:blank`. Active Record will look up this key in the namespaces: +此时,错误信息的键是 `:blank`。Active Record 会在命名空间中查找这个键: -```ruby +``` activerecord.errors.models.[model_name].attributes.[attribute_name] activerecord.errors.models.[model_name] activerecord.errors.messages @@ -786,9 +908,9 @@ errors.attributes.[attribute_name] errors.messages ``` -Thus, in our example it will try the following keys in this order and return the first result: +因此,在本例中,Active Record 会按顺序查找下列键,并返回第一个结果: -```ruby +``` activerecord.errors.models.user.attributes.name.blank activerecord.errors.models.user.blank activerecord.errors.messages.blank @@ -796,9 +918,9 @@ errors.attributes.name.blank errors.messages.blank ``` -When your models are additionally using inheritance then the messages are looked up in the inheritance chain. +如果模型使用了继承,Active Record 还会在继承链中查找消息。 -For example, you might have an Admin model inheriting from User: +例如,对于继承自 `User` 模型的 `Admin` 模型: ```ruby class Admin < User @@ -806,9 +928,9 @@ class Admin < User end ``` -Then Active Record will look for messages in this order: +Active Record 会按下列顺序查找消息: -```ruby +``` activerecord.errors.models.admin.attributes.name.blank activerecord.errors.models.admin.blank activerecord.errors.models.user.attributes.name.blank @@ -818,50 +940,55 @@ errors.attributes.name.blank errors.messages.blank ``` -This way you can provide special translations for various error messages at different points in your models inheritance chain and in the attributes, models, or default scopes. +这样,我们就可以在模型继承链的不同位置,以及属性、模型或默认作用域中,为各种错误消息提供特殊翻译。 + + -#### Error Message Interpolation +#### 错误消息的插值 -The translated model name, translated attribute name, and value are always available for interpolation. +翻译后的模型名、属性名,以及值,始终可以通过 `model`、`attribute` 和 `value` 插值。 -So, for example, instead of the default error message `"cannot be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`. +因此,举例来说,我们可以用 `"Please fill in your %{attribute}"` 这样的属性名来代替默认的 `"cannot be blank"` 错误信息。 -* `count`, where available, can be used for pluralization if present: +当 `count` 方法可用时,可根据需要用于复数转换: -| validation | with option | message | interpolation | -| ------------ | ------------------------- | ------------------------- | ------------- | -| confirmation | - | :confirmation | - | -| acceptance | - | :accepted | - | -| presence | - | :blank | - | -| absence | - | :present | - | -| length | :within, :in | :too_short | count | -| length | :within, :in | :too_long | count | -| length | :is | :wrong_length | count | -| length | :minimum | :too_short | count | -| length | :maximum | :too_long | count | -| uniqueness | - | :taken | - | -| format | - | :invalid | - | -| inclusion | - | :inclusion | - | -| exclusion | - | :exclusion | - | -| associated | - | :invalid | - | -| numericality | - | :not_a_number | - | -| numericality | :greater_than | :greater_than | count | -| numericality | :greater_than_or_equal_to | :greater_than_or_equal_to | count | -| numericality | :equal_to | :equal_to | count | -| numericality | :less_than | :less_than | count | -| numericality | :less_than_or_equal_to | :less_than_or_equal_to | count | -| numericality | :only_integer | :not_an_integer | - | -| numericality | :odd | :odd | - | -| numericality | :even | :even | - | +| 验证 | 选项 | 信息 | 插值 | +|---|---|---|---| +| `confirmation` | - | `:confirmation` | `attribute` | +| `acceptance` | - | `:accepted` | - | +| `presence` | - | `:blank` | - | +| `absence` | - | `:present` | - | +| `length` | `:within`, `:in` | `:too_short` | `count` | +| `length` | `:within`, `:in` | `:too_long` | `count` | +| `length` | `:is` | `:wrong_length` | `count` | +| `length` | `:minimum` | `:too_short` | `count` | +| `length` | `:maximum` | `:too_long` | `count` | +| `uniqueness` | - | `:taken` | - | +| `format` | - | `:invalid` | - | +| `inclusion` | - | `:inclusion` | - | +| `exclusion` | - | `:exclusion` | - | +| `associated` | - | `:invalid` | - | +| `non-optional association` | - | `:required` | - | +| `numericality` | - | `:not_a_number` | - | +| `numericality` | `:greater_than` | `:greater_than` | `count` | +| `numericality` | `:greater_than_or_equal_to` | `:greater_than_or_equal_to` | `count` | +| `numericality` | `:equal_to` | `:equal_to` | `count` | +| `numericality` | `:less_than` | `:less_than` | `count` | +| `numericality` | `:less_than_or_equal_to` | `:less_than_or_equal_to` | `count` | +| `numericality` | `:other_than` | `:other_than` | `count` | +| `numericality` | `:only_integer` | `:not_an_integer` | - | +| `numericality` | `:odd` | `:odd` | - | +| `numericality` | `:even` | `:even` | - | -#### Translations for the Active Record `error_messages_for` Helper + -If you are using the Active Record `error_messages_for` helper, you will want to add -translations for it. +#### 为 Active Record 的 `error_messages_for` 辅助方法添加翻译 -Rails ships with the following translations: +在使用 Active Record 的 `error_messages_for` 辅助方法时,我们可以为其添加翻译。 -```yaml +Rails 自带以下翻译: + +```yml en: activerecord: errors: @@ -872,14 +999,13 @@ en: body: "There were problems with the following fields:" ``` -NOTE: In order to use this helper, you need to install [DynamicForm](https://github.com/joelmoss/dynamic_form) -gem by adding this line to your Gemfile: `gem 'dynamic_form'`. +NOTE: 要想使用 `error_messages_for` 辅助方法,我们需要在 `Gemfile` 中添加一行 `gem 'dynamic_form'`,还要安装 [DynamicForm](https://github.com/joelmoss/dynamic_form) gem。 + + -### Translations for Action Mailer E-Mail Subjects +### Action Mailer 电子邮件主题的翻译 -If you don't pass a subject to the `mail` method, Action Mailer will try to find -it in your translations. The performed lookup will use the pattern -`..subject` to construct the key. +如果没有把主题传递给 `mail` 方法,Action Mailer 会尝试在翻译中查找主题。查找时会使用 `..subject` 形式来构造键。 ```ruby # user_mailer.rb @@ -890,80 +1016,157 @@ class UserMailer < ActionMailer::Base end ``` -```yaml +```yml en: user_mailer: welcome: subject: "Welcome to Rails Guides!" ``` -### Overview of Other Built-In Methods that Provide I18n Support +要想把参数用于插值,可以在调用邮件程序时使用 `default_i18n_subject` 方法。 + +```ruby +# user_mailer.rb +class UserMailer < ActionMailer::Base + def welcome(user) + mail(to: user.email, subject: default_i18n_subject(user: user.name)) + end +end +``` + +```yml +en: + user_mailer: + welcome: + subject: "%{user}, welcome to Rails Guides!" +``` -Rails uses fixed strings and other localizations, such as format strings and other format information in a couple of helpers. Here's a brief overview. + -#### Action View Helper Methods +### 提供 I18n 支持的其他内置方法概述 -* `distance_of_time_in_words` translates and pluralizes its result and interpolates the number of seconds, minutes, hours, and so on. See [datetime.distance_in_words](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L4) translations. +在 Rails 中,我们会使用固定字符串和其他本地化元素,例如,在一些辅助方法中使用的格式字符串和其他格式信息。本小节提供了简要概述。 -* `datetime_select` and `select_month` use translated month names for populating the resulting select tag. See [date.month_names](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L15) for translations. `datetime_select` also looks up the order option from [date.order](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18) (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the [datetime.prompts](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L39) scope if applicable. + -* The `number_to_currency`, `number_with_precision`, `number_to_percentage`, `number_with_delimiter`, and `number_to_human_size` helpers use the number format settings located in the [number](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L37) scope. +#### Action View 辅助方法 -#### Active Model Methods +* `distance_of_time_in_words` 辅助方法翻译并以复数形式显示结果,同时插入秒、分钟、小时的数值。更多介绍请参阅 [datetime.distance_in_words](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L4)。 +* `datetime_select` 和 `select_month` 辅助方法使用翻译后的月份名称来填充生成的 `select` 标签。更多介绍请参阅 [date.month_names](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L15)。`datetime_select` 辅助方法还会从 [date.order](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18) 中查找 `order` 选项(除非我们显式传递了 `order` 选项)。如果可能,所有日期选择辅助方法在翻译提示信息时,都会使用 [datetime.prompts](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L39) 作用域中的翻译。 +* `number_to_currency`、`number_with_precision`、`number_to_percentage`、`number_with_delimiter` 和 `number_to_human_size` 辅助方法使用 [number](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L37) 作用域中的数字格式设置。 -* `model_name.human` and `human_attribute_name` use translations for model names and attribute names if available in the [activerecord.models](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L36) scope. They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes". + -* `ActiveModel::Errors#generate_message` (which is used by Active Model validations but may also be used manually) uses `model_name.human` and `human_attribute_name` (see above). It also translates the error message and supports translations for inherited class names as explained above in "Error message scopes". +#### Active Model 方法 -* `ActiveModel::Errors#full_messages` prepends the attribute name to the error message using a separator that will be looked up from [errors.format](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml#L4) (and which defaults to `"%{attribute} %{message}"`). +* `model_name.human` 和 `human_attribute_name` 方法会使用 [activerecord.models](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L36) 作用域中可用的模型名和属性名的翻译。像 [错误消息的作用域](#error-message-scopes)中介绍的那样,这两个方法也支持继承的类名的翻译(例如,用于 `STI`)。 +* `ActiveModel::Errors#generate_message` 方法(在 Active Model 验证时使用,也可以手动使用)会使用上面介绍的 `model_name.human` 和 `human_attribute_name` 方法。像 [错误消息的作用域](#error-message-scopes)中介绍的那样,这个方法也会翻译错误消息,并支持继承的类名的翻译。 +* `ActiveModel::Errors#full_messages` 方法使用分隔符把属性名添加到错误消息的开头,然后在 [errors.format](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml#L4) 中查找(默认格式为 `"%{attribute} %{message}"`)。 -#### Active Support Methods + -* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L33) scope. +#### Active Support 方法 -Customize your I18n Setup -------------------------- +* `Array#to_sentence` 方法使用 [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L33) 作用域中的格式设置。 -### Using Different Backends + -For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_[^3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but cannot dynamically store them to any format. +## 如何储存自定义翻译 -That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs. E.g. you could exchange it with Globalize's Static backend: +Active Support 自带的简单后端,允许我们用纯 Ruby 或 YAML 格式储存翻译。 + +通过 Ruby 散列储存翻译的示例如下: + +```ruby +{ + pt: { + foo: { + bar: "baz" + } + } +} +``` + +对应的 YAML 文件如下: + +```yml +pt: + foo: + bar: baz +``` + +正如我们看到的,在这两种情况下,顶层的键是区域设置。`:foo` 是命名空间的键,`:bar` 是翻译 `"baz"` 的键。 + +下面是来自 Active Support 自带的 YAML 格式的翻译文件 `en.yml` 的“真实”示例: + +```yml +en: + date: + formats: + default: "%Y-%m-%d" + short: "%b %d" + long: "%B %d, %Y" +``` + +因此,下列查找效果相同,都会返回短日期格式 `"%b %d"`: + +```ruby +I18n.t 'date.formats.short' +I18n.t 'formats.short', scope: :date +I18n.t :short, scope: 'date.formats' +I18n.t :short, scope: [:date, :formats] +``` + +一般来说,我们推荐使用 YAML 作为储存翻译的格式。然而,在有些情况下,我们可能需要把 Ruby lambda 作为储存的区域设置信息的一部分,例如特殊的日期格式。 + + + +## 自定义 I18n 设置 + + + +### 使用不同的后端 + +由于某些原因,Active Support 自带的简单后端只为 Ruby on Rails 做了“完成任务所需的最少量工作”,这意味着只有对英语以及和英语高度类似的语言,简单后端才能保证正常工作。此外,简单后端只能读取翻译,而不能动态地把翻译储存为任何格式。 + +这并不意味着我们会被这些限制所困扰。Ruby I18n gem 让我们能够轻易地把简单后端替换为其他更适合实际需求的后端。例如,我们可以把简单后端替换为 Globalize 的 Static 后端: ```ruby I18n.backend = Globalize::Backend::Static.new ``` -You can also use the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. For example, you could use the Active Record backend and fall back to the (default) Simple backend: +我们还可以使用 Chain 后端,把多个后端链接在一起。当我们想要通过简单后端使用标准翻译,同时把自定义翻译储存在数据库或其他后端中时,链接多个后端的方式非常有用。例如,我们可以使用 Active Record 后端,并在需要时退回到默认的简单后端: ```ruby I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend) ``` -### Using Different Exception Handlers + -The I18n API defines the following exceptions that will be raised by backends when the corresponding unexpected conditions occur: +### 使用不同的异常处理程序 + +I18n API 定义了下列异常,这些异常会在相应的意外情况发生时由后端抛出: ```ruby -MissingTranslationData # no translation was found for the requested key -InvalidLocale # the locale set to I18n.locale is invalid (e.g. nil) -InvalidPluralizationData # a count option was passed but the translation data is not suitable for pluralization -MissingInterpolationArgument # the translation expects an interpolation argument that has not been passed -ReservedInterpolationKey # the translation contains a reserved interpolation variable name (i.e. one of: scope, default) -UnknownFileType # the backend does not know how to handle a file type that was added to I18n.load_path +MissingTranslationData # 找不到键对应的翻译 +InvalidLocale # I18n.locale 的区域设置无效(例如 nil) +InvalidPluralizationData # 传递了 count 参数,但翻译数据无法转换为复数形式 +MissingInterpolationArgument # 翻译所需的插值参数未传递 +ReservedInterpolationKey # 翻译包含的插值变量名使用了保留关键字(例如,scope 或 default) +UnknownFileType # 后端不知道应该如何处理添加到 I18n.load_path 中的文件类型 ``` -The I18n API will catch all of these exceptions when they are thrown in the backend and pass them to the default_exception_handler method. This method will re-raise all exceptions except for `MissingTranslationData` exceptions. When a `MissingTranslationData` exception has been caught, it will return the exception's error message string containing the missing key/scope. +当后端抛出上述异常时,I18n API 会捕获这些异常,把它们传递给 `default_exception_handler` 方法。这个方法会再次抛出除了 `MissingTranslationData` 之外的异常。当捕捉到 `MissingTranslationData` 异常时,这个方法会返回异常的错误消息字符串,其中包含了所缺少的键/作用域。 -The reason for this is that during development you'd usually want your views to still render even though a translation is missing. +这样做的原因是,在开发期间,我们通常希望在缺少翻译时仍然渲染视图。 -In other contexts you might want to change this behavior, though. E.g. the default exception handling does not allow to catch missing translations during automated tests easily. For this purpose a different exception handler can be specified. The specified exception handler must be a method on the I18n module or a class with `#call` method: +不过,在其他上下文中,我们可能想要改变此行为。例如,默认的异常处理程序不允许在自动化测试期间轻易捕获缺少的翻译;要改变这一行为,可以使用不同的异常处理程序。所使用的异常处理程序必需是 I18n 模块中的方法,或具有 `#call` 方法的类。 ```ruby module I18n class JustRaiseExceptionHandler < ExceptionHandler def call(exception, locale, key, options) - if exception.is_a?(MissingTranslation) + if exception.is_a?(MissingTranslationData) raise exception.to_exception else super @@ -975,69 +1178,55 @@ end I18n.exception_handler = I18n::JustRaiseExceptionHandler.new ``` -This would re-raise only the `MissingTranslationData` exception, passing all other input to the default exception handler. +这个例子中使用的异常处理程序只会重新抛出 `MissingTranslationData` 异常,并把其他异常传递给默认的异常处理程序。 -However, if you are using `I18n::Backend::Pluralization` this handler will also raise `I18n::MissingTranslationData: translation missing: en.i18n.plural.rule` exception that should normally be ignored to fall back to the default pluralization rule for English locale. To avoid this you may use additional check for translation key: +不过,如果我们使用了 `I18n::Backend::Pluralization` 异常处理程序,则还会抛出 `I18n::MissingTranslationData: translation missing: en.i18n.plural.rule` 异常,而这个异常通常应该被忽略,以便退回到默认的英语区域设置的复数转换规则。为了避免这种情况,我们可以对翻译键进行附加检查: ```ruby -if exception.is_a?(MissingTranslation) && key.to_s != 'i18n.plural.rule' +if exception.is_a?(MissingTranslationData) && key.to_s != 'i18n.plural.rule' raise exception.to_exception else super end ``` -Another example where the default behavior is less desirable is the Rails TranslationHelper which provides the method `#t` (as well as `#translate`). When a `MissingTranslationData` exception occurs in this context, the helper wraps the message into a span with the CSS class `translation_missing`. +默认行为不太适用的另一个例子,是 Rails 的 `TranslationHelper` 提供的 `#t` 辅助方法(和 `#translate` 辅助方法)。当上下文中出现了 `MissingTranslationData` 异常时,这个辅助方法会把错误消息放到 `` 元素中。 -To do so, the helper forces `I18n#translate` to raise exceptions no matter what exception handler is defined by setting the `:raise` option: +不管是什么异常处理程序,这个辅助方法都能够通过设置 `:raise` 选项,强制 `I18n#translate` 方法抛出异常: ```ruby -I18n.t :foo, raise: true # always re-raises exceptions from the backend +I18n.t :foo, raise: true # 总是重新抛出来自后端的异常 ``` -Conclusion ----------- - -At this point you should have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project. - -If you find anything missing or wrong in this guide, please file a ticket on our [issue tracker](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview). If you want to discuss certain portions or have questions, please sign up to our [mailing list](http://groups.google.com/group/rails-i18n). - - -Contributing to Rails I18n --------------------------- - -I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in plugins and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core. + -Thus we encourage everybody to experiment with new ideas and features in plugins or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n!)) +## 结论 -If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data and send a [pull request](https://github.com/guides/pull-requests). +现在,我们已经对 Ruby on Rails 的 I18n 支持有了较为全面的了解,可以开始着手翻译自己的项目了。 +如果想参加讨论或寻找问题的解答,可以注册 [rails-i18n 邮件列表](http://groups.google.com/group/rails-i18n)。 -Resources ---------- + -* [rails-i18n.org](http://rails-i18n.org) - Homepage of the rails-i18n project. You can find lots of useful resources on the [wiki](http://rails-i18n.org/wiki). -* [Google group: rails-i18n](http://groups.google.com/group/rails-i18n) - The project's mailing list. -* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n/tree/master) - Code repository for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases. -* [GitHub: i18n](https://github.com/svenfuchs/i18n/tree/master) - Code repository for the i18n gem. -* [Lighthouse: rails-i18n](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview) - Issue tracker for the rails-i18n project. -* [Lighthouse: i18n](http://i18n.lighthouseapp.com/projects/14947-ruby-i18n/overview) - Issue tracker for the i18n gem. +## 为 Rails I18n 作贡献 +I18n 是在 Ruby on Rails 2.2 中引入的,并且仍在不断发展。该项目继承了 Ruby on Rails 开发的优良传统,各种解决方案首先应用于 gem 和真实应用,然后再把其中最好和最广泛使用的部分纳入 Rails 核心。 -Authors -------- +因此,Rails 鼓励每个人在 gem 或其他库中试验新想法和新特性,并将它们贡献给社区。(别忘了在邮件列表上宣布我们的工作!) -* [Sven Fuchs](http://www.workingwithrails.com/person/9963-sven-fuchs) (initial author) -* [Karel Minařík](http://www.workingwithrails.com/person/7476-karel-mina-k) +如果在 Ruby on Rails 的[示例翻译数据](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale)库中没找到想要的区域设置(语言),可以[派生仓库](https://github.com/guides/fork-a-project-and-submit-your-modifications),添加翻译数据,然后发送[拉取请求](https://help.github.com/articles/about-pull-requests/)。 -If you found this guide useful, please consider recommending its authors on [workingwithrails](http://www.workingwithrails.com). + +## 资源 -Footnotes ---------- +* [rails-i18n Google 群组](http://groups.google.com/group/rails-i18n):项目的邮件列表。 +* [GitHub 中的 rails-i18n 仓库](https://github.com/svenfuchs/rails-i18n):rails-i18n 项目的代码仓库和问题跟踪器。最重要的是,我们可以在这里找到很多 Rails 的[示例翻译](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale),在大多数情况下,它们都适用于我们的应用。 +* [GitHub 中的 i18n 仓库](https://github.com/svenfuchs/i18n):i18n gem 的代码仓库和问题追踪系统。 -[^1]: Or, to quote [Wikipedia](http://en.wikipedia.org/wiki/Internationalization_and_localization): _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_ + -[^2]: Other backends might allow or require to use other formats, e.g. a GetText backend might allow to read GetText files. +## 作者 -[^3]: One of these reasons is that we don't want to imply any unnecessary load for applications that do not need any I18n capabilities, so we need to keep the I18n library as simple as possible for English. Another reason is that it is virtually impossible to implement a one-fits-all solution for all problems related to I18n for all existing languages. So a solution that allows us to exchange the entire implementation easily is appropriate anyway. This also makes it much easier to experiment with custom features and extensions. +* [Sven Fuchs](http://svenfuchs.com/)(最初的作者) +* [Karel Minařík](http://www.karmi.cz/) diff --git a/source/zh-CN/index.html.erb b/source/zh-CN/index.html.erb index 9119c2e..9016c65 100644 --- a/source/zh-CN/index.html.erb +++ b/source/zh-CN/index.html.erb @@ -1,5 +1,5 @@ <% content_for :page_title do %> -Ruby on Rails 指南 +Ruby on Rails Guides <% end %> <% content_for :header_section do %> @@ -9,7 +9,10 @@ Ruby on Rails 指南 <% content_for :index_section do %>
    -
    由此图案标记的指南代表原文正在撰写中,或是尚待翻译。正在撰写中的原文不会收录在上方的指南目录里。可能包含不完整的资讯与错误。
    +
    +
    Rails 指南同时提供 <%= link_to 'Kindle', 'https://github.com/ruby-china/rails-guides/releases' %> 版。
    +
    如果需要 Epub、PDF 格式,可以购买<%= link_to '安道维护的电子书', 'https://rails.guide/' %>。
    +
    标记了这个图标的指南还在编写中,不会出现在指南索引。这些指南可能包含不完整的信息甚至错误。您可以帮忙检查并且提交评论和修正。
    <% end %> @@ -18,7 +21,7 @@ Ruby on Rails 指南

    <%= section['name'] %>

    <% section['documents'].each do |document| %> - <%= guide(document['name'], document['url'], work_in_progress: document['work_in_progress'], needs_translation: document['needs_translation']) do %> + <%= guide(document['name'], document['url'], work_in_progress: document['work_in_progress']) do %>

    <%= document['description'] %>

    <% end %> <% end %> diff --git a/source/zh-CN/initialization.md b/source/zh-CN/initialization.md index b81b048..1b742b6 100644 --- a/source/zh-CN/initialization.md +++ b/source/zh-CN/initialization.md @@ -1,74 +1,66 @@ -The Rails Initialization Process -================================ +# Rails 初始化过程 -This guide explains the internals of the initialization process in Rails -as of Rails 4. It is an extremely in-depth guide and recommended for advanced Rails developers. +本文介绍 Rails 初始化过程的内部细节,内容较深,建议 Rails 高级开发者阅读。 -After reading this guide, you will know: +读完本文后,您将学到: -* How to use `rails server`. -* The timeline of Rails' initialization sequence. -* Where different files are required by the boot sequence. -* How the Rails::Server interface is defined and used. +* 如何使用 `rails server`; +* Rails 初始化过程的时间线; +* 引导过程中所需的不同文件的所在位置; +* `Rails::Server` 接口的定义和使用方式。 --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -This guide goes through every method call that is -required to boot up the Ruby on Rails stack for a default Rails 4 -application, explaining each part in detail along the way. For this -guide, we will be focusing on what happens when you execute `rails server` -to boot your app. +NOTE: 本文原文尚未完工! -NOTE: Paths in this guide are relative to Rails or a Rails application unless otherwise specified. +本文介绍默认情况下,Rails 应用初始化过程中的每一个方法调用,详细解释各个步骤的具体细节。本文将聚焦于使用 `rails server` 启动 Rails 应用时发生的事情。 -TIP: If you want to follow along while browsing the Rails [source -code](https://github.com/rails/rails), we recommend that you use the `t` -key binding to open the file finder inside GitHub and find files -quickly. +NOTE: 除非另有说明,本文中出现的路径都是相对于 Rails 或 Rails 应用所在目录的相对路径。 -Launch! -------- +TIP: 如果想一边阅读本文一边查看 [Rails 源代码](https://github.com/rails/rails),推荐在 GitHub 中使用 `t` 快捷键打开文件查找器,以便快速查找相关文件。 -Let's start to boot and initialize the app. A Rails application is usually -started by running `rails console` or `rails server`. + -### `railties/bin/rails` +## 启动 -The `rails` in the command `rails server` is a ruby executable in your load -path. This executable contains the following lines: +首先介绍 Rails 应用引导和初始化的过程。我们可以通过 `rails console` 或 `rails server` 命令启动 Rails 应用。 + + + +### `railties/exe/rails` 文件 + +`rails server` 命令中的 `rails` 是位于加载路径中的一个 Ruby 可执行文件。这个文件包含如下内容: ```ruby version = ">= 0" load Gem.bin_path('railties', 'rails', version) ``` -If you try out this command in a Rails console, you would see that this loads -`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the -following code: +在 Rails 控制台中运行上述代码,可以看到加载的是 `railties/exe/rails` 文件。`railties/exe/rails` 文件的部分内容如下: ```ruby require "rails/cli" ``` -The file `railties/lib/rails/cli` in turn calls -`Rails::AppRailsLoader.exec_app_rails`. +`railties/lib/rails/cli` 文件又会调用 `Rails::AppLoader.exec_app` 方法。 + + -### `railties/lib/rails/app_rails_loader.rb` +### `railties/lib/rails/app_loader.rb` 文件 -The primary goal of the function `exec_app_rails` is to execute your app's -`bin/rails`. If the current directory does not have a `bin/rails`, it will -navigate upwards until it finds a `bin/rails` executable. Thus one can invoke a -`rails` command from anywhere inside a rails application. +`exec_app` 方法的主要作用是执行应用中的 `bin/rails` 文件。如果在当前文件夹中未找到 `bin/rails` 文件,就会继续在上层文件夹中查找,直到找到为止。因此,我们可以在 Rails 应用中的任何位置执行 `rails` 命令。 -For `rails server` the equivalent of the following command is executed: +执行 `rails server` 命令时,实际执行的是等价的下述命令: -```bash +```sh $ exec ruby bin/rails server ``` -### `bin/rails` + -This file is as follows: +### `bin/rails` 文件 + +此文件包含如下内容: ```ruby #!/usr/bin/env ruby @@ -77,61 +69,57 @@ require_relative '../config/boot' require 'rails/commands' ``` -The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot` file referenced here is the `config/boot.rb` file in our application which is responsible for loading Bundler and setting it up. +其中 `APP_PATH` 常量稍后将在 `rails/commands` 中使用。所加载的 `config/boot` 是应用中的 `config/boot.rb` 文件,用于加载并设置 Bundler。 + + -### `config/boot.rb` +### `config/boot.rb` 文件 -`config/boot.rb` contains: +此文件包含如下内容: ```ruby -# Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -``` - -In a standard Rails application, there's a `Gemfile` which declares all -dependencies of the application. `config/boot.rb` sets -`ENV['BUNDLE_GEMFILE']` to the location of this file. If the Gemfile -exists, then `bundler/setup` is required. The require is used by Bundler to -configure the load path for your Gemfile's dependencies. - -A standard Rails application depends on several gems, specifically: - -* actionmailer -* actionpack -* actionview -* activemodel -* activerecord -* activesupport -* arel -* builder -* bundler -* erubis -* i18n -* mail -* mime-types -* polyglot -* rack -* rack-cache -* rack-mount -* rack-test -* rails -* railties -* rake -* sqlite3 -* thor -* treetop -* tzinfo - -### `rails/commands.rb` - -Once `config/boot.rb` has finished, the next file that is required is -`rails/commands`, which helps in expanding aliases. In the current case, the -`ARGV` array simply contains `server` which will be passed over: - -```ruby -ARGV << '--help' if ARGV.empty? +require 'bundler/setup' # 设置 Gemfile 中列出的所有 gem +``` + +标准的 Rails 应用中包含 `Gemfile` 文件,用于声明应用的所有依赖关系。`config/boot.rb` 文件会把 `ENV['BUNDLE_GEMFILE']` 设置为 `Gemfile` 文件的路径。如果 `Gemfile` 文件存在,就会加载 `bundler/setup`,Bundler 通过它设置 Gemfile 中依赖关系的加载路径。 + +标准的 Rails 应用依赖多个 gem,包括: + +* actionmailer +* actionpack +* actionview +* activemodel +* activerecord +* activesupport +* activejob +* arel +* builder +* bundler +* erubis +* i18n +* mail +* mime-types +* rack +* rack-cache +* rack-mount +* rack-test +* rails +* railties +* rake +* sqlite3 +* thor +* tzinfo + + + +### `rails/commands.rb` 文件 + +执行完 `config/boot.rb` 文件,下一步就要加载 `rails/commands`,其作用是扩展命令别名。在本例中(输入的命令为 `rails server`),`ARGV` 数组只包含将要传递的 `server` 命令: + +```ruby +require "rails/command" aliases = { "g" => "generate", @@ -139,92 +127,81 @@ aliases = { "c" => "console", "s" => "server", "db" => "dbconsole", - "r" => "runner" + "r" => "runner", + "t" => "test" } command = ARGV.shift command = aliases[command] || command -require 'rails/commands/commands_tasks' - -Rails::CommandsTasks.new(ARGV).run_command!(command) +Rails::Command.invoke command, ARGV ``` -TIP: As you can see, an empty ARGV list will make Rails show the help -snippet. +如果输入的命令使用的是 `s` 而不是 `server`,Rails 就会在上面定义的 `aliases` 散列中查找对应的命令。 -If we had used `s` rather than `server`, Rails would have used the `aliases` -defined here to find the matching command. + -### `rails/commands/command_tasks.rb` +### `rails/command.rb` 文件 -When one types an incorrect rails command, the `run_command` is responsible for -throwing an error message. If the command is valid, a method of the same name -is called. +输入 Rails 命令时,`invoke` 尝试查找指定命名空间中的命令,如果找到就执行那个命令。 + +如果找不到命令,Rails 委托 Rake 执行同名任务。 + +如下述代码所示,`args` 为空时,`Rails::Command` 自动显示帮助信息。 ```ruby -COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help) +module Rails::Command + class << self + def invoke(namespace, args = [], **config) + namespace = namespace.to_s + namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) + namespace = "version" if %w( -v --version ).include? namespace -def run_command!(command) - command = parse_command(command) - if COMMAND_WHITELIST.include?(command) - send(command) - else - write_error_message(command) + if command = find_by_namespace(namespace) + command.perform(namespace, args, config) + else + find_by_namespace("rake").perform(namespace, args, config) + end + end end end ``` -With the `server` command, Rails will further run the following code: +本例中输入的是 `server` 命令,因此 Rails 会进一步运行下述代码: ```ruby -def set_application_directory! - Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru")) -end - -def server - set_application_directory! - require_command!("server") - - Rails::Server.new.tap do |server| - # We need to require application after the server sets environment, - # otherwise the --environment option given to the server won't propagate. - require APP_PATH - Dir.chdir(Rails.application.root) - server.start +module Rails + module Command + class ServerCommand < Base # :nodoc: + def perform + set_application_directory! + + Rails::Server.new.tap do |server| + # Require application after server sets environment to propagate + # the --environment option. + require APP_PATH + Dir.chdir(Rails.application.root) + server.start + end + end + end end end - -def require_command!(command) - require "rails/commands/#{command}" -end ``` -This file will change into the Rails root directory (a path two directories up -from `APP_PATH` which points at `config/application.rb`), but only if the -`config.ru` file isn't found. This then requires `rails/commands/server` which -sets up the `Rails::Server` class. +仅当 `config.ru` 文件无法找到时,才会切换到 Rails 应用根目录(`APP_PATH` 所在文件夹的上一层文件夹,其中 `APP_PATH` 指向 `config/application.rb` 文件)。然后运行 `Rails::Server` 类。 -```ruby -require 'fileutils' -require 'optparse' -require 'action_dispatch' -require 'rails' + -module Rails - class Server < ::Rack::Server -``` +### `actionpack/lib/action_dispatch.rb` 文件 -`fileutils` and `optparse` are standard Ruby libraries which provide helper functions for working with files and parsing options. +Action Dispatch 是 Rails 框架的路由组件,提供路由、会话、常用中间件等功能。 -### `actionpack/lib/action_dispatch.rb` + -Action Dispatch is the routing component of the Rails framework. -It adds functionality like routing, session, and common middlewares. +### `rails/commands/server/server_command.rb` 文件 -### `rails/commands/server.rb` - -The `Rails::Server` class is defined in this file by inheriting from `Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` method in `rails/commands/server.rb`: +此文件中定义的 `Rails::Server` 类,继承自 `Rack::Server` 类。当调用 `Rails::Server.new` 方法时,会调用此文件中定义的 `initialize` 方法: ```ruby def initialize(*) @@ -233,13 +210,15 @@ def initialize(*) end ``` -Firstly, `super` is called which calls the `initialize` method on `Rack::Server`. +首先调用的 `super` 方法,会调用 `Rack::Server` 类的 `initialize` 方法。 + + -### Rack: `lib/rack/server.rb` +### Rack:`lib/rack/server.rb` 文件 -`Rack::Server` is responsible for providing a common server interface for all Rack-based applications, which Rails is now a part of. +`Rack::Server` 类负责为所有基于 Rack 的应用(包括 Rails)提供通用服务器接口。 -The `initialize` method in `Rack::Server` simply sets a couple of variables: +`Rack::Server` 类的 `initialize` 方法的作用是设置几个变量: ```ruby def initialize(options = nil) @@ -248,9 +227,9 @@ def initialize(options = nil) end ``` -In this case, `options` will be `nil` so nothing happens in this method. +在本例中,`options` 的值是 `nil`,因此这个方法什么也没做。 -After `super` has finished in `Rack::Server`, we jump back to `rails/commands/server.rb`. At this point, `set_environment` is called within the context of the `Rails::Server` object and this method doesn't appear to do much at first glance: +当 `super` 方法完成 `Rack::Server` 类的 `initialize` 方法的调用后,程序执行流程重新回到 `rails/commands/server/server_command.rb` 文件中。此时,会在 `Rails::Server` 对象的上下文中调用 `set_environment` 方法。乍一看这个方法什么也没做: ```ruby def set_environment @@ -258,7 +237,7 @@ def set_environment end ``` -In fact, the `options` method here does quite a lot. This method is defined in `Rack::Server` like this: +实际上,其中的 `options` 方法做了很多工作。`options` 方法在 `Rack::Server` 类中定义: ```ruby def options @@ -266,13 +245,13 @@ def options end ``` -Then `parse_options` is defined like this: +而 `parse_options` 方法的定义如下: ```ruby def parse_options(args) options = default_options - # Don't evaluate CGI ISINDEX parameters. + # 请不要计算 CGI `ISINDEX` 参数的值。 # http://www.meb.uni-bonn.de/docs/cgi/cl.html args.clear if ENV.include?("REQUEST_METHOD") @@ -283,25 +262,23 @@ def parse_options(args) end ``` -With the `default_options` set to this: +其中 `default_options` 方法的定义如下: ```ruby def default_options - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - { - :environment => environment, - :pid => nil, - :Port => 9292, - :Host => default_host, - :AccessLog => [], - :config => "config.ru" - } + super.merge( + Port: ENV.fetch("https://github.com/PORT", 3000).to_i, + Host: ENV.fetch("https://github.com/HOST", "localhost").dup, + DoNotReverseLookup: true, + environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup, + daemonize: false, + caching: nil, + pid: Options::DEFAULT_PID_PATH, + restart_cmd: restart_command) end ``` -There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server`: +在 `ENV` 散列中不存在 `REQUEST_METHOD` 键,因此可以跳过该行。下一行会合并 `opt_parser` 方法返回的选项,其中 `opt_parser` 方法在 `Rack::Server` 类中定义: ```ruby def opt_parser @@ -309,40 +286,40 @@ def opt_parser end ``` -The class **is** defined in `Rack::Server`, but is overwritten in `Rails::Server` to take different arguments. Its `parse!` method begins like this: +`Options` 类在 `Rack::Server` 类中定义,但在 `Rails::Server` 类中被覆盖了,目的是为了接受不同参数。`Options` 类的 `parse!` 方法的定义如下: ```ruby def parse!(args) args, options = args.dup, {} - opt_parser = OptionParser.new do |opts| - opts.banner = "Usage: rails server [mongrel, thin, etc] [options]" - opts.on("-p", "--port=port", Integer, - "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } - ... + option_parser(options).parse! args + + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" + options[:server] = args.shift + options +end ``` -This method will set up keys for the `options` which Rails will then be -able to use to determine how its server should run. After `initialize` -has finished, we jump back into `rails/server` where `APP_PATH` (which was -set earlier) is required. +此方法为 `options` 散列的键赋值,稍后 Rails 将使用此散列确定服务器的运行方式。`initialize` 方法运行完成后,程序执行流程会跳回 `server` 命令,然后加载之前设置的 `APP_PATH`。 -### `config/application` + -When `require APP_PATH` is executed, `config/application.rb` is loaded (recall -that `APP_PATH` is defined in `bin/rails`). This file exists in your application -and it's free for you to change based on your needs. +### `config/application.rb` 文件 -### `Rails::Server#start` +执行 `require APP_PATH` 时,会加载 `config/application.rb` 文件(前文说过 `APP_PATH` 已经在 `bin/rails` 中定义)。这个文件也是应用的一部分,我们可以根据需要修改这个文件的内容。 -After `config/application` is loaded, `server.start` is called. This method is -defined like this: + + +### `Rails::Server#start` 方法 + +`config/application.rb` 文件加载完成后,会调用 `server.start` 方法。这个方法的定义如下: ```ruby def start print_boot_information trap(:INT) { exit } create_tmp_directories + setup_dev_caching log_to_stdout if options[:log_stdout] super @@ -350,40 +327,38 @@ def start end private - def print_boot_information ... puts "=> Run `rails server -h` for more startup options" - ... - puts "=> Ctrl-C to shutdown server" unless options[:daemonize] end def create_tmp_directories - %w(cache pids sessions sockets).each do |dir_to_make| + %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) end end + def setup_dev_caching + if options[:environment] == "development" + Rails::DevCaching.enable_by_argument(options[:caching]) + end + end + def log_to_stdout - wrapped_app # touch the app so the logger is set up + wrapped_app # 对应用执行 touch 操作,以便设置记录器 - console = ActiveSupport::Logger.new($stdout) + console = ActiveSupport::Logger.new(STDOUT) console.formatter = Rails.logger.formatter console.level = Rails.logger.level + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) end ``` -This is where the first output of the Rails initialization happens. This -method creates a trap for `INT` signals, so if you `CTRL-C` the server, -it will exit the process. As we can see from the code here, it will -create the `tmp/cache`, `tmp/pids`, `tmp/sessions` and `tmp/sockets` -directories. It then calls `wrapped_app` which is responsible for -creating the Rack app, before creating and assigning an -instance of `ActiveSupport::Logger`. +这是 Rails 初始化过程中第一次输出信息。`start` 方法为 `INT` 信号创建了一个陷阱,只要在服务器运行时按下 `CTRL-C`,服务器进程就会退出。我们看到,上述代码会创建 `tmp/cache`、`tmp/pids` 和 `tmp/sockets` 文件夹。然后,如果运行 `rails server` 命令时指定了 `--dev-caching` 参数,在开发环境中启用缓存。最后,调用 `wrapped_app` 方法,其作用是先创建 Rack 应用,再创建 `ActiveSupport::Logger` 类的实例。 -The `super` method will call `Rack::Server.start` which begins its definition like this: +`super` 方法会调用 `Rack::Server.start` 方法,后者的定义如下: ```ruby def start &blk @@ -409,8 +384,8 @@ def start &blk check_pid! if options[:pid] - # Touch the wrapped app, so that the config.ru is loaded before - # daemonization (i.e. before chdir, etc). + # 对包装后的应用执行 touch 操作,以便在创建守护进程之前 + # 加载 `config.ru` 文件(例如在 `chdir` 等操作之前) wrapped_app daemonize_app if options[:daemonize] @@ -429,15 +404,13 @@ def start &blk end ``` -The interesting part for a Rails app is the last line, `server.run`. Here we encounter the `wrapped_app` method again, which this time -we're going to explore more (even though it was executed before, and -thus memoized by now). +代码块最后一行中的 `server.run` 非常有意思。这里我们再次遇到了 `wrapped_app` 方法,这次我们要更深入地研究它(前文已经调用过 `wrapped_app` 方法,现在需要回顾一下)。 ```ruby @wrapped_app ||= build_app app ``` -The `app` method here is defined like so: +其中 `app` 方法定义如下: ```ruby def app @@ -460,17 +433,16 @@ private end ``` -The `options[:config]` value defaults to `config.ru` which contains this: +`options[:config]` 的默认值为 `config.ru`,此文件包含如下内容: ```ruby -# This file is used by Rack-based servers to start the application. +# 基于 Rack 的服务器使用此文件来启动应用。 -require ::File.expand_path('../config/environment', __FILE__) +require_relative 'config/environment' run <%= app_const %> ``` - -The `Rack::Builder.parse_file` method here takes the content from this `config.ru` file and parses it using this code: +`Rack::Builder.parse_file` 方法读取 `config.ru` 文件的内容,并使用下述代码解析文件内容: ```ruby app = new_from_string cfgfile, config @@ -483,87 +455,89 @@ def self.new_from_string(builder_script, file="(rackup)") end ``` -The `initialize` method of `Rack::Builder` will take the block here and execute it within an instance of `Rack::Builder`. This is where the majority of the initialization process of Rails happens. The `require` line for `config/environment.rb` in `config.ru` is the first to run: +`Rack::Builder` 类的 `initialize` 方法会把接收到的代码块在 `Rack::Builder` 类的实例中执行,Rails 初始化过程中的大部分工作都在这一步完成。在 `config.ru` 文件中,加载 `config/environment.rb` 文件的这一行代码首先被执行: ```ruby -require ::File.expand_path('../config/environment', __FILE__) +require_relative 'config/environment' ``` -### `config/environment.rb` + + +### `config/environment.rb` 文件 -This file is the common file required by `config.ru` (`rails server`) and Passenger. This is where these two ways to run the server meet; everything before this point has been Rack and Rails setup. +`config.ru` 文件(`rails server`)和 Passenger 都需要加载此文件。这两种运行服务器的方式直到这里才出现了交集,此前的一切工作都只是围绕 Rack 和 Rails 的设置进行的。 -This file begins with requiring `config/application.rb`: +此文件以加载 `config/application.rb` 文件开始: ```ruby -require File.expand_path('../application', __FILE__) +require_relative 'application' ``` -### `config/application.rb` + -This file requires `config/boot.rb`: +### `config/application.rb` 文件 + +此文件会加载 `config/boot.rb` 文件: ```ruby -require File.expand_path('../boot', __FILE__) +require_relative 'boot' ``` -But only if it hasn't been required before, which would be the case in `rails server` -but **wouldn't** be the case with Passenger. +对于 `rails server` 这种启动服务器的方式,之前并未加载过 `config/boot.rb` 文件,因此这里会加载该文件;对于 Passenger,之前已经加载过该文件,这里就不会重复加载了。 + +接下来,有趣的事情就要开始了! -Then the fun begins! + -Loading Rails -------------- +## 加载 Rails -The next line in `config/application.rb` is: +`config/application.rb` 文件的下一行是: ```ruby require 'rails/all' ``` -### `railties/lib/rails/all.rb` + + +### `railties/lib/rails/all.rb` 文件 -This file is responsible for requiring all the individual frameworks of Rails: +此文件负责加载 Rails 中所有独立的框架: ```ruby require "rails" %w( - active_record - action_controller - action_view - action_mailer - rails/test_unit - sprockets -).each do |framework| + active_record/railtie + action_controller/railtie + action_view/railtie + action_mailer/railtie + active_job/railtie + action_cable/engine + rails/test_unit/railtie + sprockets/railtie +).each do |railtie| begin - require "#{framework}/railtie" + require railtie rescue LoadError end end ``` -This is where all the Rails frameworks are loaded and thus made -available to the application. We won't go into detail of what happens -inside each of those frameworks, but you're encouraged to try and -explore them on your own. +这些框架加载完成后,就可以在 Rails 应用中使用了。这里不会深入介绍每个框架,而是鼓励读者自己动手试验和探索。 -For now, just keep in mind that common functionality like Rails engines, -I18n and Rails configuration are all being defined here. +现在,我们只需记住,Rails 的常见功能,例如 Rails 引擎、I18n 和 Rails 配置,都在这里定义好了。 -### Back to `config/environment.rb` + -The rest of `config/application.rb` defines the configuration for the -`Rails::Application` which will be used once the application is fully -initialized. When `config/application.rb` has finished loading Rails and defined -the application namespace, we go back to `config/environment.rb`, -where the application is initialized. For example, if the application was called -`Blog`, here we would find `Rails.application.initialize!`, which is -defined in `rails/application.rb`. +### 回到 `config/environment.rb` 文件 -### `railties/lib/rails/application.rb` +`config/application.rb` 文件的其余部分定义了 `Rails::Application` 的配置,当应用的初始化全部完成后就会使用这些配置。当 `config/application.rb` 文件完成了 Rails 的加载和应用命名空间的定义后,程序执行流程再次回到 `config/environment.rb` 文件。在这里会通过 `rails/application.rb` 文件中定义的 `Rails.application.initialize!` 方法完成应用的初始化。 -The `initialize!` method looks like this: + + +### `railties/lib/rails/application.rb` 文件 + +`initialize!` 方法的定义如下: ```ruby def initialize!(group=:default) #:nodoc: @@ -574,8 +548,7 @@ def initialize!(group=:default) #:nodoc: end ``` -As you can see, you can only initialize an app once. The initializers are run through -the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb`: +我们看到,一个应用只能初始化一次。`railties/lib/rails/initializable.rb` 文件中定义的 `run_initializers` 方法负责运行初始化程序: ```ruby def run_initializers(group=:default, *args) @@ -587,24 +560,17 @@ def run_initializers(group=:default, *args) end ``` -The `run_initializers` code itself is tricky. What Rails is doing here is -traversing all the class ancestors looking for those that respond to an -`initializers` method. It then sorts the ancestors by name, and runs them. -For example, the `Engine` class will make all the engines available by -providing an `initializers` method on them. +`run_initializers` 方法的代码比较复杂,Rails 会遍历所有类的祖先,以查找能够响应 `initializers` 方法的类。对于找到的类,首先按名称排序,然后依次调用 `initializers` 方法。例如,`Engine` 类通过为所有的引擎提供 `initializers` 方法而使它们可用。 + +`railties/lib/rails/application.rb` 文件中定义的 `Rails::Application` 类,定义了 `bootstrap`、`railtie` 和 `finisher` 初始化程序。`bootstrap` 初始化程序负责完成应用初始化的准备工作(例如初始化记录器),而 `finisher` 初始化程序(例如创建中间件栈)总是最后运行。`railtie` 初始化程序在 `Rails::Application` 类自身中定义,在 `bootstrap` 之后、`finishers` 之前运行。 -The `Rails::Application` class, as defined in `railties/lib/rails/application.rb` -defines `bootstrap`, `railtie`, and `finisher` initializers. The `bootstrap` initializers -prepare the application (like initializing the logger) while the `finisher` -initializers (like building the middleware stack) are run last. The `railtie` -initializers are the initializers which have been defined on the `Rails::Application` -itself and are run between the `bootstrap` and `finishers`. +应用初始化完成后,程序执行流程再次回到 `Rack::Server` 类。 -After this is done we go back to `Rack::Server`. + -### Rack: lib/rack/server.rb +### Rack:`lib/rack/server.rb` 文件 -Last time we left when the `app` method was being defined: +程序执行流程上一次离开此文件是在定义 `app` 方法时: ```ruby def app @@ -627,8 +593,7 @@ private end ``` -At this point `app` is the Rails app itself (a middleware), and what -happens next is Rack will call all the provided middlewares: +此时,`app` 就是 Rails 应用本身(一个中间件),接下来 Rack 会调用所有已提供的中间件: ```ruby def build_app(app) @@ -642,16 +607,13 @@ def build_app(app) end ``` -Remember, `build_app` was called (by `wrapped_app`) in the last line of `Server#start`. -Here's how it looked like when we left: +记住,在 `Server#start` 方法定义的最后一行代码中,通过 `wrapped_app` 方法调用了 `build_app` 方法。让我们回顾一下这行代码: ```ruby server.run wrapped_app, options, &blk ``` -At this point, the implementation of `server.run` will depend on the -server you're using. For example, if you were using Puma, here's what -the `run` method would look like: +此时,`server.run` 方法的实现方式取决于我们所使用的服务器。例如,如果使用的是 Puma,`run` 方法的实现方式如下: ```ruby ... @@ -663,7 +625,7 @@ DEFAULT_OPTIONS = { } def self.run(app, options = {}) - options = DEFAULT_OPTIONS.merge(options) + options = DEFAULT_OPTIONS.merge(options) if options[:Verbose] app = Rack::CommonLogger.new(app, STDOUT) @@ -697,10 +659,6 @@ def self.run(app, options = {}) end ``` -We won't dig into the server configuration itself, but this is -the last piece of our journey in the Rails initialization process. +我们不会深入介绍服务器配置本身,不过这已经是 Rails 初始化过程的最后一步了。 -This high level overview will help you understand when your code is -executed and how, and overall become a better Rails developer. If you -still want to know more, the Rails source code itself is probably the -best place to go next. +本文高度概括的介绍,旨在帮助读者理解 Rails 应用的代码何时执行、如何执行,从而使读者成为更优秀的 Rails 开发者。要想掌握更多这方面的知识,Rails 源代码本身也许是最好的研究对象。 diff --git a/source/zh-CN/kindle/layout.html.erb b/source/zh-CN/kindle/layout.html.erb index f0a2862..fd87467 100644 --- a/source/zh-CN/kindle/layout.html.erb +++ b/source/zh-CN/kindle/layout.html.erb @@ -14,12 +14,12 @@ <% if content_for? :header_section %> <%= yield :header_section %> -
    +
    <% end %> <% if content_for? :index_section %> <%= yield :index_section %> -
    +
    <% end %> <%= yield.html_safe %> diff --git a/source/zh-CN/kindle/rails_guides.opf.erb b/source/zh-CN/kindle/rails_guides.opf.erb index 547abcb..c1153e5 100644 --- a/source/zh-CN/kindle/rails_guides.opf.erb +++ b/source/zh-CN/kindle/rails_guides.opf.erb @@ -5,15 +5,15 @@ - Ruby on Rails Guides (<%= @version %>) + Ruby on Rails 指南 (<%= @version %>) - en-us + zh-CN Ruby on Rails Ruby on Rails Reference <%= Time.now.strftime('%Y-%m-%d') %> - These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together. + 这份指南旨在使您立即获得 Rails 的生产力,并帮助您了解所有组件如何组合在一起。 diff --git a/source/zh-CN/kindle/toc.html.erb b/source/zh-CN/kindle/toc.html.erb index f310edd..38bf1dd 100644 --- a/source/zh-CN/kindle/toc.html.erb +++ b/source/zh-CN/kindle/toc.html.erb @@ -1,5 +1,5 @@ <% content_for :page_title do %> -Ruby on Rails Guides +Ruby on Rails 指南 <% end %>

    Table of Contents

    @@ -14,7 +14,7 @@ Ruby on Rails Guides <% if document['work_in_progress']%>(WIP)<% end %>
  • <% end %> -
+ <% end %>