Skip to content

Commit

Permalink
Clean up Avram::Model to prepare for database views (#552)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewmcgarvey authored Dec 4, 2020
1 parent a76a1d7 commit 38c74a7
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 87 deletions.
18 changes: 0 additions & 18 deletions spec/model_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,6 @@ describe Avram::Model do
user.available_for_hire?.should be_false
end

it "can be used for params" do
now = Time.utc

user = User.new id: 123_i64,
name: "Name",
age: 24,
year_born: 1990_i16,
joined_at: now,
created_at: now,
updated_at: now,
nickname: "nick",
total_score: nil,
average_score: nil,
available_for_hire: nil

user.to_param.should eq "123"
end

it "sets up getters that parse the values" do
user = QueryMe.new id: 123_i64,
created_at: Time.utc,
Expand Down
5 changes: 4 additions & 1 deletion src/avram/base_query_template.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ class Avram::BaseQueryTemplate

def_clone
include Avram::Queryable({{ type }})
include Avram::PrimaryKeyQueryable({{ type }})

{% if type.resolve.has_constant?("PRIMARY_KEY_NAME") %}
include Avram::PrimaryKeyQueryable({{ type }})
{% end %}

macro generate_criteria_method(query_class, name, type)
def \{{ name }}
Expand Down
77 changes: 10 additions & 67 deletions src/avram/model.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,82 +8,27 @@ abstract class Avram::Model
include Avram::Polymorphic
include Avram::SchemaEnforcer

abstract def id

macro inherited
COLUMNS = [] of Nil # types are not checked in macros
ASSOCIATIONS = [] of Nil # types are not checked in macros
end

def_equals id, model_name
def self.primary_key_name : Symbol?
nil
end

def model_name
self.class.name
end

def to_param
id.to_s
end

# Reload the model with the latest information from the database
#
# This method will return a new model instance with the
# latest data from the database. Note that this does
# **not** change the original instance, so you may need to
# assign the result to a variable or work directly with the return value.
#
# Example:
#
# ```crystal
# user = SaveUser.create!(name: "Original")
# SaveUser.update!(user, name: "Updated")
#
# # Will be "Original"
# user.name
# # Will return "Updated"
# user.reload.name # Will be "Updated"
# # Will still be "Original" since the 'user' is the same model instance.
# user.name
#
# Instead re-assign the variable. Now 'name' will return "Updated" since
# 'user' references the reloaded model.
# user = user.reload
# user.name
# ```
# Refer to `PrimaryKeyMethods#reload`
def reload : self
base_query_class.find(id)
{% raise "Unable to call Avram::Model#reload on #{@type.name} because it does not have a primary key." %}
end

# Same as `reload` but allows passing a block to customize the query.
#
# This is almost always used to preload additional relationships.
#
# Example:
#
# ```crystal
# user = SaveUser.create(params)
#
# # We want to display the list of articles the user has commented on, so let's #
# # preload them to avoid N+1 performance issues
# user = user.reload(&.preload_comments(CommentQuery.new.preload_article))
#
# # Now we can safely get all the comment authors
# user.comments.map(&.article)
# ```
#
# Note that the yielded query is the `BaseQuery` so it will not have any
# methods defined on your customized query. This is usually fine since
# typically reload only uses preloads.
#
# If you do need to do something more custom you can manually reload:
#
# ```crystal
# user = SaveUser.create!(name: "Helen")
# UserQuery.new.some_custom_preload_method.find(user.id)
# ```
def reload : self
query = yield base_query_class.new
query.find(id)
# Refer to `PrimaryKeyMethods#reload`
def reload(&block) : self
{% raise "Unable to call Avram::Model#reload on #{@type.name} because it does not have a primary key." %}
end

macro table(table_name = nil)
Expand Down Expand Up @@ -115,13 +60,11 @@ abstract class Avram::Model
column {{ type_declaration.var }} : {{ type_declaration.type }}, autogenerated: true
alias PrimaryKeyType = {{ type_declaration.type }}

def self.primary_key_name : Symbol
def self.primary_key_name : Symbol?
:{{ type_declaration.var.stringify }}
end

def primary_key_name : Symbol
self.class.primary_key_name
end
include Avram::PrimaryKeyMethods

# If not using default 'id' primary key
{% if type_declaration.var.id != "id".id %}
Expand Down
68 changes: 68 additions & 0 deletions src/avram/primary_key_methods.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module Avram::PrimaryKeyMethods
def_equals id, model_name

def primary_key_name : Symbol?
self.class.primary_key_name
end

# Reload the model with the latest information from the database
#
# This method will return a new model instance with the
# latest data from the database. Note that this does
# **not** change the original instance, so you may need to
# assign the result to a variable or work directly with the return value.
#
# Example:
#
# ```crystal
# user = SaveUser.create!(name: "Original")
# SaveUser.update!(user, name: "Updated")
#
# # Will be "Original"
# user.name
# # Will return "Updated"
# user.reload.name # Will be "Updated"
# # Will still be "Original" since the 'user' is the same model instance.
# user.name
#
# Instead re-assign the variable. Now 'name' will return "Updated" since
# 'user' references the reloaded model.
# user = user.reload
# user.name
# ```
def reload : self
base_query_class.find(id)
end

# Same as `reload` but allows passing a block to customize the query.
#
# This is almost always used to preload additional relationships.
#
# Example:
#
# ```crystal
# user = SaveUser.create(params)
#
# # We want to display the list of articles the user has commented on, so let's #
# # preload them to avoid N+1 performance issues
# user = user.reload(&.preload_comments(CommentQuery.new.preload_article))
#
# # Now we can safely get all the comment authors
# user.comments.map(&.article)
# ```
#
# Note that the yielded query is the `BaseQuery` so it will not have any
# methods defined on your customized query. This is usually fine since
# typically reload only uses preloads.
#
# If you do need to do something more custom you can manually reload:
#
# ```crystal
# user = SaveUser.create!(name: "Helen")
# UserQuery.new.some_custom_preload_method.find(user.id)
# ```
def reload : self
query = yield base_query_class.new
query.find(id)
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Avram::SchemaEnforcer::EnsureUUIDPrimaryKeyHasDefault < Avram::SchemaEnforcer::Validation
def validate!
return unless uuid_primary_key? && missing_column_default?
return unless has_primary_key? && uuid_primary_key? && missing_column_default?

message = <<-TEXT
Primary key on the '#{table_name.colorize.bold}' table has the type set as uuid but does not have a default value.
Expand All @@ -22,6 +22,10 @@ class Avram::SchemaEnforcer::EnsureUUIDPrimaryKeyHasDefault < Avram::SchemaEnfor
raise Avram::SchemaMismatchError.new(message)
end

def has_primary_key?
!model_class.primary_key_name.nil?
end

def uuid_primary_key?
primary_key_info.data_type == "uuid"
end
Expand Down

0 comments on commit 38c74a7

Please sign in to comment.