Skip to content

Commit

Permalink
Add reload method (#491)
Browse files Browse the repository at this point in the history
* Add #reload method to Granite::Querying

Co-authored-by: jphaward <jphaward@gmail.com>

* Add documentation for #reload

Co-authored-by: jphaward <jphaward@gmail.com>

* Only define #reload when `Spec` module is defined

* Move macro condition inside #reload

Co-authored-by: Seth T. <crimsonknightstudios@gmail.com>

---------

Co-authored-by: jphaward <jphaward@gmail.com>
Co-authored-by: Seth T. <crimsonknightstudios@gmail.com>
  • Loading branch information
3 people authored Oct 26, 2023
1 parent a996473 commit faa7ea2
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 117 deletions.
12 changes: 12 additions & 0 deletions docs/crud.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ end
post = Post.first! # raises when no records exist
```

### reload

Returns the record with the attributes reloaded from the database.

**Note:** this method is only defined when the `Spec` module is present.

```
post = Post.create(name: "Granite Rocks!", body: "Check this out.")
# record gets updated by another process
post.reload # performs another find to fetch the record again
```

### where, order, limit, offset, group_by

See [querying](./querying.md) for more details of using the QueryBuilder.
Expand Down
24 changes: 24 additions & 0 deletions spec/granite/querying/reload_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require "../../spec_helper"

describe "#reload" do
before_each do
Parent.clear
end

it "reloads the record from the database" do
parent = Parent.create(name: "Parent")

Parent.find!(parent.id).update(name: "Other")

parent.reload.name.should eq "Other"
end

it "raises an error if the record no longer exists" do
parent = Parent.create(name: "Parent")
parent.destroy

expect_raises(Granite::Querying::NotFound) do
parent.reload
end
end
end
3 changes: 2 additions & 1 deletion src/granite/base.cr
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ abstract class Granite::Base
include ValidationHelpers
include Migrator
include Select
include Querying

include ConnectionManagement

extend Columns::ClassMethods
extend Tables::ClassMethods
extend Granite::Migrator::ClassMethods

extend Querying
extend Querying::ClassMethods
extend Query::BuilderMethods
extend Transactions::ClassMethods
extend Integrators
Expand Down
252 changes: 136 additions & 116 deletions src/granite/querying.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,153 +2,173 @@ module Granite::Querying
class NotFound < Exception
end

# Entrypoint for creating a new object from a result set.
def from_rs(result : DB::ResultSet) : self
model = new
model.new_record = false
model.from_rs result
model
end
module ClassMethods
# Entrypoint for creating a new object from a result set.
def from_rs(result : DB::ResultSet) : self
model = new
model.new_record = false
model.from_rs result
model
end

def raw_all(clause = "", params = [] of Granite::Columns::Type)
rows = [] of self
adapter.select(select_container, clause, params) do |results|
results.each do
rows << from_rs(results)
def raw_all(clause = "", params = [] of Granite::Columns::Type)
rows = [] of self
adapter.select(select_container, clause, params) do |results|
results.each do
rows << from_rs(results)
end
end
rows
end
rows
end

# All will return all rows in the database. The clause allows you to specify
# a WHERE, JOIN, GROUP BY, ORDER BY and any other SQL92 compatible query to
# your table. The result will be a Collection(Model) object which lazy loads
# an array of instantiated instances of your Model class.
# This allows you to take full advantage of the database
# that you are using so you are not restricted or dummied down to support a
# DSL.
# Lazy load prevent running unnecessary queries from unused variables.
def all(clause = "", params = [] of Granite::Columns::Type, use_primary_adapter = true)
switch_to_writer_adapter if use_primary_adapter == true
Collection(self).new(->{ raw_all(clause, params) })
end
# All will return all rows in the database. The clause allows you to specify
# a WHERE, JOIN, GROUP BY, ORDER BY and any other SQL92 compatible query to
# your table. The result will be a Collection(Model) object which lazy loads
# an array of instantiated instances of your Model class.
# This allows you to take full advantage of the database
# that you are using so you are not restricted or dummied down to support a
# DSL.
# Lazy load prevent running unnecessary queries from unused variables.
def all(clause = "", params = [] of Granite::Columns::Type, use_primary_adapter = true)
switch_to_writer_adapter if use_primary_adapter == true
Collection(self).new(->{ raw_all(clause, params) })
end

# First adds a `LIMIT 1` clause to the query and returns the first result
def first(clause = "", params = [] of Granite::Columns::Type)
all([clause.strip, "LIMIT 1"].join(" "), params, false).first?
end
# First adds a `LIMIT 1` clause to the query and returns the first result
def first(clause = "", params = [] of Granite::Columns::Type)
all([clause.strip, "LIMIT 1"].join(" "), params, false).first?
end

def first!(clause = "", params = [] of Granite::Columns::Type)
first(clause, params) || raise NotFound.new("No #{{{@type.name.stringify}}} found with first(#{clause})")
end
def first!(clause = "", params = [] of Granite::Columns::Type)
first(clause, params) || raise NotFound.new("No #{{{@type.name.stringify}}} found with first(#{clause})")
end

# find returns the row with the primary key specified. Otherwise nil.
def find(value)
first("WHERE #{primary_name} = ?", [value])
end
# find returns the row with the primary key specified. Otherwise nil.
def find(value)
first("WHERE #{primary_name} = ?", [value])
end

# find returns the row with the primary key specified. Otherwise raises an exception.
def find!(value)
find(value) || raise Granite::Querying::NotFound.new("No #{{{@type.name.stringify}}} found where #{primary_name} = #{value}")
end
# find returns the row with the primary key specified. Otherwise raises an exception.
def find!(value)
find(value) || raise Granite::Querying::NotFound.new("No #{{{@type.name.stringify}}} found where #{primary_name} = #{value}")
end

# Returns the first row found that matches *criteria*. Otherwise `nil`.
def find_by(**criteria : Granite::Columns::Type)
find_by criteria.to_h
end
# Returns the first row found that matches *criteria*. Otherwise `nil`.
def find_by(**criteria : Granite::Columns::Type)
find_by criteria.to_h
end

# :ditto:
def find_by(criteria : Granite::ModelArgs)
clause, params = build_find_by_clause(criteria)
first "WHERE #{clause}", params
end
# :ditto:
def find_by(criteria : Granite::ModelArgs)
clause, params = build_find_by_clause(criteria)
first "WHERE #{clause}", params
end

# Returns the first row found that matches *criteria*. Otherwise raises a `NotFound` exception.
def find_by!(**criteria : Granite::Columns::Type)
find_by!(criteria.to_h)
end
# Returns the first row found that matches *criteria*. Otherwise raises a `NotFound` exception.
def find_by!(**criteria : Granite::Columns::Type)
find_by!(criteria.to_h)
end

# :ditto:
def find_by!(criteria : Granite::ModelArgs)
find_by(criteria) || raise NotFound.new("No #{{{@type.name.stringify}}} found where #{criteria.map { |k, v| %(#{k} #{v.nil? ? "is NULL" : "= #{v}"}) }.join(" and ")}")
end
# :ditto:
def find_by!(criteria : Granite::ModelArgs)
find_by(criteria) || raise NotFound.new("No #{{{@type.name.stringify}}} found where #{criteria.map { |k, v| %(#{k} #{v.nil? ? "is NULL" : "= #{v}"}) }.join(" and ")}")
end

def find_each(clause = "", params = [] of Granite::Columns::Type, batch_size limit = 100, offset = 0, &)
find_in_batches(clause, params, batch_size: limit, offset: offset) do |batch|
batch.each do |record|
yield record
def find_each(clause = "", params = [] of Granite::Columns::Type, batch_size limit = 100, offset = 0, &)
find_in_batches(clause, params, batch_size: limit, offset: offset) do |batch|
batch.each do |record|
yield record
end
end
end
end

def find_in_batches(clause = "", params = [] of Granite::Columns::Type, batch_size limit = 100, offset = 0, &)
if limit < 1
raise ArgumentError.new("batch_size must be >= 1")
def find_in_batches(clause = "", params = [] of Granite::Columns::Type, batch_size limit = 100, offset = 0, &)
if limit < 1
raise ArgumentError.new("batch_size must be >= 1")
end

loop do
results = all "#{clause} LIMIT ? OFFSET ?", params + [limit, offset], false
break if results.empty?
yield results
offset += limit
end
end

loop do
results = all "#{clause} LIMIT ? OFFSET ?", params + [limit, offset], false
break if results.empty?
yield results
offset += limit
# Returns `true` if a records exists with a PK of *id*, otherwise `false`.
def exists?(id : Number | String | Nil) : Bool
return false if id.nil?
exec_exists "#{primary_name} = ?", [id]
end
end

# Returns `true` if a records exists with a PK of *id*, otherwise `false`.
def exists?(id : Number | String | Nil) : Bool
return false if id.nil?
exec_exists "#{primary_name} = ?", [id]
end
# Returns `true` if a records exists that matches *criteria*, otherwise `false`.
def exists?(**criteria : Granite::Columns::Type) : Bool
exists? criteria.to_h
end

# Returns `true` if a records exists that matches *criteria*, otherwise `false`.
def exists?(**criteria : Granite::Columns::Type) : Bool
exists? criteria.to_h
end
# :ditto:
def exists?(criteria : Granite::ModelArgs) : Bool
exec_exists *build_find_by_clause(criteria)
end

# :ditto:
def exists?(criteria : Granite::ModelArgs) : Bool
exec_exists *build_find_by_clause(criteria)
end
# count returns a count of all the records
def count : Int32
scalar "SELECT COUNT(*) FROM #{quoted_table_name}", &.to_s.to_i
end

# count returns a count of all the records
def count : Int32
scalar "SELECT COUNT(*) FROM #{quoted_table_name}", &.to_s.to_i
end
def exec(clause = "")
switch_to_writer_adapter
adapter.open(&.exec(clause))
end

def exec(clause = "")
switch_to_writer_adapter
adapter.open(&.exec(clause))
end
def query(clause = "", params = [] of Granite::Columns::Type, &)
switch_to_writer_adapter
adapter.open { |db| yield db.query(clause, args: params) }
end

def query(clause = "", params = [] of Granite::Columns::Type, &)
switch_to_writer_adapter
adapter.open { |db| yield db.query(clause, args: params) }
end
def scalar(clause = "", &)
switch_to_writer_adapter
adapter.open { |db| yield db.scalar(clause) }
end

def scalar(clause = "", &)
switch_to_writer_adapter
adapter.open { |db| yield db.scalar(clause) }
end
private def exec_exists(clause : String, params : Array(Granite::Columns::Type)) : Bool
self.adapter.exists? quoted_table_name, clause, params
end

private def exec_exists(clause : String, params : Array(Granite::Columns::Type)) : Bool
self.adapter.exists? quoted_table_name, clause, params
end
private def build_find_by_clause(criteria : Granite::ModelArgs)
keys = criteria.keys
criteria_hash = criteria.dup

private def build_find_by_clause(criteria : Granite::ModelArgs)
keys = criteria.keys
criteria_hash = criteria.dup
clauses = keys.map do |name|
if criteria_hash.has_key?(name) && !criteria_hash[name].nil?
matcher = "= ?"
else
matcher = "IS NULL"
criteria_hash.delete name
end

clauses = keys.map do |name|
if criteria_hash.has_key?(name) && !criteria_hash[name].nil?
matcher = "= ?"
else
matcher = "IS NULL"
criteria_hash.delete name
"#{quoted_table_name}.#{quote(name.to_s)} #{matcher}"
end

"#{quoted_table_name}.#{quote(name.to_s)} #{matcher}"
{clauses.join(" AND "), criteria_hash.values}
end

{clauses.join(" AND "), criteria_hash.values}
end

{% if parse_type("Spec").resolve? %}
# Returns the record with the attributes reloaded from the database.
#
# **Note:** this method is only defined when the `Spec` module is present.
#
# ```
# post = Post.create(name: "Granite Rocks!", body: "Check this out.")
# # record gets updated by another process
# post.reload # performs another find to fetch the record again
# ```
def reload
{% if !@top_level.has_constant? "Spec" %}
raise "#reload is a convenience method for testing only, please use #find in your application code"
{% end %}
self.class.find!(primary_key_value)
end
{% end %}
end

0 comments on commit faa7ea2

Please sign in to comment.