activerecord-exclusive-arc 0.2.1 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|