17611


この質問は、異なる名前空間に定義された同名のモデルが実行時に期待したようにロードされないことに関する質問です。

Ruby on Rails 4で作ったアプリケーションで、私は投稿とカテゴリを表す2つのモデルを使っています。

[code]

    class Post < ActiveRecord::Base
      belongs_to :category, required: true
    end
    
    class Category < ActiveRecord::Base
      has_many :posts, dependent: :restrict_with_error
    end
    
[/code]

このアプリケーションは大きく分けて次の2つの画面を持っており、各モデルのパラメータに使う属性が異なります。

  * **管理画面** : 変わらないURLを利用するために、モデルのパラメータに`id`を使う
  * **一般画面** : わかりやすいURLを利用するために、モデルのパラメータに`name`を使う

この仕様を満たすために、私は各画面に対応する2つのMountable Engineでモデルを以下のように定義しました。

[code]

    module Content
      class Post < ::Post
        def to_param
          name
        end
      end
    
      class Category < ::Category
        def to_param
          name
        end
      end
    end
    
    module Admin
      class Post < ::Post
      end
    
      class Category < ::Category
      end
    end
    
[/code]

さて、実際の振る舞いを確認してみます。私は以下のように`CategoriesController`でcategoryモデルに属するpostモデルのリンクを表示してみました。Content名前空間で実行しているため、パラメータはname属性になると私は期待しています。

しかし、パラメータがid属性になる時があったときがありました。この問題が難しい理由は、パラメータがname属性になる時もあったからです

[code]

    module Content
      class CategoriesController < ApplicationController
        def show
          @category = Category.includes(:posts).find(params[:id])
        end
      end
    end
    
    ul
      - @category.posts.each do |post|
        li
          = link_to post.title, post
    
[/code]

アプリケーションはdevelopment環境で実行しました。Rails Guidesの以下の章がヒントになるかと思いましたが、原因はわかりませんでした。

Autoloading and Reloading Constants  
<http://guides.rubyonrails.org/autoloading_and_reloading_constants.html>

なお、モデルのアソシエーションを名前空間で再定義する方法でこの問題を解決することも考えました。しかし、それはDRYでなく良い解決方法と感じています。

* * *

追記します。

同一のアクション/ビューで、どちらの結果になるときもあるということです。  
全くのランダムかは、確かめる方法がないため分かりません。

実際のサンプルアプリケーションを以下にコミットしました。

<https://github.com/lhside/namespace_app>

ふと思い当たるふしがあり、Admin::Postを保存した直後にContent::PostsControllerのindexアクションにリクエストすると、id属性がパラメータとして表示されました。  
その時のログは、先のリポジトリのREADMEにコピーしています。


CC BY-SA 3.0
2015-10-09T15:46:01.100
17481
2015-10-13T21:55:57.397
2015-10-13T13:03:00.823
7420
7420
question
ruby-on-rails ruby
異なる名前空間に定義された同名のモデルのローディング


サンプルアプリケーションのコミット、ありがとうございました。  
手元でちょっとデバッグしてpull requestを作ってみました。

<https://github.com/lhside/namespace_app/pull/1>

修正ポイントは主に以下の2点です。

  1. admin/app/models/category.rb を admin/app/models/admin/category.rb に移動
  2. controller に require_dependency '...' を追加

[code]

    require_dependency 'admin/post'
    module Admin
      class PostsController < ApplicationController
        def index
          @posts = Post.ordered
        end
        # ...
      end
    end
    
    require_dependency 'content/post'
    module Content
      class PostsController < ApplicationController
        def index
          @posts = Post.ordered
        end
        # ...
      end
    end
    
[/code]

特に重要なのは require_dependency の方です。  
この説明は以下のページに書いてあります。

[10.6 When Constants aren't
Missed](http://guides.rubyonrails.org/autoloading_and_reloading_constants.html#when-
constants-aren-t-missed)

> That code depends on the execution path.  
>  These kind of ambiguities can often be resolved using qualified constants:
[code]

    module BellX1
      class Plane
        def flight_model
          @flight_model ||= BellX1::FlightModel.new
        end
      end
    end
    
[/code]

> Also, require_dependency is a solution:
[code]

    require_dependency 'bell_x1/flight_model'
    
    module BellX1
      class Plane
        def flight_model
          @flight_model ||= FlightModel.new
        end
      end
    end
    
[/code]

上のように、Rails
Guidesでは「明示的にネームスペース付きのクラス名にする方法」と「require_dependencyを使う方法」の2パターンが紹介されていますが、今回は後者を使ってみました。

簡単に説明すると、::Post は読み込まれているが Content::Post はまだ読み込まれていない状態で
Content::PostsController にアクセスすると、コード中の Post が Content::Post ではなく、 ::Post
として解釈されるのが原因です。  
なので、 require_dependency 'content/post' を使って、強制的に Content::Post を読み込んでいます。  
こうすると同じモジュール内の Post （つまり Content::Post）が使われるようになります。

実際のアプリケーションでも試してみてください。


CC BY-SA 3.0
2015-10-13T21:03:52.910
17611
2015-10-13T21:55:57.397
2015-10-13T21:55:57.397
85
85
17481
answer