kashmir 0.1
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/Gemfile +4 -0
- data/Gemfile.lock +49 -0
- data/LICENSE.txt +22 -0
- data/README.md +563 -0
- data/Rakefile +11 -0
- data/kashmir.gemspec +35 -0
- data/lib/kashmir.rb +42 -0
- data/lib/kashmir/caching.rb +50 -0
- data/lib/kashmir/dsl.rb +41 -0
- data/lib/kashmir/extensions.rb +18 -0
- data/lib/kashmir/inline_dsl.rb +13 -0
- data/lib/kashmir/patches/active_record.rb +68 -0
- data/lib/kashmir/plugins/active_record_representation.rb +14 -0
- data/lib/kashmir/plugins/ar.rb +49 -0
- data/lib/kashmir/plugins/ar_relation.rb +35 -0
- data/lib/kashmir/plugins/memcached_caching.rb +83 -0
- data/lib/kashmir/plugins/memory_caching.rb +66 -0
- data/lib/kashmir/plugins/null_caching.rb +42 -0
- data/lib/kashmir/representable.rb +88 -0
- data/lib/kashmir/representation.rb +117 -0
- data/lib/kashmir/version.rb +3 -0
- data/test/activerecord_test.rb +127 -0
- data/test/activerecord_tricks_test.rb +54 -0
- data/test/ar_test_helper.rb +34 -0
- data/test/caching_test.rb +197 -0
- data/test/dsl_test.rb +162 -0
- data/test/inline_dsl_test.rb +108 -0
- data/test/kashmir_test.rb +216 -0
- data/test/support/ar_models.rb +69 -0
- data/test/support/factories.rb +33 -0
- data/test/support/schema.rb +32 -0
- data/test/test_helper.rb +9 -0
- metadata +217 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'ar_test_helper'
|
2
|
+
|
3
|
+
describe 'ActiveRecord performance tricks' do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
TestData.create_tom
|
7
|
+
TestData.create_netto
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "Query preload to avoid N+1 queries" do
|
11
|
+
|
12
|
+
it 'tries to preload records whenever possible' do
|
13
|
+
selects = track_queries do
|
14
|
+
AR::Chef.all.each do |chef|
|
15
|
+
chef.recipes.to_a
|
16
|
+
end
|
17
|
+
end
|
18
|
+
# SELECT * FROM chefs
|
19
|
+
# SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
|
20
|
+
# SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
|
21
|
+
assert_equal 3, selects.size
|
22
|
+
|
23
|
+
selects = track_queries do
|
24
|
+
AR::Chef.all.represent([:recipes])
|
25
|
+
end
|
26
|
+
# SELECT "chefs".* FROM "chefs"
|
27
|
+
# SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" IN (1, 2)
|
28
|
+
assert_equal 2, selects.size
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'preloads queries per each level in the tree' do
|
32
|
+
selects = track_queries do
|
33
|
+
AR::Chef.all.each do |chef|
|
34
|
+
chef.recipes.each do |recipe|
|
35
|
+
recipe.ingredients.to_a
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
# SELECT "chefs".* FROM "chefs"
|
40
|
+
# (2x) SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
|
41
|
+
# (4x) SELECT "ingredients".* FROM "ingredients" INNER JOIN "recipes_ingredients" ...
|
42
|
+
assert_equal 7, selects.size
|
43
|
+
|
44
|
+
selects = track_queries do
|
45
|
+
AR::Chef.all.represent([ :recipes => [ :ingredients => [:name] ] ])
|
46
|
+
end
|
47
|
+
# SELECT "chefs".* FROM "chefs"
|
48
|
+
# SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" IN (1, 2)
|
49
|
+
# SELECT "recipes_ingredients".* FROM "recipes_ingredients" WHERE "recipes_ingredients"."recipe_id" IN (1, 2, 3, 4)
|
50
|
+
# SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."id" IN (1, 2, 3, 4, 5, 6, 7, 8)
|
51
|
+
assert_equal 4, selects.size
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'minitest/around'
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
7
|
+
|
8
|
+
require 'support/schema'
|
9
|
+
require 'support/ar_models'
|
10
|
+
require 'support/factories'
|
11
|
+
|
12
|
+
|
13
|
+
class Minitest::Test
|
14
|
+
def around(&block)
|
15
|
+
ActiveRecord::Base.transaction do
|
16
|
+
block.call
|
17
|
+
raise ActiveRecord::Rollback
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def track_queries
|
23
|
+
selects = []
|
24
|
+
queries_collector = lambda do |name, start, finish, id, payload|
|
25
|
+
selects << payload
|
26
|
+
end
|
27
|
+
|
28
|
+
ActiveRecord::Base.connection.clear_query_cache
|
29
|
+
ActiveSupport::Notifications.subscribed(queries_collector, 'sql.active_record') do
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
|
33
|
+
selects.map { |sel| sel[:sql] }
|
34
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'ar_test_helper'
|
2
|
+
|
3
|
+
describe 'Caching' do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
|
7
|
+
Kashmir.init(
|
8
|
+
cache_client: Kashmir::Caching::Memory.new
|
9
|
+
)
|
10
|
+
|
11
|
+
TestData.create_tom
|
12
|
+
@restaurant = AR::Restaurant.find_by_name('Chef Tom Belly Burgers')
|
13
|
+
end
|
14
|
+
|
15
|
+
before(:each) do
|
16
|
+
Kashmir.caching.flush!
|
17
|
+
end
|
18
|
+
|
19
|
+
def from_cache(definition, model)
|
20
|
+
Kashmir.caching.from_cache(definition, model)
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'flat data' do
|
24
|
+
it 'loads the same exact data from cache' do
|
25
|
+
representation = @restaurant.represent([:name])
|
26
|
+
@restaurant.reload
|
27
|
+
cached_representation = @restaurant.represent([:name])
|
28
|
+
assert_equal representation, cached_representation
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'stores the data in cache' do
|
32
|
+
representation = @restaurant.represent([:name])
|
33
|
+
cached_representation = from_cache([:name], @restaurant)
|
34
|
+
assert_equal representation, cached_representation
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'references to other representations' do
|
39
|
+
it 'does not perform the same query twice' do
|
40
|
+
selects = track_queries do
|
41
|
+
@restaurant.represent([:name, :owner])
|
42
|
+
end
|
43
|
+
assert_equal 1, selects.size
|
44
|
+
|
45
|
+
# clear active record cache for this instance
|
46
|
+
@restaurant.reload
|
47
|
+
selects = track_queries do
|
48
|
+
@restaurant.represent([:name, :owner])
|
49
|
+
end
|
50
|
+
assert_equal 0, selects.size
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'loads the same exact data from cache' do
|
54
|
+
representation = @restaurant.represent([:name, :owner])
|
55
|
+
@restaurant.reload
|
56
|
+
cached_representation = @restaurant.represent([:name, :owner])
|
57
|
+
assert_equal representation, cached_representation
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'stores the data in cache at every level' do
|
61
|
+
representation = @restaurant.represent([:name, owner: [:name] ])
|
62
|
+
cached_restaurant_with_chef = from_cache([:name, owner: [ :name ]], @restaurant)
|
63
|
+
|
64
|
+
assert_equal representation, cached_restaurant_with_chef
|
65
|
+
|
66
|
+
chef = @restaurant.owner
|
67
|
+
cached_chef = from_cache([:name], chef)
|
68
|
+
|
69
|
+
assert_equal cached_chef, representation[:owner]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'nesting' do
|
74
|
+
before(:all) do
|
75
|
+
@chef = @restaurant.owner
|
76
|
+
@chef.reload
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'caches at every level' do
|
80
|
+
representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
|
81
|
+
fully_cached_chef = from_cache([:name, :restaurant =>[ :name, :rating =>[ :value ]]], @chef)
|
82
|
+
assert_equal representation, fully_cached_chef
|
83
|
+
|
84
|
+
fully_cached_restaurant = from_cache([ :name, :rating => [:value] ], @restaurant)
|
85
|
+
assert_equal representation[:restaurant], fully_cached_restaurant
|
86
|
+
|
87
|
+
cached_rating = from_cache([:value], @restaurant.rating)
|
88
|
+
assert_equal representation[:restaurant][:rating], cached_rating
|
89
|
+
|
90
|
+
assert_equal 3, Kashmir.caching.keys.size
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'tries to hit the cache at every level' do
|
94
|
+
selects = track_queries do
|
95
|
+
representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
|
96
|
+
end
|
97
|
+
# SELECT "restaurants".* FROM "restaurants" WHERE "restaurants"."owner_id" = ? LIMIT 1
|
98
|
+
# SELECT "ratings".* FROM "ratings" WHERE "ratings"."restaurant_id" = ? LIMIT 1
|
99
|
+
assert_equal selects.size, 2
|
100
|
+
|
101
|
+
@chef.reload
|
102
|
+
selects = track_queries do
|
103
|
+
representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
|
104
|
+
end
|
105
|
+
assert_equal selects.size, 0
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'tries to fill holes in the cache graph' do
|
109
|
+
definition = [:name, :restaurant =>[ :name, :rating =>[ :value ]]]
|
110
|
+
representation = @chef.represent(definition)
|
111
|
+
Kashmir.caching.clear(definition, @chef)
|
112
|
+
|
113
|
+
assert_equal 2, Kashmir.caching.keys.size
|
114
|
+
|
115
|
+
@chef.reload
|
116
|
+
selects = track_queries do
|
117
|
+
@chef.represent(definition)
|
118
|
+
end
|
119
|
+
# ratings is still cached
|
120
|
+
# SELECT "restaurants".* FROM "restaurants" WHERE "restaurants"."owner_id" = ? LIMIT 1
|
121
|
+
assert_equal selects.size, 1
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe 'collections' do
|
126
|
+
it 'caches every item' do
|
127
|
+
presented_recipes = AR::Recipe.all.represent([:title])
|
128
|
+
|
129
|
+
cached_keys = %w(
|
130
|
+
presenter:AR::Recipe:1:[:title]
|
131
|
+
presenter:AR::Recipe:2:[:title]
|
132
|
+
)
|
133
|
+
|
134
|
+
assert_equal cached_keys, Kashmir.caching.keys
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'presents from cache' do
|
138
|
+
selects = track_queries do
|
139
|
+
AR::Recipe.all.represent([:title, :ingredients => [:name]])
|
140
|
+
end
|
141
|
+
# SELECT "recipes_ingredients".* FROM "recipes_ingredients" WHERE "recipes_ingredients"."recipe_id" IN (1, 2)
|
142
|
+
# SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."id" IN (1, 2, 3, 4)
|
143
|
+
assert_equal 3, selects.size
|
144
|
+
|
145
|
+
selects = track_queries do
|
146
|
+
AR::Recipe.all.represent([:title, :ingredients => [:name]])
|
147
|
+
end
|
148
|
+
# SELECT "recipes".* FROM "recipes"
|
149
|
+
assert_equal 1, selects.size
|
150
|
+
|
151
|
+
cache_keys = [
|
152
|
+
"presenter:AR::Ingredient:1:[:name]",
|
153
|
+
"presenter:AR::Ingredient:2:[:name]",
|
154
|
+
"presenter:AR::Recipe:1:[:title, {:ingredients=>[:name]}]",
|
155
|
+
"presenter:AR::Ingredient:3:[:name]",
|
156
|
+
"presenter:AR::Ingredient:4:[:name]",
|
157
|
+
"presenter:AR::Recipe:2:[:title, {:ingredients=>[:name]}]"
|
158
|
+
]
|
159
|
+
|
160
|
+
assert_equal cache_keys, Kashmir.caching.keys
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe 'misc' do
|
165
|
+
|
166
|
+
it 'does not cache already cached results' do
|
167
|
+
@restaurant.represent([:name])
|
168
|
+
assert_equal 1, Kashmir.caching.keys.size
|
169
|
+
|
170
|
+
Kashmir.caching.expects(:store_presenter).never
|
171
|
+
@restaurant.represent([:name])
|
172
|
+
end
|
173
|
+
|
174
|
+
describe 'skipping cache' do
|
175
|
+
# see current_customer_count in test/support/ar_models.rb
|
176
|
+
|
177
|
+
before(:each) do
|
178
|
+
@restaurant.update_attributes(current_customer_count: 10)
|
179
|
+
@representation = @restaurant.represent([:name, :current_customer_count])
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'still renders everything normally' do
|
183
|
+
assert_equal @representation, { name: 'Chef Tom Belly Burgers', current_customer_count: 10 }
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'does not include that field in the key' do
|
187
|
+
assert_equal %w(presenter:AR::Restaurant:1:[:name]), Kashmir.caching.keys
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'does not insert value in the cached results' do
|
191
|
+
assert_nil Kashmir.caching.from_cache([:name, :current_customar_count], @restaurant)
|
192
|
+
assert_equal Kashmir.caching.from_cache([:name], @restaurant), { name: 'Chef Tom Belly Burgers' }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
data/test/dsl_test.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Kashmir do
|
4
|
+
|
5
|
+
module DSLTesting
|
6
|
+
class Recipe < OpenStruct
|
7
|
+
include Kashmir
|
8
|
+
|
9
|
+
representations do
|
10
|
+
rep(:num_steps)
|
11
|
+
rep(:title)
|
12
|
+
rep(:chef)
|
13
|
+
rep(:ingredients)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Chef < OpenStruct
|
18
|
+
include Kashmir
|
19
|
+
|
20
|
+
representations do
|
21
|
+
rep(:full_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def full_name
|
25
|
+
"#{first_name} #{last_name}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class SimpleRecipeRepresenter
|
30
|
+
include Kashmir::Dsl
|
31
|
+
|
32
|
+
prop :num_steps
|
33
|
+
prop :title
|
34
|
+
end
|
35
|
+
|
36
|
+
class ChefRepresenter
|
37
|
+
include Kashmir::Dsl
|
38
|
+
|
39
|
+
prop :full_name
|
40
|
+
end
|
41
|
+
|
42
|
+
class RecipeWithChefRepresenter
|
43
|
+
include Kashmir::Dsl
|
44
|
+
|
45
|
+
prop :title
|
46
|
+
embed :chef, ChefRepresenter
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
before do
|
51
|
+
@chef = DSLTesting::Chef.new(first_name: 'Netto', last_name: 'Farah')
|
52
|
+
@brisket = DSLTesting::Recipe.new(title: 'BBQ Brisket', num_steps: 2, chef: @chef)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'translates to representation definitions' do
|
56
|
+
definitions = DSLTesting::SimpleRecipeRepresenter.definitions
|
57
|
+
assert_equal definitions, [:num_steps, :title]
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'generates the same representations as hardcoded definitions' do
|
61
|
+
cooked_brisket = @brisket.represent([:num_steps, :title])
|
62
|
+
assert_equal cooked_brisket, { title: 'BBQ Brisket', num_steps: 2 }
|
63
|
+
assert_equal cooked_brisket, @brisket.represent(DSLTesting::SimpleRecipeRepresenter.definitions)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'generates nested representations' do
|
67
|
+
brisket_with_chef = @brisket.represent([:title, { :chef => [:full_name] }])
|
68
|
+
assert_equal brisket_with_chef, {
|
69
|
+
title: 'BBQ Brisket',
|
70
|
+
chef: {
|
71
|
+
full_name: 'Netto Farah'
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
assert_equal brisket_with_chef, @brisket.represent(DSLTesting::RecipeWithChefRepresenter.definitions)
|
76
|
+
end
|
77
|
+
|
78
|
+
describe 'Nested inline representations' do
|
79
|
+
|
80
|
+
module DSLTesting
|
81
|
+
class RecipeWithInlineChefRepresenter
|
82
|
+
include Kashmir::Dsl
|
83
|
+
|
84
|
+
prop :title
|
85
|
+
|
86
|
+
inline :chef do
|
87
|
+
prop :full_name
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Ingredient < OpenStruct
|
92
|
+
include Kashmir
|
93
|
+
|
94
|
+
representations do
|
95
|
+
rep(:name)
|
96
|
+
rep(:quantity)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'generates nested representations' do
|
102
|
+
brisket_with_chef = @brisket.represent([:title, { :chef => [:full_name] }])
|
103
|
+
assert_equal brisket_with_chef, {
|
104
|
+
title: 'BBQ Brisket',
|
105
|
+
chef: {
|
106
|
+
full_name: 'Netto Farah'
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
assert_equal brisket_with_chef, @brisket.represent(DSLTesting::RecipeWithInlineChefRepresenter.definitions)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'Collections' do
|
115
|
+
module DSLTesting
|
116
|
+
class RecipeWithIngredientsRepresenter
|
117
|
+
include Kashmir::Dsl
|
118
|
+
|
119
|
+
prop :title
|
120
|
+
|
121
|
+
inline :ingredients do
|
122
|
+
prop :name
|
123
|
+
prop :quantity
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'generates nested collections' do
|
129
|
+
@brisket.ingredients = [
|
130
|
+
DSLTesting::Ingredient.new(name: 'Beef', quantity: '8oz'),
|
131
|
+
DSLTesting::Ingredient.new(name: 'BBQ Sauce', quantity: 'plenty!'),
|
132
|
+
]
|
133
|
+
|
134
|
+
cooked_brisket = {
|
135
|
+
title: 'BBQ Brisket',
|
136
|
+
ingredients: [
|
137
|
+
{ name: 'Beef', quantity: '8oz' },
|
138
|
+
{ name: 'BBQ Sauce', quantity: 'plenty!' }
|
139
|
+
]
|
140
|
+
}
|
141
|
+
|
142
|
+
assert_equal cooked_brisket, @brisket.represent(DSLTesting::RecipeWithIngredientsRepresenter.definitions)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "Kashmir.props" do
|
147
|
+
|
148
|
+
module DSLTesting
|
149
|
+
|
150
|
+
class MultiPropRecipeRepresenter
|
151
|
+
include Kashmir::Dsl
|
152
|
+
|
153
|
+
props :num_steps, :title
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'allows the client to specify many props at once' do
|
158
|
+
definitions = DSLTesting::MultiPropRecipeRepresenter.definitions
|
159
|
+
assert_equal definitions, [:num_steps, :title]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|