-
Notifications
You must be signed in to change notification settings - Fork 87
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
Refactor initialization #370
Refactor initialization #370
Conversation
Don't make columns nilable by default anymore No longer include *::Serializable by default
Use column_names when initializing from result_set Use property! for not nilable columns Add converter for bool type (for Sqlite)
Simplify initialization again
I like the idea of empowering the developer with the ability to override the functionality of the initalizer, but I also to boilerplate to a minimum. My priorities go something like this:
This seems like it might be closely related with the idea that we could remove the column macro and replace it with a columns macro. |
How would this help? Personally I'd rather see the @[Granite::Column(auto: true, primary: true, nilable: true)]
getter id : Int64?
@[Granite::Column(nilable: true)]
property year : Int32?
@[Granite::Column]
property! name : String One thought I had was if we don't define an initializer, and make the ivars nilable behind the scenes, then you could still do I suppose even if we still define our built in initializers, they could still define their own if they wanted more custom behavior? I'm conflicted on how to balance everything :/ |
@robacarp @Blacksmoke16 My take is that the user will not expect (Roberts' #2) an initializer to be created for me. I would not expect a not_nilable property to be nilable, so I would expect that I would need to create an To me, the I vote we remove some of the magic. maybe we can remove the Also, adding |
Right, if we go that way the only initializer that will be defined by default is the argless one that comes with abstract class Base
end
class User < Base
property name : String?
end
User.new But if you want to initialize class User < Base
property name : String?
def initialize(@name : String); end
end
User.new "Fred" Obviously if you add in
I think there might be some misunderstanding on our viewpoints of this. My thought is that internally all properties should be nilable, not the getter/setter for those properties. The current implementation of using
Using this class: class User
property! name : String
end
# Are still able to new up a model
user = User.new # => @name=nil
# Trying to access the value at this point would raise an exception
# After https://github.com/crystal-lang/crystal/pull/8296 is merged it would be like
user.name # => Unhandled exception: User#name cannot be nil (NilAssertionError)
# The default getter removes `Nil` from the union so you won't run into like
# `undefined method '+' for Nil (compile-time type is (String | Nil))`
user.name = "Fred"
typeof(user.name) # => String
# A nilable query method is also added
typeof(user.name?) # => String?
# `Nil` is removed from the setter types so this won't compile
#
# Error: no overload matches 'User#name=' with type Nil
#
# Overloads are:
# - User#name=(name : String)
user.name = nil IMO this gives us more options internally while still giving good protection to the end user; such as partial hydration, allowing some properties to be set later (that would be not null in the DB) instead of requiring everything up front. An example of when this would be useful would be: Imagine a JSON api where a user can create a blog post with a body like: {
"title": "Title of blog",
"published": true,
...
} Every post was created by a user, so there would also be a
I'm assuming this would handle |
@Blacksmoke16 thanks for the details.
would it make more sense to use |
I don't think so. Documentation would probably do the trick. I'm not sure what benefit it would bring, the type of the column should be enough indication of what it will do. |
it "ignores the value in default" do | ||
Parent.new(id: 1_i64).id.should eq(nil) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wouldn't compile anymore assuming the user doesn't include id
within the initializer (which wouldn't make sense since its auto increment anyway).
pending "does not save a model with type conversion errors" do | ||
model = Comment.new(articleid: "foo") | ||
model.errors.size.should eq 1 | ||
model.save.should be_false | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This wouldn't compile anymore since the initializer would be expecting an Int64?
not String
. If the user wanted they could accept both and set articleid
within the initialize method; converting the input value if needed.
pending "does not update when the conflicted primary key is given to the new record" do | ||
parent1 = Parent.new | ||
parent1.name = "Test Parent" | ||
parent1.save.should be_true | ||
|
||
parent2 = Parent.new | ||
parent2.id = parent1.id | ||
parent2.name = "Test Parent2" | ||
parent2.save.should be_false | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't be able to do this anymore by default
In spec/granite/transactions/save_spec.cr:49:13
49 | parent2.id = parent1.id
^-
Error: undefined method 'id=' for Parent
src/granite/transactions.cr
Outdated
@@ -187,16 +185,6 @@ module Granite::Transactions | |||
save || raise Granite::RecordNotSaved.new(self.class.name, self) | |||
end | |||
|
|||
def update(*args, **named_args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably support #update
if we wanted.
- Call
#initialize
with the values - Support only named arguments and do a loop with a case statement to set the ivar when there is a match
@robacarp @drujensen I updated the code to support no default initializers, as that seems to be the direction we're learning towards. I also left some comments on test cases that I removed with the reasoning behind why they were removed. So far from there I would suggest checking out this branch on some small local projects and see how things work. From there we'll still need to:
|
@robacarp @drujensen One other thing I thought of. Currently we require the PK to be nilable since it wouldn't have one until it was saved. However that makes things like user = User.first!
user.id + 1 fail since I'm thinking the majority of cases are going to be working with objects that originate from a DB query, thus having a PK. So:
I'm thinking 2. |
Require PK type be not nilable Add specs to make sure internal ivars are not included in .to_json/yaml
@Blacksmoke16 hiding a nilable field from the implementer feels risky to me. I think at this point in my Crystal experience, I'd like to see a different object which represents persisted database entries and has a non-nilable primary key and other fields. I digress. That said, I think the switch you made here is pretty straightforward. Having a nilable PK field is a pain over and over again -- any nilable fields are. |
I should probably stop saying
A whole separate object isn't ideal IMO. What would that get us that |
Well, the point of using a non-nilable field is that you get compile time checks against it. And for situations where the field is actually nil able, an unsaved model, you get compile time enforcement that the field’s nil-ability must be handled. With the |
My thinking is that this is not much different than before where we had This implementation provides the best middleground of protection and useability. As i mentioned in #370 (comment) |
@Blacksmoke16 @robacarp Where did we end up with this PR? This is almost a year old and not sure what we decided. |
@drujensen IIRC, we never came to a consensus if this is something we wanted to move forward with; given it's a breaking change and changes the semantics quite a bit. |
Preface: This is still a WIP and are a few things left to do. However I wanted to make the draft to start to discuss how we expect this to ultimately work.
Changes
*::Serializable
Granite::Type.convert_type
set_attributes
#initialize
methods#create
and#update
to use themHash
Initialization/ivars
The big questions I have are:
Model.new hash
?property!
?#initialize
for the user in first place?From what I did so far, these are my findings (not diving in to deep).
Supporting 1:
String
when column is typed as aBool
Supporting 2:
nil
nil
or the column's default if definedreadonly
, not able to be set by the client.Supporting 3:
Once we figure this out I'll update tests to make sure everything works as expected for the chosen implementation.