Support Join
Seems like currently we can't do a join operation.
@waterlink Will you support it in the upcoming version?
Yeah currently functionality is very basic. If you want you can try to create a PR for that. I can guide you through current codebase if you want to do that. Otherwise it is on my to-do list 😀
I'd really like to try that out :+1:
@waterlink how shall i start with this :smile:
First we need to decide on the interface. Which args should join receive? What will it return?
Well i really like AR and would really like the have the same interface as possible :+1:
So I imagine something along these lines:
class Model
# args map directly to 'JOIN #{table_name} ON #{on_query}'
def join(model_klass, on_query : Hash|Query) : JoinedModels
end
And the usage example is:
Post.join(User, {"posts.author_id" => "users.id"})
# or equivalent
Post.join(User, criteria("posts.author_id") == criteria("users.id"))
# => JoinedModels < Post, User, ... >
Looks like JoinedModels should have the same querying methods as Model, like:
whereall
I am not sure what should they return. Array(Post) ? In that case there is no access to fields of other table..
Maybe every model could have something like this:
posts = Post.join(User, criteria("posts.author_id") == criteria("users.id")).all
post = posts.first
post.title # => "Hello world post"
user = User.from_joined(post)
user.name # => "Oleksii"
Other way to do it is to define all such possible relationships beforehand (aka belongs_to and has_one and has_many):
class Post < ActiveRecord::Model
belongs_to User
end
posts = Post.join(User, criteria("posts.author_id") == criteria("users.id")).all
post.first.title # => "Hello world post"
post.first.user.name # => "Oleksii"
Though the problem awaits here:
class Post < ActiveRecord::Model
# at this point you get compile error, since `User` is not defined yet
belongs_to User
end
class User < ActiveRecord::Model
has_many Post
end
You get compile error, because User is not defined yet.
Since it is macro, we can fix it by not using User right away, but rather define new method inside of macro:
macro belongs_to(model)
{% model_name = model.id.stringify.gsub(/.+:/, "").downcase %}
def {{model_name.id}}
{{model.id}}.from_joined(self)
end
end
Maybe belongs_to (and friends) could provide optional parameter for name of the method to help user avoid conflicts, like:
macro belongs_to(model, method_name = nil)
{% unless method_name %}
{% method_name = model.id.stringify.gsub(/.+:/, "").downcase %}
{% end %}
def {{method_name.id}}
{{model.id}}.from_joined(self)
end
end
WDYT @sdogruyol ?
@waterlink I like this and your way of handling compiler error :+1:
class Post < ActiveRecord::Model
belongs_to User
end
posts = Post.join(User, criteria("posts.author_id") == criteria("users.id")).all
post.first.title # => "Hello world post"
post.first.user.name # => "Oleksii"
Ok then. There is a plan for public interface now :+1:
Now on the internals. Currently all the heavy lifting (actual querying) is done by Adapter protocol implementations.
You can see this protocol here: https://github.com/waterlink/active_record.cr/blob/master/src/adapter.cr#L19-L30
Currently, as you can see, adapter is supposed to have knowledge only about one table.
I was thinking about extending it like this:
abstract def self.build(table_name, join_specs : Array(JoinSpec), primary_field, fields, register = true)
where JoinSpec is:
struct JoinSpec
property table_name :: String
property on_query :: Query
def initialize(@table_name, @on_query) end
end
By default, when you are not using join, join_specs is empty [] of JoinSpec.
Adapters, accordingly will add relevant query to make a join, or blow up with an error if it is not supported (some databases don't have concept of join).
@waterlink Would you mind starting this and let me join on the way :smile:
No problem :)
Hi! Any idea when you'll be able to implement joins? Really would love to port a rb project to cr, but without joins, it will be nearly impossible.
https://twitter.com/waterlink000/status/831403009724788736