From 3333eb07ce66d43b3dd84f8ffcb166a94917d58f Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Tue, 6 Dec 2016 08:54:28 -0600 Subject: [PATCH 01/10] belongs_to and has_many working --- spec/repo_spec.cr | 49 +++++++++++++++++++++++++++++++++ spec/spec_helper.cr | 24 +++++++--------- src/crecto.cr | 2 +- src/crecto/query.cr | 30 +++++++++++++------- src/crecto/repo.cr | 16 ++++++++--- src/crecto/schema.cr | 12 ++++---- src/crecto/schema/belongs_to.cr | 20 +++++++++++++- src/crecto/schema/has_many.cr | 22 +++++++++++++-- 8 files changed, 137 insertions(+), 38 deletions(-) diff --git a/spec/repo_spec.cr b/spec/repo_spec.cr index 2c3ac73..98273d2 100644 --- a/spec/repo_spec.cr +++ b/spec/repo_spec.cr @@ -236,12 +236,60 @@ describe Crecto do end end + describe "has_many" do + it "should load associations" do + user = User.new + user.name = "fridge" + user = Crecto::Repo.insert(user).instance + + post = Post.new + post.user_id = user.id.as(Int32) + Crecto::Repo.insert(post) + Crecto::Repo.insert(post) + + address = Address.new + address.user_id = user.id.as(Int32) + Crecto::Repo.insert(address) + + posts = Crecto::Repo.assoc(user, :posts).as(Array) + posts.size.should eq(2) + posts[0].user_id.should eq(user.id) + + addresses = Crecto::Repo.assoc(user, :addresses).as(Array) + addresses.size.should eq(1) + addresses[0].user_id.should eq(user.id) + end + end + + describe "belongs_to" do + it "should load the association" do + user = User.new + user.name = "fridge" + user = Crecto::Repo.insert(user).instance + + post = Post.new + post.user_id = user.id.as(Int32) + post = Crecto::Repo.insert(post).instance + + users = Crecto::Repo.assoc(post, :user).as(Array) + users[0].id.should eq(post.user_id) + end + end + describe "#delete_all" do it "should remove all records" do + Crecto::Repo.delete_all(Post) + Crecto::Repo.delete_all(Address) Crecto::Repo.delete_all(User) Crecto::Repo.delete_all(UserDifferentDefaults) Crecto::Repo.delete_all(UserLargeDefaults) + posts = Crecto::Repo.all(Post).as(Array) + posts.size.should eq 0 + + addresses = Crecto::Repo.all(Address).as(Array) + addresses.size.should eq 0 + users = Crecto::Repo.all(User).as(Array) users.size.should eq 0 @@ -252,5 +300,6 @@ describe Crecto do users.size.should eq 0 end end + end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 937e6bd..8bb4e13 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -13,8 +13,8 @@ class User field :yep, Bool field :some_date, Time field :pageviews, Int64 - has_many :posts, Post - has_one :thing, Thing + has_many :posts, Post, foreign_key: :user_id + has_many :addresses, Address, foreign_key: :user_id end validate_required :name @@ -153,26 +153,22 @@ class UserMultipleValidations inclusion: {in: 1..100} end -class Thing +class Address include Crecto::Schema + extend Crecto::Changeset(Address) - schema "things" do - belongs_to :user, User, foreign_key: "owner_id" + schema "addresses" do + field :user_id, Int32 + belongs_to :user, User end end class Post include Crecto::Schema + extend Crecto::Changeset(Post) schema "posts" do - belongs_to :user, User - end -end - -class Tester - include Crecto::Schema - - schema "testers" do - field :oof, String + field :user_id, Int32 + belongs_to :user, User, foreign_key: :user_id end end diff --git a/src/crecto.cr b/src/crecto.cr index d17e268..d7cc4c4 100644 --- a/src/crecto.cr +++ b/src/crecto.cr @@ -8,4 +8,4 @@ alias DbValue = Bool | Float64 | Int64 | Int32 | String | Time | Nil module Crecto -end +end \ No newline at end of file diff --git a/src/crecto/query.cr b/src/crecto/query.cr index 69dbac2..5bf63e8 100644 --- a/src/crecto/query.cr +++ b/src/crecto/query.cr @@ -26,16 +26,21 @@ module Crecto self.new.where(**wheres) end - # Key => Value pair(s) used in query `OR WHERE` - def self.or_where(**or_wheres) - self.new.or_where(**or_wheres) - end - # Query where with a string (i.e. `.where("users.id > 10")) def self.where(where_string : String, params : Array(DbValue)) self.new.where(where_string, params) end + # Query where with a Symbol and DbValue + def self.where(where_sym : Symbol, param : DbValue) + self.new.where(where_sym, param) + end + + # Key => Value pair(s) used in query `OR WHERE` + def self.or_where(**or_wheres) + self.new.or_where(**or_wheres) + end + # TODO: not done yet def self.join(klass, joins) self.new.join(klass, joins) @@ -73,14 +78,19 @@ module Crecto self end - def or_where(**or_wheres) - or_wheres = or_wheres.to_h - @or_wheres.push or_wheres + def where(where_string : String, params : Array(DbValue)) + @wheres.push({clause: where_string, params: params.map{|p| p.as(DbValue)}}) self end - def where(where_string : String, params : Array(DbValue)) - @wheres.push({clause: where_string, params: params.map{|p| p.as(DbValue)}}) + def where(where_sym : Symbol, param : DbValue) + @wheres.push(Hash.zip([where_sym], [param])) + self + end + + def or_where(**or_wheres) + or_wheres = or_wheres.to_h + @or_wheres.push or_wheres self end diff --git a/src/crecto/repo.cr b/src/crecto/repo.cr index dc568d2..1bf3f1a 100644 --- a/src/crecto/repo.cr +++ b/src/crecto/repo.cr @@ -7,10 +7,18 @@ module Crecto # query = Query.where(name: "fred") # users = Repo.all(User, query) # ``` - def self.all(queryable, query = Query.new) - query = Crecto::Adapters::Postgres.run(:all, queryable, query) - query.to_hash.map{|row| queryable.from_sql(row) } unless query.nil? - end + def self.all(queryable, query : Query = Query.new, **opts) + q = Crecto::Adapters::Postgres.run(:all, queryable, query) + q.to_hash.map{|row| queryable.from_sql(row) } unless q.nil? + end + + macro assoc(queryable_instance, association) + q = Crecto::Adapters::Postgres.run(:all, {{queryable_instance.id}}.class_for_association_{{association.id}}, Crecto::Repo::Query.where({{queryable_instance.id}}.foreign_key_for_association_{{association.id}}, {{queryable_instance}}.value_for_association_{{association.id}})) + unless q.nil? + {{association.id}} = q.to_hash.map{|row| {{queryable_instance.id}}.class_for_association_{{association.id}}.from_sql(row) } + {{queryable_instance}}.{{association.id}} = {{association.id}} + end + end # Return a single insance of `queryable` by primary key with *id*. # diff --git a/src/crecto/schema.cr b/src/crecto/schema.cr index 53a61ce..23f1f42 100644 --- a/src/crecto/schema.cr +++ b/src/crecto/schema.cr @@ -34,9 +34,6 @@ module Crecto # * `updated_at_field nil` - dont use the updated_at timestamp # module Schema - include Crecto::Schema::HasMany - include Crecto::Schema::HasOne - include Crecto::Schema::BelongsTo # Class constants CREATED_AT_FIELD = "created_at" @@ -45,10 +42,14 @@ module Crecto # schema block macro macro schema(table_name, &block) + include Crecto::Schema::HasMany + include Crecto::Schema::HasOne + include Crecto::Schema::BelongsTo + # macro constants VALID_FIELD_TYPES = [String, Int64, Int32, Float64, Bool, Time] VALID_FIELD_OPTIONS = [:primary_key, :virtual] - FIELDS = [] of String + FIELDS = [] of String # Class variables @@table_name = {{table_name.id.stringify}} @@ -150,7 +151,7 @@ module Crecto # Returns the value of the primary key field def pkey_value - self.{{PRIMARY_KEY_FIELD.id}} + self.{{PRIMARY_KEY_FIELD.id}}.as(Int32) end def update_primary_key(val) @@ -195,7 +196,6 @@ module Crecto @@changeset_fields end - # Class method to get the table name def self.table_name @@table_name diff --git a/src/crecto/schema/belongs_to.cr b/src/crecto/schema/belongs_to.cr index 4feeead..93492d4 100644 --- a/src/crecto/schema/belongs_to.cr +++ b/src/crecto/schema/belongs_to.cr @@ -4,7 +4,25 @@ module Crecto VALID_BELONGS_TO_OPTIONS = [:foreign_key] macro belongs_to(association_name, klass, **opts) - # puts "belongs to " + {{association_name.id.stringify}} + property {{association_name.id}} : Array({{klass}})? + + def class_for_association_{{association_name.id}} + {{klass}} + end + + def foreign_key_for_association_{{association_name.id}} + :id + end + + def value_for_association_{{association_name.id}} + val = {{klass.id.symbolize.downcase.id}}_id.as(Int32 | Int64) + + {% if opts[:foreign_key] %} + val = {{opts[:foreign_key].id}}.as(Int32 | Int64) + {% end %} + + val + end end end end diff --git a/src/crecto/schema/has_many.cr b/src/crecto/schema/has_many.cr index ed2f00e..fa87593 100644 --- a/src/crecto/schema/has_many.cr +++ b/src/crecto/schema/has_many.cr @@ -3,8 +3,26 @@ module Crecto module HasMany VALID_HAS_MANY_OPTIONS = [:foreign_key] - macro has_many(association_name, klass, **opts) - # puts "has many " + {{association_name.id.stringify}} + macro has_many(association_name, klass, **opts) + property {{association_name.id}} : Array({{klass}})? + + def class_for_association_{{association_name.id}} + {{klass}} + end + + def foreign_key_for_association_{{association_name.id}} + foreign_key = {{@type.id.symbolize.downcase}}_id + + {% if opts[:foreign_key] %} + foreign_key = {{opts[:foreign_key]}} + {% end %} + + foreign_key + end + + def value_for_association_{{association_name.id}} + pkey_value + end end end end From b6c91da0b2308170b60ebd3750805bf085a8d529 Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Wed, 7 Dec 2016 21:51:54 -0600 Subject: [PATCH 02/10] has many working (even with preload), belongs to is close --- .crystal-version | 2 +- spec/migrations/20161120183426_users.sql | 42 ++++++++++++++++ spec/repo_spec.cr | 27 ++++++++--- spec/spec_helper.cr | 62 +++++------------------- src/crecto/adapters/postgres_adapter.cr | 2 +- src/crecto/model.cr | 8 +++ src/crecto/query.cr | 9 ++++ src/crecto/repo.cr | 52 +++++++++++++++++--- src/crecto/schema.cr | 21 ++++++++ src/crecto/schema/belongs_to.cr | 27 +++++------ src/crecto/schema/has_many.cr | 27 +++++------ 11 files changed, 185 insertions(+), 94 deletions(-) create mode 100644 src/crecto/model.cr diff --git a/.crystal-version b/.crystal-version index 5a03fb7..847e9ae 100644 --- a/.crystal-version +++ b/.crystal-version @@ -1 +1 @@ -0.20.0 +0.20.1 diff --git a/spec/migrations/20161120183426_users.sql b/spec/migrations/20161120183426_users.sql index 60a4ae9..1206530 100644 --- a/spec/migrations/20161120183426_users.sql +++ b/spec/migrations/20161120183426_users.sql @@ -61,6 +61,44 @@ ALTER TABLE ONLY users_large_defaults ADD CONSTRAINT users_large_defaults_pkey P ALTER TABLE ONLY users_large_defaults ALTER COLUMN id SET DEFAULT nextval('users_large_defaults_id_seq'::regclass); CREATE UNIQUE INDEX users_4asdf ON users_large_defaults (id); +CREATE TABLE posts( + id INTEGER NOT NULL, + user_id INTEGER references users(id), + created_at timestamp without time zone, + updated_at timestamp without time zone +); + +CREATE SEQUENCE posts_id_seq + START WITH 1121 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE posts_id_seq OWNED BY posts.id; +ALTER TABLE ONLY posts ADD CONSTRAINT posts_pkey PRIMARY KEY (id); +ALTER TABLE ONLY posts ALTER COLUMN id SET DEFAULT nextval('posts_id_seq'::regclass); +CREATE UNIQUE INDEX posts_df8sdd ON posts (id); + +CREATE TABLE addresses( + id INTEGER NOT NULL, + user_id INTEGER references users(id), + created_at timestamp without time zone, + updated_at timestamp without time zone +); + +CREATE SEQUENCE addresses_id_seq + START WITH 1121 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE addresses_id_seq OWNED BY addresses.id; +ALTER TABLE ONLY addresses ADD CONSTRAINT addresses_pkey PRIMARY KEY (id); +ALTER TABLE ONLY addresses ALTER COLUMN id SET DEFAULT nextval('addresses_id_seq'::regclass); +CREATE UNIQUE INDEX addresses_dfd7fs7ss ON addresses (id); + COMMIT; -- +micrate Down @@ -70,3 +108,7 @@ DROP INDEX users_different_defaults_kljl3kj; DROP TABLE users_different_defaults; DROP INDEX users_4asdf; DROP TABLE users_large_defaults; +DROP INDEX posts_df8sdd; +DROP TABLE posts; +DROP INDEX addresses_dfd7fs7ss; +DROP TABLE addresses; diff --git a/spec/repo_spec.cr b/spec/repo_spec.cr index 98273d2..523ab1a 100644 --- a/spec/repo_spec.cr +++ b/spec/repo_spec.cr @@ -251,13 +251,13 @@ describe Crecto do address.user_id = user.id.as(Int32) Crecto::Repo.insert(address) - posts = Crecto::Repo.assoc(user, :posts).as(Array) + posts = Crecto::Repo.all(user, :posts).as(Array) posts.size.should eq(2) - posts[0].user_id.should eq(user.id) + posts[0].as(Post).user_id.should eq(user.id) - addresses = Crecto::Repo.assoc(user, :addresses).as(Array) + addresses = Crecto::Repo.all(user, :addresses).as(Array) addresses.size.should eq(1) - addresses[0].user_id.should eq(user.id) + addresses[0].as(Address).user_id.should eq(user.id) end end @@ -271,8 +271,23 @@ describe Crecto do post.user_id = user.id.as(Int32) post = Crecto::Repo.insert(post).instance - users = Crecto::Repo.assoc(post, :user).as(Array) - users[0].id.should eq(post.user_id) + users = Crecto::Repo.all(post, :user).as(Array) + users[0].as(User).id.should eq(post.user_id) + end + end + + describe "preload" do + it "should preload the association" do + user = User.new + user.name = "tester" + user = Crecto::Repo.insert(user).instance + + post = Post.new + post.user_id = user.id.as(Int32) + Crecto::Repo.insert(post) + Crecto::Repo.insert(post) + + users = Crecto::Repo.all(User, Crecto::Repo::Query.new, preload: [:posts]).as(Array) end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 8bb4e13..4fb5713 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,10 +1,7 @@ require "spec" require "../src/crecto" -class User - include Crecto::Schema - extend Crecto::Changeset(User) - +class User < Crecto::Model schema "users" do field :name, String field :things, Int32 @@ -20,10 +17,7 @@ class User validate_required :name end -class UserDifferentDefaults - include Crecto::Schema - extend Crecto::Changeset(UserDifferentDefaults) - +class UserDifferentDefaults < Crecto::Model created_at_field "xyz" updated_at_field nil @@ -35,10 +29,7 @@ class UserDifferentDefaults validate_required :name end -class UserLargeDefaults - include Crecto::Schema - extend Crecto::Changeset(UserLargeDefaults) - +class UserLargeDefaults < Crecto::Model created_at_field nil updated_at_field nil @@ -48,10 +39,7 @@ class UserLargeDefaults end end -class UserRequired - include Crecto::Schema - extend Crecto::Changeset(UserRequired) - +class UserRequired < Crecto::Model schema "users_required" do field :name, String field :age, Int32 @@ -62,10 +50,7 @@ class UserRequired validate_required [:age, :is_admin] end -class UserFormat - include Crecto::Schema - extend Crecto::Changeset(UserFormat) - +class UserFormat < Crecto::Model schema "users_required" do field :name, String field :age, Int32 @@ -75,10 +60,7 @@ class UserFormat validate_format :name, /[*a-zA-Z]/ end -class UserInclusion - include Crecto::Schema - extend Crecto::Changeset(UserInclusion) - +class UserInclusion < Crecto::Model schema "users_required" do field :name, String field :age, Int32 @@ -88,10 +70,7 @@ class UserInclusion validate_inclusion :name, ["bill", "ted"] end -class UserExclusion - include Crecto::Schema - extend Crecto::Changeset(UserExclusion) - +class UserExclusion < Crecto::Model schema "users_required" do field :name, String field :age, Int32 @@ -101,10 +80,7 @@ class UserExclusion validate_exclusion :name, ["bill", "ted"] end -class UserLength - include Crecto::Schema - extend Crecto::Changeset(UserLength) - +class UserLength < Crecto::Model schema "users_required" do field :name, String field :age, Int32 @@ -114,10 +90,7 @@ class UserLength validate_length :name, max: 5 end -class UserGenericValidation - include Crecto::Schema - extend Crecto::Changeset(UserGenericValidation) - +class UserGenericValidation < Crecto::Model schema "user_generic" do field :id, Int32, primary_key: true field :password, String, virtual: true @@ -131,10 +104,7 @@ class UserGenericValidation end end -class UserMultipleValidations - include Crecto::Schema - extend Crecto::Changeset(UserMultipleValidations) - +class UserMultipleValidations < Crecto::Model schema "users" do field :first_name, String field :last_name, String @@ -153,22 +123,16 @@ class UserMultipleValidations inclusion: {in: 1..100} end -class Address - include Crecto::Schema - extend Crecto::Changeset(Address) - +class Address < Crecto::Model schema "addresses" do field :user_id, Int32 belongs_to :user, User end end -class Post - include Crecto::Schema - extend Crecto::Changeset(Post) - +class Post < Crecto::Model schema "posts" do field :user_id, Int32 - belongs_to :user, User, foreign_key: :user_id + belongs_to :user, User end end diff --git a/src/crecto/adapters/postgres_adapter.cr b/src/crecto/adapters/postgres_adapter.cr index ecda771..1ecec97 100644 --- a/src/crecto/adapters/postgres_adapter.cr +++ b/src/crecto/adapters/postgres_adapter.cr @@ -203,7 +203,7 @@ module Crecto where.keys.map do |key| [where[key]].flatten.each{|param| params.push(param) } - resp = " #{queryable.table_name}.#{key}" + resp = " #{queryable.table_name}.#{key.to_s}".gsub(":", "") resp += if where[key].is_a?(Array) " IN (" + where[key].as(Array).map{|p| "?" }.join(", ") + ")" else diff --git a/src/crecto/model.cr b/src/crecto/model.cr new file mode 100644 index 0000000..b5b6c7b --- /dev/null +++ b/src/crecto/model.cr @@ -0,0 +1,8 @@ +module Crecto + abstract class Model + macro inherited + include Crecto::Schema + extend Crecto::Changeset({{@type}}) + end + end +end \ No newline at end of file diff --git a/src/crecto/query.cr b/src/crecto/query.cr index 5bf63e8..713d750 100644 --- a/src/crecto/query.cr +++ b/src/crecto/query.cr @@ -36,6 +36,10 @@ module Crecto self.new.where(where_sym, param) end + def self.where(where_sym : Symbol, params : Array(DbValue)) + self.new.where(where_sym, params) + end + # Key => Value pair(s) used in query `OR WHERE` def self.or_where(**or_wheres) self.new.or_where(**or_wheres) @@ -88,6 +92,11 @@ module Crecto self end + def where(where_sym : Symbol, params : Array(DbValue)) + @wheres.push({where_sym => params}) + self + end + def or_where(**or_wheres) or_wheres = or_wheres.to_h @or_wheres.push or_wheres diff --git a/src/crecto/repo.cr b/src/crecto/repo.cr index 1bf3f1a..91a785d 100644 --- a/src/crecto/repo.cr +++ b/src/crecto/repo.cr @@ -7,19 +7,57 @@ module Crecto # query = Query.where(name: "fred") # users = Repo.all(User, query) # ``` - def self.all(queryable, query : Query = Query.new, **opts) + def self.all(queryable, query : Query? = Query.new, **opts) + q = Crecto::Adapters::Postgres.run(:all, queryable, query) - q.to_hash.map{|row| queryable.from_sql(row) } unless q.nil? + return nil if q.nil? + + results = q.to_hash.map{|row| queryable.from_sql(row) }.as(Array) + + if preload = opts[:preload]? + add_preloads(results, queryable, preload) + end + + results + end + + def self.all(queryable_instance, association_name : Symbol) + query = Crecto::Repo::Query.where(queryable_instance.class.foreign_key_for_association(association_name), queryable_instance.pkey_value) + all(queryable_instance.class.klass_for_association(association_name), query) end - macro assoc(queryable_instance, association) - q = Crecto::Adapters::Postgres.run(:all, {{queryable_instance.id}}.class_for_association_{{association.id}}, Crecto::Repo::Query.where({{queryable_instance.id}}.foreign_key_for_association_{{association.id}}, {{queryable_instance}}.value_for_association_{{association.id}})) - unless q.nil? - {{association.id}} = q.to_hash.map{|row| {{queryable_instance.id}}.class_for_association_{{association.id}}.from_sql(row) } - {{queryable_instance}}.{{association.id}} = {{association.id}} + private def self.add_preloads(results, queryable, preloads) + preloads.each do |preload| + case queryable.association_type_for_association(preload) + when :has_many + has_many_preload(results, queryable, preload) + when :belongs_to + belongs_to_preload(results, queryable, preload) + end end end + private def self.has_many_preload(results, queryable, preload) + ids = results.map(&.pkey_value) + query = Crecto::Repo::Query.where(queryable.foreign_key_for_association(preload), ids) + k = queryable.klass_for_association(preload) + relation_items = all(k, query) + unless relation_items.nil? + relation_items = relation_items.group_by{|t| queryable.foreign_key_value_for_association(preload, t) } + + results.each do |result| + if relation_items.has_key?(result.id) + items = relation_items[result.id] + queryable.set_value_for_association(preload, result, items) + end + end + end + end + + private def self.belongs_to_preload(results, queryable, preload) + + end + # Return a single insance of `queryable` by primary key with *id*. # # ``` diff --git a/src/crecto/schema.cr b/src/crecto/schema.cr index 23f1f42..fbcb94f 100644 --- a/src/crecto/schema.cr +++ b/src/crecto/schema.cr @@ -39,6 +39,7 @@ module Crecto CREATED_AT_FIELD = "created_at" UPDATED_AT_FIELD = "updated_at" PRIMARY_KEY_FIELD = "id" + ASSOCIATIONS = Array(NamedTuple(association_type: Symbol, key: Symbol, klass: Model.class, foreign_key: Symbol, foreign_key_value: Proc(Model, (Int32 | Int64 | Nil)), set_association: Proc(Model, Array(Model), Nil) )).new # schema block macro macro schema(table_name, &block) @@ -201,6 +202,26 @@ module Crecto @@table_name end + def self.klass_for_association(association : Symbol) : Crecto::Model.class + ASSOCIATIONS.select{|a| a[:key] == association}[0][:klass] + end + + def self.foreign_key_for_association(association : Symbol) : Symbol + ASSOCIATIONS.select{|a| a[:key] == association}[0][:foreign_key] + end + + def self.foreign_key_value_for_association(association : Symbol, item) : (Int32 | Int64 | Nil) + ASSOCIATIONS.select{|a| a[:key] == association}[0][:foreign_key_value].call(item) + end + + def self.set_value_for_association(association : Symbol, item, items) + ASSOCIATIONS.select{|a| a[:key] == association}[0][:set_association].call(item, items) + end + + def self.association_type_for_association(association : Symbol) + ASSOCIATIONS.select{|a| a[:key] == association}[0][:association_type] + end + end end end diff --git a/src/crecto/schema/belongs_to.cr b/src/crecto/schema/belongs_to.cr index 93492d4..db7d2ce 100644 --- a/src/crecto/schema/belongs_to.cr +++ b/src/crecto/schema/belongs_to.cr @@ -6,23 +6,20 @@ module Crecto macro belongs_to(association_name, klass, **opts) property {{association_name.id}} : Array({{klass}})? - def class_for_association_{{association_name.id}} - {{klass}} - end + {% foreign_key = "id" %} - def foreign_key_for_association_{{association_name.id}} - :id - end + {% if opts[:foreign_key] %} + {% foreign_key = opts[:foreign_key] %} + {% end %} - def value_for_association_{{association_name.id}} - val = {{klass.id.symbolize.downcase.id}}_id.as(Int32 | Int64) - - {% if opts[:foreign_key] %} - val = {{opts[:foreign_key].id}}.as(Int32 | Int64) - {% end %} - - val - end + ASSOCIATIONS.push({ + association_type: :belongs_to, + key: {{association_name}}, + klass: {{klass}}, + foreign_key: {{foreign_key.symbolize}}, + foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}} }, + set_association: ->(self_item : Crecto::Model,items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.map{|i| i.as({{klass}}) };nil } + }) end end end diff --git a/src/crecto/schema/has_many.cr b/src/crecto/schema/has_many.cr index fa87593..e9c2933 100644 --- a/src/crecto/schema/has_many.cr +++ b/src/crecto/schema/has_many.cr @@ -6,23 +6,20 @@ module Crecto macro has_many(association_name, klass, **opts) property {{association_name.id}} : Array({{klass}})? - def class_for_association_{{association_name.id}} - {{klass}} - end + {% foreign_key = @type.id.stringify.downcase + "_id" %} - def foreign_key_for_association_{{association_name.id}} - foreign_key = {{@type.id.symbolize.downcase}}_id + {% if opts[:foreign_key] %} + {% foreign_key = opts[:foreign_key] %} + {% end %} - {% if opts[:foreign_key] %} - foreign_key = {{opts[:foreign_key]}} - {% end %} - - foreign_key - end - - def value_for_association_{{association_name.id}} - pkey_value - end + ASSOCIATIONS.push({ + association_type: :has_many, + key: {{association_name}}, + klass: {{klass}}, + foreign_key: {{foreign_key.symbolize}}, + foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}}.as(Int32 | Int64 | Nil) }, + set_association: ->(self_item : Crecto::Model,items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.map{|i| i.as({{klass}}) };nil } + }) end end end From 3e16d20140e254c6bd7c68c37db9ffbd3773236b Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Fri, 9 Dec 2016 09:55:40 -0600 Subject: [PATCH 03/10] added PkeyValue type, fixed belongs_to association --- spec/repo_spec.cr | 9 +++++---- src/crecto.cr | 1 + src/crecto/changeset/changeset.cr | 2 +- src/crecto/query.cr | 17 ++++++++++------- src/crecto/schema.cr | 10 +++++----- src/crecto/schema/belongs_to.cr | 4 ++-- src/crecto/schema/has_many.cr | 2 +- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/spec/repo_spec.cr b/spec/repo_spec.cr index 523ab1a..e794784 100644 --- a/spec/repo_spec.cr +++ b/spec/repo_spec.cr @@ -262,7 +262,7 @@ describe Crecto do end describe "belongs_to" do - it "should load the association" do + it "should set the belongs_to property" do user = User.new user.name = "fridge" user = Crecto::Repo.insert(user).instance @@ -270,9 +270,9 @@ describe Crecto do post = Post.new post.user_id = user.id.as(Int32) post = Crecto::Repo.insert(post).instance + post.user = user - users = Crecto::Repo.all(post, :user).as(Array) - users[0].as(User).id.should eq(post.user_id) + post.user.should eq(user) end end @@ -287,7 +287,8 @@ describe Crecto do Crecto::Repo.insert(post) Crecto::Repo.insert(post) - users = Crecto::Repo.all(User, Crecto::Repo::Query.new, preload: [:posts]).as(Array) + users = Crecto::Repo.all(User, Crecto::Repo::Query.where(id: user.id), preload: [:posts]).as(Array) + users[0].posts.as(Array).size.should eq(2) end end diff --git a/src/crecto.cr b/src/crecto.cr index d7cc4c4..3f03a92 100644 --- a/src/crecto.cr +++ b/src/crecto.cr @@ -5,6 +5,7 @@ require "./crecto/changeset/*" require "./crecto/*" alias DbValue = Bool | Float64 | Int64 | Int32 | String | Time | Nil +alias PkeyValue = Int32 | Int64 | String | Nil module Crecto diff --git a/src/crecto/changeset/changeset.cr b/src/crecto/changeset/changeset.cr index 123f4d9..4b6e2c1 100644 --- a/src/crecto/changeset/changeset.cr +++ b/src/crecto/changeset/changeset.cr @@ -135,7 +135,7 @@ module Crecto end private def diff_from_initial_values! - @initial_values = {} of Symbol => Int32 | Int64 | String | Float64 | Bool | Time | Nil if @initial_values.nil? + @initial_values = {} of Symbol => DbValue if @initial_values.nil? @changes.clear @instance_hash.each do |field, value| @changes.push({field => value}) if @initial_values.as(Hash).fetch(field, nil) != value diff --git a/src/crecto/query.cr b/src/crecto/query.cr index 713d750..e6ada71 100644 --- a/src/crecto/query.cr +++ b/src/crecto/query.cr @@ -1,7 +1,7 @@ module Crecto module Repo - alias WhereType = Hash(Symbol, Int32) | Hash(Symbol, String) | Hash(Symbol, Array(Int32)) | Hash(Symbol, Array(String)) | Hash(Symbol, Int32 | String) | Hash(Symbol, Int32 | Int64 | String | Nil) | NamedTuple(clause: String, params: Array(DbValue)) + alias WhereType = Hash(Symbol, PkeyValue) | Hash(Symbol, DbValue) | Hash(Symbol, Array(DbValue)) | Hash(Symbol, Array(PkeyValue)) | Hash(Symbol, Array(Int32)) | Hash(Symbol, Array(Int64)) | Hash(Symbol, Array(String)) | Hash(Symbol, Int32 | String) | Hash(Symbol, Int32) | Hash(Symbol, Int64) | Hash(Symbol, String) | Hash(Symbol, Array(PkeyValue)) | NamedTuple(clause: String, params: Array(DbValue | PkeyValue)) # Queries are used to retrieve and manipulate data from a repository. Syntax is much like that of ActiveRecord: # @@ -27,7 +27,7 @@ module Crecto end # Query where with a string (i.e. `.where("users.id > 10")) - def self.where(where_string : String, params : Array(DbValue)) + def self.where(where_string : String, params : Array(DbValue | PkeyValue)) self.new.where(where_string, params) end @@ -36,7 +36,7 @@ module Crecto self.new.where(where_sym, param) end - def self.where(where_sym : Symbol, params : Array(DbValue)) + def self.where(where_sym : Symbol, params : Array(DbValue | PkeyValue)) self.new.where(where_sym, params) end @@ -78,7 +78,9 @@ module Crecto # :nodoc: def where(**wheres) wheres = wheres.to_h - @wheres.push wheres + # w = {} of Symbol => DbValue | PkeyValue | Array(DbValue | PkeyValue) + # w[wheres.first_key] = wheres.first_value.as(DbValue | PkeyValue | Array(DbValue | PkeyValue)) + @wheres.push(Hash.zip(wheres.keys, wheres.values)) self end @@ -93,7 +95,9 @@ module Crecto end def where(where_sym : Symbol, params : Array(DbValue)) - @wheres.push({where_sym => params}) + w = {} of Symbol => Array(DbValue) + w[where_sym] = params.map{|x| x.as(DbValue) } + @wheres.push(w) self end @@ -128,5 +132,4 @@ module Crecto end end end -end - +end \ No newline at end of file diff --git a/src/crecto/schema.cr b/src/crecto/schema.cr index fbcb94f..f683441 100644 --- a/src/crecto/schema.cr +++ b/src/crecto/schema.cr @@ -39,7 +39,7 @@ module Crecto CREATED_AT_FIELD = "created_at" UPDATED_AT_FIELD = "updated_at" PRIMARY_KEY_FIELD = "id" - ASSOCIATIONS = Array(NamedTuple(association_type: Symbol, key: Symbol, klass: Model.class, foreign_key: Symbol, foreign_key_value: Proc(Model, (Int32 | Int64 | Nil)), set_association: Proc(Model, Array(Model), Nil) )).new + ASSOCIATIONS = Array(NamedTuple(association_type: Symbol, key: Symbol, klass: Model.class, foreign_key: Symbol, foreign_key_value: Proc(Model, PkeyValue), set_association: Proc(Model, Array(Model), Nil) )).new # schema block macro macro schema(table_name, &block) @@ -90,7 +90,7 @@ module Crecto # set `property` {% if field_type.id == "Int64" %} - property {{field_name.id}} : (Int64 | Int32 | Nil) + property {{field_name.id}} : PkeyValue {% else %} property {{field_name.id}} : {{field_type}}? {% end %} @@ -118,7 +118,7 @@ module Crecto macro setup extend BuildFromSQL - property {{PRIMARY_KEY_FIELD.id}} : (Int32 | Int64 | Nil) + property {{PRIMARY_KEY_FIELD.id}} : PkeyValue {% unless CREATED_AT_FIELD == nil %} property {{CREATED_AT_FIELD.id}} : Time? @@ -152,7 +152,7 @@ module Crecto # Returns the value of the primary key field def pkey_value - self.{{PRIMARY_KEY_FIELD.id}}.as(Int32) + self.{{PRIMARY_KEY_FIELD.id}}.as(PkeyValue) end def update_primary_key(val) @@ -210,7 +210,7 @@ module Crecto ASSOCIATIONS.select{|a| a[:key] == association}[0][:foreign_key] end - def self.foreign_key_value_for_association(association : Symbol, item) : (Int32 | Int64 | Nil) + def self.foreign_key_value_for_association(association : Symbol, item) : PkeyValue ASSOCIATIONS.select{|a| a[:key] == association}[0][:foreign_key_value].call(item) end diff --git a/src/crecto/schema/belongs_to.cr b/src/crecto/schema/belongs_to.cr index db7d2ce..2940760 100644 --- a/src/crecto/schema/belongs_to.cr +++ b/src/crecto/schema/belongs_to.cr @@ -4,7 +4,7 @@ module Crecto VALID_BELONGS_TO_OPTIONS = [:foreign_key] macro belongs_to(association_name, klass, **opts) - property {{association_name.id}} : Array({{klass}})? + property {{association_name.id}} : {{klass}}? {% foreign_key = "id" %} @@ -18,7 +18,7 @@ module Crecto klass: {{klass}}, foreign_key: {{foreign_key.symbolize}}, foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}} }, - set_association: ->(self_item : Crecto::Model,items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.map{|i| i.as({{klass}}) };nil } + set_association: ->(self_item : Crecto::Model, items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items[0].as({{klass}});nil } }) end end diff --git a/src/crecto/schema/has_many.cr b/src/crecto/schema/has_many.cr index e9c2933..ac7ddce 100644 --- a/src/crecto/schema/has_many.cr +++ b/src/crecto/schema/has_many.cr @@ -17,7 +17,7 @@ module Crecto key: {{association_name}}, klass: {{klass}}, foreign_key: {{foreign_key.symbolize}}, - foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}}.as(Int32 | Int64 | Nil) }, + foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}}.as(PkeyValue) }, set_association: ->(self_item : Crecto::Model,items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.map{|i| i.as({{klass}}) };nil } }) end From 9209258ffa1ecbcb3db8870349fec91829cf2f2e Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Sat, 10 Dec 2016 00:57:45 -0600 Subject: [PATCH 04/10] belongs_to and has_many working (really this time) --- spec/repo_spec.cr | 15 ++++++++++++++- spec/spec_helper.cr | 8 ++++++++ src/crecto/repo.cr | 21 +++++++++++++++++---- src/crecto/schema.cr | 12 ++++++------ src/crecto/schema/belongs_to.cr | 15 +++++++++------ src/crecto/schema/has_many.cr | 13 ++++++++----- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/spec/repo_spec.cr b/spec/repo_spec.cr index e794784..426decb 100644 --- a/spec/repo_spec.cr +++ b/spec/repo_spec.cr @@ -277,7 +277,7 @@ describe Crecto do end describe "preload" do - it "should preload the association" do + it "should preload the has_many association" do user = User.new user.name = "tester" user = Crecto::Repo.insert(user).instance @@ -290,6 +290,19 @@ describe Crecto do users = Crecto::Repo.all(User, Crecto::Repo::Query.where(id: user.id), preload: [:posts]).as(Array) users[0].posts.as(Array).size.should eq(2) end + + it "should preload the belongs_to association" do + user = User.new + user.name = "tester" + user = Crecto::Repo.insert(user).instance + + post = Post.new + post.user_id = user.id.as(Int32) + post = Crecto::Repo.insert(post).instance + + posts = Crecto::Repo.all(Post, Crecto::Repo::Query.where(id: post.id), preload: [:user]).as(Array) + posts[0].user.as(User).id.should eq(user.id) + end end describe "#delete_all" do diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 4fb5713..9c25706 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -24,6 +24,7 @@ class UserDifferentDefaults < Crecto::Model schema "users_different_defaults" do field :user_id, Int32, primary_key: true field :name, String + has_many :things, Thing end validate_required :name @@ -136,3 +137,10 @@ class Post < Crecto::Model belongs_to :user, User end end + +class Thing < Crecto::Model + schema "things" do + field :user_different_defaults_id, Int32 + belongs_to :user, User, foreign_key: :user_different_defaults_id + end +end diff --git a/src/crecto/repo.cr b/src/crecto/repo.cr index 91a785d..a54c1a9 100644 --- a/src/crecto/repo.cr +++ b/src/crecto/repo.cr @@ -40,14 +40,13 @@ module Crecto private def self.has_many_preload(results, queryable, preload) ids = results.map(&.pkey_value) query = Crecto::Repo::Query.where(queryable.foreign_key_for_association(preload), ids) - k = queryable.klass_for_association(preload) - relation_items = all(k, query) + relation_items = all(queryable.klass_for_association(preload), query) unless relation_items.nil? relation_items = relation_items.group_by{|t| queryable.foreign_key_value_for_association(preload, t) } results.each do |result| - if relation_items.has_key?(result.id) - items = relation_items[result.id] + if relation_items.has_key?(result.pkey_value) + items = relation_items[result.pkey_value] queryable.set_value_for_association(preload, result, items) end end @@ -55,7 +54,21 @@ module Crecto end private def self.belongs_to_preload(results, queryable, preload) + ids = results.map{|r| queryable.foreign_key_value_for_association(preload, r)} + query = Crecto::Repo::Query.where(id: ids) + relation_items = all(queryable.klass_for_association(preload), query) + unless relation_items.nil? + relation_items = relation_items.group_by{|t| t.pkey_value } + + results.each do |result| + fkey = queryable.foreign_key_value_for_association(preload, result) + if relation_items.has_key?(fkey) + items = relation_items[fkey] + queryable.set_value_for_association(preload, result, items) + end + end + end end # Return a single insance of `queryable` by primary key with *id*. diff --git a/src/crecto/schema.cr b/src/crecto/schema.cr index f683441..ff13017 100644 --- a/src/crecto/schema.cr +++ b/src/crecto/schema.cr @@ -39,7 +39,7 @@ module Crecto CREATED_AT_FIELD = "created_at" UPDATED_AT_FIELD = "updated_at" PRIMARY_KEY_FIELD = "id" - ASSOCIATIONS = Array(NamedTuple(association_type: Symbol, key: Symbol, klass: Model.class, foreign_key: Symbol, foreign_key_value: Proc(Model, PkeyValue), set_association: Proc(Model, Array(Model), Nil) )).new + ASSOCIATIONS = Array(NamedTuple(association_type: Symbol, key: Symbol, this_klass: Model.class, klass: Model.class, foreign_key: Symbol, foreign_key_value: Proc(Model, PkeyValue), set_association: Proc(Model, Array(Model), Nil) )).new # schema block macro macro schema(table_name, &block) @@ -203,23 +203,23 @@ module Crecto end def self.klass_for_association(association : Symbol) : Crecto::Model.class - ASSOCIATIONS.select{|a| a[:key] == association}[0][:klass] + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:klass] end def self.foreign_key_for_association(association : Symbol) : Symbol - ASSOCIATIONS.select{|a| a[:key] == association}[0][:foreign_key] + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:foreign_key] end def self.foreign_key_value_for_association(association : Symbol, item) : PkeyValue - ASSOCIATIONS.select{|a| a[:key] == association}[0][:foreign_key_value].call(item) + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:foreign_key_value].call(item) end def self.set_value_for_association(association : Symbol, item, items) - ASSOCIATIONS.select{|a| a[:key] == association}[0][:set_association].call(item, items) + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:set_association].call(item, items) end def self.association_type_for_association(association : Symbol) - ASSOCIATIONS.select{|a| a[:key] == association}[0][:association_type] + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:association_type] end end diff --git a/src/crecto/schema/belongs_to.cr b/src/crecto/schema/belongs_to.cr index 2940760..6e8582a 100644 --- a/src/crecto/schema/belongs_to.cr +++ b/src/crecto/schema/belongs_to.cr @@ -6,21 +6,24 @@ module Crecto macro belongs_to(association_name, klass, **opts) property {{association_name.id}} : {{klass}}? - {% foreign_key = "id" %} + {% + foreign_key = klass.id.stringify.underscore.downcase + "_id" - {% if opts[:foreign_key] %} - {% foreign_key = opts[:foreign_key] %} - {% end %} + if opts[:foreign_key] + foreign_key = opts[:foreign_key] + end + %} ASSOCIATIONS.push({ association_type: :belongs_to, key: {{association_name}}, + this_klass: {{@type}}, klass: {{klass}}, foreign_key: {{foreign_key.symbolize}}, - foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}} }, + foreign_key_value: ->(item : Crecto::Model){ item.as({{@type}}).{{foreign_key.id}}.as(PkeyValue) }, set_association: ->(self_item : Crecto::Model, items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items[0].as({{klass}});nil } }) end end end -end \ No newline at end of file +end diff --git a/src/crecto/schema/has_many.cr b/src/crecto/schema/has_many.cr index ac7ddce..eaf2442 100644 --- a/src/crecto/schema/has_many.cr +++ b/src/crecto/schema/has_many.cr @@ -6,19 +6,22 @@ module Crecto macro has_many(association_name, klass, **opts) property {{association_name.id}} : Array({{klass}})? - {% foreign_key = @type.id.stringify.downcase + "_id" %} + {% + foreign_key = @type.id.stringify.underscore.downcase + "_id" - {% if opts[:foreign_key] %} - {% foreign_key = opts[:foreign_key] %} - {% end %} + if opts[:foreign_key] + foreign_key = opts[:foreign_key] + end + %} ASSOCIATIONS.push({ association_type: :has_many, key: {{association_name}}, + this_klass: {{@type}}, klass: {{klass}}, foreign_key: {{foreign_key.symbolize}}, foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}}.as(PkeyValue) }, - set_association: ->(self_item : Crecto::Model,items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.map{|i| i.as({{klass}}) };nil } + set_association: ->(self_item : Crecto::Model, items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.map{|i| i.as({{klass}}) };nil } }) end end From 4dd3ca702a2b3e325edf7a73ea628df627086313 Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Sat, 10 Dec 2016 12:04:35 -0600 Subject: [PATCH 05/10] updated readme, more association related test, small bug fix --- README.md | 49 ++++++++++++++++++------- spec/repo_spec.cr | 2 +- spec/schema_spec.cr | 41 +++++++++++++++++++++ spec/spec_helper.cr | 2 +- src/crecto/adapters/postgres_adapter.cr | 2 +- src/crecto/schema.cr | 24 ++++++++---- src/crecto/schema/belongs_to.cr | 2 +- src/crecto/schema/has_many.cr | 2 +- src/crecto/version.cr | 2 +- 9 files changed, 99 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index af7d785..e72e5de 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,11 @@ Make sure you have `Set ENV['PG_URL']` set #### Roadmap (in no particular order) - [ ] MySQL adapter - - [ ] Choose adapter in config -- [ ] Associations - - [ ] Preload - - [ ] Joins +- [ ] SQLite adapter +- [ ] Choose adapter in config +- [x] Associations +- [x] Preload +- [ ] Joins ## Usage @@ -32,21 +33,28 @@ require "crecto" # # Define table name, fields and validations in your class # -class User - include Crecto::Schema - extend Crecto::Changeset(User) +class User < Crecto::Model schema "users" do field :age, Int32 field :name, String field :is_admin, Bool field :temporary_info, Float64, virtual: true + has_many :posts end validate_required [:name, :age] validate_format :name, /[*a-zA-Z]/ end +class Post < Crecto::Model + + schema "posts" do + field :user_id, PkValue + belongs_to :user + end +end + user = User.new user.name = "123" user.age = 123 @@ -87,28 +95,43 @@ query = Crecto::Repo::Query .limit(1) # -# all +# All # users = Crecto::Repo.all(User, query) users.as(Array) unless users.nil? # -# get by primary key +# Get by primary key # user = Crecto::Repo.get(User, 1) user.as(User) unless user.nil? # -# get by fields +# Get by fields # Crecto::Repo.get_by(User, name: "new name", id: 1121) user.as(User) unless user.nil? # -# delete +# Delete # changeset = Crecto::Repo.delete(user) ``` +# +# Associations +# + +user = Crecto::Repo.get(User, id).as(User) +posts = Crecto::Repo.all(user, :posts) + +# +# Preload associations +# +users = Crecto::Repo.all(User, Crecto::Query.new, preload: [:posts]) +users[0].posts # has_many relation is preloaded + +posts = Crecto::Repo.all(Post, Crecto::Query.new, preload: [:user]) +posts[0].user # belongs_to relation preloaded ## Performance @@ -121,9 +144,7 @@ changeset = Crecto::Repo.delete(user) ```Crystal require "crecto" -class User - include Crecto::Schema - extend Crecto::Changeset(User) +class User < Crecto::Model schema "users" do field :name, String diff --git a/spec/repo_spec.cr b/spec/repo_spec.cr index 426decb..b3cf43b 100644 --- a/spec/repo_spec.cr +++ b/spec/repo_spec.cr @@ -245,7 +245,7 @@ describe Crecto do post = Post.new post.user_id = user.id.as(Int32) Crecto::Repo.insert(post) - Crecto::Repo.insert(post) + post = Crecto::Repo.insert(post).instance address = Address.new address.user_id = user.id.as(Int32) diff --git a/spec/schema_spec.cr b/spec/schema_spec.cr index 7882417..24be302 100644 --- a/spec/schema_spec.cr +++ b/spec/schema_spec.cr @@ -116,5 +116,46 @@ describe Crecto do u.xyz.as(Time).epoch_ms.should be_close(Time.now.epoch_ms, 2000) end end + + describe "#klass_for_association" do + it "should return the correct class" do + User.klass_for_association(:posts).should eq(Post) + User.klass_for_association(:addresses).should eq(Address) + UserDifferentDefaults.klass_for_association(:things).should eq(Thing) + Address.klass_for_association(:user).should eq(User) + Post.klass_for_association(:user).should eq(User) + end + end + + describe "#foreign_key_for_association" do + it "should return the correct foreign key symbol" do + User.foreign_key_for_association(:posts).should eq(:user_id) + User.foreign_key_for_association(:addresses).should eq(:user_id) + Post.foreign_key_for_association(:user).should eq(:user_id) + end + end + + describe "#foreign_key_value_for_association" do + it "should return the correct foreign key value for associations" do + user = User.new + user.name = "tester" + user = Crecto::Repo.insert(user).instance + + post = Post.new + post.user_id = user.id.as(Int32) + post = Crecto::Repo.insert(post).instance + + User.foreign_key_value_for_association(:posts, post).should eq(post.user_id) + Post.foreign_key_value_for_association(:user, post).should eq(user.id) + end + end + + describe "#association_type_for_association" do + it "should return the association type symbol" do + User.association_type_for_association(:posts).should eq(:has_many) + User.association_type_for_association(:addresses).should eq(:has_many) + Post.association_type_for_association(:user).should eq(:belongs_to) + end + end end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 9c25706..e7c5939 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -141,6 +141,6 @@ end class Thing < Crecto::Model schema "things" do field :user_different_defaults_id, Int32 - belongs_to :user, User, foreign_key: :user_different_defaults_id + belongs_to :user, UserDifferentDefaults, foreign_key: :user_different_defaults_id end end diff --git a/src/crecto/adapters/postgres_adapter.cr b/src/crecto/adapters/postgres_adapter.cr index 1ecec97..a9190a8 100644 --- a/src/crecto/adapters/postgres_adapter.cr +++ b/src/crecto/adapters/postgres_adapter.cr @@ -203,7 +203,7 @@ module Crecto where.keys.map do |key| [where[key]].flatten.each{|param| params.push(param) } - resp = " #{queryable.table_name}.#{key.to_s}".gsub(":", "") + resp = " #{queryable.table_name}.#{key.to_s}" resp += if where[key].is_a?(Array) " IN (" + where[key].as(Array).map{|p| "?" }.join(", ") + ")" else diff --git a/src/crecto/schema.cr b/src/crecto/schema.cr index ff13017..7f2abd8 100644 --- a/src/crecto/schema.cr +++ b/src/crecto/schema.cr @@ -202,24 +202,34 @@ module Crecto @@table_name end - def self.klass_for_association(association : Symbol) : Crecto::Model.class - ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:klass] + # Get the Class for the assocation name + # i.e. :posts => Post + def self.klass_for_association(association : Symbol) + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}.first[:klass] end + # Get the foreign key for the association + # i.e. :posts => :user_id def self.foreign_key_for_association(association : Symbol) : Symbol - ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:foreign_key] + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}.first[:foreign_key] end - def self.foreign_key_value_for_association(association : Symbol, item) : PkeyValue - ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:foreign_key_value].call(item) + # Get the foreign key value from the relation object + # i.e. :posts, post => post.user_id + def self.foreign_key_value_for_association(association : Symbol, item) + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}.first[:foreign_key_value].call(item) end + # Set the value for the association + # i.e. :posts, user, [posts] => user.posts = [posts] def self.set_value_for_association(association : Symbol, item, items) - ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:set_association].call(item, items) + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}.first[:set_association].call(item, items) end + # Get the association type for the association + # i.e. :posts => :has_many def self.association_type_for_association(association : Symbol) - ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}[0][:association_type] + ASSOCIATIONS.select{|a| a[:key] == association && a[:this_klass] == self}.first[:association_type] end end diff --git a/src/crecto/schema/belongs_to.cr b/src/crecto/schema/belongs_to.cr index 6e8582a..50abd68 100644 --- a/src/crecto/schema/belongs_to.cr +++ b/src/crecto/schema/belongs_to.cr @@ -19,7 +19,7 @@ module Crecto key: {{association_name}}, this_klass: {{@type}}, klass: {{klass}}, - foreign_key: {{foreign_key.symbolize}}, + foreign_key: {{foreign_key.id.symbolize}}, foreign_key_value: ->(item : Crecto::Model){ item.as({{@type}}).{{foreign_key.id}}.as(PkeyValue) }, set_association: ->(self_item : Crecto::Model, items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items[0].as({{klass}});nil } }) diff --git a/src/crecto/schema/has_many.cr b/src/crecto/schema/has_many.cr index eaf2442..4de7e21 100644 --- a/src/crecto/schema/has_many.cr +++ b/src/crecto/schema/has_many.cr @@ -19,7 +19,7 @@ module Crecto key: {{association_name}}, this_klass: {{@type}}, klass: {{klass}}, - foreign_key: {{foreign_key.symbolize}}, + foreign_key: {{foreign_key.id.symbolize}}, foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}}.as(PkeyValue) }, set_association: ->(self_item : Crecto::Model, items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.map{|i| i.as({{klass}}) };nil } }) diff --git a/src/crecto/version.cr b/src/crecto/version.cr index 27a2f97..5693c96 100644 --- a/src/crecto/version.cr +++ b/src/crecto/version.cr @@ -1,3 +1,3 @@ module Crecto - VERSION = "0.2.0" + VERSION = "0.3.0" end From bd06765a6f4318e5b910cbe16a64e7a4bdb8303a Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Sat, 10 Dec 2016 16:53:03 -0600 Subject: [PATCH 06/10] set foreign key when setting belongs_to object --- README.md | 5 ++++- spec/repo_spec.cr | 10 ++++++++++ src/crecto/schema/belongs_to.cr | 6 ++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e72e5de..01e00b3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Make sure you have `Set ENV['PG_URL']` set - [ ] Choose adapter in config - [x] Associations - [x] Preload +- [ ] Need to be able to get parent relation from repo (`Repo.all/get(post, :user)`) +- [x] Need to be able to set parent object, instead of setting id manually before inserting - [ ] Joins ## Usage @@ -116,7 +118,7 @@ user.as(User) unless user.nil? # Delete # changeset = Crecto::Repo.delete(user) -``` + # # Associations # @@ -132,6 +134,7 @@ users[0].posts # has_many relation is preloaded posts = Crecto::Repo.all(Post, Crecto::Query.new, preload: [:user]) posts[0].user # belongs_to relation preloaded +``` ## Performance diff --git a/spec/repo_spec.cr b/spec/repo_spec.cr index b3cf43b..c1aa5d0 100644 --- a/spec/repo_spec.cr +++ b/spec/repo_spec.cr @@ -303,6 +303,16 @@ describe Crecto do posts = Crecto::Repo.all(Post, Crecto::Repo::Query.where(id: post.id), preload: [:user]).as(Array) posts[0].user.as(User).id.should eq(user.id) end + + it "should set the foreign key when setting the object" do + user = User.new + user.name = "tester" + user = Crecto::Repo.insert(user).instance + + post = Post.new + post.user = user + post.user_id.should eq(user.id) + end end describe "#delete_all" do diff --git a/src/crecto/schema/belongs_to.cr b/src/crecto/schema/belongs_to.cr index 50abd68..c14ae48 100644 --- a/src/crecto/schema/belongs_to.cr +++ b/src/crecto/schema/belongs_to.cr @@ -14,6 +14,12 @@ module Crecto end %} + def {{association_name.id}}=(val : {{klass}}?) + @{{association_name.id}} = val + return if val.nil? + @{{foreign_key.id}} = val.pkey_value.as(Int32) + end + ASSOCIATIONS.push({ association_type: :belongs_to, key: {{association_name}}, From f4d4706651b17e87ccdc425bc7eccb7b8e5c6f52 Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Sun, 11 Dec 2016 13:25:52 -0600 Subject: [PATCH 07/10] update ready --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 01e00b3..ab221ae 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Make sure you have `Set ENV['PG_URL']` set #### Roadmap (in no particular order) +- [ ] `select` in query - [ ] MySQL adapter - [ ] SQLite adapter - [ ] Choose adapter in config From 2047c1e6e911342c470cfa89b87df35b56a96112 Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Sat, 17 Dec 2016 11:14:02 -0600 Subject: [PATCH 08/10] associations working again! --- src/crecto/repo.cr | 4 ++-- src/crecto/schema/has_many.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crecto/repo.cr b/src/crecto/repo.cr index 2198b07..24ab794 100644 --- a/src/crecto/repo.cr +++ b/src/crecto/repo.cr @@ -52,7 +52,7 @@ module Crecto results.each do |result| if relation_items.has_key?(result.pkey_value) items = relation_items[result.pkey_value] - queryable.set_value_for_association(preload, result, items) + queryable.set_value_for_association(preload, result, items.map{|i| i.as(Crecto::Model) }) end end end @@ -70,7 +70,7 @@ module Crecto fkey = queryable.foreign_key_value_for_association(preload, result) if relation_items.has_key?(fkey) items = relation_items[fkey] - queryable.set_value_for_association(preload, result, items) + queryable.set_value_for_association(preload, result, items.map{|i| i.as(Crecto::Model) }) end end end diff --git a/src/crecto/schema/has_many.cr b/src/crecto/schema/has_many.cr index f109819..4de7e21 100644 --- a/src/crecto/schema/has_many.cr +++ b/src/crecto/schema/has_many.cr @@ -21,7 +21,7 @@ module Crecto klass: {{klass}}, foreign_key: {{foreign_key.id.symbolize}}, foreign_key_value: ->(item : Crecto::Model){ item.as({{klass}}).{{foreign_key.id}}.as(PkeyValue) }, - set_association: ->(self_item : Crecto::Model, items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.as(Array({{klass}}));nil } + set_association: ->(self_item : Crecto::Model, items : Array(Crecto::Model)){ self_item.as({{@type}}).{{association_name.id}} = items.map{|i| i.as({{klass}}) };nil } }) end end From 0f9e0a7326dbcb39fec971789971a184a6e62094 Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Sat, 17 Dec 2016 11:18:49 -0600 Subject: [PATCH 09/10] updated readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index d85f8c3..1bbec10 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,6 @@ Make sure you have `ENV['PG_URL']` set - [ ] Choose adapter in config - [x] Associations - [x] Preload -- [ ] Need to be able to get parent relation from repo (`Repo.all/get(post, :user)`) -- [x] Need to be able to set parent object, instead of setting id manually before inserting - [ ] Joins ## Usage From 26666ef3200185f3f68694fd7b8314ee4da87e31 Mon Sep 17 00:00:00 2001 From: Nick Franken Date: Sat, 17 Dec 2016 11:24:44 -0600 Subject: [PATCH 10/10] updated changelog, prep for v0.3.0 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7161f23..7beaa46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased + + +# [0.3.0] 2016-12-17 * Check for result.rows.size in queries - [@neovintage](https://github.com/neovintage) * `or_where` queries * `update_all` queries * `delete_all` queries * raw (aribtrary) sql queries (i.e. `Crecto::Repo.query("select * from users")`) - [@neovintage](https://github.com/neovintage) +* now using [`crystal-db`](https://github.com/crystal-lang/crystal-db) +* `has_many` associations, with preload +* `belongs_to` assocaitions ## [0.2.0] 2016-11-30 * Added this changelog @@ -24,4 +30,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * Query * Postgres Adapter +[0.3.0]: https://github.com/fridgerator/crecto/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/fridgerator/crecto/compare/0.1.0...v0.2.0