Bitbucket

自分用の技術メモ

Getting all classes in current Ruby process

現時点で定義されている全てのクラスを得るためには ObjectSpace.each_object を利用する。

1
2
3
def get_classes
  ObjectSpace.each_object(Class).to_a
end

用途

通常のプログラム中で役に立つ場面はあまり無いが、 少し特殊な用途には役立つことがある。 具体的にはプラグイン機構を作るため、次のように利用した。

  1. プラグインファイルを require する
  2. 追加されたクラス = プラグインのクラス を取得する
  3. プラグインの各クラスに対して処理 (インスタンスの作成や初期化) を行う
プラグインのロード
1
2
3
4
5
6
7
def load_plugins(files)
  classes_before = get_classes
  files.each do |file|
    require file
  end
  get_classes - classes_before
end

Cinch を利用した IRC bot プログラム (Ruby初心者の頃に作成したものなので、酷いコードが混じっているはず) では、 Cinch::Plugininclude しているものをプラグインのクラスとして判定した。

1
get_classes.select { |c| c.include?(Cinch::Plugin) }

Apache DirectorySlash directive

Apache には、ディレクトリに “/” が無い場合には自動的に補完してリダイレクトする機能があるが、これは DirectorySlash ディレクティブによって制御することができる。 この書き換えは mod_rewrite の前に行われるため、特殊な書き換えを行う場合には Off にする必要がある。(特に理由が無い限り、規定値の On のままで良いだろう。)

たとえば、ディレクトリに対して “/” 無しでアクセスさせたい場合には、 mod_rewrite のディレクティブと組み合わせて次のように記載する。

1
2
3
4
DirectorySlash Off
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^(.*[^/])$ $1/

これにより、たとえば http://www.example.com/foo というURLにアクセスすると http://www.example.com/foo/ と同一の内容が表示される。

open-uri and redirection (open-uriのリダイレクト処理)

Rubyのopen-uriで基本認証付のURIをオープンする場合、ユーザ、パスワードが間違っていない場合でも “401 Unauthorized” の例外 (OpenURI::HTTPError) が発生することがある。このような事象が発生しているとき、アクセス先がリダイレクト (3xx) のステータスを返している可能性がある。

open-uri ではリダイレクトが行われた場合には、無条件で基本認証に関する削除しているため、リダイレクト先が同一ホスト・realmであった場合でも認証情報が渡されないためこのような事象が発生する。

open-uri.rb より抜粋
1
2
3
4
5
6
7
if redirect
  ...
  if options.include? :http_basic_authentication
    # send authentication only for the URI directly specified.
    options = options.dup
    options.delete :http_basic_authentication
  end

オプションとして :redirect => true が指定され、同一サーバ・realmであった場合にはリダイレクト先に認証情報を引き継ぐのが正しいと思うが、そもそも realm を見ていないようだ。

リダイレクトされる可能性のあるURIに認証情報を渡す場合 :redirect => false として、OpenURI::HTTPRedirect の例外を捕捉して自前でリダイレクトを処理する必要がある。

1
2
3
4
5
6
7
8
9
10
begin
  fh = open(
    uri,
    {:http_basic_authentication => [user, password],
     :redirect => false}
  )
rescue OpenURI::HTTPRedirect => redirect
  uri = redirect.uri
  retry
end

※実際にはリダイレクト先のURIとrealmを検証するべき。

Active Record associations (Active Record の関連付け)

関連付け (associations) とは各モデルのデータを定義して、容易にアクセスするための仕組み。自分でアクセスするコードを書いても良いが、関連付けを行うことで他のモデルへのアクセスを簡単に書くことができる。

1対多

あるキャラクタ (Character) が複数のスキル (Skill) を使える、つまりCharacterは複数のSkillを持ち (has many)、SkillはCharacterに属して (belongs to) いる関係を考える。

ここでモデルは次のように定義する。

app/model/character.rb
1
2
3
4
class Character < ActiveRecord::Base
  attr_accessible :name
  has_many :skills
end
app/model/skill.rb
1
2
3
4
class Skill < ActiveRecord::Base
  attr_accessible :character_id, :name
  belongs_to :character
end

has_manybelongs_to をこのように書くことでActiveRecordの規約に従い、Skill.character で、character_id を通して対応する Character インスタンスにアクセス、Character.skills で、Skillを0個以上持つ Array としてアクセスできる。(規約に従わない命名をした場合、:class_name:foreign_key で明示する必要がある。)

ここでテーブルにはそれぞれ以下のようなデータが格納されているとする。

characters:

id name
1 フェイト・テスタロッサ
2 高町なのは

skills:

id name character_id
1 サンダースマッシャー 1
2 プラズマザンバーブレイカー 1
3 サンダーレイジ 1
4 スターライトブレイカー 2
5 ディバインバスター 2
6 クロスファイアシュート 2

以下のように Character のインスタンスから Skill を、Skill のインスタンスから Character にアクセスすることができる。

1
2
3
4
5
6
7
8
9
10
11
fate = Character.find 1
# => #<Character id: 1, name: "フェイト・テスタロッサ">
fate.skills
# => [#<Skill id: 1, name: "サンダースマッシャー", character_id: 1>,
#     #<Skill id: 2, name: "プラズマザンバーブレイカー", character_id: 1>,
#     #<Skill id: 3, name: "サンダーレイジ", character_id: 1>]

slb = Skill.find 4
# => #<Skill id: 4, name: "スターライトブレイカー", character_id: 2>
slb.character
# => #<Character id: 2, name: "高町なのは">

Character.skills を変更することで、関連付けられた Skill を変更することができる。

1
2
3
4
5
6
7
8
9
10
11
tr = Skill.find 3
# => #<Skill id: 3, name: "サンダーレイジ", character_id: 1>
fate.skills.delete tr

fate = Character.find 1
fate.skills
# => [#<Skill id: 1, name: "サンダースマッシャー", character_id: 1>,
#     #<Skill id: 2, name: "プラズマザンバーブレイカー", character_id: 1>]+

Skill.find 3
# => #<Skill id: 3, name: "サンダーレイジ", character_id: nil>

多対多

ここまでは、あるスキルを使えるキャラクタが1人であるという前提で1対多の関連づけを行っていたが、”クロスファイアシュート” はティアナ・ランスターも使うことができるため、このままでは問題がおきる。(要は設計ミス。) そこで、あるCharacterが複数のSkillを持ち、Skillが複数のCharacterを持つ、多対多の関係を定義することにする。

このような場合、Character と Skill の関係を表す中間モデル (ここでは、CharacterSkillRelation) を作り、間接的に関連づける。なお、has_and_belongs_to_many を利用する方法は非推奨 (deprecated) となっているため取り上げない。

まずは、次のような CharacterSkillRelation モデルを作成する。

app/model/character_skill_relation.rb
1
2
3
4
5
class CharacterSkillRelation < ActiveRecord::Base
  attr_accessible :character_id, :skill_id
  belongs_to :character
  belongs_to :skill
end

Character と Skill を次のように修正する。

app/model/character.rb
1
2
3
4
5
class Character < ActiveRecord::Base
  attr_accessible :name
  has_many :character_skill_relations
  has_many :skills, :through => :character_skill_relations
end
app/model/skill.rb
1
2
3
4
5
class Skill < ActiveRecord::Base
  attr_accessible :name
  has_many :character_skill_relations
  has_many :characters, :through => :character_skill_relations
end

ここで、characters には以下のデータが含まれているとする。

id name
1 フェイト・テスタロッサ
2 高町なのは
3 ティアナ・ランスター

Character から Skill を更新:

1
2
3
4
5
6
7
8
9
10
11
nanoha = Character.find 2
# => #<Character id: 2, name: "高町なのは">
nanoha.skills = Skill.find [4, 5, 6]
# => [#<Skill id: 4, name: "スターライトブレイカー">,
#     #<Skill id: 5, name: "ディバインバスター">,
#     #<Skill id: 6, name: "クロスファイアシュート">]

cfs = Skill.find 6
# => #<Skill id: 6, name: "クロスファイアシュート">
cfs.characters
# => [#<Character id: 2, name: "高町なのは">]

Skill から Character を更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tiana = Character.find 3
tiana.skills
# => []

cfs.characters
# => [#<Character id: 2, name: "高町なのは">]
cfs.characters << tiana
cfs.characters
# => [#<Character id: 2, name: "高町なのは">,
#     #<Character id: 3, name: "ティアナ・ランスター">]

tiana = Character.find 3
tiana.skills
# => [#<Skill id: 6, name: "クロスファイアシュート">]

参考サイト