activerecord-exclusive-arc 0.2.1 → 0.2.3
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 +4 -4
- data/README.md +32 -37
- data/lib/exclusive_arc/version.rb +1 -1
- data/lib/generators/exclusive_arc_generator.rb +55 -8
- data/lib/generators/templates/migration.rb.erb +2 -6
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 315a60d914bac0fcce5129cddb165afa90604355e2c4bc9c46cad1b2d5aac79a
|
4
|
+
data.tar.gz: 5b93a4be4ddba10b47eaaaddbd5ba7cb5df072b2462114e01945e2c2f9b051d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 502088f01538ad5c31fbb4052f2a03c7f4dbe5723533d452b07abb331dae7d20c6dc4b87f3c5e019731c2b6cfce43834726f4cc89febf32e971593d2ecb39e44
|
7
|
+
data.tar.gz: 74c62d1c094c033ff0224af98128fe0d65a114e1f36099845cf1d3312cbd39d34a73db71f65b275eda92746be51d063e62ca65feb31849578833951af8115976
|
data/README.md
CHANGED
@@ -1,31 +1,31 @@
|
|
1
|
-
|
1
|
+
## 💫 `activerecord-exclusive-arc` 💫
|
2
2
|
|
3
|
-
|
4
|
-
models.
|
3
|
+
A RubyGem that allows an ActiveRecord model to exclusively belong to one of any number of different
|
4
|
+
types of ActiveRecord models.
|
5
5
|
|
6
|
-
### Doesn’t Rails already provide this?
|
6
|
+
### Doesn’t Rails already provide a way to do this?
|
7
7
|
|
8
|
-
It does, but there are decent arguments against the default Rails way of doing polymorphism.
|
9
|
-
fact that the Ruby class name is stored in the database as a string. If you want to
|
10
|
-
Ruby class used for such reasons, you must also update the database strings
|
11
|
-
of application-layer definitions into the database may become a
|
8
|
+
It does, but there are decent arguments against the default Rails way of doing polymorphism.
|
9
|
+
Consider the fact that the Ruby class name is stored in the database as a string. If you want to
|
10
|
+
change the name of the Ruby class used for such reasons, you must also update the database strings
|
11
|
+
that represent it. The seeping of application-layer definitions into the database may become a
|
12
|
+
liability.
|
12
13
|
|
13
|
-
Another common argument concerns referential integrity. _Foreign Key Constraints_ are a common
|
14
|
-
ensure primary keys of tables can be reliably used as foreign keys on others.
|
15
|
-
when a column that represents a Ruby class is
|
16
|
-
|
17
|
-
There are also quality of life considerations, such as not being able to eager-load the `belongs_to ...
|
18
|
-
polymorphic: true` relationship and the fact that polymorphic indexes require multiple columns.
|
14
|
+
Another common argument concerns referential integrity. _Foreign Key Constraints_ are a common
|
15
|
+
mechanism to ensure primary keys of database tables can be reliably used as foreign keys on others.
|
16
|
+
This becomes harder to enforce in the databse when a string column that represents a Ruby class is
|
17
|
+
one of the components required for unique identification.
|
19
18
|
|
20
19
|
### So how does this work?
|
21
20
|
|
22
|
-
It reduces the boilerplate of managing a _Polymorphic Assication_ modeled as a pattern called an
|
23
|
-
Arc_
|
24
|
-
|
21
|
+
It reduces the boilerplate of managing a _Polymorphic Assication_ modeled as a pattern called an
|
22
|
+
_Exclusive Arc_, where each potential polymorphic reference has its own foreign key. This maps
|
23
|
+
nicely to a set of optional `belongs_to` relationships, some polymorphic convenience methods, and a
|
24
|
+
database check constraint with a matching `ActiveRecord` validation.
|
25
25
|
|
26
26
|
## How to use
|
27
27
|
|
28
|
-
Firstly,
|
28
|
+
Firstly, add the gem to your `Gemfile` and `bundle install`:
|
29
29
|
|
30
30
|
```ruby
|
31
31
|
gem "activerecord-exclusive-arc"
|
@@ -37,8 +37,8 @@ The feature set of this gem is offered via a Rails generator command:
|
|
37
37
|
bin/rails g exclusive_arc <Model> <arc> <belongs_to1> <belongs_to2> ...
|
38
38
|
```
|
39
39
|
|
40
|
-
This assumes you already have a `<Model>`. The `<arc>` is the name of the polymorphic association
|
41
|
-
establish that may either be a `<belongs_to1>`, `<belongs_to2>`, etc. Say we ran:
|
40
|
+
This assumes you already have a `<Model>`. The `<arc>` is the name of the polymorphic association
|
41
|
+
you want to establish that may either be a `<belongs_to1>`, `<belongs_to2>`, etc. Say we ran:
|
42
42
|
|
43
43
|
```
|
44
44
|
bin/rails g exclusive_arc Comment commentable post comment
|
@@ -84,7 +84,8 @@ class Comment < ApplicationRecord
|
|
84
84
|
end
|
85
85
|
```
|
86
86
|
|
87
|
-
Continuing with our example, the generator command would also produce a migration that looks like
|
87
|
+
Continuing with our example, the generator command would also produce a migration that looks like
|
88
|
+
this:
|
88
89
|
|
89
90
|
```ruby
|
90
91
|
class CommentCommentableExclusiveArc < ActiveRecord::Migration[7.0]
|
@@ -100,9 +101,9 @@ class CommentCommentableExclusiveArc < ActiveRecord::Migration[7.0]
|
|
100
101
|
end
|
101
102
|
```
|
102
103
|
|
103
|
-
The check constraint ensures `ActiveRecord` validations can’t be bypassed to break the fabeled
|
104
|
-
Can Only Be One
|
105
|
-
lookup performance for each individual polymorphic assoication.
|
104
|
+
The check constraint ensures `ActiveRecord` validations can’t be bypassed to break the fabeled
|
105
|
+
rule - "There Can Only Be One️". Traditional foreign key constraints can be used and the partial
|
106
|
+
indexes provide improved lookup performance for each individual polymorphic assoication.
|
106
107
|
|
107
108
|
### Exclusive Arc Options
|
108
109
|
|
@@ -114,30 +115,24 @@ Usage:
|
|
114
115
|
rails generate exclusive_arc NAME [arc belongs_to1 belongs_to2 ...] [options]
|
115
116
|
|
116
117
|
Options:
|
117
|
-
[--skip-namespace], [--no-skip-namespace] # Skip namespace (affects only isolated engines)
|
118
|
-
[--skip-collision-check], [--no-skip-collision-check] # Skip collision check
|
119
118
|
[--optional], [--no-optional] # Exclusive arc is optional
|
120
119
|
[--skip-foreign-key-constraints], [--no-skip-foreign-key-constraints] # Skip foreign key constraints
|
121
120
|
[--skip-foreign-key-indexes], [--no-skip-foreign-key-indexes] # Skip foreign key partial indexes
|
122
121
|
[--skip-check-constraint], [--no-skip-check-constraint] # Skip check constraint
|
123
122
|
|
124
|
-
Runtime options:
|
125
|
-
-f, [--force] # Overwrite files that already exist
|
126
|
-
-p, [--pretend], [--no-pretend] # Run but do not make any changes
|
127
|
-
-q, [--quiet], [--no-quiet] # Suppress status output
|
128
|
-
-s, [--skip], [--no-skip] # Skip files that already exist
|
129
|
-
|
130
123
|
Adds an Exclusive Arc to an ActiveRecord model and generates the migration for it
|
131
124
|
```
|
132
125
|
|
133
|
-
Notably, if you want to make an Exclusive Arc optional, you can use the `--optional` flag. This will
|
134
|
-
the definition in your `ActiveRecord` model and loosen both the validation and database check
|
135
|
-
that there can be 0 or 1 foreign keys set for the polymorphic
|
126
|
+
Notably, if you want to make an Exclusive Arc optional, you can use the `--optional` flag. This will
|
127
|
+
adjust the definition in your `ActiveRecord` model and loosen both the validation and database check
|
128
|
+
constraint so that there can be 0 or 1 foreign keys set for the polymorphic reference.
|
136
129
|
|
137
130
|
### Compatibility
|
138
131
|
|
139
|
-
Currently `activerecord-exclusive-arc` is tested against a matrix of
|
140
|
-
|
132
|
+
Currently `activerecord-exclusive-arc` is tested against a matrix of:
|
133
|
+
* Ruby 2.7 and 3.2
|
134
|
+
* Rails 6.1 and 7.0
|
135
|
+
* `postgresql` and `sqlite3` database adapters
|
141
136
|
|
142
137
|
### Contributing
|
143
138
|
|
@@ -31,10 +31,16 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
31
31
|
model_file_path,
|
32
32
|
class_name.demodulize,
|
33
33
|
<<~RB
|
34
|
-
#{indents}include ExclusiveArc::Model
|
35
34
|
#{indents}has_exclusive_arc #{model_exclusive_arcs}
|
36
35
|
RB
|
37
36
|
)
|
37
|
+
inject_into_class(
|
38
|
+
model_file_path,
|
39
|
+
class_name.demodulize,
|
40
|
+
<<~RB
|
41
|
+
#{indents}include ExclusiveArc::Model
|
42
|
+
RB
|
43
|
+
)
|
38
44
|
end
|
39
45
|
|
40
46
|
no_tasks do
|
@@ -44,12 +50,30 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
44
50
|
string
|
45
51
|
end
|
46
52
|
|
53
|
+
def add_references
|
54
|
+
belong_tos.map do |reference|
|
55
|
+
add_reference(reference)
|
56
|
+
end.join("\n")
|
57
|
+
end
|
58
|
+
|
47
59
|
def add_reference(reference)
|
48
|
-
|
49
|
-
type = reference_type(reference)
|
50
|
-
|
51
|
-
|
52
|
-
|
60
|
+
foreign_key = foreign_key_name(reference)
|
61
|
+
type = reference_type(reference).downcase
|
62
|
+
if foreign_key == "#{reference}_id"
|
63
|
+
string = " add_reference :#{table_name}, :#{reference}"
|
64
|
+
string += ", type: :#{type}" unless /int/.match?(type)
|
65
|
+
string += ", foreign_key: true" unless options[:skip_foreign_key_constraints]
|
66
|
+
string += ", index: {where: \"#{foreign_key} IS NOT NULL\"}" unless options[:skip_foreign_key_indexes]
|
67
|
+
else
|
68
|
+
string = " add_column :#{table_name}, :#{foreign_key}, :#{type}"
|
69
|
+
unless options[:skip_foreign_key_constraints]
|
70
|
+
referenced_table_name = reference_table_name(reference)
|
71
|
+
string += "\n add_foreign_key :#{table_name}, :#{referenced_table_name}, column: :#{foreign_key}"
|
72
|
+
end
|
73
|
+
unless options[:skip_foreign_key_indexes]
|
74
|
+
string += "\n add_index :#{table_name}, :#{foreign_key}, where: \"#{foreign_key} IS NOT NULL\""
|
75
|
+
end
|
76
|
+
end
|
53
77
|
string
|
54
78
|
end
|
55
79
|
|
@@ -62,15 +86,38 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
62
86
|
end
|
63
87
|
|
64
88
|
def reference_type(reference)
|
65
|
-
klass =
|
89
|
+
klass = class_name.constantize.reflections[reference].klass
|
66
90
|
klass.columns.find { |col| col.name == klass.primary_key }.sql_type
|
67
91
|
rescue
|
68
92
|
"bigint"
|
69
93
|
end
|
70
94
|
|
95
|
+
def reference_table_name(reference)
|
96
|
+
class_name.constantize.reflections[reference].klass.table_name
|
97
|
+
rescue
|
98
|
+
reference.tableize
|
99
|
+
end
|
100
|
+
|
101
|
+
def foreign_key_name(reference)
|
102
|
+
class_name.constantize.reflections[reference].foreign_key
|
103
|
+
rescue
|
104
|
+
"#{reference}_id"
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_check_constraint
|
108
|
+
return if options[:skip_check_constraint]
|
109
|
+
<<-RUBY
|
110
|
+
add_check_constraint(
|
111
|
+
:#{table_name},
|
112
|
+
"#{check_constraint}",
|
113
|
+
name: "#{arc}"
|
114
|
+
)
|
115
|
+
RUBY
|
116
|
+
end
|
117
|
+
|
71
118
|
def check_constraint
|
72
119
|
reference_checks = belong_tos.map do |reference|
|
73
|
-
"CASE WHEN #{reference}
|
120
|
+
"CASE WHEN #{foreign_key_name(reference)} IS NULL THEN 0 ELSE 1 END"
|
74
121
|
end
|
75
122
|
condition = options[:optional] ? "<= 1" : "= 1"
|
76
123
|
"(#{reference_checks.join(" + ")}) #{condition}"
|
@@ -1,10 +1,6 @@
|
|
1
1
|
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
2
|
def change
|
3
|
-
|
4
|
-
|
5
|
-
:<%= table_name %>,
|
6
|
-
"<%= check_constraint %>",
|
7
|
-
name: :<%= arc %>
|
8
|
-
)<% end %>
|
3
|
+
<%= add_references %>
|
4
|
+
<%= add_check_constraint %>
|
9
5
|
end
|
10
6
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-exclusive-arc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- justin talbott
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
89
|
- !ruby/object:Gem::Version
|
90
90
|
version: '0'
|
91
91
|
requirements: []
|
92
|
-
rubygems_version: 3.4.
|
92
|
+
rubygems_version: 3.4.12
|
93
93
|
signing_key:
|
94
94
|
specification_version: 4
|
95
95
|
summary: An ActiveRecord extension for polymorphic exclusive arc relationships
|