rails_sync 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +165 -0
- data/README.md +34 -0
- data/Rakefile +32 -0
- data/app/assets/config/rails_sync_manifest.js +0 -0
- data/app/controllers/rails_sync_admin/base_controller.rb +4 -0
- data/app/controllers/rails_sync_admin/sync_audits_controller.rb +40 -0
- data/app/jobs/audit_apply_job.rb +9 -0
- data/app/models/mysql/mysql_column.rb +5 -0
- data/app/models/mysql/mysql_engine.rb +5 -0
- data/app/models/mysql/mysql_index.rb +5 -0
- data/app/models/mysql/mysql_server.rb +5 -0
- data/app/models/mysql/mysql_table.rb +5 -0
- data/app/models/sync_audit.rb +89 -0
- data/app/views/rails_sync_admin/sync_audits/_filter.html.erb +8 -0
- data/app/views/rails_sync_admin/sync_audits/_form.html.erb +4 -0
- data/app/views/rails_sync_admin/sync_audits/_search_form.html.erb +35 -0
- data/app/views/rails_sync_admin/sync_audits/edit.html.erb +9 -0
- data/app/views/rails_sync_admin/sync_audits/index.html.erb +57 -0
- data/app/views/rails_sync_admin/sync_audits/new.html.erb +9 -0
- data/app/views/rails_sync_admin/sync_audits/show.html.erb +0 -0
- data/app/views/rails_sync_admin/sync_audits/synchros.html.erb +41 -0
- data/config/locales/en.yml +21 -0
- data/config/locales/zh.yml +8 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20180319031059_create_sync_audits.rb +16 -0
- data/lib/rails_sync.rb +16 -0
- data/lib/rails_sync/active_record.rb +87 -0
- data/lib/rails_sync/adapter.rb +46 -0
- data/lib/rails_sync/analyzer.rb +119 -0
- data/lib/rails_sync/config.rb +10 -0
- data/lib/rails_sync/engine.rb +7 -0
- data/lib/rails_sync/table.rb +110 -0
- data/lib/rails_sync/version.rb +3 -0
- data/lib/tasks/the_sync_tasks.rake +4 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 690c52679acf76eca8c80da03e893888a4937ce7ac85341fdd0f895f4e07f1a9
|
|
4
|
+
data.tar.gz: 163e59a515c618404bc010ec61dbac2832a0e71b3a23ab86f49fe3d62ac2d06f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f5ac64f117442021284f15446c2f710e85f2cf66258758e917c8d4f3df078a89160f932e7bd94e1ba1207ad64d1769eaf3b80b6cdbc883749c0ab100046cdfde
|
|
7
|
+
data.tar.gz: 8a6bc5e78d1092dfdcb5a9fb26b09396100bff48c98f59fae42283bbc1cc0723ec9ad5b3f5873dd2f3c2673b8829d0bf14ccdd9dbd6ff4333de5e59083d6f8dd
|
data/LICENSE
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
GNU LESSER GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2018 Mingyuan Qin.
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
This version of the GNU Lesser General Public License incorporates
|
|
10
|
+
the terms and conditions of version 3 of the GNU General Public
|
|
11
|
+
License, supplemented by the additional permissions listed below.
|
|
12
|
+
|
|
13
|
+
0. Additional Definitions.
|
|
14
|
+
|
|
15
|
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
|
16
|
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
|
17
|
+
General Public License.
|
|
18
|
+
|
|
19
|
+
"The Library" refers to a covered work governed by this License,
|
|
20
|
+
other than an Application or a Combined Work as defined below.
|
|
21
|
+
|
|
22
|
+
An "Application" is any work that makes use of an interface provided
|
|
23
|
+
by the Library, but which is not otherwise based on the Library.
|
|
24
|
+
Defining a subclass of a class defined by the Library is deemed a mode
|
|
25
|
+
of using an interface provided by the Library.
|
|
26
|
+
|
|
27
|
+
A "Combined Work" is a work produced by combining or linking an
|
|
28
|
+
Application with the Library. The particular version of the Library
|
|
29
|
+
with which the Combined Work was made is also called the "Linked
|
|
30
|
+
Version".
|
|
31
|
+
|
|
32
|
+
The "Minimal Corresponding Source" for a Combined Work means the
|
|
33
|
+
Corresponding Source for the Combined Work, excluding any source code
|
|
34
|
+
for portions of the Combined Work that, considered in isolation, are
|
|
35
|
+
based on the Application, and not on the Linked Version.
|
|
36
|
+
|
|
37
|
+
The "Corresponding Application Code" for a Combined Work means the
|
|
38
|
+
object code and/or source code for the Application, including any data
|
|
39
|
+
and utility programs needed for reproducing the Combined Work from the
|
|
40
|
+
Application, but excluding the System Libraries of the Combined Work.
|
|
41
|
+
|
|
42
|
+
1. Exception to Section 3 of the GNU GPL.
|
|
43
|
+
|
|
44
|
+
You may convey a covered work under sections 3 and 4 of this License
|
|
45
|
+
without being bound by section 3 of the GNU GPL.
|
|
46
|
+
|
|
47
|
+
2. Conveying Modified Versions.
|
|
48
|
+
|
|
49
|
+
If you modify a copy of the Library, and, in your modifications, a
|
|
50
|
+
facility refers to a function or data to be supplied by an Application
|
|
51
|
+
that uses the facility (other than as an argument passed when the
|
|
52
|
+
facility is invoked), then you may convey a copy of the modified
|
|
53
|
+
version:
|
|
54
|
+
|
|
55
|
+
a) under this License, provided that you make a good faith effort to
|
|
56
|
+
ensure that, in the event an Application does not supply the
|
|
57
|
+
function or data, the facility still operates, and performs
|
|
58
|
+
whatever part of its purpose remains meaningful, or
|
|
59
|
+
|
|
60
|
+
b) under the GNU GPL, with none of the additional permissions of
|
|
61
|
+
this License applicable to that copy.
|
|
62
|
+
|
|
63
|
+
3. Object Code Incorporating Material from Library Header Files.
|
|
64
|
+
|
|
65
|
+
The object code form of an Application may incorporate material from
|
|
66
|
+
a header file that is part of the Library. You may convey such object
|
|
67
|
+
code under terms of your choice, provided that, if the incorporated
|
|
68
|
+
material is not limited to numerical parameters, data structure
|
|
69
|
+
layouts and accessors, or small macros, inline functions and templates
|
|
70
|
+
(ten or fewer lines in length), you do both of the following:
|
|
71
|
+
|
|
72
|
+
a) Give prominent notice with each copy of the object code that the
|
|
73
|
+
Library is used in it and that the Library and its use are
|
|
74
|
+
covered by this License.
|
|
75
|
+
|
|
76
|
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
|
77
|
+
document.
|
|
78
|
+
|
|
79
|
+
4. Combined Works.
|
|
80
|
+
|
|
81
|
+
You may convey a Combined Work under terms of your choice that,
|
|
82
|
+
taken together, effectively do not restrict modification of the
|
|
83
|
+
portions of the Library contained in the Combined Work and reverse
|
|
84
|
+
engineering for debugging such modifications, if you also do each of
|
|
85
|
+
the following:
|
|
86
|
+
|
|
87
|
+
a) Give prominent notice with each copy of the Combined Work that
|
|
88
|
+
the Library is used in it and that the Library and its use are
|
|
89
|
+
covered by this License.
|
|
90
|
+
|
|
91
|
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
|
92
|
+
document.
|
|
93
|
+
|
|
94
|
+
c) For a Combined Work that displays copyright notices during
|
|
95
|
+
execution, include the copyright notice for the Library among
|
|
96
|
+
these notices, as well as a reference directing the user to the
|
|
97
|
+
copies of the GNU GPL and this license document.
|
|
98
|
+
|
|
99
|
+
d) Do one of the following:
|
|
100
|
+
|
|
101
|
+
0) Convey the Minimal Corresponding Source under the terms of this
|
|
102
|
+
License, and the Corresponding Application Code in a form
|
|
103
|
+
suitable for, and under terms that permit, the user to
|
|
104
|
+
recombine or relink the Application with a modified version of
|
|
105
|
+
the Linked Version to produce a modified Combined Work, in the
|
|
106
|
+
manner specified by section 6 of the GNU GPL for conveying
|
|
107
|
+
Corresponding Source.
|
|
108
|
+
|
|
109
|
+
1) Use a suitable shared library mechanism for linking with the
|
|
110
|
+
Library. A suitable mechanism is one that (a) uses at run time
|
|
111
|
+
a copy of the Library already present on the user's computer
|
|
112
|
+
system, and (b) will operate properly with a modified version
|
|
113
|
+
of the Library that is interface-compatible with the Linked
|
|
114
|
+
Version.
|
|
115
|
+
|
|
116
|
+
e) Provide Installation Information, but only if you would otherwise
|
|
117
|
+
be required to provide such information under section 6 of the
|
|
118
|
+
GNU GPL, and only to the extent that such information is
|
|
119
|
+
necessary to install and execute a modified version of the
|
|
120
|
+
Combined Work produced by recombining or relinking the
|
|
121
|
+
Application with a modified version of the Linked Version. (If
|
|
122
|
+
you use option 4d0, the Installation Information must accompany
|
|
123
|
+
the Minimal Corresponding Source and Corresponding Application
|
|
124
|
+
Code. If you use option 4d1, you must provide the Installation
|
|
125
|
+
Information in the manner specified by section 6 of the GNU GPL
|
|
126
|
+
for conveying Corresponding Source.)
|
|
127
|
+
|
|
128
|
+
5. Combined Libraries.
|
|
129
|
+
|
|
130
|
+
You may place library facilities that are a work based on the
|
|
131
|
+
Library side by side in a single library together with other library
|
|
132
|
+
facilities that are not Applications and are not covered by this
|
|
133
|
+
License, and convey such a combined library under terms of your
|
|
134
|
+
choice, if you do both of the following:
|
|
135
|
+
|
|
136
|
+
a) Accompany the combined library with a copy of the same work based
|
|
137
|
+
on the Library, uncombined with any other library facilities,
|
|
138
|
+
conveyed under the terms of this License.
|
|
139
|
+
|
|
140
|
+
b) Give prominent notice with the combined library that part of it
|
|
141
|
+
is a work based on the Library, and explaining where to find the
|
|
142
|
+
accompanying uncombined form of the same work.
|
|
143
|
+
|
|
144
|
+
6. Revised Versions of the GNU Lesser General Public License.
|
|
145
|
+
|
|
146
|
+
The Free Software Foundation may publish revised and/or new versions
|
|
147
|
+
of the GNU Lesser General Public License from time to time. Such new
|
|
148
|
+
versions will be similar in spirit to the present version, but may
|
|
149
|
+
differ in detail to address new problems or concerns.
|
|
150
|
+
|
|
151
|
+
Each version is given a distinguishing version number. If the
|
|
152
|
+
Library as you received it specifies that a certain numbered version
|
|
153
|
+
of the GNU Lesser General Public License "or any later version"
|
|
154
|
+
applies to it, you have the option of following the terms and
|
|
155
|
+
conditions either of that published version or of any later version
|
|
156
|
+
published by the Free Software Foundation. If the Library as you
|
|
157
|
+
received it does not specify a version number of the GNU Lesser
|
|
158
|
+
General Public License, you may choose any version of the GNU Lesser
|
|
159
|
+
General Public License ever published by the Free Software Foundation.
|
|
160
|
+
|
|
161
|
+
If the Library as you received it specifies that a proxy can decide
|
|
162
|
+
whether future versions of the GNU Lesser General Public License shall
|
|
163
|
+
apply, that proxy's public statement of acceptance of any version is
|
|
164
|
+
permanent authorization for you to choose that version for the
|
|
165
|
+
Library.
|
data/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# RailsSync
|
|
2
|
+
|
|
3
|
+
* [English](./README.en.md)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## 适用场景
|
|
7
|
+
1. 应用之间同步数据;
|
|
8
|
+
2. 新老应用间的数据迁移;
|
|
9
|
+
2. 两个 `table` 字段名称可能不同;
|
|
10
|
+
3. 两个 `table` 基于唯一的id进行同步数据,id可能是主键,也可能不是;
|
|
11
|
+
|
|
12
|
+
## 注意
|
|
13
|
+
1. 如果同一个表,两边应用都对其同一个字段有内容修改,业务上会比较混乱,建议避免;
|
|
14
|
+
2. 由于是直接数据库层面同步数据,不容易在目标应用内直接触发业务逻辑,可在本应用内基于Audit触发业务逻辑;
|
|
15
|
+
|
|
16
|
+
## 特性
|
|
17
|
+
1. 绝对靠谱,不依赖binlog,基于主键(支持联合主键)对两个表的各个字段进行比较,找出字段值的变化,新增的记录,删除的记录;
|
|
18
|
+
2. 高性能,10万以内的计算毫秒级;
|
|
19
|
+
3. 兼容性强,可支持各个关系型数据库(依赖 linked DB, mysql FEDERATED引擎等);
|
|
20
|
+
4. 安全,对数据的改变先记录而不应用改变,可基于记录人工决定是否应用该改变,或者revert相应的改动;
|
|
21
|
+
5. 实时性强,可以通过应用里的数据改变动作触发;
|
|
22
|
+
6. 可以同步多个来源的数据;
|
|
23
|
+
|
|
24
|
+
## 实现原理
|
|
25
|
+
1. 处理表;
|
|
26
|
+
a. 对于不同节点,建立临时表,映射远程数据库节点中的对应表(只映射需要比较改变值的字段),在mysql中采用`FEDERATED`引擎;
|
|
27
|
+
2. 通过 inner_join 计算两张表不一样的字段;
|
|
28
|
+
3. 通过 left_join 找出左表多出的记录;
|
|
29
|
+
4. 通过 right_join 找出右表多出的记录;
|
|
30
|
+
|
|
31
|
+
## Remote Table
|
|
32
|
+
* postgresql: [dblink](https://www.postgresql.org/docs/10/static/dblink.html)
|
|
33
|
+
* mysql: [FEDERATED](https://dev.mysql.com/doc/refman/5.7/en/federated-storage-engine.html)
|
|
34
|
+
* mariadb: [CONNECT](https://mariadb.com/kb/en/library/connect/)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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 = 'RailsSync'
|
|
12
|
+
rdoc.options << '--line-numbers'
|
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
18
|
+
load 'rails/tasks/engine.rake'
|
|
19
|
+
|
|
20
|
+
load 'rails/tasks/statistics.rake'
|
|
21
|
+
|
|
22
|
+
require 'bundler/gem_tasks'
|
|
23
|
+
|
|
24
|
+
require 'rake/testtask'
|
|
25
|
+
|
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
|
27
|
+
t.libs << 'test'
|
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
|
29
|
+
t.verbose = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
task default: :test
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class RailsSyncAdmin::SyncAuditsController < RailsSyncAdmin::BaseController
|
|
2
|
+
before_action :set_sync_audit, only: [:show, :apply, :destroy]
|
|
3
|
+
|
|
4
|
+
def index
|
|
5
|
+
q_params = params.permit(:synchro_type, :state, :operation)
|
|
6
|
+
@sync_audits = SyncAudit.default_where(q_params).order(id: :desc).page(params[:page])
|
|
7
|
+
@synchro_types = RailsSync.synchro_types.uniq | SyncAudit.synchro_types
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sync
|
|
11
|
+
@synchro_model = params[:synchro_type].constantize
|
|
12
|
+
@synchro_model.cache_all_diffs
|
|
13
|
+
|
|
14
|
+
redirect_to admin_sync_audits_url, notice: 'Sync Run successfully '
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def batch
|
|
18
|
+
AuditApplyJob.perform_later params[:synchro_type]
|
|
19
|
+
redirect_to admin_sync_audits_url, notice: 'Batch Apply Sync Run successfully in backend'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def apply
|
|
26
|
+
@sync_audit.apply_changes
|
|
27
|
+
redirect_to admin_sync_audits_url, notice: 'Sync audit was successfully applied.'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def destroy
|
|
31
|
+
@sync_audit.destroy
|
|
32
|
+
redirect_to admin_sync_audits_url, notice: 'Sync audit was successfully destroyed.'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
def set_sync_audit
|
|
37
|
+
@sync_audit = SyncAudit.find(params[:id])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AuditApplyJob < ActiveJob::Base
|
|
2
|
+
queue_as :default
|
|
3
|
+
|
|
4
|
+
def perform(synchro_type, operation: ['update', 'insert', 'delete'])
|
|
5
|
+
SyncAudit.apply_synchro(synchro_type, operation: operation)
|
|
6
|
+
SyncAudit.apply_callback(synchro_type, operation: operation)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
class SyncAudit < ApplicationRecord
|
|
2
|
+
serialize :audited_changes, Hash
|
|
3
|
+
serialize :synchro_params, Hash
|
|
4
|
+
|
|
5
|
+
enum state: {
|
|
6
|
+
init: 'init',
|
|
7
|
+
applied: 'applied',
|
|
8
|
+
finished: 'finished'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
enum operation: {
|
|
12
|
+
update: 'update',
|
|
13
|
+
delete: 'delete',
|
|
14
|
+
insert: 'insert'
|
|
15
|
+
}, _prefix: true
|
|
16
|
+
|
|
17
|
+
belongs_to :synchro, polymorphic: true, optional: true
|
|
18
|
+
belongs_to :operator, polymorphic: true, optional: true
|
|
19
|
+
|
|
20
|
+
after_initialize if: :new_record? do
|
|
21
|
+
self.state = 'init'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def apply_changes
|
|
25
|
+
if self.operation_update?
|
|
26
|
+
_synchro = self.synchro || synchro_model.find_by(synchro_params)
|
|
27
|
+
_synchro.assign_attributes to_apply_params
|
|
28
|
+
self.state = 'applied'
|
|
29
|
+
self.synchro_id = _synchro.id
|
|
30
|
+
self.class.transaction do
|
|
31
|
+
_synchro.save!
|
|
32
|
+
self.save!
|
|
33
|
+
end
|
|
34
|
+
elsif self.operation_delete? && self.synchro
|
|
35
|
+
self.class.transaction do
|
|
36
|
+
self.synchro.destroy!
|
|
37
|
+
self.update! state: 'applied'
|
|
38
|
+
end
|
|
39
|
+
elsif self.operation_insert?
|
|
40
|
+
_synchro = synchro_model.find_or_initialize_by(synchro_params)
|
|
41
|
+
_synchro.assign_attributes to_apply_params
|
|
42
|
+
self.class.transaction do
|
|
43
|
+
_synchro.save_sneakily!
|
|
44
|
+
self.update! synchro_id: _synchro.id, state: 'applied'
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def synchro_model
|
|
50
|
+
@synchro_model ||= self.synchro_type.constantize
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_apply_params
|
|
54
|
+
x = {}
|
|
55
|
+
audited_changes.each do |key, v|
|
|
56
|
+
if synchro_model.columns_hash[key].type == :string
|
|
57
|
+
x[key] = v[1].to_s
|
|
58
|
+
else
|
|
59
|
+
x[key] = v[1]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
x
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.synchro_types
|
|
67
|
+
SyncAudit.select(:synchro_type).distinct.pluck(:synchro_type).compact
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.apply_callback(type, operation: ['update', 'delete', 'insert'])
|
|
71
|
+
SyncAudit.where(state: :applied, synchro_type: type, operation: operation).find_each do |sync|
|
|
72
|
+
SyncAudit.transaction do
|
|
73
|
+
sync.synchro.respond_to?(:after_sync) && sync.synchro.after_sync
|
|
74
|
+
sync.update! state: 'finished'
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.apply_synchro(type, operation: ['update', 'delete', 'insert'])
|
|
80
|
+
SyncAudit.where(state: 'init', synchro_type: type, operation: operation).find_each do |sync_audit|
|
|
81
|
+
begin
|
|
82
|
+
sync_audit.apply_changes
|
|
83
|
+
rescue SystemStackError, ActiveRecord::ActiveRecordError => e
|
|
84
|
+
logger.warn e.message
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="ui top attached segment">
|
|
2
|
+
<% if params[:synchro_type] %>
|
|
3
|
+
<%= link_to 'Sync', sync_admin_sync_audits_path(synchro_type: params[:synchro_type]), method: :post, class: 'ui blue button' %>
|
|
4
|
+
<%= link_to 'Batch Apply', batch_admin_sync_audits_path(synchro_type: params[:synchro_type]), method: :post, class: 'ui yellow button' %>
|
|
5
|
+
<% end %>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<%= search_form_with do |f| %>
|
|
2
|
+
<div class="fields">
|
|
3
|
+
<%= f.submit 'Search' %>
|
|
4
|
+
</div>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<dl>
|
|
8
|
+
<dt>Synchro Type:</dt>
|
|
9
|
+
<dd>
|
|
10
|
+
<%= link_to 'All', filter_params(except: [:synchro_type]), class: active_params(synchro_type: '', active_class: 'ui basic blue button', item_class: 'ui basic white button') %>
|
|
11
|
+
<% @synchro_types.each do |synchro_type| %>
|
|
12
|
+
<%= link_to SyncAudit.enum_i18n(:synchro_type, synchro_type), filter_params(synchro_type: synchro_type), class: active_params(synchro_type: synchro_type, active_class: 'ui basic blue button', item_class: 'ui basic white button') %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</dd>
|
|
15
|
+
</dl>
|
|
16
|
+
|
|
17
|
+
<dl>
|
|
18
|
+
<dt>State:</dt>
|
|
19
|
+
<dd>
|
|
20
|
+
<%= link_to 'All', filter_params(except: [:state]), class: active_params(state: '', active_class: 'ui basic blue button', item_class: 'ui basic white button') %>
|
|
21
|
+
<% ['init', 'applied', 'finished'].each do |state| %>
|
|
22
|
+
<%= link_to SyncAudit.enum_i18n(:state, state), filter_params(state: state), class: active_params(state: state, active_class: 'ui basic blue button', item_class: 'ui basic white button') %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</dd>
|
|
25
|
+
</dl>
|
|
26
|
+
|
|
27
|
+
<dl>
|
|
28
|
+
<dt>Action:</dt>
|
|
29
|
+
<dd>
|
|
30
|
+
<%= link_to 'All', filter_params(except: [:operation]), class: active_params(operation: '', active_class: 'ui basic blue button', item_class: 'ui basic white button') %>
|
|
31
|
+
<% ['update', 'insert', 'delete'].each do |operation| %>
|
|
32
|
+
<%= link_to SyncAudit.enum_i18n(:operation, operation), filter_params(operation: operation), class: active_params(operation: operation, active_class: 'ui basic blue button', item_class: 'ui basic white button') %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</dd>
|
|
35
|
+
</dl>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<div class="ui top attached borderless menu">
|
|
2
|
+
<div class="header item">Sync Audits</div>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="ui attached segment">
|
|
6
|
+
<%= render 'search_form' %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<%= render 'filter' %>
|
|
10
|
+
<table class="ui bottom attached fixed table">
|
|
11
|
+
<thead>
|
|
12
|
+
<tr>
|
|
13
|
+
<th class="two wide"><%= SyncAudit.human_attribute_name(:id) %></th>
|
|
14
|
+
<th class="two wide">
|
|
15
|
+
<p><%= SyncAudit.human_attribute_name(:synchro_type) %></p>
|
|
16
|
+
<p><%= SyncAudit.human_attribute_name(:synchro_id) %></p>
|
|
17
|
+
</th>
|
|
18
|
+
<th class="two wide">
|
|
19
|
+
<%= SyncAudit.human_attribute_name(:synchro_params) %>
|
|
20
|
+
</th>
|
|
21
|
+
<th class="one wide"><%= SyncAudit.human_attribute_name(:action) %></th>
|
|
22
|
+
<th class="seven wide"><%= SyncAudit.human_attribute_name(:audited_changes) %></th>
|
|
23
|
+
<th class="two wide">
|
|
24
|
+
<%= SyncAudit.human_attribute_name(:state) %>
|
|
25
|
+
</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
|
|
29
|
+
<tbody>
|
|
30
|
+
<% @sync_audits.each do |sync_audit| %>
|
|
31
|
+
<tr>
|
|
32
|
+
<td><%= sync_audit.id %></td>
|
|
33
|
+
<td>
|
|
34
|
+
<p><%= sync_audit.synchro_type %></p>
|
|
35
|
+
<p><%= sync_audit.synchro_id %></p>
|
|
36
|
+
</td>
|
|
37
|
+
<td><%= simple_format(sync_audit.synchro_params) %></td>
|
|
38
|
+
<td><%= sync_audit.operation_i18n %></td>
|
|
39
|
+
<td><%= simple_format_hash sync_audit.audited_changes %></td>
|
|
40
|
+
<td class="ui labels">
|
|
41
|
+
<span class="ui label"><%= sync_audit.state_i18n %></span>
|
|
42
|
+
<% unless sync_audit.applied? %>
|
|
43
|
+
<%= link_to 'apply', apply_admin_sync_audit_path(sync_audit), method: :patch, data: {confirm: 'Are you sure?'}, class: 'ui yellow label' %>
|
|
44
|
+
<% end %>
|
|
45
|
+
<%= link_to admin_sync_audit_path(sync_audit), class: 'ui mini blue icon button' do %>
|
|
46
|
+
<i class="pencil alternate icon"></i>
|
|
47
|
+
<% end %>
|
|
48
|
+
<%= link_to admin_sync_audit_path(sync_audit), method: :delete, data: {confirm: 'Are you sure?'}, class: 'ui mini red icon button' do %>
|
|
49
|
+
<i class="times icon"></i>
|
|
50
|
+
<% end %>
|
|
51
|
+
</td>
|
|
52
|
+
</tr>
|
|
53
|
+
<% end %>
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
|
|
57
|
+
<%= paginate @sync_audits %>
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<div class="ui top attached borderless menu">
|
|
2
|
+
<div class="header item">Sync Audits</div>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="ui attached segment">
|
|
6
|
+
<%= render 'search_form' %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<table class="ui bottom attached table">
|
|
10
|
+
<thead>
|
|
11
|
+
<tr>
|
|
12
|
+
<th><%= SyncAudit.human_attribute_name(:id) %></th>
|
|
13
|
+
<th><%= SyncAudit.human_attribute_name(:synchro_type) %></th>
|
|
14
|
+
<th><%= SyncAudit.human_attribute_name(:synchro_id) %></th>
|
|
15
|
+
<th><%= SyncAudit.human_attribute_name(:action) %></th>
|
|
16
|
+
<th><%= SyncAudit.human_attribute_name(:audited_changes) %></th>
|
|
17
|
+
<th><%= SyncAudit.human_attribute_name(:state) %></th>
|
|
18
|
+
<th>Actions</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
|
|
22
|
+
<tbody>
|
|
23
|
+
<% @sync_types.each do |sync_type| %>
|
|
24
|
+
<tr>
|
|
25
|
+
<td><%= sync_type %></td>
|
|
26
|
+
<td><%= sync_audit.synchro_type %></td>
|
|
27
|
+
<td><%= sync_audit.synchro_id %></td>
|
|
28
|
+
<td><%= sync_audit.operation_i18n %></td>
|
|
29
|
+
<td><%= simple_format_hash sync_audit.audited_changes %></td>
|
|
30
|
+
<td><%= sync_audit.state_i18n %></td>
|
|
31
|
+
<td>
|
|
32
|
+
<%= link_to 'apply', apply_admin_sync_audit_path(sync_audit), method: :patch, data: { confirm: 'Are you sure?' }, class: 'ui yellow label' %>
|
|
33
|
+
<%= link_to 'Show', admin_sync_audit_path(sync_audit), class: 'ui blue label' %>
|
|
34
|
+
<%= link_to 'Destroy', admin_sync_audit_path(sync_audit), method: :delete, data: { confirm: 'Are you sure?' }, class: 'ui red label' %>
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
|
|
41
|
+
<%= paginate @sync_audits %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
en:
|
|
2
|
+
the_trade_sync:
|
|
3
|
+
payments:
|
|
4
|
+
index:
|
|
5
|
+
add_new: add
|
|
6
|
+
import: Import
|
|
7
|
+
init: 预核销
|
|
8
|
+
confirmed: 未预核销
|
|
9
|
+
item:
|
|
10
|
+
edit: Edit
|
|
11
|
+
destroy: Destroy
|
|
12
|
+
show: 支付方式
|
|
13
|
+
checking: 核销
|
|
14
|
+
analyze: 分析支付方式
|
|
15
|
+
adjust: 平账
|
|
16
|
+
activerecord:
|
|
17
|
+
attributes:
|
|
18
|
+
sync_audit/state:
|
|
19
|
+
init: Pending
|
|
20
|
+
applied: Applied
|
|
21
|
+
finished: Finished
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateSyncAudits < ActiveRecord::Migration[5.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :sync_audits do |t|
|
|
4
|
+
t.references :operator, polymorphic: true
|
|
5
|
+
t.references :synchro, polymorphic: true
|
|
6
|
+
t.references :destined
|
|
7
|
+
t.string :synchro_params
|
|
8
|
+
t.string :operation
|
|
9
|
+
t.string :audited_changes, limit: 4096
|
|
10
|
+
t.string :note, limit: 1024
|
|
11
|
+
t.string :state
|
|
12
|
+
t.datetime :apply_at
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/rails_sync.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'rails_sync/engine'
|
|
2
|
+
require 'rails_sync/config'
|
|
3
|
+
|
|
4
|
+
require 'rails_sync/active_record'
|
|
5
|
+
require 'rails_sync/adapter'
|
|
6
|
+
|
|
7
|
+
module RailsSync
|
|
8
|
+
mattr_accessor :synchro_types do
|
|
9
|
+
[]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.options
|
|
13
|
+
@options ||= Rails.application.config_for('rails_sync').with_indifferent_access
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require 'rails_sync/adapter'
|
|
2
|
+
require 'rails_sync/table'
|
|
3
|
+
require 'rails_sync/analyzer'
|
|
4
|
+
|
|
5
|
+
module RailsSync::ActiveRecord
|
|
6
|
+
# source
|
|
7
|
+
# source_client
|
|
8
|
+
# dest_table
|
|
9
|
+
def acts_as_sync(options = {})
|
|
10
|
+
@syncs ||= []
|
|
11
|
+
|
|
12
|
+
options[:dest_table] ||= self.table_name
|
|
13
|
+
_mappings = Array(options.delete(:mapping)).to_h
|
|
14
|
+
if options[:only]
|
|
15
|
+
_filter_columns = self.column_names & Array(options.delete(:only))
|
|
16
|
+
else
|
|
17
|
+
_filter_columns = self.column_names - Array(options.delete(:except))
|
|
18
|
+
end
|
|
19
|
+
options[:primary_key] = Array(options[:primary_key] || self.primary_key).map { |i| i.to_s }
|
|
20
|
+
options[:dest_primary_key] = Array(options[:dest_primary_key] || options[:primary_key]).map { |i| i.to_s }
|
|
21
|
+
options[:dest_conditions] = options[:dest_conditions]
|
|
22
|
+
options[:full_mappings] = _filter_columns.map { |column_name|
|
|
23
|
+
next if options[:primary_key].include?(column_name)
|
|
24
|
+
if _mappings.key?(column_name)
|
|
25
|
+
[column_name, _mappings[column_name]]
|
|
26
|
+
else
|
|
27
|
+
[column_name, column_name]
|
|
28
|
+
end
|
|
29
|
+
}.compact
|
|
30
|
+
|
|
31
|
+
options[:server_id] = self.server_id
|
|
32
|
+
options[:analyzer] = RailsSync::Analyzer.new(record: self, **options)
|
|
33
|
+
|
|
34
|
+
RailsSync.synchro_types << self.name
|
|
35
|
+
@syncs << options
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def server_id
|
|
39
|
+
begin
|
|
40
|
+
result = connection.query('select @@server_uuid')
|
|
41
|
+
rescue
|
|
42
|
+
result = connection.query('select @@server_id')
|
|
43
|
+
end
|
|
44
|
+
_id = result.to_a.flatten.first
|
|
45
|
+
if _id.is_a?(Hash)
|
|
46
|
+
_id.values.first
|
|
47
|
+
else
|
|
48
|
+
_id
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def analyze_diffs(type = 'update')
|
|
53
|
+
@syncs.flat_map do |options|
|
|
54
|
+
next if !options[:primary_key].include?(self.primary_key) && type == 'delete'
|
|
55
|
+
options[:analyzer].analyze_diffs(type)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cache_diffs(type = 'update')
|
|
60
|
+
@syncs.flat_map do |options|
|
|
61
|
+
next if !options[:primary_key].include?(self.primary_key) && type == 'delete'
|
|
62
|
+
options[:analyzer].cache_diffs(type)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cache_all_diffs(*types)
|
|
67
|
+
types = ['update', 'insert', 'delete'] if types.blank?
|
|
68
|
+
types.each do |type|
|
|
69
|
+
cache_diffs(type)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def id_insert?
|
|
74
|
+
@syncs.find { |i| i[:primary_key] == ['id'] }.present?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def prepare_sync
|
|
78
|
+
@syncs.flat_map do |options|
|
|
79
|
+
options[:analyzer].reset_temp_table unless options[:analyzer].same_server?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
ActiveSupport.on_load :active_record do
|
|
86
|
+
extend RailsSync::ActiveRecord
|
|
87
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module RailsSync
|
|
2
|
+
class Adapter
|
|
3
|
+
extend ActiveRecord::ConnectionHandling
|
|
4
|
+
mattr_accessor :connection_handler, instance_writer: false
|
|
5
|
+
self.connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
|
6
|
+
|
|
7
|
+
def initialize(spec, options = {})
|
|
8
|
+
@adapter_options = RailsSync.options.fetch(spec, {})
|
|
9
|
+
@adapter_options[:name] = spec
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
self.connection
|
|
13
|
+
rescue
|
|
14
|
+
client = self.class.connection_handler.establish_connection(@adapter_options)
|
|
15
|
+
puts "established connection: #{client}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def retrieve_connection
|
|
20
|
+
self.class.connection_handler.retrieve_connection(@adapter_options[:name])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def server_id
|
|
24
|
+
begin
|
|
25
|
+
result = connection.query('select @@server_uuid')
|
|
26
|
+
rescue
|
|
27
|
+
result = connection.query('select @@server_id')
|
|
28
|
+
end
|
|
29
|
+
_id = result.to_a.flatten.first
|
|
30
|
+
if _id.is_a?(Hash)
|
|
31
|
+
_id.values.first
|
|
32
|
+
else
|
|
33
|
+
_id
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def connection
|
|
38
|
+
retrieve_connection
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def url
|
|
42
|
+
@url ||= "mysql://#{@adapter_options[:username]}:#{@adapter_options[:password]}@#{@adapter_options[:host]}:#{@adapter_options[:port]}/#{@adapter_options[:database]}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
class RailsSync::Analyzer
|
|
2
|
+
include RailsSync::Table
|
|
3
|
+
attr_reader :adapter, :my_arel_table, :dest_arel_table, :synchro_type
|
|
4
|
+
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
@adapter = RailsSync::Adapter.new(options[:dest])
|
|
7
|
+
@dest = options[:dest]
|
|
8
|
+
@record = options[:record]
|
|
9
|
+
@server_id = options[:server_id]
|
|
10
|
+
|
|
11
|
+
@synchro_type = @record.name
|
|
12
|
+
@table_name = @record.table_name
|
|
13
|
+
@dest_table = options[:dest_table]
|
|
14
|
+
@primary_key = options[:primary_key]
|
|
15
|
+
@dest_primary_key = options[:dest_primary_key]
|
|
16
|
+
@dest_conditions = Hash(options[:dest_conditions])
|
|
17
|
+
|
|
18
|
+
@full_mappings = options[:full_mappings]
|
|
19
|
+
@my_columns = @primary_key + @full_mappings.map { |col| col[0] }
|
|
20
|
+
@dest_columns = @dest_primary_key + @full_mappings.map { |col| col[1] }
|
|
21
|
+
|
|
22
|
+
instance_table
|
|
23
|
+
@my_arel_table ||= Arel::Table.new(@table_name)
|
|
24
|
+
@dest_arel_table ||= Arel::Table.new(@dest_table_name, as: 't1')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def connection
|
|
28
|
+
@record.connection
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def skip_analyze?(type)
|
|
32
|
+
( type == 'delete' && !@primary_key.include?(@record.primary_key) ) ||
|
|
33
|
+
( type == 'insert' && @record.id_insert? && !@primary_key.include?(@record.primary_key) )
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def cache_diffs(type = 'update')
|
|
37
|
+
analyze_diffs(type).each do |diff|
|
|
38
|
+
audit = SyncAudit.new synchro_type: synchro_type
|
|
39
|
+
|
|
40
|
+
_params = {}
|
|
41
|
+
@primary_key.each do |primary_key|
|
|
42
|
+
_params[primary_key] = diff.delete(primary_key).compact.first
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
audit.synchro_params = _params
|
|
46
|
+
audit.synchro_id = _params['id']
|
|
47
|
+
|
|
48
|
+
audit.operation = type
|
|
49
|
+
audit.audited_changes = diff
|
|
50
|
+
begin
|
|
51
|
+
audit.save
|
|
52
|
+
rescue ActiveRecord::ValueTooLong => e # todo not require active record
|
|
53
|
+
puts e.message
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def analyze_diffs(type = 'update')
|
|
59
|
+
return [] if skip_analyze?(type)
|
|
60
|
+
sql = fetch_diffs(type)
|
|
61
|
+
results = connection.execute(sql)
|
|
62
|
+
fields = results.fields.in_groups(2).first
|
|
63
|
+
results.map do |result|
|
|
64
|
+
r = result.in_groups(2)
|
|
65
|
+
hash_value = fields.zip( r[0].zip(r[1]) ).to_h
|
|
66
|
+
hash_value.select do |key, v|
|
|
67
|
+
v[0].to_s != v[1].to_s || @primary_key.include?(key)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def fetch_diffs(type = 'update')
|
|
73
|
+
if type == 'update'
|
|
74
|
+
query = analyze_table.join(dest_arel_table).on(on_conditions)
|
|
75
|
+
query.where(analyze_conditions)
|
|
76
|
+
elsif type == 'insert'
|
|
77
|
+
query = analyze_table.join(dest_arel_table, Arel::Nodes::RightOuterJoin).on(on_conditions)
|
|
78
|
+
query.where(my_arel_table[@primary_key[0]].eq(nil))
|
|
79
|
+
elsif type == 'delete'
|
|
80
|
+
query = analyze_table.join(dest_arel_table, Arel::Nodes::OuterJoin).on(on_conditions)
|
|
81
|
+
query.where(my_arel_table[@primary_key[0]].not_eq(nil).and(dest_arel_table[@dest_primary_key[0]].eq(nil)))
|
|
82
|
+
else
|
|
83
|
+
query = analyze_table.join(dest_arel_table, Arel::Nodes::FullOuterJoin).on(on_conditions)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
query.where(where_conditions) if where_conditions.present?
|
|
87
|
+
query.to_sql
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def analyze_table
|
|
91
|
+
attrs = @my_columns.map { |col| my_arel_table[col] }
|
|
92
|
+
attrs += @dest_columns.map { |col| dest_arel_table[col] }
|
|
93
|
+
my_arel_table.project(*attrs)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def analyze_conditions
|
|
97
|
+
mappings = @full_mappings.map do |mapping|
|
|
98
|
+
my_arel_table[mapping[0]].not_eq(dest_arel_table[mapping[1]])
|
|
99
|
+
end
|
|
100
|
+
Arel::Nodes::SqlLiteral.new mappings.map(&:to_sql).join(' OR ')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def on_conditions
|
|
104
|
+
mappings = @primary_key.map.with_index do |left_key, index|
|
|
105
|
+
my_arel_table[left_key].eq(dest_arel_table[@dest_primary_key[index]])
|
|
106
|
+
end
|
|
107
|
+
Arel::Nodes::SqlLiteral.new mappings.map(&:to_sql).join(' AND ')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def where_conditions
|
|
111
|
+
cons = @dest_conditions.map do |key, value|
|
|
112
|
+
col, meth = key.to_s.split('-')
|
|
113
|
+
dest_arel_table[col].send meth, value
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
Arel::Nodes::SqlLiteral.new cons.map(&:to_sql).join(' AND ')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module RailsSync
|
|
2
|
+
module Table
|
|
3
|
+
attr_reader :dest_table_name
|
|
4
|
+
|
|
5
|
+
def instance_table
|
|
6
|
+
if same_server?
|
|
7
|
+
# `source.table_name`
|
|
8
|
+
@dest_table_name = @adapter.instance_variable_get(:@adapter_options)[:database].to_s + '.' + @dest_table.to_s
|
|
9
|
+
else
|
|
10
|
+
# `source_table_name`
|
|
11
|
+
@dest_table_name = @dest.to_s + '_' + @table_name + '-' + @dest_table.to_s
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def same_server?
|
|
16
|
+
@server_id == adapter.server_id
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def dest_columns
|
|
20
|
+
adapter.connection.columns(@dest_table)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def dest_indexes
|
|
24
|
+
results = adapter.connection.indexes(@dest_table)
|
|
25
|
+
results = results.map { |result| { result.name => result.columns } }
|
|
26
|
+
results.to_combined_hash # rails_com core ext
|
|
27
|
+
results
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dest_primary_key
|
|
31
|
+
adapter.connection.primary_key(@dest_table)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def dest_sql_table(only: [], except: [], pure: true)
|
|
35
|
+
if only.size > 0
|
|
36
|
+
_columns = dest_columns.select { |column| only.include?(column.name) }
|
|
37
|
+
else
|
|
38
|
+
_columns = dest_columns.reject { |column| except.include?(column.name) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if pure
|
|
42
|
+
sql = ""
|
|
43
|
+
else
|
|
44
|
+
sql = "CREATE TABLE `#{@dest_table}` (\n"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
_columns.each do |column|
|
|
48
|
+
sql << " `#{column.name}` #{column.sql_type}"
|
|
49
|
+
sql << " COLLATE #{column.collation}" if column.collation.present?
|
|
50
|
+
sql << " NOT NULL" if column.null.is_a?(FalseClass)
|
|
51
|
+
if column.default
|
|
52
|
+
sql << " DEFAULT '#{column.default}',\n"
|
|
53
|
+
elsif column.default.nil? && column.null
|
|
54
|
+
sql << " DEFAULT NULL,\n"
|
|
55
|
+
else
|
|
56
|
+
sql << ",\n"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sql << " PRIMARY KEY (`#{dest_primary_key}`)"
|
|
61
|
+
|
|
62
|
+
_indexes = dest_indexes.reject { |_, value| (Array(value) & _columns.map { |col| col.name }).blank? }
|
|
63
|
+
|
|
64
|
+
if _indexes.present?
|
|
65
|
+
sql << ",\n"
|
|
66
|
+
else
|
|
67
|
+
sql << "\n"
|
|
68
|
+
end
|
|
69
|
+
_indexes.each do |index, columns|
|
|
70
|
+
sql << " KEY `#{index}` ("
|
|
71
|
+
sql << Array(columns).map { |col| "`#{col}`" }.join(',')
|
|
72
|
+
sql << "),\n"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sql.chomp!(",\n")
|
|
76
|
+
|
|
77
|
+
if pure
|
|
78
|
+
sql
|
|
79
|
+
else
|
|
80
|
+
sql << ")"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reset_temp_table
|
|
85
|
+
drop_temp_table
|
|
86
|
+
create_temp_table
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def create_temp_table
|
|
90
|
+
unless @dest_columns.include?(dest_primary_key)
|
|
91
|
+
@dest_columns.unshift dest_primary_key
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sql = "CREATE TABLE `#{@dest_table_name}` (\n"
|
|
95
|
+
sql << dest_sql_table(only: @dest_columns)
|
|
96
|
+
sql << ")"
|
|
97
|
+
sql << "ENGINE=FEDERATED\n"
|
|
98
|
+
sql << "CONNECTION='#{adapter.url}/#{@dest_table}';"
|
|
99
|
+
|
|
100
|
+
connection.execute(sql)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def drop_temp_table
|
|
104
|
+
sql = "DROP TABLE IF EXISTS `#{@dest_table_name}`"
|
|
105
|
+
|
|
106
|
+
connection.execute(sql)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
end
|
|
110
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_sync
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- qinmingyuan
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-09-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
description: Description of RailsSync.
|
|
28
|
+
email:
|
|
29
|
+
- mingyuan0715@foxmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- Rakefile
|
|
37
|
+
- app/assets/config/rails_sync_manifest.js
|
|
38
|
+
- app/controllers/rails_sync_admin/base_controller.rb
|
|
39
|
+
- app/controllers/rails_sync_admin/sync_audits_controller.rb
|
|
40
|
+
- app/jobs/audit_apply_job.rb
|
|
41
|
+
- app/models/mysql/mysql_column.rb
|
|
42
|
+
- app/models/mysql/mysql_engine.rb
|
|
43
|
+
- app/models/mysql/mysql_index.rb
|
|
44
|
+
- app/models/mysql/mysql_server.rb
|
|
45
|
+
- app/models/mysql/mysql_table.rb
|
|
46
|
+
- app/models/sync_audit.rb
|
|
47
|
+
- app/views/rails_sync_admin/sync_audits/_filter.html.erb
|
|
48
|
+
- app/views/rails_sync_admin/sync_audits/_form.html.erb
|
|
49
|
+
- app/views/rails_sync_admin/sync_audits/_search_form.html.erb
|
|
50
|
+
- app/views/rails_sync_admin/sync_audits/edit.html.erb
|
|
51
|
+
- app/views/rails_sync_admin/sync_audits/index.html.erb
|
|
52
|
+
- app/views/rails_sync_admin/sync_audits/new.html.erb
|
|
53
|
+
- app/views/rails_sync_admin/sync_audits/show.html.erb
|
|
54
|
+
- app/views/rails_sync_admin/sync_audits/synchros.html.erb
|
|
55
|
+
- config/locales/en.yml
|
|
56
|
+
- config/locales/zh.yml
|
|
57
|
+
- config/routes.rb
|
|
58
|
+
- db/migrate/20180319031059_create_sync_audits.rb
|
|
59
|
+
- lib/rails_sync.rb
|
|
60
|
+
- lib/rails_sync/active_record.rb
|
|
61
|
+
- lib/rails_sync/adapter.rb
|
|
62
|
+
- lib/rails_sync/analyzer.rb
|
|
63
|
+
- lib/rails_sync/config.rb
|
|
64
|
+
- lib/rails_sync/engine.rb
|
|
65
|
+
- lib/rails_sync/table.rb
|
|
66
|
+
- lib/rails_sync/version.rb
|
|
67
|
+
- lib/tasks/the_sync_tasks.rake
|
|
68
|
+
homepage: https://github.com/yougexiangfa/rails_sync
|
|
69
|
+
licenses:
|
|
70
|
+
- LGPL-3.0
|
|
71
|
+
metadata: {}
|
|
72
|
+
post_install_message:
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubyforge_project:
|
|
88
|
+
rubygems_version: 2.7.7
|
|
89
|
+
signing_key:
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Summary of RailsSync.
|
|
92
|
+
test_files: []
|