Bitwise and condition in a Laravel relation (Eloquent ORM) - php

I am trying to create a specialized relation on a user (Model) to the group model. In the schema I am working with, the groups have a type attribute that is a bitmask, where each bit defines a certain characteristic for a group.
For example, we might have a group:
name: New York
type: 33554436 (1<<25 | 1<<24)
With plain SQL, I can get the groups of interest in with the query:
select g.* from foobar_group g where (type & 1<<25) != 0
I wish to define this relation in the user model for convenience and what have is:
<?php
class UserModel extends Illuminate\Database\Eloquent\Model
{
public function visitedCities()
{
return $this->belongsToMany(
GroupModel::class,
'foobar_group_member',
'user_id',
'group_id')
->with([ 'type' => function ($belongsToMany) {
$belongsToMany->where('type', '&',
GroupModel::CITY_TYPE,
GroupModel::CITY_TYPE)
}])
;
}
}
In essence, I am trying to add the where condition from the SQL query above to the join statement (relation). Do you know how to do that? Do I need to extend the Eloquent\Relation class?

You can use whereRawto execute raw where clauses.
Eloquent doesn't support more fancy where clauses than like or = and the standard stuff. I suggest you take a look at the documentation
I your case you probably want to use the something like:
$belongsToMany->whereRaw('(type & 1<<25) != 0')
and add it to your select clause.
If you want to debug your SQL you can use:
DB::enableQueryLog();
//Execute Eloquent select
dd(DB::getQueryLog());
That way you can also see that if you put anything like bitmasks into the middlepart of the where clause it gets replaced with =

Related

SQL select show table if not same value from another table (Laravel)

This is my query, I tried this query it works.
SELECT *
FROM conference_venue
WHERE id_venue NOT IN (SELECT id_venue FROM submission_data WHERE id_submission = 1);
i want to display data in conference_venue. but I don't want to display data whose id_venue is the same as the submission_data table (same as id_venue whose id_submission is mentioned).
I'm trying to make a query for the laravel version, but it's a blank white screen with no errors.
DB::table('conference_venue')
->whereNotIn('id_venue', function($q){
$q->select('id_venue')
->from('submission_data')
->where('id_submission', '=', 1);
})->select('*')->get();
This query works when I try it in sql query console but fails when I try it with Laravel query builder.
You can try this:
DB::table('conference_venue')
->select('*')
->whereRaw(
'conference_venue.id_venue NOT IN (SELECT submission_data.id_venue FROM submission_data WHERE id_submission = 1)'
);
Or better yet, create a Model for conference_venue and submission_data (ie: ConferenceVenue, SubmissionData) and you can add Eloquent relationships for ConferenceVenue and SubmissionData.
Eloquent relationships, which supports a variety of common
relationships (One To One, One To Many, Many To Many, etc.), are
defined as methods on your Eloquent model classes. Since relationships
also serve as powerful query builders, defining relationships as
methods provides powerful method chaining and querying capabilities.
Eloquent: Relationships
On you ConferenceVenue Class, you can add a method something similar to the following:
public function available() {
return this->hasMany(SubmissionData, 'id_venue')
->select('*') // You can also specify relevant columns ONLY
->whereRaw(
'conference_venue.id_venue NOT IN (SELECT submission_data.id_venue FROM submission_data WHERE id_submission = 1)'
);
}
Where you can use the relationship method as follows:
$available = ConferenceVenue::with('available')->get();

How to create a laravel relationship model with a SQL query like this

I have query like this
SELECT * FROM 'discussions' INNER JOIN comments ON comments.commentable_id = discussions.id WHERE discussions.user_id = 1 ORDER BY comments.id DESC
How to create relationships in laravel models
Not with query builder like this
DB::table('discussions')
->join('comments', 'comments.commentable_id', '=', 'discussions.id')
->where('discussions.user_id', '=', 1)
->orderBy('comments.id', 'DESC')
->get();```
First you need to know what type of relationship it is.
One:One, One:Many, Many:Many, Many:One
After that you can take a look at the documentation
You create 2 models by calling to php artisan make:model <name> within your terminal, or by manually creating them.
After that you just do whatever documentation tells you to do.
Say you have one-to-many where one discussion has many comments, it is important you specify the foreign_key in the 2nd parameter if it's non-conventional naming.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Discussion extends Model
{
/**
* Get the comments for the discussions post.
*/
public function comments()
{
// Watch the foreign_key in this instance 'commentable_id'
return $this->hasMany(Comment::class, 'commentable_id');
}
}
After that you can call something like
$comments = Discussion::where('user_id', 1)->first()->comments
To get all comments belonging to that discussion
I don't want to be a jerk, but the documentation is truly a wonderful source of information. And I advice you to read everything before starting your development journey.

CakePHP ORM : Query with a join on the same table with a non-standard association

I have a very particular use case, and i can't find a clean solution with the ORM. I've searched a lot, and maybe my database model is not correct, I'm not sure.
I use CakePHP 3.8.11.
So, I have a table "MaintenanceTypes" with 3 important fields : id, name, and periodicity. Periodicity (in days) means "this maintenance is to be done every (for instance) 30 days".
Periodicity are like 7 (week), 30 (month), 90 (trimester) and so on.
I also have a table "Operations", they are little unit tests that belongs to a "MaintenanceType" (fields are id, name, maintenance_type_id).
What is special in this case, is that, as a business rule, Operations belonging to a MaintenanceType with a periodicity of 7 days is "included" in every MaintenanceType with a greater periodicity; that means that every trimester, you should do every Operations associated directly to the trimester, but also every Operations associated with the month, and the week, etc.
In raw SQL it's trivial :slight_smile:
mt_ref is the reference MaintenanceType, mt_inc are the included MaintenanceTypes (with a lesser periodicity) and finally, every Operations belonging to any of the MaintenanceTypes found.
SELECT mt_ref.id, mt_ref.name, mt_ref.periodicity,
mt_inc.name, mt_inc.periodicity, o.name
FROM maintenance_types mt_ref
LEFT JOIN maintenance_types mt_inc
ON (mt_inc.periodicity <= mt_ref.periodicity)
LEFT JOIN operations o ON (o.maintenance_type_id = mt_inc.id)
WHERE mt_ref.id = 3
I've tried to declare the association between MaintenanceTypes, but I can't find a way to declare that the association is done on the periodicity field, and, extra points, not on a equality but on a "less or equal".
To add extra difficulties, I use this query for a (very good) JQuery Datatables CakePHP plugin (https://github.com/allanmcarvalho/cakephp-datatables), so I can't simply pass the raw SQL, and I must use the ORM...
I hope this is clear, and that someone could help me on this one !
Thanks a lot !
If you need query builder instances, then pretty much have two options here that are more or less straightforward, that is either use associations with custom conditions, or manual joins.
Custom association conditions
With associations you'd probably do something like a self-association with MaintenanceTypes with a disabled foreign key and custom conditions, like so in your MaintenanceTypesTable class:
$this
->hasMany('MaintenanceTypesInc')
->setClassName('MaintenanceTypes')
->setForeignKey(false)
->setConditions(function (
\Cake\Database\Expression\QueryExpression $exp,
\Cake\ORM\Query $query
) {
return $exp->lte(
$query->identifier('MaintenanceTypesInc.periodicity'),
$query->identifier('MaintenanceTypes.periodicity')
);
});
Disabling the foreign key will prevent the ORM from creating the default A.fk = B.fk condition when joining in the association. It should be noted that you cannot contain a hasMany association with a disabled foreign key, you can only join it in! You could use a hasOne or even belongsTo association instead, but it would kinda be a lie, as you don't have a 1:1 relationship here (at least as far as I understand it).
Also note that you don't neccesarily have to use a callback with all expressions for the conditions, you could pass the conditions as a key => value array with a manually instantiated identifier expression, or even as simple string (the latter however will not be recognized when using automatic identifier quoting):
->setConditions([
'MaintenanceTypesInc.periodicity <=' =>
new \Cake\Database\Expression\IdentifierExpression('MaintenanceTypes.periodicity');
]);
->setConditions('MaintenanceTypesInc.periodicity <= MaintenanceTypes.periodicity');
Assuming you also have an association for Operations in your MaintenanceTypesTable class, you should be able to join in both, the new association and the Operations association via the query builders *JoinWith() methods, like this:
$query = $maintenanceTypesTable
->find()
->select([
'MaintenanceTypes.id', 'MaintenanceTypes.name', 'MaintenanceTypes.periodicity',
'MaintenanceTypesInc.name', 'MaintenanceTypesInc.periodicity',
'Operations.name',
])
->leftJoinWith('MaintenanceTypesInc.Operations');
In the results, the association data will be put under the _matchingData key, ie you can obtain it like $entity->_matchingData->MaintenanceTypesInc and $entity->_matchingData->Operations. If you don't want that, then you need to use aliases for the fields of the associations, like:
->select([
'MaintenanceTypes.id', 'MaintenanceTypes.name', 'MaintenanceTypes.periodicity',
'mt_inc_name' => 'MaintenanceTypesInc.name', 'mt_inc_periodicity' => 'MaintenanceTypesInc.periodicity',
'op_name' => 'Operations.name',
])
If you don't want to select all the fields everytime, use a custom finder as in the manual joins example below.
Manual joins
Using manual joins gives you complete freedom, with the query builders *Join() methods you can create whatever joins you like, and you don't have to use possible workarounds with associations.
You can add them in a custom finder for proper reusability, it could look something like this in your MaintenanceTypesTable class:
public function findWithIncludedMaintenanceTypes(\Cake\ORM\Query $query, array $options)
{
return $query
->select(/* ... */)
->leftJoin(
['MaintenanceTypesInc' => 'maintenance_types'],
function (
\Cake\Database\Expression\QueryExpression $exp,
\Cake\ORM\Query $query
) {
return $exp->lte(
$query->identifier('MaintenanceTypesInc.periodicity'),
$query->identifier('MaintenanceTypes.periodicity')
);
}
)
->leftJoin(
['Operations' => 'operations'],
function (
\Cake\Database\Expression\QueryExpression $exp,
\Cake\ORM\Query $query
) {
return $exp->equalFields(
'Operations.maintenance_type_id ',
'MaintenanceTypesInc.id'
);
}
);
}
Then you simply use the finder wherever you need it, like this:
$query = $maintenanceTypesTable
->find('withIncludedMaintenanceTypes');
Note that just like in the associations example, you can use string or array conditions for the custom joins too.
See also
Cookbook > Database Access & ORM > Associations - Linking Tables Together
Cookbook > Database Access & ORM > Query Builder > Loading Associations
Cookbook > Database Access & ORM > Query Builder > Loading Associations > Using leftJoinWith
Cookbook > Database Access & ORM > Query Builder > Loading Associations > Adding Joins
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Using Finders to Load Data
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods

Laravel 5 issue with wherePivot

I am working with Laravel 5 and I am having issue getting ->wherePivot() to work on a Many-to-Many relationship. When I dd() the SQL it looks like Eloquent is looking for records in the pivot table with a `pose_state`.`pose_id` is null`.
I am hoping it is a simple error and not a bug. Any ideas are appreciated.
Database Structure
pose
id
name
type
state
id
name
machine_name
pose_state
pose_id
state_id
status
Models
Pose
<?php namespace App;
use DB;
use App\State;
use Illuminate\Database\Eloquent\Model;
class Pose extends Model {
public function states()
{
return $this->belongsToMany('App\State')
->withPivot('status_id')
->withTimestamps();
}
public function scopeWithPendingReviews()
{
return $this->states()
->wherePivot('status_id',10);
}
}
State
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class State extends Model {
public function poses()
{
return $this->belongsToMany('Pose')
->withPivot('status_id')
->withTimestamps();
}
}
PosesController function
public function listPosesForReview(){
$poses = Pose::withPendingReviews()->get();
dd($poses->toArray() );
}
SQL
select
`states`.*, `pose_state`.`pose_id` as `pivot_pose_id`,
`pose_state`.`state_id` as `pivot_state_id`,
`pose_state`.`status_id` as `pivot_status_id`,
`pose_state`.`created_at` as `pivot_created_at`,
`pose_state`.`updated_at` as `pivot_updated_at`
from
`states` inner join `pose_state` on `states`.`id` = `pose_state`.`state_id`
where
`pose_state`.`pose_id` is null and `pose_state`.`status_id` = ?
EDIT
When I updated my code to removing the scope it worked. Thanks #Deefour for putting me on the right path! Maybe scope has something else to that I am missing.
public function pendingReviews()
{
return $this->states()
->wherePivot('status_id','=', 10);
}
YET ANOTHER EDIT
I finally got this to work. The solution above was giving me duplicate entries. No idea why this works, but it does, so I will stick with it.
public function scopeWithStatusCode($query, $tag)
{
$query->with(['states' => function($q) use ($tag)
{
$q->wherePivot('status_id','=', $tag);
}])
->whereHas('states',function($q) use ($tag)
{
$q->where('status_id', $tag);
});
}
I think your implementation of scopeWithPendingReviews() is an abuse of the intended use of scopes.
A scope should be thought of as a reusable set of conditions to append to an existing query, even if that query is simply
SomeModel::newQuery()
The idea is that a pre-existing query would be further refined (read: 'scoped') by the conditions within the scope method, not to generate a new query, and definitely not to generate a new query based on an associated model.
By default, the first and only argument passed to a scope method is the query builder instance itself.
Your scope implementation on your Pose model was really a query against the states table as soon as you did this
$this->states()
This is why your SQL appears as it does. It's also a clear indicator you're misusing scopes. A scope might instead look like this
public function scopeWithPendingReviews($query) {
$query->join('pose_state', 'poses.id', '=', 'pose_state.pose.id')
->where('status_id', 10);
}
Unlike your new pendingReviews() method which is returning a query based on the State model, this scope will refine a query on the Pose model.
Now you can use your scope as you originally intended.
$poses = Pose::withPendingReviews();
which could be translated into the more verbose
$poses = Pose::newQuery()->withPendingReviews();
Notice also the scope above doesn't return a value. It's accepting the existing query builder object and adding onto it.
The other answer to this question is filled with misinformation.
You cannot use wherePivot() as is claims.
Your use of withTimestamps() is not at all related to your problem
You don't have to do any "custom work" to get timestamps working. Adding the withTimestamps() call as you did is all that is needed. Just make sure you have a created_at and updated_at column in your join table.
I think that your implementation of scopes is fine, the problem I see is just a typo. Your schema shows that the field is called status but your where condition is referring to a status_id
Try:
->wherePivot('status', 10);
Also, the withTimestamps() method is causing issues. You don't have timestamps in your schema for the pivot (as I can see) so you shouldn't be putting these in the your relation definitions as it's trying to fetch the timestamps relating to when the relation was created/updated. You can do this if you set up your pivot table schema to have the timestamp fields, but I think you'll have to do some custom work to get the timestamps to save properly.
This worked for me (Laravel 5.3):
$task = App\Models\PricingTask::find(1);
$task->products()->wherePivot('taggable_type', 'product')->get();
You can also have this problem (return no results) if the column you are using in wherePivot hasn't been added to withPivot.

Laravel, How to use where conditions for relation's column?

I'm using Laravel and having a small problem with Eloquent ORM.. I can get this working simply with SQL query using a JOIN but I can't seem to get it working with Eloquent!
This is what I want, I have two tabels. one is 'Restaurants' and other is 'Restaurant_Facilities'.
The tables are simple.. and One-To-One relations. like there is a restaurant table with id, name, slug, etc and another table called restaurant_facilities with id, restaurant_id, wifi, parking, etc
Now what I want to do is.. load all restaurants which have wifi = 1 or wifi = 0..
How can i do that with Eloquent ? I have tried eager loading, pivot tables, with(), collections() and nothing seems to work!
The same problem I have for a Many-To-Many relation for cuisines!
I have the same restaurant table and a cuisine table and a restaurant_cuisine_connection table..
but how do I load all restaurants inside a specific cuisine using it's ID ?
This works.
Cuisine::find(6)->restaurants()->get();
but I wanna load this from Restaurant:: model not from cuisines.. because I have many conditions chained together.. its for a search and filtering / browse page.
Any ideas or ways ? I've been struggling with this for 3 days and still no answer.
Example Models :
class Restaurant extends Eloquent {
protected $table = 'restaurants';
public function facilities() {
return $this->hasOne('Facilities');
}
}
class Facilities extends Eloquent {
protected $table = 'restaurants_facilities';
public function restaurant() {
return $this->belongsTo('Restaurant');
}
}
PS :
This seems to be working.. but this is not Eloquent way right ?
Restaurant::leftJoin(
'cuisine_restaurant',
'cuisine_restaurant.restaurant_id',
'=', 'restaurants.id'
)
->where('cuisine_id', 16)
->get();
Also what is the best method to find a count of restaurants which have specific column value without another query ? like.. i have to find the total of restaurants which have parking = 1 and wifi = 1 ?
Please help on this.
Thank you.
I don't see anything wrong with doing the left join here, if you have to load from the Restaurant model. I might abstract it away to a method on my Restaurant model, like so:
class Restaurant extends Eloquent {
protected $table = 'restaurants'; // will be default in latest L4 beta
public function facility()
{
return $this->hasOne('Facility');
}
// Or, better, make public, and inject instance to controller.
public static function withWifi()
{
return static::leftJoin(
'restaurant_facilities',
'restaurants.id', '=', 'restaurant_facilities.restaurant_id'
)->where('wifi', '=', 1);
}
}
And then, from your routes:
Route::get('/', function()
{
return Restaurant::withWifi()->get();
});
On the go - haven't tested that code, but I think it should work. You could instead use eager loading with a constraint, but that will only specify whether the facility object is null or not. It would still return all restaurants, unless you specify a where clause.
(P.S. I'd stick with the singular form of Facility. Notice how hasOne('Facilities') doesn't read correctly?)
I stumbled across this post while trying to improve my REST API methodology when building a new sharing paradigm. You want to use Eager Loading Constraints. Let's say you have an api route where your loading a shared item and it's collection of subitems such as this:
/api/shared/{share_id}/subitem/{subitem_id}
When hitting this route with a GET request, you want to load that specific subitem. Granted you could just load that model by that id, but what if we need to validate if the user has access to that shared item in the first place? One answer recommended loading the inversed relationship, but this could lead to a confusing and muddled controller very quickly. Using constraints on the eager load is a more 'eloquent' approach. So we'd load it like this:
$shared = Shared::where('id', $share_id)
->with([ 'subitems' => function($query) use ($subitem_id) {
$query->where('subitem_id', $subitem_id)
}]);
So where only want the subitem that has that id. Now we can check if it was found or not by doing something like this:
if ($shared->subitems->isEmpty())
Since subitems is a collection (array of subitems) we return the subitem[0] with this:
return $shared->subitems[0];
Use whereHas to filter by any relationship. It won't join the relation but it will filter the current model by a related property. Also look into local scopes to help with situations like this https://laravel.com/docs/5.3/eloquent#local-scopes
Your example would be:
Restaurant::whereHas('facilities', function($query) {
return $query->where('wifi', true);
})->get();
Restaurant::whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
})->get();
To achieve the same thing with local scopes:
class Restaurant extends Eloquent
{
// Relations here
public function scopeHasWifi($query)
{
return $query->whereHas('facilities', function($query) {
return $query->where('wifi', true);
});
}
public function scopeHasCuisine($query, $cuisineId)
{
return $query->whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
});
}
}
For local scopes you DO NOT want to define them as static methods on your model as this creates a new instance of the query builder and would prevent you from chaining the methods. Using a local scope will injects and returns the current instance of the query builder so you can chain as many scopes as you want like:
Restaurant::hasWifi()->hasCuisine(6)->get();
Local Scopes are defined with the prefix scope in the method name and called without scope in the method name as in the example abover.
Another solution starring whereHas() function:
$with_wifi = function ($query) {
$query->where('wifi', 1);
};
Facilities::whereHas('restaurant', $with_wifi)
Nice and tidy.
Do you absolutely have to load it from the Restaurant model? In order to solve the problem, I usually approach it inversely.
Facilities::with('restaurant')->where('wifi' ,'=', 0)->get();
This will get all the restaurant facilities that match your conditions, and eager load the restaurant.
You can chain more conditions and count the total like this..
Facilities::with('restaurant')
->where('wifi' ,'=', 1)
->where('parking','=', 1)
->count();
This will work with cuisine as well
Cuisine::with('restaurant')->where('id','=',1)->get();
This grabs the cuisine object with the id of 1 eager loaded with all the restaurants that have this cuisine

Categories