I have a problem with joining tables into one huge table so that all colums are accessible without nested FORs or using relations in them. Situation is like this:
cars
========
id_producer (PK)
id_model (PK)
length
weight
...
texts
============
id_model (PK)
language (PK)
text
...
Tables share 1 key: id_model
I want to join these tables like this:
SELECT *
FROM cars c
JOIN texts t ON c.id_model = t.id_model
WHERE t.language = 'english'
.. it will return 1 row for each car.
SQL is easy, Yii is not :(
I tried to do it usinq scope or relation, but never got desired output.
I want to write following:
$carsWithTexts = Cars::model()-> ... something ... ->findAll()
foreach ($carsWithTexts as $c)
{
echo $c->id_producer;
echo $c->id_model;
echo $c->id_text;
}
.. No nested FORs, no relations or scopes in the FOR.
Is this possible? Or do i have to always use following construction:
$carsWithTexts = Cars::model()-> ... relation ... ->findAll()
foreach ($carsWithTexts as $c)
{
echo $c->id_producer;
echo $c->id_model;
echo $c->relation[0]["id_text"]; // or nested for
}
I know I can use commands "with" and "together", I did, but it didnt work as I want.
Yii is not difficult, you just need to read the documentation and apply their examples, what you wanna do can be done like this:
I'll asume you have generated the code using Gii, so the relation name (you can check it at Car class) will be texts. Now you have two approachs to do it, the easy way:
Cars::model()->with('texts')->findAll("texts.language = 'english'");
This will return all cars with its text information, where the text has english language. I recommend you to read Relational Active Record from the official guide to learn more about this.
And the other approach is to use CDbCommand to execute an SQL command like this:
$connection=Yii::app()->db; // assuming you have configured a "db" connection
$command=$connection->createCommand();
$command->from('cars');
$command->join('texts', 't.id_model = texts.id_model');
$command->where('texts.language=:language', array(':id'=>'english'));
$rows=$command->queryAll();
This way is more SQL friendly but as you can see there are a lot of more PHP lines.
Related
In our application, the Recipe model has many ingredients (many-to-many relationship implemented using :through). There is a query to return all the recipes where at least one ingredient from the list is contained (using ILIKE or SIMILAR TO clause). I would like to pose two questions:
What is the cleanest way to write the query which will return this in Rails 6 with ActiveRecord. Here is what we ended up with
ingredients_clause = '%(' + params[:ingredients].map { |i| i.downcase }.join("|") + ')%'
recipes = recipes.where("LOWER(ingredients.name) SIMILAR TO ?", ingredients_clause)
Note that recipes is already created before this point.
However, this is a bit dirty solution.
I also tried to use ILIKE = any(array['ing1', 'ing2',..]) with the following:
ingredients_clause = params[:ingredients].map { |i| "'%#{i}%'" }.join(", ")
recipes = recipes.where("ingredients.name ILIKE ANY(ARRAY[?])", ingredients_clause)
This won't work since ? automatically adds single quotes so it would be
ILIKE ANY (ARRAY[''ing1', 'ing2', 'ing3'']) which is of course wrong.
Here, ? is used to sanitise parameters for SQL query, so avoid possible SQL injection attacks. That is why I don't want to write a plain query formed from params.
Is there any better way to do this?
What is the best approach to order results by the number of ingredients that are matched? For example, if I search for all recipes that contains ingredients ing1 and ing2 it should return those which contains both before those which contains only one ingredient.
Thanks in advance
For #1, a possible solution would be something like (assuming the ingredients table is already joined):
recipies = recipies.where(Ingredients.arel_table[:name].lower.matches_any(params[:ingredients]))
You can find more discussion on this kind of topic here: Case-insensitive search in Rails model
You can access a lot of great SQL query features via #arel_table.
#2 If we assume all the where clauses are applied to recipies already:
recipies = recipies
.group("recipies.id")
# Lets Rails know you meant to put a raw SQL expression here
.order(Arel.sql("count(*) DESC"))
It's rather trivial to retrieve data from multiple tables that are related through foreign keys using raw SQL. I can do, for example:
SELECT title, domestic_sales
FROM movies
JOIN boxoffice
ON movies.id = boxoffice.movie_id;
This would give me a table with two colums: title and domestic_sales, where the data in the first column comes from the table movies and the data in the second column comes from the table boxoffice.
How can I do this in Rails using Ruby code? I can, of course, get the same result if I use raw SQL. So, I could do the following:
ActiveRecord::Base.connection.execute(<<-SQL)
SELECT title, domestic_sales
FROM movies
JOIN boxoffice
ON movies.id = boxoffice.movie_id;
SQL
This would give me a PG::Result object with the data I want. But this is super inelegant. I would like to be able to get this information without using raw SQL.
So, this is the first thing that comes to mind is:
Movie.select(:name, :domestic_sales).joins(:box_office)
The problem, however, is that the aforementioned line of code returns a bunch of Movie objects. Since the Movie class doesn't have the domestic_sales attribute, I don't get access to that information.
The next thing I thought was to use a loop. So, I could do something like:
Movie.joins(:box_office).to_a.map do |m|
{name: m.name, rating: m.box_office.domestic_sales}
end
This gives me exactly the data I want. But it costs n + 1 SQL queries, which is not good. I should be able to get this with just one query...
So: How can I retrieve the data I want without using raw SQL and without using loops that cost multiple queries?
SELECT title, domestic_sales
FROM movies
JOIN boxoffice
ON movies.id = boxoffice.movie_id;
translated to ActiveRecord would look like this
Movie
.select(:title, :domestice_sales)
.joins("boxoffice ON movies.id = boxoffice.movie_id")
When you have proper associations defined in your models you would would be able to write:
Movie
.select(:title, :domestice_sales)
.joins(:boxoffices)
And when you do not need an instance of ActiveRecord and would be fine with a nested array, you can even write:
Movie
.joins(:boxoffices)
.pluck(:title, :domestice_sales)
Try this way.
Movie.joins(:box_office).pluck(:title, :domestic_sales)
I have a domain object:
class Business {
String name
List subUnits
static hasMany = [
subUnits : SubUnit,
]
}
I want to get name and subUnits using HQL, but I get an error
Exception: org.springframework.orm.hibernate4.HibernateQueryException: not an entity
when using:
List businesses = Business.executeQuery("select business.name, business.subUnits from Business as business")
Is there a way I can get subUnits returned in the result query result as a List using HQL? When I use a left join, the query result is a flattened List that duplicates name. The actual query is more complicated - this is a simplified version, so I can't just use Business.list().
I thought I should add it as an answer, since I been doing this sort of thing for a while and a lot of knowledge that I can share with others:
As per suggestion from Yariash above:
This is forward walking through a domain object vs grabbing info as a flat list (map). There is expense involved when having an entire object then asking it to loop through and return many relations vs having it all in one contained list
#anonymous1 that sounds correct with left join - you can take a look at 'group by name' added to end of your query. Alternatively when you have all the results you can use businesses.groupBy{it.name} (this is a cool groovy feature} take a look at the output of the groupBy to understand what it has done to the
But If you are attempting to grab the entire object and map it back then actually the cost is still very hefty and is probably as costly as the suggestion by Yariash and possibly worse.
List businesses = Business.executeQuery("select new map(business.name as name, su.field1 as field1, su.field2 as field2) from Business b left join b.subUnits su ")
The above is really what you should be trying to do, left joining then grabbing each of the inner elements of the hasMany as part of your over all map you are returning within that list.
then when you have your results
def groupedBusinesses=businesses.groupBy{it.name} where name was the main object from the main class that has the hasMany relation.
If you then look at you will see each name has its own list
groupedBusinesses: [name1: [ [field1,field2,field3], [field1,field2,field3] ]
you can now do
groupedBusinesses.get(name) to get entire list for that hasMany relation.
Enable SQL logging for above hql query then compare it to
List businesses = Business.executeQuery("select new map(b.name as name, su as subUnits) from Business b left join b.subUnits su ")
What you will see is that the 2nd query will generate huge SQL queries to get the data since it attempts to map entire entry per row.
I have tested this theory and it always tends to be around an entire page full of query if not maybe multiple pages of SQL query created from within HQL compared to a few lines of query created by first example.
In a rails 4 app, in one model I have a column containing multiple ids as a string with comma separated values.
"123,4568,12"
I have a "search" engine that I use to retrieve the records with one or many values using the full text search of postgresql I can do something like this which is very useful:
records = MyModel.where("my_models.col_name ## ?", ["12","234"])
This return all the records that have both 12 and 234 in the targeted column. The array comes from a form with a multiple select.
Now I'm trying to make a query that will find all the records that have either 12 or 234 in there string.
I was hopping to be able to do something like:
records = MyModel.where("my_models.col_name IN (?)", ["12","234"])
But it's not working.
Should I iterate through all the values in the array to build a query with multiple OR ? Is there something more appropriate to do this?
EDIT / TL;DR
#BoraMa answer is a good way to achieve this.
To find all the records containing one or more ids referenced in the request use:
records = MyModel.where("my_models.col_name ## to_tsquery(?)", ["12","234"].join('|'))
You need the to_tsquery(?) and the join with a single pipe |to do a OR like query.
To find all the records containing exactly all the ids in the query use:
records = MyModel.where("my_models.col_name ## ?", ["12","234"])
And of course replace ["12","234"] with something like params[:params_from_my_form]
Postgres documentation for full text search
If you already started to use the fulltext search in Postgres in the first place,I'd try to leverage it again. I think you can use a fulltext OR query which can be constructed like this:
records = MyModel.where("my_models.col_name ## to_tsquery(?)", ["12","234"].join(" | "));
This uses the | operator for ORing fulltext queries in Postgres. I have not tested this and maybe you'll need to do to_tsvector('my_models.col_name') for this to work.
See the documentation for more info.
Suppose your ids are :
a="1,2,3,4"
You can simply use:
ModelName.find(a)
This will give you all the record of that model whose id is present in a.
I just think a super simple solution, we just sort the ids in saving callback of MyModel, then the query must be easier:
class MyModel < ActiveRecord::Base
before_save :sort_ids_in_col_name, if: :col_name_changed?
private
def sort_ids_in_col_name
self.col_name = self.col_name.to_s.split(',').sort.join(',')
end
end
Then the query will be easy:
ids = ["12","234"]
records = MyModel.where(col_name: ids.sort.join(',')
I want to create a search function on my website, and I don't want to use a plugin for this thing, because it's very simple, but I can't solve this problem:
I give the keyword to the model which creates a query, but I couldn't figure out how to put joker characters in this query.
I'm using Propel
Dennis
The filterByXXX() query functions will use LIKE when your query contains wildcards:
$books = BookQuery::create()
->filterByTitle('War%')
->find();
// example Query generated for a MySQL database
$query = 'SELECT book.* from `book` WHERE book.TITLE LIKE :p1'; // :p1 => 'War%'
Remember, the wildcards you can use in SQL are _ for exactly one and % for zero or more characters. So not ? or *.