Provide JOINS and INCLUDES querying methods and some thoughts
OPENING FOR DISCUSSION
After reading the QUERYING section of the README https://github.com/amberframework/granite-orm#queries it seems more natural and explicit to have an includes and joins methods since current approach is implicit on how to use joins.
One of the drawbacks that I see with the current implementation is that the JOIN would have to live with the query was defined making the query less reusable.
Examples:
# Simple Joins
Post.joins(:users).all('...')
# Custom Joins
Post.joins("JOINS users u ON u.id = posts.id").all('...')
# Nested Joins
Post.joins(users: :addresses).all('...')
# Scoped methods
def self.with_users
joins(:users)
end
def self.with_users_addresses
joins(users: :addresses)
end
# Usage
Post.with_users.all('where age > 20')
Post.with_users.all('where age > 40 AND age < 50')
Benefits
- Easily express simple and nested joins.
- Explicitly defined joins
- Allow scoped methods
- More reusable queries
- Complement the association methods to build through associations.
Other Thoughts/Questions
What about adding a where to allow even more reusable queries for scopes? Or this is currently possible by chaining all?
def self.with_users
joins(:users)
end
def self.above_twenty
where('age > 20') # or where(age: [40..50])
end
Conclusion
I know we would like to keep the DSL simple, but we are going to need to implement more methods where, limit, select, merge, to simplified and make queries methods more reusable.
I was with you until I read your conclusion. The assumption that you need a DSL to support OOP and ActiveRecord design pattern is questionable but...
I am in agreement that providing helper methods to simplify complex joins has value. I'm not yet convinced we should DSL the whole query language into an AREL like language just because. I find SQL is easier to read and understand but I may be in the minority.
@drujensen I have clarified my thoughts. It was not what I meant to say. I had a thought never finished writing it
I had this thought...*
As Per Active Record and OOP if feels like Granite should be more of an ORM mapper than Active Record style. Active Record heavily favors encapsulation to the point where testing without a database is quite difficult. So writing unit tests for Models would require mocking the entire database (query). This is beneficial since it keeps unit tests super fast and with no dependencies as unit tests should be.
Many people put business rule methods in their Active Record classes; which makes them appear to be objects when they should actually be service objects. This leads to a dilemma. On which side of the line does the Active Record really fall? Is it an object? Or is it a data structure?
ORM mappers separate this 2 concerns (Object and Persistent Layer Access), this is a broader discussion and it should not probably affect the direction of Granite at this moment it was just that was crossing my mind at the time of writing.
@eliasjpr Thanks for the clarification. I agree with your comment regarding Active Record and it's one reason we should be careful not to head down the slippery slope of trying to do too much in Granite. I also see people tend to put too much logic in their models. It's easy to break the S in SOLID.
Regarding your question of what is Active Record, an object or a data structure? It's an object that has a single responsibility of "retrieving and storing persistent data". This is a good reason to look at promoting Serializers Pattern instead of trying to add JSON mapping logic into Granite.
We want to keep Granite focused on the single responsibility of "retrieving and storing persistent data".
Your comment of ORM mappers separating the 2 concerns, I don't really follow that. An object always has the data structure and the methods that work on that data. This to me is a single concern. You could separate the data from the functionality but then you are no longer doing OO and you lose the capabilities of encapsulation, inheritance and other advantages that OO brings. We can debate if these two concerns should be separated or united and that debate has been raging for decades. You can probably tell where I fall. ;-)
With that said, I like the idea of providing both models (Crecto and Granite) and letting the developer decide which best fits their needs.
I been thinking about this lately a bit as well with the idea that:
Should association data be stored in an ivar?
For example:
class Customer < Granite::Base
table customers
column id : Int64, primary: true
has_one :owner
end
customer = Customer.first!
customer.owner # Hits DB
customer.owner # Hits DB
Since the has_one macro just defines a method that does the query, every time you use that method, it re-queries the database. However, if we introduce ivars to store the data in, has_one could expand to like:
@owner : User
def owner : User
@owner ||= User.find_by! customer_id: self.id
end
This would essentially cache the result in memory and prevent extra hits to the database. It also would make it easier to serialize associated data, as it would just inherently be handled via the JSON::Serializable stuff, or an external serialization shard. An annotation or arg on the macro could also be used if that relationship should be set with the rest of the columns, or when used for the first time.
The benefit of this for the includes method would be that now, all that would have to be done is for that method to simply hydrate the ivars with the actual data. I haven't really thought a lot of how that would work yet tho.