Laravel Eloquent ManyToMany with Pivot, secondary query with variables in pivot table? - php

I've been playing with laravel a bit and came across a weird edge case I can't quite figure out
I've got the following table structure:
CREATE TABLE `community_address` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`address_id` int(10) unsigned NOT NULL,
`community_id` int(10) unsigned NOT NULL,
`is_billing` tinyint(1) NOT NULL DEFAULT '1',
`is_service` tinyint(1) NOT NULL DEFAULT '1',
`is_mailing` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
)
CREATE TABLE `communities` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`),
)
CREATE TABLE `addresses` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`address_1` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Street address',
`address_2` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Street adddress 2 (Company name, Suite, etc)',
`city` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'City',
`state` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'State / Province',
`zip` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Zip / Postal Code',
`country_id` int(10) unsigned NOT NULL COMMENT 'Country ID',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
)
Which i've represented with the following Laravel Model for a community
class Community extends Model
{
public function addresses(){
return $this->belongsToMany(Address::class, 'community_address', 'community_id', 'address_id');
}
}
$community->addresses() does in fact return only addresses for the community, but say I want to filter by address type in my pivot table (billing, mailing, etc)
I can try this:
public function getBillingAddress(){
return $this->addresses()->wherePivot('is_billing','=', true)->firstOrFail()->get();
}
Which does return results, however it's EVERY row in my pivot table matching my query, not running my query off the existing addresses
So my second idea was to use the 'and' boolean argument like so
public function getBillingAddress(){
return $this->addresses()->wherePivot('community_id', '=', $this->id, true)->wherePivot('is_billing','=', true)->firstOrFail()->get();
}
Which results in the following SQL which errors out (for obvious reasons), but also doesn't quite look like it's searching for what i'd want, even if it did work?
select `addresses`.*, `community_address`.`community_id` as `pivot_community_id`, `community_address`.`address_id` as `pivot_address_id` from `addresses` inner join `community_address` on `addresses`.`id` = `community_address`.`address_id` where `community_address`.`community_id` = 2 1 `community_address`.`community_id` = 2 and `community_address`.`is_billing` = 1 limit 1
Which looks to me like the "and" value is not, in fact, a boolean value, but is printing the value as a string straight to the query.
I tried the obvious, and tried to swap the forth argument with "and" and the following sql was generated, which doesn't fail, but returns all addresses, not just addresses linked to my community
select `addresses`.*, `community_address`.`community_id` as `pivot_community_id`, `community_address`.`address_id` as `pivot_address_id` from `addresses` inner join `community_address` on `addresses`.`id` = `community_address`.`address_id` where `community_address`.`community_id` = 2 and `community_address`.`community_id` = 2 and `community_address`.`is_billing` = 1 limit 1)
Am I missing something obvious here?
With some tinkering with the result SQL I can get what I want, which is the following raw sql query:
select `addresses`.*,
`community_address`.`community_id` as `pivot_community_id`,
`community_address`.`address_id` as `pivot_address_id`
from `addresses`
inner join `community_address` on `addresses`.`id` = `community_address`.`address_id` and `community_address`.`community_id` = 2 and `community_address`.`is_billing` = 1
limit 1
How can I achieve the same SQL being generated for me via eloquent?

I think This will be userfull For you If I come up with an example
we have Users Role And Role_User Tables
we have connect Users To Role with belongs To Many And we want use select:
Users models:
function Roles()
{
return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id');
}
in Our Controller we can write any select like bellow:
class exampleController extends Controller
{
public function index()
{
User::with(['Roles'=>function($query){$query->where(....)->get();}])->get();
}
}
you can Use any select on query and return what ever you want..
just be carefull if you need to use any varible in your select you must use
bellow format
class exampleController extends Controller
{
public function index()
{
$var =...;
User::with(['Roles'=>function($query) use ($var){$query->where(....,$var)->get();}])->get();
}
}
i hope this will solve your problem...

It seems I misunderstood how wherePivot() worked, changing the code to the following worked:
public function getBillingAddress(){
return $this->addresses()->wherePivot('is_billing', '=', true)->get()->first->all();
}
Where the new code is trying to call the is_billing column of the pivot table to further filter the existing table, the old one was trying to filter it by what it was already filtered by, but since it was an inner join, it was returning all the rows (At least I think?)
Either way, this is solved, hope this helps someone in the future.

Related

How to access to sub related model when the related model returns a collection

I have 3 models: User, Payment and Log. A User has many Payment and both User and Payment have many Log.
User Model
class User
{
public function payments()
{
return $this->hasMany('Payment', 'user_id');
}
public function logs()
{
return $this->morphMany(Log::class, 'loggable');
}
}
users table
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Payment Model
class Payment
{
public function user()
{
return $this->belongsTo('User', 'user_id');
}
public function logs()
{
return $this->morphMany(Log::class, 'loggable');
}
}
payments table
CREATE TABLE `payments` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`status` varchar(50),
`amount` int(11) NOT NULL,
`collection_date` date NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`user_id` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_payments_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Log Model
class Log
{
public function loggable()
{
return $this->morphTo();
}
}
logs table
CREATE TABLE `logs` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`loggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`loggable_id` bigint(20) unsigned NOT NULL,
`old_values` text COLLATE utf8mb4_unicode_ci,
`new_values` text COLLATE utf8mb4_unicode_ci,
`user_id` bigint(20) unsigned DEFAULT NULL, /* the user that made the change, if any */
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
The Log model stores all changes made to any other model (it's a polymorphic relationship), so if the user changes its name, the Log model will store the older name and the new name. The same applies to Payment: if a payment status changes the Log model will have a new record with the old status and the new status.
I need to show a paginated list of all Log records for a specific User ordered by date. So my code is:
$user = App\User::find($id);
$allLogs = $user->logs();
// Now I need to join (I'm using union) both sets of logs
$allLogs->union($user->payments->logs());
However, since a User can have many Payment, $user->payments returns a Collection, so is no longer a query builder/eloquent object and it fails when I try to call ->logs().
$user->payments()->logs() also doesn't work, because $user->payments() returns a HasMany object and the ->logs() method doesn't exist.
I'm trying to avoid getting each collection of Log separately and then processing them using php (it would be perfect to delegate that task to MySql).
I believe it can be done, because I can write the query on MySql:
select l.*
from payments p
join logs l on p.id = l.loggable_id and l.loggable_type = 'App\\Payments'
where p.user_id = SOMEUSERID
Thanks in advance
Eager load the relations(reduces number of queries)
$user = User::with(['payments.logs', 'logs'])->find($id);
Query using the Log model.
$logs = Log::where([
'loggable_id' => $user->id,
'loggable_type' => 'User',
])
->orWhere(function($query){
$query->whereIn('loggable_id',
$user->payments()->pluck('id'))
->where('loggable_type', 'Payment');
})->get();
OR
Get them individually and then combine them.
$all_logs = collect([]);
$all_logs->push($user->logs);
foreach($user->payments as $p){
$all_logs->push($p->logs);
}
$final_logs = $all_logs->collapse();
OR
Just use the relations, without iterating over the payments. You can combine the results if you want(as shown in the previous approach).
$user_logs = $user->logs;
$payment_logs = $user->payments->pluck('logs')->collapse();

Grocery relation sql databases

I use grocery-crud for a simple SQL select
$crud->set_table('lista_ab');
$crud->set_relation('id_ab','lista_ab_term','Expire');
The problem is that it does not make the relation for 'id_ab'
My database looks
CREATE TABLE `lista_ab` (
`id_ab` int(10) NOT NULL,
`Subname` varchar(255) DEFAULT NULL,
`Name` varchar(255) DEFAULT NULL,
`Inregistrat` date DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `lista_ab_term` (
`ID` int(10) NOT NULL,
`id_ab` int(10) DEFAULT NULL,
`Expire` date DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
In final I want to extract Subname and Expire.
You cannot create dropdown list and show field name of the first table : Subname, but you can have as many fields you like to call from the other table and the syntax is really simple.
Just at the 3rd field you will have the symbol { and } . So it will be for example:
$crud->set_relation('id_ab','lista_ab_term','{Expire} - {ID}');
You can use join query like this for your expected results:
SELECT t1.Subname, t2.Expire FROM lista_ab t1 LEFT JOIN lista_ab_term t2 ON t1.id_ab = t2.id_ab
Or In Codeigniter
$this->db->select('lista_ab.Subname,
lista_ab_term.Expire');
$this->db->from('lista_ab');
$this->db->join('lista_ab_term', 'lista_ab.id_ab= lista_ab_term.id_ab');
$q = $this->db->get();

Validation while defining a join

I'm trying to define a validation, to include only users that meet specific criteria - mainly, to take into account users whose metadata (that is stored in another table) has field supplier_id with specific value. As such, I've done this in UpdateXRequest class:
public function rules()
{
$journey = $this->route()->parameter('journey');
return ['driver_id' => [
Rule::exists('users', 'id')
->where(function($query) use ($journey){
$query->join('users_meta', 'users.id', '=', 'users_meta.user_id')
->where('users_meta.key', 'supplier_id')
->where('users_meta.value', $journey->supplier_id);
})
]
];
}
However, I'm getting Column not found: 1054 Unknown column 'users_meta.key' in 'where clause' (SQL: select count(*) as aggregate from users where id = x and (users_meta.key = supplier_id and users_meta.value = y)) error.
How can I accomplish this?
I'm looking for answers that use the default Laravel logic, rather than writing my own validator
Per request, here're the schema queries:
CREATE TABLE IF NOT EXISTS `users` (
`id` int(10) unsigned NOT NULL,
`email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`role_id` int(10) unsigned NOT NULL
);
CREATE TABLE IF NOT EXISTS `users_meta` (
`id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned NOT NULL,
`type` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT 'null',
`key` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`value` text COLLATE utf8_unicode_ci,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL
);
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `users_email_unique` (`email`);
ALTER TABLE `users`
MODIFY `id` int(10) unsigned NOT NULL AUTO_INCREMENT;
ALTER TABLE `users_meta`
ADD PRIMARY KEY (`id`),
ADD KEY `users_meta_user_id_index` (`user_id`),
ADD KEY `users_meta_key_index` (`key`);
ALTER TABLE `users_meta`
MODIFY `id` int(10) unsigned NOT NULL AUTO_INCREMENT;
INSERT INTO `users` (`id`, `email`, `password`, `created_at`, `updated_at`, `role_id`) VALUES (6898, 'a#b.com', '', '2017-08-09 12:15:05', '2017-08-09 13:19:56', 4);
INSERT INTO `users_meta` (`id`, `user_id`, `type`, `key`, `value`, `created_at`, `updated_at`) VALUES (18, 6898, 'string', 'supplier_id', '6897', '2017-08-09 12:15:05', '2017-08-09 12:15:05');
The query existed in OP, but per request added SQL query that is produced taken from the query log:
select count(*) as aggregate from `users` where `id` = 7011 and (`users_meta`.`key` = supplier_id and `users_meta`.`value` = 6897)
Thinking more about it, it all makes perfect sense why it's not working - the internal join doesn't have any conditions on it (it's still weird that it doesn't appear in query log - maybe it's an optimisation done by Laravel to not unnecessarily join tables for where as opposed to select). Internal wheres at this moment are run on users, and not on the table from the join - and per https://laravel.com/docs/5.4/queries#joins (Advanced Join Clauses), they need to be inside join for it to work (per #DarkMukke's answer)
Not a 100% sure, but logically it sounds like the where conditions on your join should be in a Closure
public function rules()
{
$journey = $this->route()->parameter('journey');
return ['driver_id' => [
Rule::exists('users', 'id')
->where(function($query) use ($journey){
$query->join('users_meta', function ($join) use ($journey) {
$join->on('users.id', '=', 'users_meta.user_id')
->where('users_meta.key', 'supplier_id')
->where('users_meta.value', $journey->supplier_id);
});
})
]
];
}
And even then, you might not need the first closure, eg
I was wrong, the where relates to the condition of the Rule, not the where condition of a query builder.
EDIT : I've not tried this code, but from my experience, and some double checking in the docs, this should be correct.
Additionally, since the conditions are all on the join, you could make it faster by joining less data, by adding conditions instead
public function rules()
{
$journey = $this->route()->parameter('journey');
return [
'driver_id' => [
Rule::exists('users', 'id')
->where(function ($query) use ($journey) {
$query->join('users_meta', function ($join) use ($journey) {
$join->on('users.id', '=', 'users_meta.user_id')
->on('users_meta.key', '=', 'supplier_id')
->on('users_meta.value', '=', $journey->supplier_id);
});
})
]
];
}

Cake PHP - how to check the condition only for the last

I am following instructions from Bookmarks tutorial, and i have a problem with one of the queries.
I have baked all models from my database (like in tutorial) and now i want to prepare custom finder.
I have two tables Academic Teachers and Evaluations
CREATE TABLE evaluations (
ID int(10) NOT NULL AUTO_INCREMENT,
academic_teacher_ID int(10) NOT NULL,
framework_ID int(10) NOT NULL,
protocol_ID int(10) NOT NULL,
final_note DOUBLE,
room varchar(255),
PRIMARY KEY (ID)) ;
CREATE TABLE academic_teachers (
ID int(10) NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
surname varchar(255) NOT NULL,
national_identification_number int(10) NOT NULL,
occupation varchar(4),
degree varchar(255),
department varchar(255),
PRIMARY KEY (ID)) ;
I want to find teachers, who at their last evaluation received a low note.
public function findLowNotes(Query $query, array $options)
{
return $this->find()
->distinct(['AcademicTeachers.id'])
->matching('Evaluations', function ($q) {
return $q->where(['Evaluations.final_note' < 3]);
});
But it wil find all the teachers who ever had any bad note. How should it be? Shall I somewhere use one of the ways of retrieving last ID that I found? Or there is a more clever way that will surely work? I am much confused how to combine it all together.
And - is it possible to join here with that final_note, so I could paginate it along with that teacher's data?
Yours sincerely,
Milven
This is not right.
return $q->where(['Evaluations.final_note' < 3]);
It should be.
return $q->where(['Evaluations.final_note <' => 3]);

Yii joining two table using relations in model

Hi I have these two tables that I want to join using relations in Yii, The problem is Im having a hard time figuring out how Yii relation works.
picturepost
id
title
link_stat_id
linkstat
id
link
post_count
I also have a working SQL query. This is the query I want my relation to result when I search when I want to get picturepost
SELECT picturepost.id, picturepost.title,linkstat.post_count
FROM picturepost
RIGHT JOIN linkstat
ON picturepost.link_stat_id=linkstat.link;
I want something like this when I search for a post.
$post = PicturePost::model() -> findByPk($id);
echo $post->linkCount;
Here's my table for extra info:
CREATE TABLE IF NOT EXISTS `picturepost` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`title` text COLLATE utf8_unicode_ci DEFAULT NULL,
`link_stat_id` char(64) COLLATE utf8_unicode_ci NOT NULL
) ENGINE=MyISAM;
CREATE TABLE IF NOT EXISTS `linkstat` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`link` char(64) COLLATE utf8_unicode_ci NOT NULL,
`post_count` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `post_count` (`post_count`),
KEY `link_stat_id` (`link`)
) ENGINE=InnoDB;
Thanks in advance I hope I explained it clearly.
There are a few tutorial regarding this, and I won't repeat them, but urge you to check them out.
The easiest starting point will be to create your foreign key constraints in the database, then use the Gii tool to generate the code for the model, in this case for the table picturepost.
This should result in a class Picturepost with a method relations(),
class Picturepost extends {
public function relations()
{
return array(
'picturepost_linkstats' => array(self::HAS_MANY,
'linkstat', 'link_stat_id'),
);
}
This links the 2 tables using the *link_stat_id* field as the foreign key (to the primary key of the linked table).
When you are querying the table picturepost, you can automatically pull in the linkstat records.
// Get the picturepost entry
$picturepost = PicturePost::model()->findByPk(1);
// picturepost_linkstats is the relationship name
$linkstats_records = $picturepost->picturepost_linkstats;
public function relations()
{
return array(
'linkstat' => array(self::HAS_ONE, 'Linkstat', array('link_stat_id'=>'link')),
);
}
More on yii relations.
This assumes that you have an active record model Linkstat that represents data in table linkstat.

Categories