Ruby on Rails Saturday, November 29, 2014

Null Objects:

A while back, Rails 4 introduced the concept of a Null Scope. Taking the form of Person.none (or some_company.people.none), it builds a Null Relation that behaves like a "real" Active Record relation, allowing scope chaining and other interface methods available from a scope (like #count), avoiding need for brittle nil checks. I'd like to discuss extending this a general concept of Null Forms in Active Record.

Let's start with the obvious issue of mapping SQL NULLs to Ruby nils. There is a conceptual mismatch here. SQL NULL means "unknown value", while Ruby nil means "no value". This manifests itself rather painfully in anything from JOIN to NOT IN (…) queries. To make things worse, saving data from a form-based request (the most common case of introducing data into Active Record in most Rails apps) will pass blank strings from an empty form. So now you have a mix of NULLs and blank strings in your database, depending on whether or not a form ever updated the record, even if it was saved without any intentional user input.

Null Form primitives:

It may be possible to design the database to simply not allow any NULL attribute values and instead default the appropriate Null Form:

  • '' for varchars
  • 0 for ints
  • [] for serialized arrays
  • {} for Hstore or other serialized hashes

Etc.

One could argue, though, that such a design introduces excessive logic and performance penalty on the database layer, and that instead, it should be Ruby which uses the Null Form object whenever the database contains a NULL value. So, for example, if I have a database record in the users table with id=1name=NULL, and I do User.find(1).name, I'd prefer to get '' instead of nil.

So essentially this is a question of whether Active Record type casting should cast NULL values into their Null Form object of the appropriate type, rather than nil. But I see the obvious issues with this approach as well, notably whether #attributes should return the raw nil or use the casted Null Form, especially for APIs, so perhaps manually using database default values with null: false constraints is still the best way to accomplish this.

Null Form associations:

NULL-to-nil mismatches are yet more painful when dealing with associations, like document.project.account.name. We need to do nil checks everywhere, or use try everywhere, or delegate ... allow_nil: true hacks. How much nicer would it be if there was a Null Form association? Imagine the following:

document.project_id => nil
document
.project => <#Project id=nil>
document
.project.persisted? => false
document
.project.null? => true
document
.project.account_id => nil
document
.project.account.null? => true
document
.project.account.name? => '' # (or `nil` if not also using Null Form primitives)


Similar to the Null Primitive concept, the idea is to avoid extraneous nil checks. If the context and data type is known beforehand (and it is in the case of ActiveRecord due to schema introspection, the same logic that allows Active Record to know whether an attribute should be casted to string or integer, for example), having Null Form logic would bring a lot of the benefit of types languages into Rails, while reducing (rather than increasing) the maintenance burden and logical complexity of code. In the case of associations, this information could be inferred from has_many / has_one / belongs_to definitions, and/or introspected from used foreign keys (added in Rails 4.2).

In fact, existing code (notably accepts_nested_attributes) already expects you to manually build a new child instance if it is nil and you want an inline child object form, making you write code like @user.build_profile if @user.profile.nil? .

Discussion:

Both nil primitives and nil associations can be dealt with manually, via either explicit nil checks at point of invocation, or by using the above suggestions (such as database null: false, default: '' for blank string, controller @user.build_profile if @user.profile.nil? for blank associations, etc.) But all of these approach feel tedious and brittle, and (more importantly) add unnecessary complexity to understanding and reasoning about code.

I wonder whether anyone had experience implementing some kind of programmatic Null Form behavior for either attribute primitives, associations, or both? Did you only use application-level solutions similar to above? Or did you try some kind of framework-level approach to dynamically build Null Form objects for handling database NULL values? How well did the rest of your code, as well as Rails handle it? For example, there is no way in Ruby to override the truthiness of a Null Object for `if object { a } else { b }` type comparisons, so any object other than nil would behave differently, including any custom Null Form.

Or, perhaps, there is already some literature you know, or ideas you have, about the best practices to better handle nil primitives/associations? I'd love reading about it.

Thanks!

--
You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-talk+unsubscribe@googlegroups.com.
To post to this group, send email to rubyonrails-talk@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/82c60e70-49d9-4720-8771-e985e6a04df2%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

No comments:

Post a Comment