sql select with conditional field replacement - sql

I'm using postgres. I have a single table inheritance 'listings' table. Listings can have many variations and belong to a parent through parent_id. Both parent and child have a 'title' field. Occasionally, and unfortunately, the variation title is null, and we need to keep it that way because it reflects how the original data was imported, so doing a data migration to populate variation.title with it's parent's title is not an option.
But when we display the data in our web view, we always want to show a title.
I would like to query against all the listings (parents and variation) for a merchant, and if the title is null for the child, then replace the null with the parent's title. Right now I'm doing this in ruby on rails, which can be slow as it has to loop through each ruby object. Is there a way to do this in SQL?
We are doing something like this now:
listings = Listing
.includes(:parent)
.where(:merchant_id => current_merchant.id)
.where(product_id: params[:ids])
.select(:type, :id, :sku, :product_id, :title, :quantity, :price, :channel_id, :status, :state, :parent_id)
#fix blank titles
listings.each do |l|
next if l.title
l.title = l.parent.title if l.parent_id
end

I think it is possible. Based on this docs
listings = Listing
.includes(:parent)
.where(:merchant_id => current_merchant.id)
.where(product_id: params[:ids])
.select('type, COALESCE(listings.title, parents.title), ...)
I think it will work if listings.title is NULL. If it can be blank string you can use CASE statement.
PS maybe you should use LEFT JOIN for table parents instead of including it.

Related

Rails and SQL - get related by all elements from array, entries

I have something like this:
duplicates = ['a','b','c','d']
if duplicates.length > 4
Photo.includes(:tags).where('tags.name IN (?)',duplicates)
.references(:tags).limit(15).each do |f|
returned_array.push(f.id)
end
end
duplicates is an array of tags that were duplicated with other Photo tags
What I want is to get Photo which includes all tags from duplicates array, but right now I get every Photo that include at least one tag from array.
THANKS FOR ANSWERS:
I try them and somethings starts to work but wasn't too clear for me and take some time to execute.
Today I make it creating arrays, compare them, take duplicates which exist in array more than X times and finally have uniq array of photos ids.
If you want to find photos that have all the given tags you just need to apply a GROUP and use HAVING to set a condition on the group:
class Photo
def self.with_tags(*names)
t = Tag.arel_table
joins(:tags)
.where(tags: { name: names })
.group(:id)
.having(t[:id].count.eq(tags.length)) # COUNT(tags.id) = ?
end
end
This is somewhat like a WHERE clause but it applies to the group. Using .gteq (>=) instead of .eq will give you records that can have all the tags in the list but may have more.
A better way to solve this is to use a better domain model that doesn't allow duplicates in the first place:
class Photo < ApplicationRecord
has_many :taggings
has_many :tags, through: :taggings
end
class Tag < ApplicationRecord
has_many :taggings
has_many :photos, through: :taggings
validates :name,
uniqueness: true,
presenece: true
end
class Tagging < ApplicationRecord
belongs_to :photo
belongs_to :tag
validates :tag_id,
uniqueness: { scope: :photo_id }
end
By adding unique indexes on tags.name and a compound index on taggings.tag_id and taggings.photo_id duplicates cannot be created.
The issue as I see it is that you're only doing one join, which means that you have to specify that tags.name is within the list of duplicates.
You could solve this in two places:
In the database query
In you application code
For your example the query is something like "find all records in the photos table which also have a relation to a specific set of records in the tags table". So we need to join the photos table to the tags table, and also specify that the only tags we join are those within the duplicate list.
We can use a inner join for this
select photos.* from photos
inner join tags as d1 on d1.name = 'a' and d1.photo_id = photos.id
inner join tags as d2 on d2.name = 'b' and d2.photo_id = photos.id
inner join tags as d3 on d3.name = 'c' and d3.photo_id = photos.id
inner join tags as d4 on d4.name = 'd' and d4.photo_id = photos.id
In ActiveRecord it seems we can't specify aliases for joins, but we can chain queries, so we can do something like this:
query = Photo
duplicate.each_with_index do |tag, index|
join_name = "d#{index}"
query = query.joins("inner join tags as #{join_name} on #{join_name}.name = '#{tag}' and #{join_name}.photo_id = photos.id")
end
Ugly, but gets the job done. I'm sure there would be a better way using arel instead - but it demonstrates how to construct a SQL query to find all photos that have a relation to all of the duplicate tags.
The other method is to extent what you have and filter in the application. As you already have the photos that has at least one of the tags, you could just select those which have all the tags.
Photo
.includes(:tags)
.joins(:tags)
.where('tags.name IN (?)',duplicates)
.select do |photo|
(duplicates - photo.tags.map(&:name)).empty?
end
(duplicates - photo.tags.map(&:name)).empty? takes the duplicates array and removes all occurrences of any item that is also in the photo tags. If this returns an empty array then we know that the tags in the photo had all the duplicate tags as well.
This could have performance issues if the duplicates array is large, since it could potentially return all photos from the database.

How to convert complex Postgres SQL query / subqueries to active record in Ruby on Rails 5.2?

My application requires a text field and its translations to be displayed all at once in the edit view.
Available translations and even the list of configured languages evolve over time, so that the edit view must display the available translations, and create empty fields for missing ones.
Thus, the _form.html.erb file contains nested fields to handle these translations.
<%= f.fields_for translation_fields, translations.sort_by { |e| [ e.language == current_language.to_s ? 0 : 1, e.language ] } do |locution| %>
But this only allows handling of existing translations.
To allow additional languages support, I need to create a right-join with the list of languages, and initialise empty records accordingly. The corresponding SQL query for providing translations fields for the name of Playground object with id=555 is:
select 555 as document_id, 'Playground' as document_type, 'name' as field_name, property as language,
case when property = language then existing_translations.id else null end as id,
case when property = language then existing_translations.translation else null end as translation
from
(select *
from dqm_app.translations
where document_id = 555 and field_name = 'name' and document_type = 'Playground'
) as existing_translations
right outer join
(select property
from dqm_app.parameters inner join dqm_app.parameters_lists on parameters.parameters_list_id = parameters_lists.id
where parameters_lists.CODE = 'LIST_OF_LANGUAGES') as langues on property = language
As a result, we can see herunder that only a french translation exists, and other translations are to be created:
To reproduce this in Active Record, I created subqueries, which I need to join:
<% existing_translations = Translation.where({document_id: document_id, document_type: document_type, field_name: field_name}) %>
<% languages = Parameter.where("parameters_list_id = (select id from parameters_lists where code = 'LIST_OF_LANGUAGES')") %>
How can I create a right outer join between these subqueries?
Is there a better way to achieve this?
Thanks a lot!
Note: I finally solved any complex query by using AREL, starting with
this cheat sheet.

Filtering Parents by Children

I'm doing a simple blog application - There are posts, which have many tags through a posts_tags table (my models are below). What I have implemented is if a user clicks a tag, it will show just the posts with that tag. What I want is for the user to them be able to select another tag, and it will filter to only the posts that have both of those tags, then a third, then a fourth, etc. I'm having difficulty making the active record query - especially dynamically. The closest I've gotten is listed below - however its in pure SQL and I would like to at least have it in ActiveRecord Rubyland syntax even with the complexity it contains.
Also, the "having count 2" does not work, its saying that "count" does not exist and even if I assign it a name. However, it is outputting in my table (the idea behind count is that if it contains a number that is as much as how many tags we are searching for, then theoretically/ideally it has all the tags)
My current test SQL query
select posts_tags.post_id,count(*) from posts_tags where tag_id=1 or tag_id=3 group by post_id ### having count=2
The output from the test SQL (I know it doesnt contain much but just with some simple seed data).
post_id | count
---------+-------
1 | 2
2 | 1
My Models:
/post.rb
class Post < ActiveRecord::Base
has_many :posts_tags
has_many :tags, :through => :posts_tags
end
/tag.rb
class Tag < ActiveRecord::Base
has_many :posts_tags
has_many :posts, :through => :posts_tags
end
/poststag.rb
class PostsTag < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
Give a try to:
Post.joins(:tags).where(tags: {id: [1, 3]}).select("posts.id, count(*)").group("posts.id").having("count(*) > 2")
I think "count = 2" is not correct. It should be "count(*) = 2". Your query then will be
select post_id,count(post_id)
from posts_tags
where tag_id = 1 or tag_id = 3
group by post_id
having count(post_id) = 2
In general you want to stay away from writing raw sql when using rails. Active Record has great helper methods to make your sql more readable and maintainable.
If you only have a few tags you can create scopes for each of them (http://guides.rubyonrails.org/active_record_querying.html#scopes)
Since people are clicking on tags one at a time you could just query for each tag and then use the & operator on the arrays. Because you have already requested the exact same set of data from the database the query results should be cached meaning you are only hitting the db for the newest query.

Rails: Many to one ( 0 - n ) , finding records

I've got tables items and cards where a card belongs to a user and a item may or may not have any cards for a given user.
The basic associations are set up as follows:
Class User
has_many :cards
Class Item
has_many :cards
Class Card
belongs_to :user
has_and_belongs_to_many :items
I've also created a join table, items_cards with the columns item_id and card_id. I'd like to make a query that tells me if there's a card for a given user/item. In pure SQL I can accomplish this pretty easily:
SELECT count(id)
FROM cards
JOIN items_cards
ON items_cards.card_id = cards.id
WHERE cards.user_id = ?
AND items_cards.item_id = ?
I'm looking for some guidance as to how I'd go about doing this via ActiveRecord. Thanks!
Assuming you have an Item in #item and a User in #user, this will return 'true' if a card exists for that user and that item:
Card.joins(:items).where('cards.user_id = :user_id and items.id = :item_id', :user_id => #user, :item_id => #item).exists?
Here's what's going on:
Card. - You're making a query about the Card model.
joins(:items) - Rails knows how to put together joins for the association types it supports (usually - at least). You're telling it to do whatever joins are required to allow you to query the associated items as well. This will, in this case, result in JOIN items_cards ON items_cards.card_id = cards.id JOIN items ON items_cards.item_id = items.id.
where('cards.user_id = :user_id and items.id = :item_id', :user_id => #user, :item_id => #item) - Your conditional, pretty much the same as in pure SQL. Rails will interpolate the values you specify with a colon (:user_id) using the values in the hash (:user_id => #user). If you give an ActiveRecord object as the value, Rails will automatically use the id of that object. Here, you're saying you only want results where the card belongs to the user you specify, and there is a row for the item you want.
.exists? - Loading ActiveRecord objects is inefficient, so if you only want to know if something exists, Rails can save some time and use a count based query (much like your SQL version). There's also a .count, which you could use instead if you wanted to have the query return the number of results, rather than true or false.

ActiveRecord::Relation joins with more conditions than just the foreign key

Is there any way to specify more than one conditions for a left outer join using ActiveRecord::Relation?
Take the following SQL statement for example. How can anyone rewrite this using ActiveRecord::Relation objects?
SELECT `texts`.*, `text_translations`.translation FROM `texts` LEFT OUTER JOIN `text_translations` ON `text_translations`.`id` = `texts`.`id` AND `text_translations`.`locale` = 'en'
Is there any way to do this under ActiveRecord 3.0.3+?
Thanks in advance.
first you should consider to use rails/activerecord conform relations. This means the foreign key in the text_translations table should be called text_id
Create your models and associations like this:
class Text < ActiveRecord::Base
# all possible translations!
has_many :text_translations
scope :with_translation_for, lambda { |lang| {
:select => "texts.*, tt.translation",
:joins => "LEFT OUTER JOIN text_translations AS tt ON tt.text_id = texts.id AND tt.locale = #{ActiveRecord::Base.sanitize(lang)}"
}}
# return nil if translation hasn't been loaded, otherwise you get a nasty NoMethod exception
def translation
read_attribute(:translation)
end
end
and
class TextTranslation < ActiveRecord::Base
# every translation belongs to a text
belongs_to :text
# define a scope for the language
scope :language, lambda { |lang| where(['locale = ?', lang]) }
end
How to use:
texts = Text.with_translation_for('en')
texts.each do |c_text|
unless c_text.translation.nil?
puts c_text.translation
else
puts "No translation available!"
end
end
Now to the pro and cons, the way using LEFT OUTER join will load you all texts even if there isn't a translation for a text in the desired language. The con is that you won't get the "TextTranslation" model object.
Anotherway is to load only the text which have the desired translation. You can do it like:
texts = Text.includes(:text_translations).where(:text_translations => {:locale => 'en'})
now texts[i].text_translations will return an array with all TextTranslations model object for this text matching the locale 'en'. But texts without a translation in the locale "en" won't show up.
Edit
Connected to your comment:
The problem about using .join(:tablename) on a relation is that, it will result in an INNER JOIN so this is not an option. You have to explicitly declare the LEFT join. Another thing is that if you use something like Text.includes(:text_translations).where(['text_translations.locale = ?', 'en']) the condition will be applied to the SQL query as whole and not on the possible LEFT join itself. What you actually can do is to declare associations like
has_many :english_translations, :class_name => 'TextTranslation', :conditions => ['locale = ?', 'en']
This way you can manage to load only english translations by eager loading (without any joins at all):
Text.includes(:english_translations).all
Checkt this out:
Ruby On Rails Guide about Joining Tables
ActiveRecord Association Docs, Search for LEFT OUTER JOIN