From 2485429e066b12a3f259891e85cb508cc20903dd Mon Sep 17 00:00:00 2001 From: Morgan Aubert Date: Sun, 9 Jun 2024 22:29:05 -0400 Subject: [PATCH] #236 - Make it possible to initialize new Model instances from query sets --- .../reference/query-set.md | 18 ++++++++++++ spec/marten/db/query/related_set_spec.cr | 28 +++++++++++++++++++ spec/marten/db/query/set_spec.cr | 19 +++++++++++++ src/marten/db/query/set.cr | 27 ++++++++++++++++++ 4 files changed, 92 insertions(+) diff --git a/docs/docs/models-and-databases/reference/query-set.md b/docs/docs/models-and-databases/reference/query-set.md index 3773f9560..a545846a5 100644 --- a/docs/docs/models-and-databases/reference/query-set.md +++ b/docs/docs/models-and-databases/reference/query-set.md @@ -337,6 +337,24 @@ electronic_products = Product.filter(category: "Electronics") average_rating = electronic_products.average(:rating) ``` +### `build` + +Initializes a new model instance. + +This method allows initializing a new model instance using the arguments defined in the passed double splat argument. + +```crystal +new_post = Post.all.build(title: "My blog post") +``` + +This method can also be called with a block that is executed for the new object: + +```crystal +new_post = Post.all.build(title: "My blog post") do |p| + p.complex_attribute = compute_complex_attribute +end +``` + ### `bulk_create` Bulk inserts the passed model instances into the database. diff --git a/spec/marten/db/query/related_set_spec.cr b/spec/marten/db/query/related_set_spec.cr index f0e58c35d..207b747a9 100644 --- a/spec/marten/db/query/related_set_spec.cr +++ b/spec/marten/db/query/related_set_spec.cr @@ -16,6 +16,34 @@ describe Marten::DB::Query::RelatedSet do end end + describe "#build" do + it "initializes a new record with the related field set" do + user = TestUser.create!(username: "jd1", email: "jd1@example.com", first_name: "John", last_name: "Doe") + + qset = Marten::DB::Query::RelatedSet(Post).new(user, "author_id") + + new_post = qset.build(title: "Post") + + new_post.persisted?.should be_false + new_post.author.should eq user + new_post.title.should eq "Post" + end + + it "initializes a new record with the related field set when a block is used" do + user = TestUser.create!(username: "jd1", email: "jd1@example.com", first_name: "John", last_name: "Doe") + + qset = Marten::DB::Query::RelatedSet(Post).new(user, "author_id") + + new_post = qset.build do |p| + p.title = "Post" + end + + new_post.persisted?.should be_false + new_post.author.should eq user + new_post.title.should eq "Post" + end + end + describe "#create" do it "creates a new record with the related field set" do user = TestUser.create!(username: "jd1", email: "jd1@example.com", first_name: "John", last_name: "Doe") diff --git a/spec/marten/db/query/set_spec.cr b/spec/marten/db/query/set_spec.cr index 55d9d3655..da6401d33 100644 --- a/spec/marten/db/query/set_spec.cr +++ b/spec/marten/db/query/set_spec.cr @@ -767,6 +767,25 @@ describe Marten::DB::Query::Set do end end + describe "#build" do + it "returns the non-persisted model instance initialized from the specified parameters" do + tag = Marten::DB::Query::Set(Tag).new.build(name: "New tag") + + tag.persisted?.should be_false + tag.name.should eq "New tag" + end + + it "returns the non-persisted model instance initialized from the specified parameters and block" do + tag = Marten::DB::Query::Set(Tag).new.build(is_active: true) do |o| + o.name = "New tag" + end + + tag.persisted?.should be_false + tag.is_active.should be_true + tag.name.should eq "New tag" + end + end + describe "#create" do it "returns the non-persisted model instance if it is invalid" do tag = Marten::DB::Query::Set(Tag).new.create(name: nil) diff --git a/src/marten/db/query/set.cr b/src/marten/db/query/set.cr index 8d58630a6..e9927cf68 100644 --- a/src/marten/db/query/set.cr +++ b/src/marten/db/query/set.cr @@ -163,6 +163,33 @@ module Marten @query.average(field.try(&.to_s)) end + # Initializes a new model instance. + # + # The new model instance is initialized by using the attributes defined in the `kwargs` double splat argument. + # + # ``` + # new_post = Post.all.build(title: "My blog post") + # ``` + def build(**kwargs) + build_record(**kwargs) + end + + # Initializes a new model instance. + # + # This method provides the exact same behaviour as `#build` with the ability to define a block that is executed + # for the new object. This block can be used to directly initialize the new model instance. + # + # ``` + # new_post = Post.all.build(title: "My blog post") do |p| + # p.complex_attribute = compute_complex_attribute + # end + # ``` + def build(**kwargs, &) + object = build_record(**kwargs) + yield object + object + end + # Bulk inserts the passed model instances into the database. # # This method allows to insert multiple model instances into the database in a single query. This can be useful