Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Query Cache Feature (part deux) #763

Merged
merged 5 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
branch: master

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you update some other column on users and then fetch in the same way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd get the cached version, not the updated version. Though, inserts and updates aren't cached here, which means if you did update a record, you'd have access to that updated record through the save operation. As of right now, the other option would be to flush all the cache.

We could do a cache.delete(the_key) type deal, but that goes to my question about the key. You'd have to know how it was derived... We can't just put a cache_key method on the models, because you're not necessarily caching a single model. Maybe cache_key becomes a public method?


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
14 changes: 12 additions & 2 deletions src/avram/queryable.cr
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,19 @@ module Avram::Queryable(T)
@preloads << block
end

def cache_store
Fiber.current.query_cache
end

private 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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't log anything if it returns the cached value. We can't add logging to the block because that gets cached, so would it make sense to have fetch do its own logging?

exec_query.tap do |records|
preloads.each(&.call(records))
end
end
end

Expand Down