Rails3 ActiveRecord and custom SQL JOINS with default_scope - ruby-on-rails

I need to map a non-existing table to ActiveRecord (in my case: the Wordpress Database Schema), which consists of a custom SQL statement as described below. I want to be able to use find(), first(), all() among other statements. Is there a way to accomplish this without actually overwriting all the finder methods? (i am currently redefining those methods, to accomplish similar results, but would love to know if there is a more ActiveRecord/Rails way to do so)
currently i am doing for example
class Category < ActiveRecord::Base
self.table_name="wp_terms"
def self.all
Category.find_by_sql("select distinct(wp_terms.term_id),wp_terms.name,wp_term_taxonomy.description,wp_term_taxonomy.parent,wp_term_taxonomy.count from wp_terms inner join wp_term_relationships ON wp_term_relationships.object_id = wp_terms.term_id INNER JOIN wp_term_taxonomy ON wp_term_taxonomy.term_id = wp_terms.term_id where taxonomy = 'category';")
end
end
thanks for any pointers.

You can use a default scope:
class Category < ActiveRecord::Base
self.table_name="wp_terms"
# Individual scopes for readability - the names could be improved.
scope :select_scope, select("DISTINCT(wp_terms.term_id), wp_terms.name, wp_term_taxonomy.description, wp_term_taxonomy.parent, wp_term_taxonomy.count")
scope :join_scope, joins("INNER JOIN wp_term_relationships ON wp_term_relationships.object_id = wp_terms.term_id INNER JOIN wp_term_taxonomy ON wp_term_taxonomy.term_id = wp_terms.term_id")
scope :where_scope, where("taxonomy = 'category'")
default_scope select_scope.join_scope.where_scope
end
Then you should be able to call any finder method on Category without having to implement it yourself.

Would you consider creating a view-backed model as outlined in Enterprise Rails?

Related

ActiveRecord WHERE with namespaced models

I've two models within the same namespace/module:
module ReverseAuction
class Demand < ApplicationRecord
belongs_to :purchase_order, inverse_of: :demands, counter_cache: true
end
end
module ReverseAuction
class PurchaseOrder < ApplicationRecord
has_many :demands
end
end
note that I don't have to specify the class_name for the models cause they're in the same module and the relations are working well this way.
When I try to query a includes with the name of the relation by itself, it works fine, like:
ReverseAuction::PurchaseOrder.all.includes(:demands) # all right .. AR is able to figure out that *:demands* correspond to the 'reverse_auction_demands' table
But when I try to use a where in this query, AR seems to be unable to figure out the (namespaced) table name by itself, so:
ReverseAuction::PurchaseOrder.includes(:demands).where(demands: {user_id: 1}) # gives me error: 'ERROR: missing FROM-clause entry for table "demands"'
But if I specify the full resolved (namespaced) model name, then where goes well:
ReverseAuction::PurchaseOrder.includes(:demands).where(reverse_auction_demands: {user_id: 1}) # works pretty well
Is that normal that AR can infere table name of namespaced models from relations in includes but can't in where, or am I missing the point?
Is that normal that AR can infere table name of namespaced models from
relations in includes but can't in where?
Yes. This is an example of a leaky abstraction.
Assocations are an objection oriented abstraction around SQL joins, to let you do the fun stuff while AR worries about writing the SQL to join them and maintaining the in memory couplings between the records. .joins, .left_joins .includes and .eager_load are "aware" of your assocations and go through that abstraction. Because you have this object oriented abstraction .includes is smart enough to figure out how the module nesting should effect the class names and table names when writing joins.
.where and all the other parts of the ActiveRecord query interface are not as smart. This is just an API that generates SQL queries programmatically.
When you do .where(foo: 'bar') its smart enough to translate that into WHERE table_name.foo = 'bar' because the class is aware of its own table name.
When you do .where(demands: {user_id: 1}) the method is not actually aware of your associations, other model classes or the schema and just generates WHERE demands.user_id = 1 because that's how it translates a nested hash into SQL.
And note that this really has nothing to do with namespaces. When you do:
.where(reverse_auction_demands: {user_id: 1})
It works because you're using the right table name. If you where using a non-conventional table name that didn't line up with the model you would have the exact same issue.
If you want to create a where clause based on the class without hardcoding the table name pass a scope to where:
.where(
ReverseAuction::Demand.where(user_id: 1)
)
or use Arel:
.where(
ReverseAuction::Demand.arel_table[:user_id].eq(1)
)

ActiveRecord: doing a join with unscoped doesn't skip the default_scope

I am having some issues when trying to skip a default_scope when doing an ActiveRecord join.
Even though the codebase is quite large, I am just showing the basics as I think it shows pretty well what the problem is:
class Client
belongs_to :company
end
class Company
default_scope { where(version_id: nil) }
end
I am building a complex report, so I need to join multiple tables and filter on them. However, I can't successfully skip the default scope when fetching Clients.
Client.joins(:company).to_sql
# SELECT clients.* FROM clients INNER JOIN companies
# ON companies.id = clients.company_id AND companies.version_id IS NULL
As you can see that is automatically including the Company default_scope. So I tried this:
Company.unscoped { Client.joins(:company) }.to_sql
# SELECT clients.* FROM clients INNER JOIN companies
# ON companies.id = clients.company_id AND companies.version_id IS NULL
Again, I got the same result, even when using unscoped with the block.
Then I decided to add a new association to the model, with the unscoped scope:
class Client
belongs_to :company
belongs_to :unscoped_company, -> { unscoped }, foreign_key: :company_id, class_name: "Company"
end
Having added that, I gave another try:
Client.joins(:unscoped_company).to_sql
# SELECT clients.* FROM clients INNER JOIN companies
# ON companies.id = clients.company_id AND companies.version_id IS NULL
And still the scoped is being applied.
Do you know how can I successfully join both tables without applying that default_scope?
Removing that default_scope is not an option as It is a big application and changing that will require too much time.
Rails v4.2.7
Ruby v2.2.3
I did some research without finding any straight solution.
Here a couple of workarounds. I cannot say if they're going to work in your chained joins.
First basic, do it manually:
Client.joins("INNER JOINS companies ON companies.id = clients.company_id").to_sql
Other option define a `CompanyUnscoped` class which inherits from `Company`, removing the default_scope:
class CompanyUnscoped < Company
self.default_scopes = []
end
Don't forget to add this line to Client class:
belongs_to :company_unscoped, foreign_key: :company_id
Then you should be able to call
Client.joins(:company_unscoped)
#=> SELECT "clients".* FROM "clients" INNER JOIN "companies" ON "companies"."id" = "clients"."company_id"
Apply directly as a class method
Client.unscoped.joins(:company).to_sql
You might try https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-unscope but I would've expected https://api.rubyonrails.org/classes/ActiveRecord/Scoping/Default/ClassMethods.html#method-i-unscoped to work. You might try it again without a block, letting it come after the undesired scope is applied.
Seems like there isn't any straightforward solution that you are looking for. There are couple of discussion threads for reference.
https://github.com/rails/rails/issues/20679
https://github.com/rails/rails/issues/20011
But I liked the way what #iGian has suggested.
You can do it manually:
Client.joins('inner join company on companies.id = clients.company_id')

Selecting only associations between engines

I need to grab all users that have an application. User is part of my core engine, which is used by many other engines. I'd like to keep User unaware of what is using it, which is why I don't want to add has_many :training_applications in my User model.
Here are the classes
module Account
class User < ActiveRecord::Base
end
end
module Training
class TrainingApplication < ActiveRecord::Base
belongs_to :user, class: Account::User
end
end
The following obviously won't work because User has no concept of TrainingApplication:
Account::User.joins(:training_application).distinct
Is there an elegant way to return a distinct collection of User objects that are associated with a TrainingApplication?
What I landed on as a quick solution is
Account::User.where(id: Training::TrainingApplication.all.pluck(:user_id))
but I'm thinking that there's a better solution.
In case there is no way you can add a has_many :training_applications association to the User, the following should be suitable solutions:
You could type up a joins string yourself:
t1 = Account::User.table_name
t2 = Training::TrainingApplication.table_name
Account::User.
joins("INNER JOINS #{t2} ON #{t2}.user_id = #{t1}.id").
group("#{t1}.id")
For the sake of variety, let me cover the subquery method as well:
Account::User.where("id IN (SELECT user_id FROM #{t2})")
I would go with the joins method but I believe both solutions will be faster than your current implementation.

ORDER BY and DISTINCT ON (...) in Rails

I am trying to ORDER by created_at and then get a DISTINCT set based on a foreign key.
The other part is to somehow use this is ActiveModelSerializer. Specifically I want to be able to declare:
has_many :somethings
In the serializer. Let me explain further...
I am able to get the results I need with this custom sql:
def latest_product_levels
sql = "SELECT DISTINCT ON (product_id) client_product_levels.product_id,
client_product_levels.* FROM client_product_levels WHERE client_product_levels.client_id = #{id} ORDER BY product_id,
client_product_levels.created_at DESC";
results = ActiveRecord::Base.connection.execute(sql)
end
Is there any possible way to get this result but as a condition on a has_many relationship so that I can use it in AMS?
In pseudo code: #client.products_levels
Would do something like: #client.order(created_at: :desc).select(:product_id).distinct
That of course fails for reasons that are beyond me.
Any help would be great.
Thank you.
A good way to structure this is to split your query into two parts: the first part manages the filtering of rows so that you get only your latest client product levels. The second part uses a standard has_many association to connect Client with ClientProductLevel.
Starting with your ClientProductLevel model, you can create a scope to do the latest filtering:
class ClientProductLevel < ActiveRecord::Base
scope :latest, -> {
select("distinct on(product_id) client_product_levels.product_id,
client_product_levels.*").
order("product_id, created_at desc")
}
end
You can use this scope anywhere that you have a query that returns a list of ClientProductLevel objects, e.g., ClientProductLevel.latest or ClientProductLevel.where("created_at < ?", 1.week.ago).latest, etc.
If you haven't already done so, set up your Client class with a has_many relationship:
class Client < ActiveRecord::Base
has_many :client_product_levels
end
Then in your ActiveModelSerializer try this:
class ClientSerializer < ActiveModel::Serializer
has_many :client_product_levels
def client_product_levels
object.client_product_levels.latest
end
end
When you invoke the ClientSerializer to serialize a Client object, the serializer sees the has_many declaration, which it would ordinarily forward to your Client object, but since we've got a locally defined method by that name, it invokes that method instead. (Note that this has_many declaration is not the same as an ActiveRecord has_many, which specifies a relationship between tables: in this case, it's just saying that the serializer should present an array of serialized objects under the key `client_product_levels'.)
The ClientSerializer#client_product_levels method in turn invokes the has_many association from the client object, and then applies the latest scope to it. The most powerful thing about ActiveRecord is the way it allows you to chain together disparate components into a single query. Here, the has_many generates the `where client_id = $X' portion, and the scope generates the rest of the query. Et voila!
In terms of simplification: ActiveRecord doesn't have native support for distinct on, so you're stuck with that part of the custom sql. I don't know whether you need to include client_product_levels.product_id explicitly in your select clause, as it's already being included by the *. You might try dumping it.

Model associations

I have two models Library and Book. In my Library model, I have an array - book_ids. The primary key of Book model is ID.
How do I create a has_many :books relation in my library model?
This is a legacy database we are using with rails.
Thanks.
Your database schema doesn't really conform with the prescribed Rails conventions so you will probably have a hard time making the default has_many association work. Have you tried fiddling with the custom SQL options with it thought?
If you can't get the built in has_many association to work, you'll have to roll your own. I would define the books and books= methods on your Library model, and inside them set a virtual attribute, which you then save as an array in the database. Perhaps something like this:
class Book > ActiveRecord::Base; end
class Library > ActiveRecord::Base
before_save :serialize_books
def books
#books || nil
end
def books=(new_books)
#books = new_books
end
private
def serialize_books
#attributes['books'] = "[" + #books.collect {|b| b.id }.join(',') + "]"
end
end
That up there wouldn't pull out the dataIf you wanted to go even more gung ho and support single query find operations, you could use some custom SQL in a scope or override find and add it to the default options. Comment if you want help with any of this!
If you want to use has_many you could use the options :counter_sql and :finder_sql using the MySQL LIKE or REGEX syntax. But its probably better to first load the Libary model, then parse the book_ids column and load the books, or directly build a query with that string.
Consider using :serialize method with ActiveRecord:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002284
it might do what you want

Resources