granite icon indicating copy to clipboard operation
granite copied to clipboard

Provide JOINS and INCLUDES querying methods and some thoughts

Open eliasjpr opened this issue 8 years ago • 4 comments

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.

eliasjpr avatar Sep 17 '17 12:09 eliasjpr

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 avatar Sep 17 '17 18:09 drujensen

@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 avatar Sep 18 '17 00:09 eliasjpr

@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.

drujensen avatar Sep 18 '17 02:09 drujensen

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.

Blacksmoke16 avatar Sep 16 '19 18:09 Blacksmoke16