Skip to content

Commit

Permalink
New Query Cache Feature (part deux) (#763)
Browse files Browse the repository at this point in the history
* Adds in the ability to store query results in cache. Fixes #63

* Move the query cache setup to Fiber so it's cleared per HTTP request. This also circumvents the singlton setup from Habitat that would cause your query cache to be stored in redis or something

* Caching queries for longer periods will be done at a lower level cache. Remove these

* cache any? and select_count queryable methods. Making cache_key public

* lock lucky_cache in to specific version
  • Loading branch information
jwoertink authored Nov 29, 2021
1 parent 9a5a921 commit e8cbde0
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 13 deletions.
3 changes: 3 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ dependencies:
pulsar:
github: luckyframework/pulsar
version: ~> 0.2.2
lucky_cache:
github: luckyframework/lucky_cache
version: ~> 0.1.0

development_dependencies:
ameba:
Expand Down
33 changes: 32 additions & 1 deletion spec/queryable_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,20 @@ end
class PostQuery < Post::BaseQuery
end

class UserQuery
class_property query_counter : Int32 = 0

private def exec_query
@@query_counter += 1
super
end
end

describe Avram::Queryable do
Spec.before_each do
UserQuery.query_counter = 0
end

it "can chain scope methods" do
ChainedQuery.new.young.named("Paul")
end
Expand Down Expand Up @@ -1358,7 +1371,7 @@ describe Avram::Queryable do

users = UserQuery.new.group(&.age).group(&.id)
users.query.statement.should eq "SELECT #{User::COLUMN_SQL} FROM users GROUP BY users.age, users.id"
users.map(&.name).should eq ["Dwight", "Michael", "Jim"]
users.map(&.name).sort!.should eq ["Dwight", "Jim", "Michael"]
end

it "raises an error when grouped incorrectly" do
Expand Down Expand Up @@ -1471,4 +1484,22 @@ describe Avram::Queryable do
query.to_sql.should eq original_query_sql
end
end

describe "with query cache" do
it "only runs the query once" do
# We're testing the actual caching
Fiber.current.query_cache = LuckyCache::MemoryStore.new
Avram.temp_config(query_cache_enabled: true) do
UserFactory.create &.name("Amy")
UserQuery.query_counter.should eq(0)

UserQuery.new.name("Amy").first
UserQuery.new.name("Amy").first
user = UserQuery.new.name("Amy").first

UserQuery.query_counter.should eq(1)
user.name.should eq("Amy")
end
end
end
end
4 changes: 4 additions & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Db::VerifyConnection.new(quiet: true).run_task

Spec.before_each do
TestDatabase.truncate
# All specs seem to run on the same Fiber,
# so we set back to NullStore before each spec
# to ensure queries aren't randomly cached
Fiber.current.query_cache = LuckyCache::NullStore.new
end

class SampleBackupDatabase < Avram::Database
Expand Down
2 changes: 2 additions & 0 deletions src/avram.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require "dexter"
require "wordsmith"
require "habitat"
require "pulsar"
require "lucky_cache"
require "db"
require "pg"
require "uuid"
Expand All @@ -22,6 +23,7 @@ module Avram
setting database_to_migrate : Avram::Database.class, example: "AppDatabase"
setting time_formats : Array(String) = [] of String
setting i18n_backend : Avram::I18nBackend = Avram::I18n.new, example: "Avram::I18n.new"
setting query_cache_enabled : Bool = false
end

Log = ::Log.for(Avram)
Expand Down
12 changes: 12 additions & 0 deletions src/avram/charms/fiber.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# https://crystal-lang.org/api/latest/Fiber.html
class Fiber
# This is stored on Fiber so it's released after each
# HTTP Request.
property query_cache : LuckyCache::BaseStore do
if Avram.settings.query_cache_enabled
LuckyCache::MemoryStore.new
else
LuckyCache::NullStore.new
end
end
end
1 change: 1 addition & 0 deletions src/avram/model.cr
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ abstract class Avram::Model
macro inherited
COLUMNS = [] of Nil # types are not checked in macros
ASSOCIATIONS = [] of Nil # types are not checked in macros
include LuckyCache::Cachable
end

def self.primary_key_name : Symbol?
Expand Down
40 changes: 28 additions & 12 deletions src/avram/queryable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -214,23 +214,29 @@ module Avram::Queryable(T)
end

def any? : Bool
queryable = clone
new_query = queryable.query.limit(1).select("1 AS one")
results = database.query_one?(new_query.statement, args: new_query.args, queryable: schema_class.name, as: Int32)
!results.nil?
cache_store.fetch(cache_key, as: Bool) do
queryable = clone
new_query = queryable.query.limit(1).select("1 AS one")
results = database.query_one?(new_query.statement, args: new_query.args, queryable: schema_class.name, as: Int32)
!results.nil?
end
end

def none? : Bool
!any?
end

def select_count : Int64
table = "(#{query.statement}) AS temp"
new_query = Avram::QueryBuilder.new(table).select_count
result = database.scalar new_query.statement, args: query.args, queryable: schema_class.name
result.as(Int64)
rescue e : DB::NoResultsError
0_i64
cache_store.fetch(cache_key, as: Int64) do
begin
table = "(#{query.statement}) AS temp"
new_query = Avram::QueryBuilder.new(table).select_count
result = database.scalar new_query.statement, args: query.args, queryable: schema_class.name
result.as(Int64)
rescue e : DB::NoResultsError
0_i64
end
end
end

def each
Expand All @@ -245,9 +251,19 @@ module Avram::Queryable(T)
@preloads << block
end

def cache_store
Fiber.current.query_cache
end

def cache_key : String
[query.statement, query.args].join(':')
end

def results : Array(T)
exec_query.tap do |records|
preloads.each(&.call(records))
cache_store.fetch(cache_key, as: Array(T)) do
exec_query.tap do |records|
preloads.each(&.call(records))
end
end
end

Expand Down

0 comments on commit e8cbde0

Please sign in to comment.