My tables are set up such that Child has a 1:1 relationship with Parent.
They share a primary key (id):
Parent has id and type and name
Child has id and health
The Child is a polymorphic inheritance of the Parent. My goal is that Child.find(1) should return a Child object which responds to both name and health. The SQL statement would hypothetically look something like this:
SELECT parents.id, parents.name, childs.health FROM parents
LEFT JOIN childs ON childs.id = Parents.id
WHERE parents.id = 1 AND parents.type = 'Child' LIMIT 1
Thus I've attempted to use Polymorphic Inheritance in ActiveRecord:
class Parent < ApplicationRecord
class Child < Parent
When I attempt to execute Child.find(1), I see:
SELECT `parents`.* FROM `parents` WHERE `parents`.`type` IN ('Child') AND `parents`.`ID` = 1 LIMIT 1
=> #<Child id: 1, type: "Child", name: "Hello">
Notably, there's no JOIN on the child table, yet I receive a Child object back. This leads to the unexpected behavior that the Child object does not respond to health. Curiously, if I add an explicit table association to the Child class (self.table_name = "childs"), then the query pattern changes to:
> c = Child.find(1)
Obtainable Load (0.3ms) SELECT `childs`.* FROM `childs` WHERE `childs`.`ID` = 2 LIMIT 1
Now I can access the health, but not the type or name.
How can I correctly create this JOIN association such that an attempt to load a Child object successfully JOINs the data from the parent?
Edit: these tables were created outside of an ActiveRecord migration (they are also accessed by other, pre-existing, non-Ruby applications) so I have no ability to change their schema. I can think of some fancy metaprogramming approaches, like responding to method_missing, that might let me lazy-load the missing attributes through a join... but I'm afraid I'll end up having to re-implement a bunch of ActiveRecord, like delete, create, etc. (which will lead to bugs). So I'm looking for some native/clean(ish) way to accomplish this.
This is not a typical Rails polymorphic association as described here: http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
So, in this case, when tables were created earlier by some other app, I suggest that you do something like this:
class Child < ApplicationRecord
self.table_name = "childs"
belongs_to :parent, foreign_key: :id, dependent: :destroy
delegate :name, :type, to: :parent
delegate :name=, to: :parent, allow_nil: true
after_initialize do
self.build_parent(type: "Child") if parent.nil?
end
end
class Parent < ApplicationRecord
self.inheritance_column = 'foo' # otherwise, type column will be used for STI
has_one :child, foreign_key: :id
delegate :health, to: :child
end
and now you can access the health, type and name:
> c = Child.joins(:parent).find(1)
Child Load (0.2ms) SELECT "childs".* FROM "childs" INNER JOIN "parents" ON "parents"."id" = "childs"."id" WHERE "childs"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Child id: 1, health: "Good", created_at: "2016-10-27 21:42:55", updated_at: "2016-10-27 21:44:08">
irb(main):002:0> c.health
=> "Good"
irb(main):003:0> c.type
Parent Load (0.1ms) SELECT "parents".* FROM "parents" WHERE "parents"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> "Child"
irb(main):004:0> c.name
=> "Hello"
and similar is for the parent:
p = Parent.joins(:child).find(1)
Parent Load (0.1ms) SELECT "parents".* FROM "parents" INNER JOIN "childs" ON "childs"."id" = "parents"."id" WHERE "parents"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Parent id: 1, type: nil, name: "Hello", created_at: "2016-10-27 21:40:35", updated_at: "2016-10-27 21:40:35">
irb(main):003:0> p.name
=> "Hello"
irb(main):004:0> p.health
Child Load (0.1ms) SELECT "childs".* FROM "childs" WHERE "childs"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> "Good"
If you prefer, you can specify LEFT JOIN like this:
irb(main):003:0> p = Parent.joins("LEFT JOIN childs ON (childs.id = parents.id)").select("parents.id, parents.name, childs.health").find(1)
Parent Load (0.2ms) SELECT parents.id, parents.name, childs.health FROM "parents" LEFT JOIN childs ON (childs.id = parents.id) WHERE "parents"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Parent id: 1, name: "Hello">
irb(main):004:0> p.name
=> "Hello"
irb(main):005:0> p.health
Child Load (0.1ms) SELECT "childs".* FROM "childs" WHERE "childs"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> "Good"
Related
I'm performing the following query that joins across 3 tables to pull back workouts and the tags associated with them.
(db/query {:select [:workouts.id :workouts.name :tag.tag_name]
:from [:workouts]
:left-join [[:workout_tags :workout_tag] [:= :workout_tag.workout_id :workouts.id]
[:tags :tag] [:= :tag.tag_id :workout_tag.tag_id]]
:where [:= :workouts.id 1]}))
This returns the following:
({:id 1, :name "Short", :tag_name "cardio"}
{:id 1, :name "Short", :tag_name "No weights"})
Ideally, I'd like to return to the end user a single result with the tag_name being combined into a single field. Something like:
{:id 1, :name "Short", :tag_name ["cardio" "No weights"]}
It would see that I could fairly easily do this after the fact, but I wanted to see if there's a built in MySQL function that can do what I'm looking to accomplish. It seems maybe GROUP_CONACT might do what I'm looking for, but I can't seem to get it to work in the context of HoneySQL.
The following should be close to what you need:
(require '[honeysql.core :as hc])
(hc/format {:select [:workouts.id :workouts.name [(hc/call :group_concat :tag.tag_name) :tag_name]]
:from [:workouts]
:left-join [[:workout_tags :workout_tag] [:= :workout_tag.workout_id :workouts.id]
[:tags :tag] [:= :tag.tag_id :workout_tag.tag_id]]
:where [:= :workouts.id 1]
:group-by [:tag.tag_name]})
That produces the following SQL:
SELECT workouts.id, workouts.name, group_concat(tag.tag_name) AS tag_name
FROM workouts
LEFT JOIN workout_tags workout_tag ON workout_tag.workout_id = workouts.id
LEFT JOIN tags tag ON tag.tag_id = workout_tag.tag_id
WHERE workouts.id = ?
GROUP BY tag.tag_name
You didn't show what you had tried or what failed, which would definitely be helpful for us in ascertaining what we should suggest as an answer.
I would do it after the fact:
(let [data [{:id 1, :name "Short", :tag_name "cardio"}
{:id 1, :name "Short", :tag_name "No weights"}
{:id 2, :name "Long", :tag_name "Tall"}
{:id 2, :name "Long", :tag_name "Hills"}]
grouped (group-by :id data)
id-tags (vec (for [[id data-maps] grouped]
(let [tags (mapv :tag_name data-maps)]
{:id id :tags tags})))]
(is= id-tags
[{:id 1, :tags ["cardio" "No weights"]}
{:id 2, :tags ["Tall" "Hills"]}]))
The intermediate result grouped looks like
grouped =>
{1
[{:id 1, :name "Short", :tag_name "cardio"}
{:id 1, :name "Short", :tag_name "No weights"}],
2
[{:id 2, :name "Long", :tag_name "Tall"}
{:id 2, :name "Long", :tag_name "Hills"}]}
See my favorite template project for full config details.
I'm having trouble querying a has_many association. The context is stores.
class Store < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to: store
end
Stores table:
id name
1 Macys
2 Target
3 Dillards
Items table:
id store_id name
1 1 pants
2 1 shirt
3 2 pants
4 2 shirt
5 3 shirt
I'm trying to query for stores that only sell shirts. So I need a query that returns the store record with id of 3.
When I tried to do
Store.includes(:items).where(
items: { name: %w(shirts)} ).references(:items)
it returns store_ids 1, 2, and 3 (all stores) because they all have shirts.
I ended up using:
Store.joins(:items).group('items.store_id').having("max(items.name) =
min(items.name) and min(items.name) = 'shirt'")
Store.includes(:items)
.where(items: { name: 'shirt' })
.where.not(id:
Item.where.not(name: 'shirt').select(:store_id)
)
Hopefully there's a better way... (if anyone)
In your Item model, you need to set the counter_cache:
belongs_to :store, counter_cache: true
then your query will be:
Store.joins(:items).where('items_count = ? AND items.name = ?', 1, 'shirt')
One way of doing this as mentioned in the post that stores only having items as shirts would be:
Store.joins(:item).where("items.name = ?", 'shirt').where.not(item_name: Item.where("items.name != ?", "shirt"))
Hope it helps!!
Sorry for bad English ))
I have an array of ids in my ruby code.
Example:
[[10], [], [10, 1, 3], []]
Can I load User model from MySQL table users in one query by saving grouping?
Example:
[[#<User id=10>], [], [#<User id=10>, #<User id=1>, #<User id=3>], []]
Environment: Ruby 2.5.1 | Rails 5 | MySQL
One of found solution:
I can flat my array of ids and load my model by that array into hash:
hash = User.where(id: array.flatten).index_by(&:id)
Then, during iterating, through array I can load my objects from hash in the right order like that:
array.each do |ids|
users = ids.map { |id| hash[id] }
# do smth
end
This is simple: use flatten method for array:
ids = [[123], [], [123, 1, 3, 4], [70, 80]]
user_ids = ids.flatten.reject(&:blank?).uniq
users = User.where(id: user_ids)
edited:
not optimal (recursive) method for your need:
def map_users_by_id(ids_array:, users:)
result = []
ids_array.each do |array_element|
if (array_element).is_a? Array
result << map_users_by_id(ids_array: array_element, users: users)
else
result << users[array_element]
end
end
return result
end
ids = [[123], [], [123, 1, 3, 4], [70, 80]]
user_ids = ids.flatten.reject(&:blank?).uniq
users = Hash[User.where(id: user_ids).map{|user|[user.id, user]}]
result = map_users_by_id(ids_array: ids, users: users)
# engine.rb
has_many :pistons
#piston.rb
belongs_to :engine
Piston has a column, piston_count and, of course, engine_id
My database has the following 7 records
Engine.all
#=> [#<Engine id: 1>, #<Engine id: 2>, #<Engine id: 3>]
Piston.all
#=> [#<Piston id: 1, engine_id: 1, piston_count: 1>, #<Piston id: 2, engine_id: 1, piston_count: 2>, #<Piston id: 2, engine_id: 2, piston_count: 1>, #<Piston id: 2, engine_id: 3, piston_count: 2>]
I want to write a query that says, return the Engine containing Pistons with a piston_count of 1 and also contains a piston_count of 2
I've tried...
engines = Engine.joins(:pistons).merge(Piston.where(piston_count: 1))
#=> [#, #]
engines.joins(:pistons).merge(Piston.where(piston_count:2))
#=> []
It returns an empty array because active record turns that into one AND clause. However, if I do an OR statement, it will return too many records. Any thoughts?
Figured it out. This takes the intersect of both Active Record Queries.
engine_ids = Engine.joins(:pistons).merge(Piston.where(piston_count: 1)).pluck(:id) & Engine.joins(:pistons).merge(Piston.where(piston_count: 2)).pluck(:id)
Then go back and retrieve all the intersects.
Engine.where(id: engine_ids)
I have two models, User and Profile.
A User has_one Profile and a Profile belongs_to User.
Correspondingly, the Profile model has a user_id attribute.
The association works:
p = Profile.first
=> #<Profile id: 1, name: "Jack", ... , user_id: 1>
u = User.first
=> #<User id: 1, email: "jack#example.com", ... >
u.profile.id
=> 1
p.user.id
=> 1
p.user == u
=> true
u.profile == p
=> true
I can set the user_id field on a Profile directly:
p.user_id = 2
=> 2
p.save!
=> true
p.user_id
=> 2
But why can I not set the user_id like this:
u.profile.user_id = 2
=> 2
u.profile.save!
=> 2
u.profile.user_id
=> 1
You must refresh u.profile object. Try this:
u.profile.user_id = 2
=> 2
u.profile.save!
=> 2
u.profile.reload.user_id
=> 2
This is because original profile object is still loaded on memory in u.
Hope this help :)