I am having Orders with status(Pending, and Completed), Pickup_date, dropoff_date.
And i want to order conditionally on the basis of status.
If status is Pending, it should be order by pickup_time.
If status is Completed, it should be order by dropoff_time.
We can do it in single query in scope as :
scope :order_by_time, -> { order("CASE WHEN (orders.state IN ('pending', 'picked_up')) THEN 'orders.pick_up_time' WHEN (orders.state IN ('ready_for_delivery', 'delivered')) THEN 'orders.delivery_time' END") }
I would highly recommend not to mix scope responsibilities. If you do it all in one scope called order_by_time, the naming is confusing, if I see Order.order_by_time I will assume that it only orders the result, and I will be truly surprised when I learn the reality after checking the implementation...
I would recommend some isolation, which provides more flexibility for later use:
scope :pending, -> { where(status: :pending) }
scope :completed, -> { where(status: :completed) }
scope :order_by_pickup_time, -> { order(pickup_time: :desc) }
scope :order_by_dropof_time, -> { order(dropof_time: :desc) }
Then you could use them:
Order.pending.order_by_pickup_time
Order.completed.order_by_dropof_time
In the model
scope :pending, -> { where(status: "pending").order(pickup_time: :desc) }
scope :completed, -> { where(status: "completed").order(dropof_time: :desc) }
Then use Order.pending it will order by pick up time it with all pending orders
Then use Order.completed it will order it with all conmpleted orders by dropoff time
Query using conditionals
I suggest to use conditionals query
class Orders < ApplicationRecord
scope :order_by_pickup_time, ->(status) { where("created_at < ?", pickup_time) if status == "pending" }
end
Like the other examples, this will behave similarly to a class method.
class Orders < ApplicationRecord
def self.order_by_pickup_time(status)
where("created_at < ?", pickup_time) if status == "pending"
end
end
However, there is one important caveat: A scope will always return an ActiveRecord::Relation object, even if the conditional evaluates to false, whereas a class method, will return nil. This can cause NoMethodError when chaining class methods with conditionals, if any of the conditionals return false.
Related
I'm creating scope for searching using form parameter but I don't know how to combine scope with conditions or call current scope.
Here is my source code
class Issue < ApplicationRecord
default_scope { order(created_at: :desc) }
scope :state, ->(flag = :open){
where state: flag
}
scope :sort_by, ->(field = :github_id, sort_type = :asc){
reorder({field.to_sym => (sort_type && sort_type.to_sym) || :asc })
}
scope :milestone, ->(milestone){
where milestone: milestone
}
scope :assignee, ->(assignee){
where assignee: assignee
}
scope :author, ->(author){
where author: author
}
scope :search, ->(param={}){
# assign result = default scope here and chain it using below scope
sort_by(param[:sort_by],param[:sort_type]) if param[:sort_by].present?
author(param[:author]) if param[:author].present?
assignee(param[:assignee]) if param[:assignee].present?
milestone(param[:milestone]) if param[:milestone].present?
}
end
Using plus signs (untested):
scope :search, ->(param={}) {
all
+ relation.sort_by(param[:sort_by],param[:sort_type]) if param[:sort_by].present?
+ relation.author(param[:author]) if param[:author].present?
+ relation.assignee(param[:assignee]) if param[:assignee].present?
+ relation.milestone(param[:milestone]) if param[:milestone].present?
}
Another example would be:
User.where(thing: true) + User.where(thing: false)
Because they both return ActiveRecord::Relation object collections.
You can use a local variable:
scope :search, ->(param={}) {
relation = all
relation = relation.sort_by(param[:sort_by],param[:sort_type]) if param[:sort_by].present?
relation = relation.author(param[:author]) if param[:author].present?
relation = relation.assignee(param[:assignee]) if param[:assignee].present?
relation = relation.milestone(param[:milestone]) if param[:milestone].present?
relation
}
Yii1 used to have beforeFind method in which you could modify the query or whatever else you might want to do.
In Yii2 the suggested alternative is to use the modelQuery solution for example
class MyModel extends \yii\db\ActiveRecord
{
public static function find()
{
return new MyModelQuery(get_called_class());
}
/* ... */
}
and
class MyModelQuery extends \yii\db\ActiveQuery
{
public function init( )
{
/** do something here **/
}
}
But how do I pass or reference MyModel within MyModelQuery?
For example:-
class MyModelQuery extends \yii\db\ActiveQuery
{
public function init( )
{
$sql = "SET #variable = {$MyModel->variable1}";
}
}
EDIT
For completeness, I've added a use case to help others in future.
I have nested views with group by's running under MySql and it runs VERY badly.
In my case, I have orders, order-items and order-item-fees tables, each one-to-many to the next and I want to sum the order totals. I have nested view, one at each level to sum to the level above, but at the order-item and order-item-fee levels MySql is grouping the whole table first (I cannot use algorithm=merge as I have a GROUP BY).
I'm implementing the Pushdown method where you define a SQL variable to use in sub-views to narrow down the search as outlined here: http://code.openark.org/blog/mysql/views-better-performance-with-condition-pushdown
and also here
https://www.percona.com/blog/2010/05/19/a-workaround-for-the-performance-problems-of-temptable-views/
In this way, if I can add a 'WHERE order_id=' to the where clause of the two sub-views, I reduce a 3.5 second query down to 0.003 second query.
So using, Salem's suggestion below, I can execute a SQL statement 'SET #order_id=1234' before my query, which is then picked up in the order-item and order-item-fee views using a function. Note: this is connection specific, so no danger of collisions between sessions.
A bit convoluted but fast.
It would be interesting, though, to see a performance comparison between SQL and looping in PHP perhaps....
EDIT 2
In fact, you normally use find() as a static method, so there is no way of using $this->order_id, so I changed this to over-ride the findOne method
public static function findOne( $orderId )
{
if ( isset($orderId) )
{
$sql = "SET #orderId='{$orderId}'";
Yii::$app->db->createCommand($sql)->execute();
}
return parent::findOne( $orderId );
}
I also use this view with other searches, so in the view I need to check whether the orderId is set or not ...
where (
CASE
WHEN ( NOT isnull( get_session_orderId() ) )
THEN `order`.order_id = get_session_cartref()
ELSE `order`.order_id LIKE '%'
END
)
About how to involve an ActiveQuery class check my answer here:
Yii2 : ActiveQuery Example and what is the reason to generate ActiveQuery class separately in Gii?
But if what you are trying to do doesn't require building named scopes then you may simply override the find method by something like this:
public static function find()
{
return parent::find()->where(['variable' => 'some value']);
}
Trying to do something pretty straightforward in a Rails controller. Right now, I am showing a user all results matching their user_id from a Scoreboard table.
I now want to adjust that to show results from their user_id but also only scores greater than the integer 0. So I changed my controller to:
def index
#scoreboards = Scoreboard.where(user_id: current_user.id, "score >= 0").order(score: :desc)
end
This receives a syntax error, so my comparison in the .where is probably wrong. How should I add this second condition?
I try to avoid putting these kind of database operations directly in the controller, because the model is the more appropriate place. I'd write three scopes in the model:
class Scoreboard < ActiveRecord::Base
#...
scope :for_user_id, ->(user_id) { where(user_id: user_id) }
scope :with_scores, -> { where('score > 0') }
scope :by_descending_score, -> { order(score: :desc) }
#...
end
...then in the controller, you'd merely write this:
Scoreboard.for_user_id(current_user.id).with_scores.by_descending_score
This keeps your controller thinner (and more readable) while potentially supporting re-use of these lookups in the most atomic fashion and keeping the database logic wholly contained in the model.
I would try this:
#scoreboards = Scoreboard.where(user_id: current_user.id)
.where("category >= 0").order(score: :desc)
Im developing a project which uses ORM to make project run on every database system as much as we can.
Project uses postgresql right now. Im wondering how to use database specific functions without losing ORM modularity.
For example:
I have to use "extract" function for one query like so;
DELETE FROM tokens AS t WHERE (extract(epoch from t.created_at) + t.expires) < extract(epoch from NOW())
If i want to use model class to achieve this. Soon or late i need to write extract function where clause in raw format
Tokens::whereRaw('(extract(epoch from t.created_at) + t.expires) < extract(epoch from NOW())')->get();
If i use query builder
DB::table('tokens')->whereRaw('(extract(epoch from t.created_at) + t.expires) < extract(epoch from NOW())')->select()->get();
Same things happens
I need something like when i use postgresql ORM need to use EXTRACT() function or when i use mysql ORM need to use UNIX_TIMESTAMP() function
What the ways i can use to achieve this ?
This could go in the respective drivers, but Taylor Otwell's view on driver-specific functions is, that you simply should use raw statements, just like you do.
However in Eloquent you can pretty easily do it yourself:
// BaseModel / trait / builder macro or whatever you like
public function scopeWhereUnix($query, $col, $operator = null, $value = null)
{
if (func_num_args() == 3)
{
list($value, $operator) = array($operator, '=');
}
switch (class_basename($query->getQuery()->getConnection()))
{
case 'MySqlConnection':
$col = DB::raw("unix_timestamp({$col})");
break;
case 'PostgresConnection':
$col = DB::raw("extract(epoch from {$col})");
break;
}
$query->where($col, $operator, $value);
}
Now you can do this:
Tokens::whereUnix('created_at', 'value')->toSql();
// select * from tokens where unix_timestamp(created_at) = 'value'
// or
// select * from tokens where extract(epoch from created_at) = 'value'
You have a bit more complex condition, but you still can achieve that with a little bit of hack:
Tokens::whereUnix(DB::raw('created_at) + (expires', '<', time())->toSql();
// select * from tokens where unix_timestamp(created_at) + (expires) < 12345678
// or
// select * from tokens where extract(epoch from created_at) + (expires) < 12345678
Unfortunately Query\Builder (DB::table(..)) is not that easy to extend - in fact it is not extendable at all, so you would need to swap it with your own Builder class, what is rather cumbersome.
Take this logic out of the models.
Create a repository for Postgres, let's call it PostgresTokenRepository. The constructor of this repository should look like...
<?php
class PostgresTokenRepository implements TokenRepositoryInterface
{
protected $token;
public function __construct(Token $token)
{
$this->token = $token;
}
public function getTokens()
{
return $this->token->whereRaw('(extract(epoch from t.created_at) + t.expires) < extract(epoch from NOW())')->get();
}
}
And you will need an interface... TokenRepositoryInterface
interface TokenRepositoryInterface
{
public function getTokens();
}
Now you should be all set as far as the repository goes. If you need to do a MySQL implementation, just create a MysqlTokenRepository which will look similar except the getTokens() function would use UNIX_TIMESTAMP().
Now you need to tell Laravel that when you are looking for an implementation of TokenRepositoryInterface, it should return PostgresTokenRepository. For that, we will need to create a service provider.
<?php
class UserServiceProvider extends \Illuminate\Support\ServiceProvider
{
public function register()
{
$this->app->bind('TokenRepositoryInterface', 'PostgresTokenRepository');
}
}
And now the only thing left to do is add this Service Provider to the service providers array in config/app.php.
Now whenever you need this repository in your controllers, you can have them automatically injected. Here is an example...
class TokenController extends BaseController
{
protected $token;
public function __construct(TokenRepositoryInterface $token)
{
$this->token = $token;
}
public function index()
{
$tokens = $this->token->getTokens();
return View::make('token.index')->with('tokens', $tokens);
}
}
The purpose for doing it this way is when you want to start using the MySQL implementation, all you have to do is modify the service provider to return MysqlTokenRepository instead of PostgresTokenRepository. Or if you want to write a new implementation all together, it will all be possible without having to change production code. If something doesn't work, simply change that one line back to PostgresTokenRepository.
One other benefit that sold me is this gives you the capability of keeping your models and controllers very light and very testable.
I ended up creating a global scope. Created a trait like ExpiresWithTimestampsTrait that contains the logic for whereExpires scope. The scope does adding where clause that specific to database driver.
public function scopeWhereExpired($query)
{
// Eloquent class is my helper for getting connection type based on the #jarek's answer
$type = Eloquent::getConnectionType($this);
switch ($type) {
case Eloquent::CONNECTION_POSTGRESS:
return $query->whereRaw("(round(extract(epoch from (created_at)) + expires)) < round(extract(epoch from LOCALTIMESTAMP))");
break;
default:
break;
}
}
So i just need to use that trait on the model. I need to add just an "case" clause to whereExpires scope for support mysql with where clause in the future when i start using mysql
Thanks to everybody!
My models look like this:
class Ticket < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :tickets
end
I want to have a scope that gives me all of the distinct Tickets that are not tagged unresolved (as in tag.name != "unresolved")
How would I go about doing this? For example, if 1 ticket has 6 tags (one of which being unresolved) I only want to return 1 instance of that ticket, not 5 in the scope. I've managed to do the opposite (all Tickets that are tagged unresolved) as such:
scope :unresolved, :select => "DISTINCT tickets.*", :joins => :tags, :conditions => "tags.name = 'unresolved'"
Depending on how flexible you want the scope to chain you have two options.
Use uniq within the scope (note: this may have a negative impact on chaining this scope with others, especially when adding more complex conditionals):
scope :unresolved, -> { joins(:tags).where(tags: { name: 'unresolved' }).uniq }
Use a LEFT OUTER join via includes(:tags) instead of the INNER join default used by joins(:tags)
scope :unresolved, -> { includes(:tags).where(tags: { name: 'unresolved' }) }