Sequel join: ".id" is returning the id of the the other table - ruby

I have a Ruby app which uses Ramaze and Sequel.
I have the classes Tag and Tagging which have this relationship:
class Tag < Sequel::Model
one_to_many :taggings, :class => 'Thoth::Tagging'
class Tagging < Sequel::Model(:taggings)
many_to_one :tag, :class => 'Thoth::Tag'
I want to return a list of tags in order of popularity, the ones that have the most taggings (filtering out any that have less than three taggings). I'm doing that this way:
tags = .left_outer_join(:taggings, :tag_id => :id).group(:tag_id).having("count(*) > 2").order("count(*) desc").all
This does return what seem to be tag objects, but when I call .id on them, I get the id of a tagging that points to the tag, rather than the tag itself.
On closer inspection, the results are quite different from a regular find:
> tag_regular = Tag[2]
=> #<Thoth::Tag #values={:title=>nil, :position=>nil, :parent_id=>1, :name=>"academic", :id=>2}>
> tag_from_join = Tag.join(:taggings, :tag_id => :id).group(:tag_id).having("count(*) > 2").order("count(*) desc").all.select{|tag| tag.name == "academic"}.first
=> #<Thoth::Tag #values={:tag_id=>2, :post_id=>5, :title=>nil, :position=>nil, :parent_id=>1, :name=>"academic", :id=>1611, :created_at=>nil}>
In both cases I get a Thoth::Tag, but the values are quite different, based on the different fields in the join I suppose.
All I actually need to do is get a list of regular tag objects sorted by the number of taggings, but in an efficient single-query way. Is there a better way?

The default selection is *, so you are selecting columns from both tags and taggings. If you have an id column in both tables, because Sequel returns records as a hash keyed by column name, columns in the taggings table will override columns with the same name in the tags table.
If you only want the columns from tags, add select_all(:tags) to the dataset.
The Sequel master branch has a table_select plugin that will handle this situation by default.

Related

Access an ActiveRelation in a view

I have two models with the appropriate foreign key created in the people table:
class Person < ActiveRecord::Base
belongs_to :family
class Family < ActiveRecord::Base
has_many :people
If I do the following I get an object - #family_members - as an instance variable and I have no problems:
#family_members = Family.find(1)
I can access the 'child' people table fields easily in my view:
#family_members.people.first_name
However, if I use the arel way with "where" etc. I get an "ActiveRecord::Relation", not a normal object, which leaves me stumped as to how to access the same "first_name" field form the people table like I accessed above:
#family_members = Family.where(:id => 1)
or even
#family_members = Family.joins(:people).where(:id => 1)
(is the "joins" even required??)
I understand that using ".first" will cause the query to run:
#family_members = Family.where(:id => 1).first
But it returns an array, not an object, so if I use in my view:
#family_members.people.first_name
I get a "method 'people' unknown" error.
How can I access the 'first_name' field of the people table like I did with the object created by "find" but using an ActiveRecord relation?
* added information 7/15 ********
To clarify what I am looking for -- here is what I would have written if I were writing SQL instead of Arel:
SELECT f.home_phone, f.address, p.first_name, p.last_name, p.birthday
FROM families f INNER JOIN people p ON p.family.id = f.id WHERE family_id = 1
With that query's results loaded into a result set I could access:
myResultSet("home_phone") -- the home_phone from the families table
myResultSet("address") -- the address from the families table
myResultSet("first_name") -- the first_name from the people table
myResultSet("birthdate") -- the birthdate from the people table
If the two tables in the query have a same-named field I would just use "AS" to request one of the fields by another name.
I have used this kind of query/result set for many years in web apps and I am trying to deduce how to do the same in Rails and ActiveRecord.
#family_members.people.first_name shouldn't ever work so I'm surprised you find it working ... #family_members contains a Family object, #family_members.people is an array of Person objects.
The fact that you're calling it #family_members seems to make me think you're expecting it to be an array of Persons... in which case the correct code would be...
#family_members = Family.find(1).people # finds people in first Family object
If you expect #family_members to contain just the first family member, then...
#family_members = Family.find(1).people.first
If you want an array of first names of all family members, then...
#family_members = Family.find(1).people # finds people in 1st Family object
#family_members.map {|member| member.first_name} # array of first_name
#family_members = Family.find(1) and #family_members = Family.where(:id => 1) are functionally identical.. both retrieve the first Family object in the database in each case may contain zero, one, or multiple people.
Just to be clear, the "1" in all examples above refer to which Family object is retrieved, not which Person in the Family.

How to extract Mongoid documents based on a field value in the first or last embedded document?

I wish to find Order documents based on a field in the last embedded Notificationdocument.
In the example below I wish to find all pending orders that has one or more embedded notifications, and where the last notification has a datetime that is between 5 and 10 days old.
My suggestion here dosen't seem to do the trick...:
Order.where(status: 'pending').gte('notifications.last.datetime' => 5.days.ago).lte('notifications.last.datetime' => 10.days.ago)
Here are the two models:
class Order
include Mongoid::Document
field :datetime, type: DateTime
field :status, type: String, default: 'pending'
embeds_many :notifications, :inverse_of => :order
end
class Notification
include Mongoid::Document
field :datetime, type: DateTime
embedded_in :order, :inverse_of => :notifications
end
The main issue of the question seems to be how to refer to the LAST element of an array in the query.
Unfortunately, it is impossible as of MongoDB 2.4.
The simplest way to implement this feature is to use negative value to point to an element in an array like 'notifications.-1.datetime', but it doesn't work. (Refer to [#SERVER-5565] Handle negative array offsets consistently - MongoDB.)
To make matters worse, it also seems impossible to solve this using Aggregation Framework. There is no way to
add an array index to each element when $unwinding ([#SERVER-4588] aggregation: add option to $unwind to emit array index - MongoDB) or
select the index of an array dynamically when $projecting. ([#SERVER-4589] aggregation: need an array indexing operator - MongoDB)
Therefore, the only option you have seem to change the schema to match what you want. The simplest way is to add to Order one more field which contains datetime of the last Notification.
Update:
You can first get all candidates from the server, and then narrow down them on the client side to get the final result set. This involves no schema change. If the scale of database is relatively small or some degradation of performance is acceptable, this might be the best solution.
query = Order.where(status: 'pending').elem_match(
notifications: { datetime: { '$gte' => 10.days.ago, '$lte' => 5.days.ago } })
query.select do |order|
# datetime = order.notifications[0].datetime
datetime = order.notifications[order.notifications.size - 1].datetime
10.days.ago <= datetime && datetime <= 5.days.ago
end.each do |order|
p order # result
end
I know it comes a little late, but hey, better later than never. :P
You can use JavaScript in where:
Order.where("this.notifications[this.notifications.length - 1].datetime > new Date('#{5.days.ago}')")
Just found out that and was a huge relief having not to change my models. Hope that helps!

Timestamp Column from Joined Table Becomes String

I have a table named subs which has many articles. The articles table has a timestamp column called published.
Sub.select( "subs.*,MAX(articles.published) published").joins("LEFT OUTER JOIN articles ON subs.id=articles.sub_id").group("subs.id").first.published.class
=> String
Article.select("max(published) published").group("id").first.published.class
=> ActiveSupport::TimeWithZone
I want to get an ActiveSupport::TimeWithZone object back from the first query.
Rails 3
Rails determines how to type cast attributes based on their database column definitions. For example, say you have a created_at method on your Sub model. When a record is loaded read_attribute is used (ActiveRecord::AttributeMethods::Read). This uses type_cast_attribute which determines how to cast the value based on the column info. For example, if you are using PostgreSQL it may use:
Sub.columns.detect { |c| c.name == "created_at" }.type_cast_code("v")
=> "ActiveRecord::ConnectionAdapters::PostgreSQLColumn.string_to_time(v)"
But Rails doesn't know what to do with columns that aren't on the Sub model. So it just gives back a String. If you need to work with a ActiveSupport::TimeWithZone object, you can cast the value with:
published = Sub.select( "subs.*,MAX(articles.published) published").joins("LEFT OUTER JOIN articles ON subs.id=articles.sub_id").group("subs.id").first.published
published.present? ? Time.zone.parse(published) : nil
Rails 4
In Rails 4, Rails is smarter about this kind of type-casting. When the SQL is executed, ActiveRecord::Result is created and the column_types are passed to the initializer. In your example Sub.select query, the published column would be cast as a Time object.

Rails 3 Query: How to get most viewed products/articles/whatever?

I always wondered how to query and get results that doesn't fit in a model. Similar how it's done using LINQ and projecting into anonymous objects.
So here's the simple schema:
# Product.rb
class Product < ActiveRecord::Base
has_many :product_views
# attributes: id, name, description, created_at, updated_at
end
# ProductView.rb
class ProductView < ActiveRecord::Base
belongs_to :product
# attributes: id, product_id, request_ip, created_at, updated_at
end
Basically I need to get a list of Products (preferably just id and name) along with the count of views it had. Obviously ordered by view count desc.
This is the SQL I want to get:
select
p.id,
p.name,
count(pv.product_id) as views
from
product_views pv
inner join
products p on pv.product_id = p.id
group by
pv.product_id
order by
count(product_id) desc
I tried the following and similar, but I'm getting ProductView objects, and I would like to get just an array or whatever.
ProductView.includes(:product)
.group('product_id')
.select("products.id, products.name, count(product_id)")
This kind of thing are trivial using plain SQL or LINQ, but I find myself stucked with this kind of queries in Rails. Maybe I'm not thinking in the famous 'rails way', maybe I'm missing something obvious.
So how do you do this kind of queries in Rails 3, and specifically this one? Any suggestions to improve the way I'm doing this are welcome.
Thank you
You can use Arel to do what you're looking for:
products = Product.arel_table
product_views = ProductView.arel_table
# expanded for readability:
sql = products.join(product_views)
.on(product_views[:product_id].eq(product[:id]))
.group(product_views[:product_id])
.order('views DESC')
.project(products[:id],
products[:name],
product_views[:id].count.as('views'))
products_with_views = Product.connection.select_all(sql.to_sql) # or select_rows to just get the values
Yes, it is long, but Arel is a very smart way to deal with creating complex queries that can be reused regardless of the database type.
Within a class method in the Product class:
Product.includes(:product_views).all.map { |p| [p.id, p.name, p.product_views.size] }
Then sort it however you want.
I don't know if there's a way to do it using your models. I would probably resort to:
Product.connection.select_rows(sql)
Which will give you an array of arrays. You can use select_all if you'd rather have an array of hashes.
Try this:
#product = Product.find(#product_id)
#product_views = #product.product_views.count
(Source - http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html#M000292)
Hope this helps!

ActiveRecord: Find through multiple instances

Say I have the following in my controller:
#category1
#category2
and I want to find all stores associated with those two categories...
#stores = #category1.stores + #category2.stores
this does work, but unfortunately returns an unaltered Array, rather than a AR::Base Array, and as such, I can't do things like pagination, scope, etc...
It seems to me like there's a built-in way of finding through multiple instance association... isn't there?
##stores = #category1.stores + #category2.stores
#if you want to call API methods you can just add conditions with the category id
#stores = Store.find(:all, :conditions => ['category_id=?', a || b])
With ActiveRecord, whenever you're finding a set of unique model objects, calling find on that model is usually your best bet.
Then all you need to do is constrain the join table with the categories you care about.
#stores = Store.all(:joins => :categories,
:conditions => ['category_stores.category_id in (?)', [#category1.id, #category2.id]])

Resources