Skip to content

Commit

Permalink
Merge pull request #8 from TheDro/performance-memoize-instructions
Browse files Browse the repository at this point in the history
Performance memoize instructions
  • Loading branch information
TheDro authored Mar 17, 2022
2 parents e874136 + 05647af commit c41cdd9
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 67 deletions.
37 changes: 18 additions & 19 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
PATH
remote: .
specs:
gql_serializer (2.1.1)
activerecord (>= 5.2, < 6.1)
gql_serializer (2.2.0)
activerecord (>= 5.2, < 8.0)

GEM
remote: https://rubygems.org/
specs:
activemodel (6.0.3.5)
activesupport (= 6.0.3.5)
activerecord (6.0.3.5)
activemodel (= 6.0.3.5)
activesupport (= 6.0.3.5)
activesupport (6.0.3.5)
activemodel (6.1.5)
activesupport (= 6.1.5)
activerecord (6.1.5)
activemodel (= 6.1.5)
activesupport (= 6.1.5)
activesupport (6.1.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
coderay (1.1.3)
concurrent-ruby (1.1.8)
concurrent-ruby (1.1.9)
diff-lcs (1.4.4)
i18n (1.8.9)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
method_source (1.0.0)
minitest (5.14.4)
minitest (5.15.0)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
Expand All @@ -43,10 +43,9 @@ GEM
rspec-support (~> 3.10.0)
rspec-support (3.10.0)
sqlite3 (1.4.2)
thread_safe (0.3.6)
tzinfo (1.2.9)
thread_safe (~> 0.1)
zeitwerk (2.4.2)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
zeitwerk (2.5.4)

PLATFORMS
ruby
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ In a Rails application, the configuration can be added to an initializer in `con

```ruby
GqlSerializer.configure do |config|
config.case = GqlSerializer::Configuration::NONE_CASE # no case conversion
# no case conversion
config.case = GqlSerializer::Configuration::NONE_CASE
# set to true to avoid additional query in some cases.
# The default of false avoids a potential breaking change from version 2.1 to 2.2
config.preload = false
end
```

Expand Down
2 changes: 1 addition & 1 deletion gql_serializer.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "pry", "~> 0.13.1"
spec.add_development_dependency "sqlite3", "~> 1.4.2"

spec.add_runtime_dependency "activerecord", ">= 5.2", "< 7.0"
spec.add_runtime_dependency "activerecord", ">= 5.2", "< 8.0"
end
94 changes: 56 additions & 38 deletions lib/gql_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ def self.parse_query(input)

def self.query_include(model, hasharray)
include_array = []
relations = model.reflections.keys
hasharray.each do |e|
if e.is_a? String
key = e.split(':')[0]
include_array.push(key) if relations.include?(key)
elsif e.is_a? Hash
key = e.keys.first.split(':')[0]
relations = model.reflections
hasharray.each do |element|
if element.is_a? String
key = element.split(':')[0]
include_array.push(key) if relations[key]
elsif element.is_a? Hash
key = element.keys.first.split(':')[0]
relation_model = model.reflections[key].klass
relation_hasharray = self.query_include(relation_model, e.values.first)
relation_hasharray = self.query_include(relation_model, element.values.first)
if relation_hasharray.empty?
include_array.push(key)
else
Expand All @@ -46,53 +46,71 @@ def self.query_include(model, hasharray)
include_array
end

def self.serialize(record, hasharray, options)
# example hasharray = ["id", "name", "tags", { "panels" => ["id", { "cards" => ["content"] }] }]
def self.serialize(records, hasharray, options, instructions = {})

if record.nil?
if records.nil?
return nil
end

if record.respond_to? :map
return record.map do |r|
self.serialize(r, hasharray, options)

if records.respond_to? :map
return records.map do |record|
self.serialize(record, hasharray, options, instructions)
end
end
record = records

hash = {}
model = record.class
all_relations = model.reflections.keys
id = "#{record.class}, #{hasharray}"
instruction = instructions[id]
if (!instruction)
instruction = {klass: record.class, hasharray: hasharray, relations: [], attributes: []}
instructions[id] = instruction

relations = hasharray.filter do |e|
next true if !e.is_a?(String)

key, alias_key = e.split(':')
all_relations.include?(key)
end
model = record.class
all_relations = model.reflections.keys

attributes = hasharray - relations
attributes = model.attribute_names if attributes.empty?
relations = hasharray.filter do |relation|
next true if !relation.is_a?(String)

attributes.each do |e|
key, alias_key = e.split(':')
alias_key = apply_case(alias_key || key, options[:case])
key, alias_key = relation.split(':')

hash[alias_key] = coerce_value(record.public_send(key))
end
all_relations.include?(key)
end

relations.each do |e|
if e.is_a? String
key, alias_key = e.split(':')
alias_key = apply_case(alias_key || key, options[:case])
attributes = hasharray - relations
attributes = model.attribute_names if attributes.empty?

rel_records = record.public_send(key)
hash[alias_key] = self.serialize(rel_records, [], options)
else
key, alias_key = e.keys.first.split(':')
attributes.each do |attribute|
key, alias_key = attribute.split(':')
alias_key = apply_case(alias_key || key, options[:case])

rel_records = record.public_send(key)
hash[alias_key] = self.serialize(rel_records, e.values.first, options)
instruction[:attributes].push({key: key, alias_key: alias_key})
end

relations.each do |relation|
if relation.is_a? String
key, alias_key = relation.split(':')
alias_key = apply_case(alias_key || key, options[:case])

instruction[:relations].push({key: key, alias_key: alias_key, hasharray: []})
else
key, alias_key = relation.keys.first.split(':')
alias_key = apply_case(alias_key || key, options[:case])

instruction[:relations].push({key: key, alias_key: alias_key, hasharray: relation.values.first})
end
end
end

hash = {}
instruction[:attributes].each do |attribute|
hash[attribute[:alias_key]] = coerce_value(record.public_send(attribute[:key]))
end
instruction[:relations].each do |relation|
relation_records = record.public_send(relation[:key])
hash[relation[:alias_key]] = self.serialize(relation_records, relation[:hasharray], options, instructions)
end

hash
Expand Down
3 changes: 2 additions & 1 deletion lib/gql_serializer/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def initialize
reset
end

attr_reader :case
attr_reader :case, :preload

def case=(value)
raise "Specified case '#{value}' is not supported" unless SUPPORTED_CASES.include?(value)
Expand All @@ -20,6 +20,7 @@ def case=(value)

def reset
@case = NONE_CASE
@preload = false # Default will be true in version 3+
end

def to_h
Expand Down
24 changes: 19 additions & 5 deletions lib/gql_serializer/extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ def as_gql(query = nil)

module Relation
def as_gql(query = nil, options = {})
options_with_defaults = GqlSerializer.configuration.to_h.merge(options)
query_hasharray = query ? GqlSerializer.parse_query(query) : []
include_hasharray = GqlSerializer.query_include(self.model, query_hasharray)
records = self.includes(include_hasharray).records
options_with_defaults = GqlSerializer.configuration.to_h.merge(options)
GqlSerializer.serialize(records, query_hasharray, options_with_defaults)
end
end
Expand All @@ -23,12 +23,26 @@ def self.as_gql(query = nil, options = {})
end

def as_gql(query = nil, options = {})

options_with_defaults = GqlSerializer.configuration.to_h.merge(options)
query_hasharray = query ? GqlSerializer.parse_query(query) : []
include_hasharray = GqlSerializer.query_include(self.class, query_hasharray)
record = include_hasharray.empty? ? self : self.class.where(id: self).includes(include_hasharray).first
options_with_defaults = GqlSerializer.configuration.to_h.merge(options)
GqlSerializer.serialize(record, query_hasharray, options_with_defaults)
if options_with_defaults[:preload]
GqlSerializer._preload([self], include_hasharray)
GqlSerializer.serialize(self, query_hasharray, options_with_defaults)
else
record = include_hasharray.empty? ? self : self.class.where(id: self).includes(include_hasharray).first
GqlSerializer.serialize(record, query_hasharray, options_with_defaults)
end
end
end

def self._preload(records, include_hasharray)
if ::ActiveRecord::VERSION::MAJOR >= 7
::ActiveRecord::Associations::Preloader.
new(records: records, associations: include_hasharray).call
else
::ActiveRecord::Associations::Preloader.
new.preload(records, include_hasharray)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/gql_serializer/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module GqlSerializer
VERSION = "2.1.1"
VERSION = "2.2.0"
end
23 changes: 22 additions & 1 deletion spec/gql_serializer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,27 @@ def camelCase
GqlSerializer.configuration.reset
end
end
end

describe 'preload' do
it 'reloads records when disabled' do
original_user = TestUser.create(name: 'John', email: 'john@test.com')
TestOrder.create(total: 5.0, test_user: original_user)
same_user = TestUser.find(original_user.id)
same_user.update(name: 'David')

expect(original_user.as_gql('name email test_orders {total}'))
.to eq({'name' => 'David', 'email' => 'john@test.com', 'test_orders' => [{'total' => 5.0}]})
end

it 'preloads records when enabled' do
original_user = TestUser.create(name: 'John', email: 'john@test.com')
TestOrder.create(total: 5.0, test_user: original_user)
same_user = TestUser.find(original_user.id)
same_user.update(name: 'David')

expect(original_user.as_gql('name email test_orders {total}', {preload: true}))
.to eq({'name' => 'John', 'email' => 'john@test.com', 'test_orders' => [{'total' => 5.0}]})
end
end
end
end

0 comments on commit c41cdd9

Please sign in to comment.