active_merge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +26 -0
- data/Rakefile +17 -0
- data/lib/active_merge/service.rb +166 -0
- data/lib/active_merge/version.rb +4 -0
- data/lib/active_merge.rb +26 -0
- metadata +133 -0
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2014 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.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
= ActiveMerge
|
|
2
|
+
|
|
3
|
+
Declares the <tt>ActiveMerge</tt> module for extending ActiveRecord models.
|
|
4
|
+
|
|
5
|
+
The module contains the <tt>merge</tt> class method for merging class instances
|
|
6
|
+
into the first one.
|
|
7
|
+
|
|
8
|
+
When merging a list of instances:
|
|
9
|
+
* all "has_many" relatives are reattached to the instance with the lowest id
|
|
10
|
+
* all the instances except for the first one (with the lowest id) are deleted
|
|
11
|
+
|
|
12
|
+
== Example
|
|
13
|
+
|
|
14
|
+
class Post < ActiveRecord
|
|
15
|
+
extend ActiveMerge
|
|
16
|
+
has_many :comments
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Post.all.merge!
|
|
20
|
+
# This will merge all the posts into the first one.
|
|
21
|
+
# The other posts will be deleted after their comment are reattached
|
|
22
|
+
# to the first post.
|
|
23
|
+
|
|
24
|
+
== License
|
|
25
|
+
|
|
26
|
+
This project rocks and uses link:MIT-LICENSE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require 'rdoc/task'
|
|
8
|
+
|
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
11
|
+
rdoc.title = 'ActiveMerge'
|
|
12
|
+
rdoc.options << '--line-numbers'
|
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Bundler::GemHelper.install_tasks
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
module ActiveMerge
|
|
3
|
+
|
|
4
|
+
# Сервисный объект (паттерн Service Object), отвечающий за объединение
|
|
5
|
+
# всех записей ActiveRecord, переданных в аргументе.
|
|
6
|
+
#
|
|
7
|
+
# При инициализации указывается ActiveRecord::Association или массив объектов.
|
|
8
|
+
# Из массива выбираются только сохраненные объекты ActiveRecord, причем все
|
|
9
|
+
# они должны быть объектами одного класса.
|
|
10
|
+
#
|
|
11
|
+
# При объединении:
|
|
12
|
+
# * Сохраняется первый объект (с наименьшим id)
|
|
13
|
+
# * У всех прочих объектов ассоциации has_many перепривязываются к первому объекту
|
|
14
|
+
# * После перепривязки все объекты, кроме первого, удаляются.
|
|
15
|
+
#
|
|
16
|
+
# В результате остается только один объект, к которому привязаны все подобъекты,
|
|
17
|
+
# ранее относившиеся к объединяемым записям.
|
|
18
|
+
#
|
|
19
|
+
# == Пример:
|
|
20
|
+
#
|
|
21
|
+
# # Рассмотрим модель королевства, имеющего много графств и жителей
|
|
22
|
+
# class Kingdom < ActiveRecord::Base
|
|
23
|
+
# has_many :shires
|
|
24
|
+
# has_many :men
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# class Shire < ActiveRecord::Base
|
|
28
|
+
# belongs_to :kingdom
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# class Man < ActiveRecord::Base
|
|
32
|
+
# belongs_to :kingdom
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # У Британии есть 100 тыс. жителей и 10 графств
|
|
36
|
+
# britain = Kingdom.create!
|
|
37
|
+
# 1000000.times.each{ britain.men.create! }
|
|
38
|
+
# 10.times.each{ britain.shires.create! }
|
|
39
|
+
#
|
|
40
|
+
# # У Шотландии есть 30 тыс. жителей и 5 графств
|
|
41
|
+
# scotland = Kingdom.create!
|
|
42
|
+
# 300000.times.each{ britain.men.create! }
|
|
43
|
+
# 5.times.each{ britain.shires.create! }
|
|
44
|
+
#
|
|
45
|
+
# # Объединяем королевства
|
|
46
|
+
# Service.new(Kingdom.all).submit!
|
|
47
|
+
#
|
|
48
|
+
# # К Британии (добавлена ранее) присоединены шотландские люди и графства
|
|
49
|
+
# britain.reload.men.count # => 130000
|
|
50
|
+
# britain.reload.shires.count # => 15
|
|
51
|
+
#
|
|
52
|
+
# # Королевство Шотландия удалено
|
|
53
|
+
# Kingdom.find_by(scotland.id) # => nil
|
|
54
|
+
#
|
|
55
|
+
# == Обратите внимание!
|
|
56
|
+
#
|
|
57
|
+
# Объединение происходит единой транзакцией. При любой ошибке все изменения
|
|
58
|
+
# отменяются.
|
|
59
|
+
#
|
|
60
|
+
# Если в примере выше графство не может быть перепривязано к новому
|
|
61
|
+
# королевству (незыблемость суверенитета):
|
|
62
|
+
#
|
|
63
|
+
# class Shire
|
|
64
|
+
# attr_readonly :kingdom_id
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# то объединения не произойдет!
|
|
68
|
+
#
|
|
69
|
+
class Service
|
|
70
|
+
|
|
71
|
+
def initialize(list = [])
|
|
72
|
+
list = _extract_from list
|
|
73
|
+
@item, @items = list.first, Array(list[1..-1])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attr_reader :item, :items, :klass, :klasses
|
|
77
|
+
|
|
78
|
+
# Возвращает класс объединяемых объектов
|
|
79
|
+
def klass
|
|
80
|
+
@klass ||= item.class
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Возвращает хэш, в котором ключами выступают связи has_many,
|
|
84
|
+
# а значениями - foreign keys
|
|
85
|
+
def klasses
|
|
86
|
+
@klasses ||= klass.reflect_on_all_associations(:has_many).
|
|
87
|
+
inject({}){ |hash, item| hash.merge(item.name => item.foreign_key) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Объединяет все записи из переменной #items с записью в переменной #item
|
|
91
|
+
#
|
|
92
|
+
# При этом:
|
|
93
|
+
# * все ссылки на записи из массива #items (связь has_many) перепривязываются к #item
|
|
94
|
+
# * все записи #items удаляются
|
|
95
|
+
#
|
|
96
|
+
# При любой ошибке все сделанные изменения отменяются.
|
|
97
|
+
#
|
|
98
|
+
def provide
|
|
99
|
+
ActiveRecord::Base.transaction requires_new: true do
|
|
100
|
+
_raise :blank if items.blank?
|
|
101
|
+
items.each{ |item| _merge_one(item) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Вызывает исключение указанного типа с переданными парамертами
|
|
108
|
+
def _raise(type, item = nil)
|
|
109
|
+
options = item ? { type: item.class.name.underscore, id: item.id } : {}
|
|
110
|
+
options.merge! scope: %w(active_merge errors service)
|
|
111
|
+
raise I18n.t(type, options)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Извлекает из списка только сохраненные объекты ActiveRecord
|
|
115
|
+
# Если список содержит элементы ActiveRecord разных классов, возвращает
|
|
116
|
+
# пустой массив.
|
|
117
|
+
def _extract_from(list)
|
|
118
|
+
begin
|
|
119
|
+
list = list.select{ |item| _valid? item }
|
|
120
|
+
klass = list.first.class
|
|
121
|
+
list.each { |item| raise unless item.class == klass }
|
|
122
|
+
list.sort_by { |item| item.id }
|
|
123
|
+
rescue
|
|
124
|
+
[]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Проверяет, что объект является сохраненным объектом ActiveRecord
|
|
129
|
+
def _valid?(item)
|
|
130
|
+
begin
|
|
131
|
+
item.class.ancestors.include?(ActiveRecord::Base) && item.persisted?
|
|
132
|
+
rescue
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Объединяет указанный объект с объектом из переменной #item
|
|
138
|
+
# * Перепривязывает ссылки на объект #item
|
|
139
|
+
# * Удаляет объект, переданный в аргументе
|
|
140
|
+
def _merge_one(item)
|
|
141
|
+
klasses.each do |list, foreign_key|
|
|
142
|
+
item.send(list).each { |ref| _rebind! ref, foreign_key }
|
|
143
|
+
end
|
|
144
|
+
_destroy! item
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Перепривязывает запись к первому объединяемому объекту
|
|
148
|
+
def _rebind!(item, foreign_key)
|
|
149
|
+
begin
|
|
150
|
+
item.send "#{ foreign_key }=", self.item.id
|
|
151
|
+
item.save!
|
|
152
|
+
rescue
|
|
153
|
+
_raise :rebind, item
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Удаляет запись, сохраняя ошибки
|
|
158
|
+
def _destroy!(item)
|
|
159
|
+
begin
|
|
160
|
+
item.destroy!
|
|
161
|
+
rescue
|
|
162
|
+
_raise :destroy, item
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/active_merge.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
# Модуль содержит методы объединения записей ActiveRecord
|
|
4
|
+
#
|
|
5
|
+
# После расширения класса, унаследованного от <tt>ActiveRecord::Base</tt>
|
|
6
|
+
# становится доступен метод класса <tt>::merge_all</tt>, объединяющий записи.
|
|
7
|
+
#
|
|
8
|
+
module ActiveMerge
|
|
9
|
+
extend ActiveSupport::Autoload
|
|
10
|
+
autoload :Service
|
|
11
|
+
|
|
12
|
+
# Объединение указанных записей.
|
|
13
|
+
#
|
|
14
|
+
# class Lord < ActiveRecord::Base
|
|
15
|
+
# extend ActiveMerge
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Lord.all.merge_all # => объединяет все записи
|
|
19
|
+
# Lord.where(id > 100) # => объединяет все записи с id > 100
|
|
20
|
+
#
|
|
21
|
+
# Детали см. в описании метода <tt>ActiveMerge::Service#provide</tt>
|
|
22
|
+
#
|
|
23
|
+
def merge_all
|
|
24
|
+
ActiveMerge::Service.new(self).provide
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: active_merge
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Andrew Kozin
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2014-03-08 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: 4.0.3
|
|
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: 4.0.3
|
|
30
|
+
- !ruby/object:Gem::Dependency
|
|
31
|
+
name: sqlite3
|
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
|
33
|
+
none: false
|
|
34
|
+
requirements:
|
|
35
|
+
- - ! '>='
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '0'
|
|
38
|
+
type: :development
|
|
39
|
+
prerelease: false
|
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ! '>='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rspec
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
none: false
|
|
50
|
+
requirements:
|
|
51
|
+
- - ! '>='
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
none: false
|
|
58
|
+
requirements:
|
|
59
|
+
- - ! '>='
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
- !ruby/object:Gem::Dependency
|
|
63
|
+
name: yard
|
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
|
65
|
+
none: false
|
|
66
|
+
requirements:
|
|
67
|
+
- - ! '>='
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
type: :development
|
|
71
|
+
prerelease: false
|
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
73
|
+
none: false
|
|
74
|
+
requirements:
|
|
75
|
+
- - ! '>='
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
- !ruby/object:Gem::Dependency
|
|
79
|
+
name: database_cleaner
|
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
|
81
|
+
none: false
|
|
82
|
+
requirements:
|
|
83
|
+
- - ! '>='
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
type: :development
|
|
87
|
+
prerelease: false
|
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
89
|
+
none: false
|
|
90
|
+
requirements:
|
|
91
|
+
- - ! '>='
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
description: Declares the ActiveMerge module with the 'merge!' class method.
|
|
95
|
+
email:
|
|
96
|
+
- andrew.kozin@gmail.com
|
|
97
|
+
executables: []
|
|
98
|
+
extensions: []
|
|
99
|
+
extra_rdoc_files: []
|
|
100
|
+
files:
|
|
101
|
+
- lib/active_merge/service.rb
|
|
102
|
+
- lib/active_merge/version.rb
|
|
103
|
+
- lib/active_merge.rb
|
|
104
|
+
- MIT-LICENSE
|
|
105
|
+
- Rakefile
|
|
106
|
+
- README.rdoc
|
|
107
|
+
homepage: https://github.com/nepalez/active_merge
|
|
108
|
+
licenses:
|
|
109
|
+
- MIT
|
|
110
|
+
post_install_message:
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
none: false
|
|
116
|
+
requirements:
|
|
117
|
+
- - ! '>='
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
|
+
none: false
|
|
122
|
+
requirements:
|
|
123
|
+
- - ! '>='
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
requirements: []
|
|
127
|
+
rubyforge_project:
|
|
128
|
+
rubygems_version: 1.8.24
|
|
129
|
+
signing_key:
|
|
130
|
+
specification_version: 3
|
|
131
|
+
summary: Merges instances of an ActiveRecord class.
|
|
132
|
+
test_files: []
|
|
133
|
+
has_rdoc:
|