meta_field 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,119 @@
1
+ = MetaField
2
+
3
+ Author:: Yuichi Takeuchi uzuki05@takeyu-web.com
4
+ Website:: http://takeyu-web.com/
5
+ Copyright:: Copyright 2012 Yuichi Takeuchi
6
+ License:: MIT-LICENSE.
7
+
8
+ もっと柔軟にメタデータを付けたい。いちいちカラムを増やしたくない。
9
+
10
+ == Installation
11
+
12
+ # your Gemfile
13
+ gem 'meta_field'
14
+
15
+ === Post Instration
16
+
17
+ $ rails g meta_field:migration
18
+ $ rake db:migrate
19
+
20
+ == Usage
21
+
22
+ === メタ属性の定義
23
+
24
+ モデルクラスで meta_field を宣言します。
25
+
26
+ class Book < ActiveRecord::Base
27
+ meta_field :released_at, :datetime
28
+ meta_attr :page, :integer, index: false # meta_attr は meta_field の別名
29
+ meta_field :note, :text, default: '(none)'
30
+ end
31
+
32
+ STIを使うこともできます。親のメタ属性は子でも使用できます。
33
+
34
+ class Animal < ActiveRecord::Base
35
+ meta_field :name, :string
36
+ end
37
+
38
+ class Dog < Animal
39
+ meta_field :pedigree
40
+ end
41
+
42
+ class Cat < Animal
43
+ end
44
+
45
+ === メタ属性の使用
46
+
47
+ 宣言した属性名のsetter/getterが提供されます。
48
+
49
+ animal.name #=> nil
50
+ animal.name = "Pochi"
51
+ animal.name #=> "Pochi"
52
+ animal.save
53
+ Animal.find(animal.id).name #=> "Pochi"
54
+
55
+ === メタ属性による検索
56
+
57
+ meta_joinスコープを利用できます。
58
+
59
+ meta_joinを使うと、メタ属性を使って検索したり並び替えたりするためのサブクエリがJOINされます。
60
+
61
+ # released_atの降順でソート
62
+ Book.meta_join(:released_at).order('released_at DESC')
63
+
64
+ # priceの昇順、同一の場合はreleased_atの降順でソート
65
+ Book.meta_join(:released_at, :price).order('price ASC, released_at DESC')
66
+
67
+ # priceで検索
68
+ Book.meta_join(:price).where(Arel.sql('price BETWEEN 1000 AND 2000')).order('price ASC')
69
+ # メタ属性でない通常の属性と組み合わせた検索もできます。
70
+ Book.meta_join(:price).where(t.arel_table[:hoge].matches('%HOGE%').and(Arel.sql('price BETWEEN 1000 AND 2000'))).order('price ASC')
71
+
72
+ JOIN対象を少なくするために、JOINの前に検索しておきたい場合は、 .meta_join に、属性名をキーに、検索条件を値とするハッシュを渡します。
73
+
74
+ # released_atが1年前より新しものを検索しからJOINし、価格の昇順でソート
75
+ Book.meta_join(:released_at: Book.meta[:released_at].gt(1.years.ago)).meta_join(:price).order(:price)
76
+ # または、検索条件無し=nilとして
77
+ Book.meta_join(:released_at: Book.meta[:released_at].gt(1.years.ago), price: nil).order(:price)
78
+
79
+ .meta[:released_at] は、メタ属性で検索を行う場合に、そのままではメタ属性の型毎に別々のカラムに入っていたり非常に扱いにくいため、これを緩和するプロキシオブジェクトを返します。
80
+ .meta_join内で、Arelを使った条件組み立てを行う際に、.arel_table[:hoge]と同じ感じで使用します。
81
+
82
+
83
+ == 関連先モデルのメタ属性による検索
84
+
85
+ サブクエリのJOINを使用して下さい。
86
+
87
+ # 21歳から39歳の著者の本を出版日の降順で取り出す
88
+ # 「21歳から39歳の著者」を検索するサブクエリ組み立て
89
+ books_table = Book.arel_table
90
+ selected_authors_table = Author.meta_join(age: Author.meta[:age].gt(20).and(Author.meta[:age].lt(40))).arel.as('selected_authors')
91
+ # JOINして、「出版日の降順」のSQLを組み立てる
92
+ sql = Book.meta_join(:released_at).arel
93
+ .join(selected_authors_table, Arel::Nodes::InnerJoin)
94
+ .on(books_table[:author_id].eq(selected_authors_table[:id]))
95
+ .order('released_at DESC').to_sql
96
+ # 検索実行
97
+ books = Book.find_by_sql(sql)
98
+
99
+
100
+ == インデックスの使用
101
+
102
+ 検索のためのINDEXは、デフォルトで値に対して使用されます。(:text / :binary を除く)
103
+ 検索が不要なメタ属性は index: false とすることで、明示的にインデックスの使用をキャンセルできます。
104
+
105
+
106
+ == 型などの変更
107
+
108
+ 型ごとに別々のカラムにデータが格納されるため、途中から型を変更すると、変更前の値を取得できなくなります。(未設定扱いになります)
109
+
110
+ インデックスを使用する属性と使用しない属性では、利用されるカラムが異なるため、途中からインデックスの使用・不使用を変更した場合も同様です。
111
+
112
+
113
+ == 注意
114
+
115
+ - 仕様変更があっても泣かない。
116
+
117
+ - バグっても泣かない。
118
+
119
+ - 何が起きても責任はとれません。
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'MetaField'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,38 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module MetaField
5
+ class MigrationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ desc "Generates migration for MetaField models"
9
+
10
+ def self.orm
11
+ Rails::Generators.options[:rails][:orm]
12
+ end
13
+
14
+ def self.source_root
15
+ File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) )
16
+ end
17
+
18
+ def self.orm_has_migration?
19
+ [:active_record].include? orm
20
+ end
21
+
22
+ def self.next_migration_number(dirname)
23
+ if ActiveRecord::Base.timestamped_migrations
24
+ migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
25
+ migration_number += 1
26
+ migration_number.to_s
27
+ else
28
+ "%.3d" % (current_migration_number(dirname) + 1)
29
+ end
30
+ end
31
+
32
+ def create_migration_file
33
+ if self.class.orm_has_migration?
34
+ migration_template 'migration.rb', 'db/migrate/meta_field_migration'
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ class MetaFieldMigration < ActiveRecord::Migration
2
+ def change
3
+ create_table :meta_field_metas do |t|
4
+ t.integer :obj_id
5
+ t.string :obj_type
6
+ t.string :basename
7
+
8
+ t.string :string
9
+ t.text :text
10
+ t.integer :integer
11
+ t.float :float
12
+ t.decimal :decimal
13
+ t.datetime :datetime
14
+ t.timestamp :timestamp
15
+ t.time :time
16
+ t.date :date
17
+ t.binary :binary
18
+ t.boolean :boolean
19
+ t.string :indexed_string
20
+ t.integer :indexed_integer
21
+ t.float :indexed_float
22
+ t.decimal :indexed_decimal
23
+ t.datetime :indexed_datetime
24
+ t.timestamp :indexed_timestamp
25
+ t.time :indexed_time
26
+ t.date :indexed_date
27
+ t.boolean :indexed_boolean
28
+ end
29
+ add_index :meta_field_metas, [:obj_id, :obj_type]
30
+ add_index :meta_field_metas, [:obj_id, :obj_type, :basename]
31
+
32
+ add_index :meta_field_metas, :indexed_string
33
+ add_index :meta_field_metas, :indexed_integer
34
+ add_index :meta_field_metas, :indexed_float
35
+ add_index :meta_field_metas, :indexed_decimal
36
+ add_index :meta_field_metas, :indexed_datetime
37
+ add_index :meta_field_metas, :indexed_timestamp
38
+ add_index :meta_field_metas, :indexed_time
39
+ add_index :meta_field_metas, :indexed_date
40
+ add_index :meta_field_metas, :indexed_boolean
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ module MetaField
2
+ autoload :Meta, 'meta_field/meta'
3
+ end
4
+
5
+ require 'meta_field/railtie'
6
+
@@ -0,0 +1,174 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'meta_field/proxy'
3
+
4
+ module MetaField
5
+ module Extension
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class << self
10
+ def inherited_with_meta_field(klass) #:nodoc:
11
+ inherited_without_meta_field klass
12
+ if klass != MetaField::Meta
13
+ klass.class_eval do
14
+ scope :meta_join, ->(*args){ _meta_join(*args) }
15
+ end
16
+ klass.send(:include, MetaField::ModelExtension) if klass.superclass == ActiveRecord::Base
17
+ end
18
+ end
19
+ alias_method_chain :inherited, :meta_field
20
+ end
21
+
22
+ descendants.each do |klass|
23
+ if klass != MetaField::Meta
24
+ klass.class_eval do
25
+ scope :meta_join, ->(*args){ _meta_join(*args) }
26
+ end
27
+ klass.send(:include, MetaField::ModelExtension) if klass.superclass == ActiveRecord::Base
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+
34
+ module ModelExtension
35
+ extend ActiveSupport::Concern
36
+
37
+ included do
38
+ after_save :save_meta_metas
39
+
40
+ has_many :metafield_metas, class_name: 'MetaField::Meta', as: :obj, dependent: :destroy
41
+
42
+ class << self
43
+
44
+ # メタ属性を検索・ソートに使うためにJOIN
45
+ #
46
+ # # released_atの降順でソート
47
+ # Book.meta_join(:released_at).order('released_at DESC')
48
+ #
49
+ # # priceの昇順、同一の場合はreleased_atの降順でソート
50
+ # Book.meta_join(:released_at, :price).order('price ASC, released_at DESC')
51
+ #
52
+ # # released_atが1年前より新しものを検索し、価格の昇順でソート
53
+ # Book.meta_join(:released_at: Book.meta[:released_at].gt(1.years.ago), price: nil).order(:price)
54
+ #
55
+ # scope ではSTIに対応できなかったのでscopeはinheritedで全クラスで定義し、クラスメソッドを呼ぶ形で対処
56
+ # scope :meta_join, ->(*args) do
57
+ def _meta_join(*args)
58
+ basenames = {}
59
+ case args.size
60
+ when 1
61
+ case args[0]
62
+ when Hash
63
+ basenames = args[0].dup
64
+ else
65
+ basenames[args[0]] = nil
66
+ end
67
+ else
68
+ args.each{|arg| basenames[arg] = nil }
69
+ end
70
+ query = scoped
71
+ basenames.each{ |basename, where_cond|
72
+ quoted_basename = connection.quote_string(basename.to_s)
73
+ sub = MetaField::Meta.arel_table
74
+ sub = sub.where(sub[:basename].eq(basename))
75
+ sub = sub.where(where_cond) if where_cond
76
+ sub = sub.project(Arel.sql("#{connection.quote_column_name('obj_id')} as #{ connection.quote_column_name(quoted_basename+'_obj_id') }, #{connection.quote_column_name(meta_fields[basename.to_sym][0])} as #{connection.quote_column_name(quoted_basename)}"))
77
+ query = query.joins("INNER JOIN (#{ sub.to_sql }) ON #{connection.quote_table_name(table_name)}.#{connection.quote_column_name('id')} = #{connection.quote_column_name(quoted_basename+'_obj_id')}")
78
+ }
79
+ query
80
+ end
81
+ private :_meta_join
82
+
83
+ def meta_fields
84
+ return @meta_fields if @meta_fields
85
+ ancestors_meta_fields = self.superclass == ActiveRecord::Base ? {} : self.superclass.meta_fields
86
+ @meta_fields = Marshal.load(Marshal.dump(ancestors_meta_fields))
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ module ClassMethods
93
+ # メタ属性を追加する
94
+ #
95
+ # class Book < ActiveRecord::Base
96
+ # attr_accessible :name, :released_at, :subtitle, :note
97
+ # meta_field :released_at, :datetime
98
+ # meta_field :subtitle, :string
99
+ # meta_field :page, :integer, index: false
100
+ # meta_field :note, :text, default: '特記事項無し'
101
+ # end
102
+ def meta_field(basename, datatype, meta_options = nil)
103
+ meta_options = {index: true}.merge(meta_options || {})
104
+ meta_options[:index] = false if [:text, :binary].include?(datatype.to_sym)
105
+ datatype = "indexed_#{datatype}" if meta_options[:index]
106
+ meta_fields[basename.to_sym] = [datatype.to_sym, meta_options]
107
+ class_eval <<CODE
108
+ def #{basename}_record
109
+ unless @attributes_cache[:#{basename}_record]
110
+ query = metafield_metas.where(basename: #{basename.to_s.inspect})
111
+ unless obj = query.first
112
+ obj = query.build
113
+ obj.#{datatype} = self.class.meta_fields[:#{basename}][1][:default]
114
+ end
115
+ @attributes_cache[:#{basename}_record] = obj
116
+ end
117
+ @attributes_cache[:#{basename}_record]
118
+ end
119
+ private :#{basename}_record
120
+
121
+ def #{basename}
122
+ #{basename}_record.#{datatype}
123
+ end
124
+
125
+ def #{basename}=(value)
126
+ #{basename}_record.#{datatype} = value
127
+ end
128
+ CODE
129
+ end
130
+ alias :meta_attr :meta_field
131
+ alias :meta_column :meta_field
132
+
133
+
134
+ # meta_join でメタ属性での絞り込みを行う際にArelっぽく標記するために使うProxyを得る
135
+ # メタ属性は単純なカラム名で扱うことができないため、Proxyを通して操作する
136
+ #
137
+ # Book.meta_join(:subtitle: Book.meta[:subtitle].matches_any(['%KEYWORD1%', '%KEYWORD2%']))
138
+ # # => SELECT "books".*
139
+ # # FROM "books" INNER JOIN (
140
+ # # SELECT "obj_id" as "note_obj_id", "text" as "note"
141
+ # # FROM "meta_field_metas"
142
+ # # WHERE
143
+ # # "meta_field_metas"."obj_type" = 'Book' AND "meta_field_metas"."basename" = 'note' AND
144
+ # # ("meta_field_metas"."text" LIKE '%HOGE%' OR "meta_field_metas"."text" LIKE '%FUGA%')
145
+ # # ) ON "books"."id" = "note_obj_id"
146
+ #
147
+ #
148
+ # Author.meta_join(:age: Author.meta[:age].gt(20).and(Author.meta[:age].lt(40))).order(:age)
149
+ # # => SELECT "authors".*
150
+ # # FROM "authors" INNER JOIN (
151
+ # # SELECT "obj_id" as "age_obj_id", "integer" as "age"
152
+ # # FROM "meta_field_metas"
153
+ # # WHERE
154
+ # # "meta_field_metas"."obj_type" = 'Author' AND "meta_field_metas"."basename" = 'age AND
155
+ # # "meta_field_metas"."integer" > 20 AND "meta_field_metas"."integer" < 40
156
+ # # ) ON "authors"."id" = "age_obj_id"
157
+ # # ORDER BY age
158
+
159
+ def meta
160
+ MetaField::MetaProxy.new(self)
161
+ end
162
+ end
163
+
164
+ def save_meta_metas
165
+ self.class.meta_fields.keys.each do |basename|
166
+ meta_record = send("#{basename}_record")
167
+ meta_record.obj ||= self
168
+ meta_record.save
169
+ end
170
+ end
171
+ private :save_meta_metas
172
+
173
+ end
174
+ end
@@ -0,0 +1,4 @@
1
+ class MetaField::Meta < ActiveRecord::Base
2
+ self.table_name = :meta_field_metas
3
+ belongs_to :obj, polymorphic: true
4
+ end
@@ -0,0 +1,24 @@
1
+ module MetaField
2
+ class MetaProxy
3
+ def initialize(klass)
4
+ @klass = klass
5
+ @type = klass.base_class.to_s
6
+ end
7
+ def [](basename)
8
+ t = MetaField::Meta.arel_table
9
+ NodeProxy.new(@klass.meta_fields[basename.to_sym][0], t[:obj_type].eq(@type).and(t[:basename].eq(basename.to_s)))
10
+ end
11
+ end
12
+
13
+ class NodeProxy
14
+ def initialize(datatype, obj)
15
+ @datatype = datatype
16
+ @obj = obj
17
+ end
18
+
19
+ def method_missing(action, *args)
20
+ @obj.and(MetaField::Meta.arel_table[@datatype].send(action, *args))
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,10 @@
1
+ module MetaField
2
+ class Railtie < ::Rails::Railtie #:nodoc:
3
+ initializer 'meta_field' do |_app|
4
+ ActiveSupport.on_load(:active_record) do
5
+ require 'meta_field/extension'
6
+ ::ActiveRecord::Base.send :include, MetaField::Extension
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module MetaField
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :meta_field do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: meta_field
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Yuichi Takeuchi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.0
30
+ description: ActiveRecord meta_field for Rails 3
31
+ email:
32
+ - uzuki05@takeyu-web.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/tasks/meta_field_tasks.rake
38
+ - lib/meta_field/railtie.rb
39
+ - lib/meta_field/proxy.rb
40
+ - lib/meta_field/extension.rb
41
+ - lib/meta_field/version.rb
42
+ - lib/meta_field/meta.rb
43
+ - lib/meta_field.rb
44
+ - lib/generators/meta_field/migration/templates/active_record/migration.rb
45
+ - lib/generators/meta_field/migration/migration_generator.rb
46
+ - MIT-LICENSE
47
+ - Rakefile
48
+ - README.rdoc
49
+ homepage: http://takeyu-web.com/
50
+ licenses: []
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.23
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: ActiveRecord meta_field for Rails 3
73
+ test_files: []