Can't define :joins conditions in has_many relationship? - activerecord

I have a relationship table :
create_table "animal_friends", :force => true do |t|
t.integer "animal_id"
t.integer "animal_friend_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "status_id", :default => 1
end
linking animals to others. Best way to retreive associations in SQL is :
SELECT animals.*
from animals join animal_friends as af
on animals.id =
case when af.animal_id = #{id} then af.animal_friend_id else af.animal_id end
WHERE #{id} in (af.animal_id, af.animal_friend_id)
And I can't find a way to create a proper has_many relation in rails with this. Apparently, there's no way to provide joining conditions for has_many.
I'm currently using a finder_sql :
has_many :friends, :class_name => "Animal", :finder_sql => 'SELECT animals.* from animals join animal_friends as af on animals.id = case when af.animal_id = #{id} then af.animal_friend_id else af.animal_id end ' +
'WHERE #{id} in (af.animal_id, af.animal_friend_id) and status_id = #{Status::CONFIRMED.id}'
but this method has the great disadvantage of breaking activerecord magic. For instance :
#animal.friends.first
will execute the finder_sql without limit, fetching thousands of rows, then taking the first of the array (and loosing several precious seconds / req).
I guess it's a missing feature from AR, but I'd like to be sure first :)
Thanks

You could solve this on the database level with a view, which would be the correct method anyway.
CREATE VIEW with_inverse_animal_friends (
SELECT id,
animal_id,
animal_friend_id,
created_at,
updated_at,
status_id
FROM animal_friends
UNION
SELECT id,
animal_friend_id AS animal_id,
animal_id AS animal_friend_id,
created_at,
updated_at,
status_id
FROM animal_friends
)
If you dont want to have double entries for friends with relations both ways you could do this:
CREATE VIEW unique_animal_friends (
SELECT MIN(id), animal_id, animal_friend_id, MIN(created_at), MAX(updated_at), MIN(status_id)
FROM
(SELECT id,
animal_id,
animal_friend_id,
created_at,
updated_at,
status_id
FROM animal_friends
UNION
SELECT id,
animal_friend_id AS animal_id,
animal_id AS animal_friend_id,
created_at,
updated_at,
status_id
FROM animal_friends) AS all_animal_friends
GROUP BY animal_id, animal_friend_id
)
You would need a way to decide which status_id to use in case there are two conflicting ones. I chose MIN(status_id) but that is probably not what you want.
In Rails you can do this now:
class Animal < ActiveRecord::Base
has_many :unique_animal_friends
has_many :friends, :through => :unique_animal_friends
end
class UniqueAnimalFriend < ActiveRecord::Base
belongs_to :animal
belongs_to :friend, :class_name => "Animal"
end
This is out of my head and not tested. Also, you might need some plugin for view handling in rails (like "redhillonrails-core").

There is a plugin that does what you want.
There is a post about it here.
There is an alternative here.
Both allow you do to joining conditions and are just using lazy initialization.
So you can use dynamic conditions. I find the former prettier, but you can use the latter if you don't want to install plugins.

The two ways to create many to many relationships in active record are has_and_belongs_to_many and has_many :through. This site critiques the differences between the two. You don't have to write any SQL using these methods.

Related

How can I select a column from the first record?

I created following model
module UserInfo
class User < ActiveRecord::Base
self.table_name = 'vUserDetails'
default_scope { order(date_entered: :desc) }
end
end
How can I produce the following query using this model?
SELECT TOP 1 Column1
FROM vUserDetails
WHERE vUserDetails.UserID = #user_id
ORDER BY DateEntered DESC
UserInfo.first will give you the first record, ordered by the order defined in your default_scope.
If you really want to select only the column Column1, then you would use UserInfo.select(:Column1).first.

ActiveRecord unknown column

I ran into a little problem I have a has_many through relationship here is the code for the models
class User < ActiveRecord::Base
has_many :friendships
has_many :followings, :through => :friendships, :foreign_key => "followed_id"
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :following, :class_name => "User", :foreign_key => "followed_id"
end
now at the console I can type u = User.first and then u.friendships.first.following this gives me the first user u is following, but when I type u.friendships.last.following I get this error
the SELECT statement from u.friendships.first.following
Friendship Load (0.3ms) SELECT `friendships`.* FROM `friendships` WHERE `friendships`.`user_id` = 208 LIMIT 1
User Load (0.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 209 LIMIT 1
and the SELECT statement from u.friendships.last.following
Friendship Load (0.3ms) SELECT `friendships`.* FROM `friendships` WHERE `friendships`.`user_id` = 208 ORDER BY `friendships`.`` DESC LIMIT 1
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'friendships.' in 'order
clause': SELECT `friendships`.* FROM `friendships` WHERE `friendships`.`user_id` = 208
ORDER BY `friendships`.`` DESC LIMIT 1
if I then run u.friendships and then u.friendships.last.following again, I don't get the error anymore, why is that?
Heres my sql output for friendships, straight from your code on Rails 3.2.9 / postgresql:
# u.friendships.first.following
Friendship Load (0.9ms) SELECT "friendships".* FROM "friendships" WHERE "friendships"."user_id" = 1 LIMIT 1
# u.friendships.first.following
Friendship Load (1.3ms) SELECT "friendships".* FROM "friendships" WHERE "friendships"."user_id" = 1 ORDER BY "friendships"."id" DESC LIMIT 1
So for some reason for me, id is getting picked up automatically in ORDER BY "friendships"."id" and it works. Maybe your problem has something to do with your DB?
#Statements used to create the db for reproducing this problem
CREATE TABLE users (id SERIAL PRIMARY KEY)
CREATE TABLE friendships (
id SERIAL PRIMARY KEY,
user_id integer
followed_id integer
);

How do I perform a full join in ARel?

Assume I have two tables TableA and TableB with a many-to-many relationship through a joining table TableABJoin. I would like to use ARel 3 to generate a query that performs a full join of TableA and TableB.
The query I want to generate should be something along these lines:
SELECT a.id, b.code
FROM TableA as a, TableB as b
This results in a full join of tables A and B.
The closest I have been able to get, without writing an explicit sql string, is to hack an outer join:
part_a = TableA.arel_table
part_b = TableB.arel_table
query = part_a.join(part_b, Arel::Nodes::OuterJoin).on('1=1').project(part_a[:id], part_b[:code]).to_sql
This produces the following SQL:
SELECT "TableA"."id", "TableB"."code" FROM "TableA" LEFT OUTER JOIN "TableB" ON 1=1
If I exclude the .on component I end up with a trailing NULL:
SELECT "TableA"."id", "TableB"."code" FROM "TableA" LEFT OUTER JOIN "TableB" NULL
Is there a more reasonable way to generate a proper full join or at least generate the same result without hacking a left outer join in ARel?
Actually, is not possible to perform a full outer join (and a right outer join) with arel, which only supports Inner and Outer (LEFT OUTER) joins.
Because I didn't like this, I updated arel 3-0-stable (I'm working on a 3.2.13 rails application) so that it supports right and full outer joins too. Adding those was pretty straightforward even with undocumented code, it's quite easy so you should face no issue if you try it.
Here you can find my pull request: https://github.com/rails/arel/pull/202
And here you can find the updated repository with a branch: https://github.com/Fire-Dragon-DoL/arel/tree/3-0-right-full-outer-join
You can easily use this in a rails application by adding this to your Gemfile:
gem 'arel', '~> 3.0.3.4', github: 'Fire-Dragon-DoL/arel', branch: '3-0-right-full-outer-join'
Here you can see a syntax example:
class Cat < ActiveRecord::Base
has_many :cat_diseases
end
class Disease < ActiveRecord::Base
has_many :cat_diseases
end
class CatDisease < ActiveRecord::Base
belongs_to :cat
belongs_to :disease
def self.all_diseases_for_cat(cat)
cat_diseases = self.arel_table
diseases = Disease.arel_table
scoped
.joins(
cat_diseases.join(diseases, Arel::Nodes::RightOuterJoin)
.on(cat_diseases[:disease_id].eq(diseases[:id]))
.join_sources
)
.where(
cat_diseases[:cat_id].eq(cat.id)
.or(cat_diseases[:cat_id].eq(nil))
)
end
end
I write the join in SQL. Its clearer and works fine:
part_a = TableA.arel_table
part_b = TableB.arel_table
query = part_a.joins('LEFT OUTER JOIN "TableB"').other_scopes.to_sql

Single table version controled data

So I have a table designed as such:
create_table "entries", :force => true do |t|
t.integer "id", :null => false, :autoincrement => true
t.text "text"
t.string "uuid"
t.datetime "created_at", :null => false
end
I have a similar problem to " Database - data versioning in single table" except that I have multiple sources (some of which do work offline) writing into this database, so the current bit doesn't really work well for me.
What I'm wondering is what is the best way to get the most recent of each UUID.
In sqlite: SELECT "entries".* FROM "entries" GROUP BY uuid ORDER BY updated_at DESC works great, but it's not valid syntax in postgres, and it kind of feels janky. Is there a good way to do this, or do I need to redo my schema?
One way to get the results you're after is to use a window function and a derived table. ActiveRecord doesn't understand either of those but there's always find_by_sql:
Entry.find_by_sql(%q{
select id, text, uuid, created_at
from (
select id, text, uuid, created_at,
row_number() over (partition by uuid order by created_at desc) as r
from entries
) as dt
where r = 1
})
The interesting part is this:
row_number() over (partition by uuid order by created_at desc)
partition by uuid is similar to group by uuid but it doesn't collapse the rows, it just modifies how the row_number() window function behaves so that row_number() is computed with respect to rows with matching uuids; the order by created_at desc similarly applies only to the window that row_number() will be looking at. The result is that r = 1 peels off the first row in each group.

ActiveRecord WHERE NOT EXISTS

Is there a way to use EXISTS with ActiveRecord besides find_by_sql?
I'd like a nice way to find all records without an association in a One-to-Many relationship.
SELECT DISTINCT store_type FROM stores
WHERE NOT EXISTS (SELECT * FROM cities_stores
WHERE cities_stores.store_type = stores.store_type)
Store.all(:select => "DISTINCT store_type",
:conditions => "NOT EXISTS (SELECT * FROM cities_stores WHERE cities_stores.store_type = stores.store_type)")
ActiveRecord will execute the same query as what you entered above. The returned Store instances will have a single store_type attribute.

Resources