activerecord-model_inheritance 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +451 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/README.md +255 -0
- data/Rakefile +7 -0
- data/lib/active_record/model_inheritance/error.rb +5 -0
- data/lib/active_record/model_inheritance/model.rb +169 -0
- data/lib/active_record/model_inheritance/version.rb +5 -0
- data/lib/active_record/model_inheritance/view_definition.rb +112 -0
- data/lib/active_record/model_inheritance.rb +57 -0
- data/lib/activerecord/model_inheritance.rb +1 -0
- data/lib/generators/active_record/model_inheritance/generators.rb +6 -0
- data/lib/generators/active_record/model_inheritance/model/model_generator.rb +75 -0
- data/lib/generators/active_record/model_inheritance/model/templates/definition.erb +1 -0
- data/lib/generators/active_record/model_inheritance/model/templates/model.erb +2 -0
- data/lib/generators/active_record/model_inheritance/model/templates/model_config.erb +3 -0
- data/lib/generators/active_record/model_inheritance/view/templates/create_migration.erb +13 -0
- data/lib/generators/active_record/model_inheritance/view/templates/update_migration.erb +5 -0
- data/lib/generators/active_record/model_inheritance/view/view_generator.rb +121 -0
- data/sig/active_record/model_inheritance/generators/model_generator.rbs +23 -0
- data/sig/active_record/model_inheritance/generators/view_generator.rbs +45 -0
- data/sig/active_record/model_inheritance/version.rbs +5 -0
- data/sig/active_record/model_inheritance/view_definition.rbs +20 -0
- metadata +127 -0
data/README.md
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
# Model Inheritance
|
2
|
+
|
3
|
+
An attempt at real inheritance for ActiveRecord models.
|
4
|
+
|
5
|
+
This gem leverages database views (thanks to [Scenic](https://github.com/scenic-views/scenic)) to compose models
|
6
|
+
from other models, kind of like POROs inheritance [with limitations](#limitations). Views are defined using
|
7
|
+
[Arel](https://www.rubydoc.info/gems/arel) instead of SQL, which is cleaner and allows for easier integration.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'activerecord-model_inheritance', '~> 1.0'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
bundle
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
First of all, if you want to make intensive use of this gem, being familiar on how [Scenic](https://github.com/scenic-views/scenic)
|
26
|
+
works is highly recommended.
|
27
|
+
|
28
|
+
### Quickstart
|
29
|
+
|
30
|
+
Assuming you want a new `DerivedModel` that inherits from a preexisting `BaseModel`, follow these steps:
|
31
|
+
|
32
|
+
- Generate the new model and its view definition:
|
33
|
+
```bash
|
34
|
+
rails g active_record:model_inheritance:model DerivedModel BaseModel
|
35
|
+
```
|
36
|
+
|
37
|
+
- Edit the generated model and definition, if needed
|
38
|
+
|
39
|
+
- Generate the SQL definition of the view and the initial migration:
|
40
|
+
```bash
|
41
|
+
rails g active_record:model_inheritance:view DerivedModel
|
42
|
+
```
|
43
|
+
|
44
|
+
- Edit the generated migration if needed
|
45
|
+
|
46
|
+
- Finally, run the migration:
|
47
|
+
```bash
|
48
|
+
rails db:migrate
|
49
|
+
```
|
50
|
+
|
51
|
+
Keep in mind that you need to generate a new version of the SQL definition whenever your view definition changes,
|
52
|
+
for example when you want to add some fields to the derived model.
|
53
|
+
To do so, just run the same generator as again:
|
54
|
+
```bash
|
55
|
+
rails g active_record:model_inheritance:view DerivedModel
|
56
|
+
```
|
57
|
+
This will take care of everything, including generating the migration to update the view.
|
58
|
+
It works similarly to Scenic.
|
59
|
+
|
60
|
+
### Concepts
|
61
|
+
|
62
|
+
A database view is like a _virtual table_ where the schema, as well as the data it contains, are
|
63
|
+
defined by a plain old SQL query. Of course, since views are just query results disguised as tables, you can't write into them.
|
64
|
+
So, at the end of the day, all this gem does is enabling write operations to Scenic view backed models.
|
65
|
+
|
66
|
+
To achieve something _resembling_ real inheritance between models, the **inner model** is introduced,
|
67
|
+
which is a third entity between the **base model** (the one you want to inherit from)
|
68
|
+
and the **derived model** (the new one you're creating).
|
69
|
+
|
70
|
+
The **inner model** holds the additional pieces your **derived model** should have.
|
71
|
+
|
72
|
+
When you apply changes to a **derived model**, those changes are mapped to **inner** and **base** models.
|
73
|
+
For example, if the **derived model** has the fields `foo` and `bar`, coming respectively from **inner** and **base** models,
|
74
|
+
changes to `foo` will be saved to the **inner model**, and changes to `bar` will be saved to the **base model**.
|
75
|
+
This way, the database view backing the **derived model** is always accessed in read-only mode.
|
76
|
+
|
77
|
+
### Configuration
|
78
|
+
|
79
|
+
If you're using Rails, the following is the code you would put inside an initializer
|
80
|
+
to configure this gem as it is configured by default. If you're ok with this defaults, then you don't need to
|
81
|
+
configure anything.
|
82
|
+
```ruby
|
83
|
+
# config/initializers/model_inheritance.rb
|
84
|
+
|
85
|
+
ActiveRecord::ModelInheritance.configure do |config|
|
86
|
+
## derived model options
|
87
|
+
|
88
|
+
# name of the dynamically generated inner model class
|
89
|
+
config.inner_class_name = 'Inner'
|
90
|
+
|
91
|
+
# base class of the dynamically generated inner model
|
92
|
+
config.inner_base_class = ApplicationRecord
|
93
|
+
|
94
|
+
# name of the belongs_to association from derived model to base model
|
95
|
+
config.base_reference_name = :model_inheritance_base
|
96
|
+
|
97
|
+
# name of the belongs_to association from derived model to its own inner model
|
98
|
+
config.inner_reference_name = :model_inheritance_inner
|
99
|
+
|
100
|
+
# whether to inherit enums from the base model
|
101
|
+
# only enums relevant to inherited fields will be added
|
102
|
+
config.inherit_enums = true
|
103
|
+
|
104
|
+
# whether to delegate missing methods from derived model to base model
|
105
|
+
config.delegate_missing_to_base = true
|
106
|
+
|
107
|
+
## paths options
|
108
|
+
|
109
|
+
# these are self explanatory
|
110
|
+
config.models_path = Rails.root.join('app/models')
|
111
|
+
config.migrations_path = Rails.root.join('db/migrate')
|
112
|
+
|
113
|
+
# where to save generated SQL definitions (Scenic default)
|
114
|
+
config.views_path = Rails.root.join('db/views')
|
115
|
+
|
116
|
+
# where to save view definitions
|
117
|
+
config.definitions_path = Rails.root.join('db/views/model_inheritance')
|
118
|
+
end
|
119
|
+
```
|
120
|
+
If you're not using Rails, the default configuration stays the same, except:
|
121
|
+
```ruby
|
122
|
+
config.inner_base_class = ActiveRecord::Base
|
123
|
+
|
124
|
+
config.models_path = Pathname('app/models')
|
125
|
+
config.migrations_path = Pathname('db/migrate')
|
126
|
+
|
127
|
+
config.views_path = Pathname('db/views')
|
128
|
+
config.definitions_path = Pathname('db/views/model_inheritance')
|
129
|
+
```
|
130
|
+
|
131
|
+
You can pass options to `derives_from` if you want to override the global derived models configuration on a per model basis:
|
132
|
+
```ruby
|
133
|
+
class DerivedModel < ApplicationRecord
|
134
|
+
include ActiveRecord::ModelInheritance::Model
|
135
|
+
|
136
|
+
derives_from BaseModel,
|
137
|
+
inner_class_name: 'Inner',
|
138
|
+
inner_base_class: ApplicationRecord,
|
139
|
+
base_reference_name: :model_inheritance_base,
|
140
|
+
inner_reference_name: :model_inheritance_inner,
|
141
|
+
inherit_enums: true,
|
142
|
+
delegate_missing_to_base: true
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
### View definitions
|
147
|
+
A view definition is responsible of:
|
148
|
+
- providing a convenient way of defining views using Arel
|
149
|
+
- keeping a map of which attributes belong respectively to the base and inner model
|
150
|
+
|
151
|
+
By default, the derived model will get **all** the fields from base and inner.
|
152
|
+
If that's not what you want, you can override the default behaviour like in the following example:
|
153
|
+
```ruby
|
154
|
+
# db/views/model_inheritance/derived_models.rb
|
155
|
+
|
156
|
+
ActiveRecord::ModelInheritance::ViewDefinition.define_derived_view DerivedModel do |inner_table, base_table|
|
157
|
+
inner_table
|
158
|
+
# all fields from inner
|
159
|
+
.project(inner_table[Arel.star])
|
160
|
+
# only some fields from base
|
161
|
+
.project(
|
162
|
+
base_table[:foo],
|
163
|
+
base_table[:bar],
|
164
|
+
base_table[:baz]
|
165
|
+
)
|
166
|
+
.join(base_table)
|
167
|
+
.on(inner_table[:model_inheritance_base_id].eq base_table[:id])
|
168
|
+
end
|
169
|
+
```
|
170
|
+
Here, Arel is used to describe how you want the base and inner table joined.
|
171
|
+
The block parameters `inner_table` and `base_table` are both [Arel::SelectTable](https://www.rubydoc.info/gems/arel/Arel/Table)s,
|
172
|
+
representing the inner model table and base model table respectively.
|
173
|
+
The code inside the block **must** evaluate to [Arel::SelectManager](https://www.rubydoc.info/gems/arel/Arel/SelectManager).
|
174
|
+
Note that if you set the option `base_reference_name` to something different to `:model_inheritance_base`, you have to
|
175
|
+
change the join condition accordingly.
|
176
|
+
|
177
|
+
When you run the `active_record:model_inheritance:view` generator, one of the things that's done is converting that Arel::SelectManager
|
178
|
+
(the default one or your custom provided one) to SQL. In the case of the above example, the generated SQL will look something
|
179
|
+
like this:
|
180
|
+
```sql
|
181
|
+
/* db/views/derived_models_v01.sql */
|
182
|
+
|
183
|
+
SELECT "derived_model_inners".*,
|
184
|
+
"base_models"."foo",
|
185
|
+
"base_models"."bar",
|
186
|
+
"base_models"."baz"
|
187
|
+
FROM "derived_model_inners"
|
188
|
+
INNER JOIN "base_models"
|
189
|
+
ON "derived_model_inners"."model_inheritance_base_id" = "base_models"."id"
|
190
|
+
```
|
191
|
+
This is how the database view backing the derived model will be created.
|
192
|
+
|
193
|
+
### Sharing code between derived and inner
|
194
|
+
Sometimes it could be useful to have code replicated in both derived and inner models.
|
195
|
+
This can be done by passing a block to `derives_from`.
|
196
|
+
```ruby
|
197
|
+
class DerivedModel < ApplicationRecord
|
198
|
+
include ActiveRecord::ModelInheritance::Model
|
199
|
+
|
200
|
+
derives_from BaseModel do
|
201
|
+
def foo
|
202
|
+
# ...
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
In the above example, `foo` you will be declared in both derived and inner models.
|
208
|
+
|
209
|
+
### Accessing the inner model
|
210
|
+
If for some reason you want to directly access the inner model, you can:
|
211
|
+
```ruby
|
212
|
+
DerivedModel::Inner # the inner model class
|
213
|
+
|
214
|
+
DerivedModel::Foo # in case you've set inner_class_name to 'Foo'
|
215
|
+
|
216
|
+
DerivedModel.first._model_inheritance_inner # instance of the inner model
|
217
|
+
```
|
218
|
+
|
219
|
+
### A few words on multiple inheritance
|
220
|
+
This gem doesn't strictly prohibit multiple inheritance, and in _in theory_ it should be possible to implement.
|
221
|
+
Currently there are no plans on this, but if you find a clean solution you can share your work with us! (see [Contributing](#contributing))
|
222
|
+
|
223
|
+
## Limitations
|
224
|
+
- A derived model is not a subclass of its base model
|
225
|
+
- Query methods called on base models will return only base models
|
226
|
+
- Query methods called on derived models will return only derived models
|
227
|
+
|
228
|
+
## Future developments
|
229
|
+
- Improved and more comprehensive documentation
|
230
|
+
- Some ways around current limitations
|
231
|
+
- Testing with a dummy Rails application
|
232
|
+
|
233
|
+
## Version numbers
|
234
|
+
|
235
|
+
Model Inheritance loosely follows [Semantic Versioning](https://semver.org/), with a hard guarantee that breaking changes to the public API will always coincide with an increase to the `MAJOR` number.
|
236
|
+
|
237
|
+
Version numbers are in three parts: `MAJOR.MINOR.PATCH`.
|
238
|
+
|
239
|
+
- Breaking changes to the public API increment the `MAJOR`. There may also be changes that would otherwise increase the `MINOR` or the `PATCH`.
|
240
|
+
- Additions, deprecations, and "big" non breaking changes to the public API increment the `MINOR`. There may also be changes that would otherwise increase the `PATCH`.
|
241
|
+
- Bug fixes and "small" non breaking changes to the public API increment the `PATCH`.
|
242
|
+
|
243
|
+
Notice that any feature deprecated by a minor release can be expected to be removed by the next major release.
|
244
|
+
|
245
|
+
## Changelog
|
246
|
+
|
247
|
+
Full list of changes in [CHANGELOG.md](CHANGELOG.md)
|
248
|
+
|
249
|
+
## Contributing
|
250
|
+
|
251
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/moku-io/activerecord-model_inheritance.
|
252
|
+
|
253
|
+
## License
|
254
|
+
|
255
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_record'
|
3
|
+
require_relative 'error'
|
4
|
+
require_relative 'view_definition'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module ModelInheritance
|
8
|
+
class InheritanceError < Error; end
|
9
|
+
|
10
|
+
module Model
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
class_attribute :model_inheritance_base_model
|
15
|
+
class_attribute :model_inheritance_inner_model
|
16
|
+
class_attribute :model_inheritance_base_name
|
17
|
+
class_attribute :model_inheritance_inner_name
|
18
|
+
class_attribute :model_inheritance_view_definition
|
19
|
+
class_attribute :model_inheritance_attributes_mapping
|
20
|
+
|
21
|
+
self.primary_key = :id
|
22
|
+
self.record_timestamps = false
|
23
|
+
end
|
24
|
+
|
25
|
+
class_methods do
|
26
|
+
def derives_from(base_model,
|
27
|
+
base_reference_name: ModelInheritance.base_reference_name,
|
28
|
+
inner_reference_name: ModelInheritance.inner_reference_name,
|
29
|
+
inherit_enums: ModelInheritance.inherit_enums,
|
30
|
+
inner_base_class: ModelInheritance.inner_base_class,
|
31
|
+
inner_class_name: ModelInheritance.inner_class_name,
|
32
|
+
delegate_missing_to_base: ModelInheritance.delegate_missing_to_base,
|
33
|
+
&block)
|
34
|
+
self.model_inheritance_base_name = base_reference_name
|
35
|
+
self.model_inheritance_inner_name = inner_reference_name
|
36
|
+
self.model_inheritance_base_model = base_model
|
37
|
+
|
38
|
+
base_ref_foreign_key = "#{model_inheritance_base_model.model_name.singular}_id"
|
39
|
+
base_ref = proc do
|
40
|
+
belongs_to base_reference_name,
|
41
|
+
class_name: "::#{base_model.name}",
|
42
|
+
foreign_key: base_ref_foreign_key
|
43
|
+
end
|
44
|
+
|
45
|
+
inner_model = Class.new inner_base_class do
|
46
|
+
instance_exec(&block) if block.present?
|
47
|
+
instance_exec(&base_ref)
|
48
|
+
end
|
49
|
+
instance_exec(&block) if block.present?
|
50
|
+
instance_exec(&base_ref)
|
51
|
+
|
52
|
+
const_set inner_class_name, inner_model
|
53
|
+
belongs_to inner_reference_name, class_name: inner_class_name, foreign_key: :id
|
54
|
+
|
55
|
+
# the secret ingredient
|
56
|
+
accepts_nested_attributes_for base_reference_name
|
57
|
+
accepts_nested_attributes_for inner_reference_name
|
58
|
+
|
59
|
+
self.model_inheritance_inner_model = inner_model
|
60
|
+
self.model_inheritance_view_definition = ViewDefinition.from_model self
|
61
|
+
self.model_inheritance_attributes_mapping = model_inheritance_view_definition.attributes_mapping
|
62
|
+
|
63
|
+
# prevents attributes from being touched when updating
|
64
|
+
# not strictly necessary, but better safe than sorry
|
65
|
+
attr_readonly attribute_names.map(&:to_sym)
|
66
|
+
|
67
|
+
if inherit_enums
|
68
|
+
base_model.defined_enums.each do |attribute, enum_values|
|
69
|
+
attribute = attribute.to_sym
|
70
|
+
enum attribute, enum_values if model_inheritance_attributes_mapping[:base].include? attribute
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
delegate_missing_to base_reference_name if delegate_missing_to_base
|
75
|
+
end
|
76
|
+
|
77
|
+
def partition_attributes attributes
|
78
|
+
inner_attributes = attributes.select { |key| model_inheritance_attributes_mapping[:inner].include? key.to_sym }
|
79
|
+
base_attributes = attributes.select { |key| model_inheritance_attributes_mapping[:base].include? key.to_sym }
|
80
|
+
|
81
|
+
[inner_attributes, base_attributes]
|
82
|
+
end
|
83
|
+
|
84
|
+
def create(...)
|
85
|
+
super.reload
|
86
|
+
end
|
87
|
+
|
88
|
+
def create!(...)
|
89
|
+
super.reload
|
90
|
+
end
|
91
|
+
|
92
|
+
# overriding the following methods to prevent ConnectionAdapter from touching the underlying view
|
93
|
+
|
94
|
+
def _insert_record(...)
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def _update_record(...)
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
|
102
|
+
def _delete_record(...)
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def save(**options, &)
|
108
|
+
prepare_save
|
109
|
+
super && _model_inheritance_base.save
|
110
|
+
end
|
111
|
+
|
112
|
+
def save!(**options, &)
|
113
|
+
prepare_save
|
114
|
+
super
|
115
|
+
_model_inheritance_base.save!
|
116
|
+
end
|
117
|
+
|
118
|
+
def destroy
|
119
|
+
_model_inheritance_inner.destroy
|
120
|
+
super
|
121
|
+
end
|
122
|
+
|
123
|
+
def delete
|
124
|
+
_model_inheritance_inner.delete
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def _model_inheritance_base
|
129
|
+
public_send model_inheritance_base_name
|
130
|
+
end
|
131
|
+
|
132
|
+
def _model_inheritance_inner
|
133
|
+
public_send model_inheritance_inner_name
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def prepare_save
|
139
|
+
inner_attributes, base_attributes = self.class.partition_attributes attributes_for_database
|
140
|
+
|
141
|
+
if new_record?
|
142
|
+
unless _model_inheritance_base.present?
|
143
|
+
raise InheritanceError, "#{model_inheritance_base_name} must be present"
|
144
|
+
end
|
145
|
+
|
146
|
+
# pass updated base attributes to base model
|
147
|
+
# this way it gets automatically updated
|
148
|
+
base_attributes.compact!
|
149
|
+
_model_inheritance_base.assign_attributes base_attributes if base_attributes.present?
|
150
|
+
|
151
|
+
attributes = {
|
152
|
+
model_inheritance_base_name => _model_inheritance_base,
|
153
|
+
"#{model_inheritance_inner_name}_attributes".to_sym => inner_attributes
|
154
|
+
}
|
155
|
+
else
|
156
|
+
inner_attributes[:id] = id
|
157
|
+
base_attributes[:id] = _model_inheritance_base.id
|
158
|
+
|
159
|
+
attributes = {
|
160
|
+
"#{model_inheritance_base_name}_attributes".to_sym => base_attributes,
|
161
|
+
"#{model_inheritance_inner_name}_attributes".to_sym => inner_attributes
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
assign_attributes attributes
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'arel'
|
2
|
+
require_relative 'error'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
module ModelInheritance
|
6
|
+
class DefinitionError < Error; end
|
7
|
+
|
8
|
+
class ViewDefinition
|
9
|
+
attr_reader :definition
|
10
|
+
attr_reader :model_class
|
11
|
+
|
12
|
+
def initialize model_class, definition
|
13
|
+
@model_class = model_class
|
14
|
+
@definition = definition
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_sql
|
18
|
+
@definition.to_sql
|
19
|
+
end
|
20
|
+
|
21
|
+
def attributes_mapping
|
22
|
+
definition
|
23
|
+
.projections
|
24
|
+
.each_with_object({base: [], inner: []}) do |projection, attributes_mapping|
|
25
|
+
if projection.is_a? Arel::Nodes::As
|
26
|
+
relation = projection.left.relation
|
27
|
+
name = projection.right
|
28
|
+
else
|
29
|
+
relation = projection.relation
|
30
|
+
name = projection.name
|
31
|
+
end
|
32
|
+
|
33
|
+
case relation
|
34
|
+
when model_class.model_inheritance_inner_model.arel_table
|
35
|
+
relation_type = :inner
|
36
|
+
relation_model = model_class.model_inheritance_inner_model
|
37
|
+
when model_class.model_inheritance_base_model.arel_table
|
38
|
+
relation_type = :base
|
39
|
+
relation_model = model_class.model_inheritance_base_model
|
40
|
+
else
|
41
|
+
raise DefinitionError, "Invalid \"#{relation}\" relation"
|
42
|
+
end
|
43
|
+
|
44
|
+
attributes = if name == Arel.star
|
45
|
+
relation_model.attribute_names.map(&:to_sym)
|
46
|
+
else
|
47
|
+
[name.to_sym]
|
48
|
+
end
|
49
|
+
|
50
|
+
attributes_mapping[relation_type] += attributes
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.from_model model_class
|
55
|
+
unless model_class.include? Model
|
56
|
+
raise ArgumentError, "#{model_class.name} doesn't include ActiveRecord::ModelInheritance::Model"
|
57
|
+
end
|
58
|
+
|
59
|
+
ViewDefinition.from_name model_class.model_name.plural
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.from_name name
|
63
|
+
definition_filename = Pathname(ModelInheritance.config.definitions_path).join "#{name}.rb"
|
64
|
+
raise ArgumentError, "Definition for \"#{name}\" doesn't exist" unless definition_filename.file?
|
65
|
+
|
66
|
+
eval File.read definition_filename
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.define_derived_view model_class, &block
|
70
|
+
inner_model = model_class.model_inheritance_inner_model
|
71
|
+
base_model = model_class.model_inheritance_base_model
|
72
|
+
|
73
|
+
inner_table = inner_model.arel_table
|
74
|
+
base_table = base_model.arel_table
|
75
|
+
|
76
|
+
definition = if block_given?
|
77
|
+
block.call(inner_table, base_table).tap do |d|
|
78
|
+
unless d.is_a? Arel::SelectManager
|
79
|
+
raise DefinitionError, 'Defined view must evaluate to Arel::SelectManager'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
else
|
83
|
+
selected_base_columns = if inner_model.primary_key == base_model.primary_key
|
84
|
+
# this is a common naming conflict problem
|
85
|
+
# makes sense to try and solve automatically
|
86
|
+
|
87
|
+
# just delete the base primary key from columns that will be selected
|
88
|
+
base_model
|
89
|
+
.column_names
|
90
|
+
.dup
|
91
|
+
.delete_if { |column_name| column_name == base_model.primary_key }
|
92
|
+
else
|
93
|
+
base_model.column_names
|
94
|
+
end.map { |column_name| base_table[column_name.to_sym] }
|
95
|
+
|
96
|
+
base_reference = model_class
|
97
|
+
.reflect_on_association(model_class.model_inheritance_base_name)
|
98
|
+
.foreign_key
|
99
|
+
.to_sym
|
100
|
+
|
101
|
+
inner_table
|
102
|
+
.project(inner_table[Arel.star])
|
103
|
+
.project(*selected_base_columns)
|
104
|
+
.join(base_table)
|
105
|
+
.on(inner_table[base_reference].eq base_table[base_model.primary_key])
|
106
|
+
end
|
107
|
+
|
108
|
+
ViewDefinition.new model_class, definition
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'scenic'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_support'
|
4
|
+
require_relative 'model_inheritance/version'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module ModelInheritance
|
8
|
+
include ActiveSupport::Configurable
|
9
|
+
|
10
|
+
config.define_singleton_method :define_lazy_property do |key, &block|
|
11
|
+
self[key] = nil
|
12
|
+
|
13
|
+
define_singleton_method key do
|
14
|
+
self[key] || (self[key] = block.call)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
config.define_singleton_method :define_lazy_path do |name, *dirs|
|
19
|
+
define_lazy_property name do
|
20
|
+
if defined?(Rails.root) && Rails.root
|
21
|
+
Rails.root.join(*dirs)
|
22
|
+
else
|
23
|
+
Pathname(dirs.join '/')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
define_singleton_method "#{name}=".to_sym do |path|
|
28
|
+
self[name] = Pathname(path)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
config.base_reference_name = :model_inheritance_base
|
33
|
+
config.inner_reference_name = :model_inheritance_inner
|
34
|
+
config.inner_class_name = 'Inner'
|
35
|
+
config.inherit_enums = true
|
36
|
+
config.delegate_missing_to_base = true
|
37
|
+
|
38
|
+
config.define_lazy_path :views_path, 'db', 'views'
|
39
|
+
config.define_lazy_path :models_path, 'app', 'models'
|
40
|
+
config.define_lazy_path :migrations_path, 'db', 'migrate'
|
41
|
+
config.define_lazy_path :definitions_path, 'db', 'views', 'model_inheritance'
|
42
|
+
|
43
|
+
config.define_lazy_property :inner_base_class do
|
44
|
+
if defined? ApplicationRecord
|
45
|
+
ApplicationRecord
|
46
|
+
else
|
47
|
+
ActiveRecord::Base
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
singleton_class.delegate(*config.keys, to: :config)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
require_relative 'model_inheritance/error'
|
56
|
+
require_relative 'model_inheritance/model'
|
57
|
+
require_relative 'model_inheritance/view_definition'
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative '../active_record/model_inheritance'
|