I am having some trouble trying to fetch associated models when selecting fields. My version of CakePHP is 3.0beta2.
Three MYSQL tables that are relevant to this question:
users:
`id` int(11) NOT NULL,
`username` varchar(30) COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`role` varchar(20) COLLATE utf8_unicode_ci NOT NULL,
... (more information)
-
presets:
`id` int(11) NOT NULL,
`name` varchar(60) COLLATE utf8_unicode_ci NOT NULL,
`user_id` int(11) NOT NULL,
... (more information)
-
favorites:
`id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`preset_id` int(11) NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
My associations are defined as shown below:
// From FavoritesTable.php
public function initialize(array $config) {
$this->belongsTo('Users');
$this->belongsTo('Presets');
}
// From PresetsTable.php
public function initialize(array $config) {
$this->belongsTo('Users');
$this->hasMany('Favorites');
}
// From UsersTable.php
public function initialize(array $config) {
$this->hasMany('Presets');
$this->hasMany('Favorites');
}
What I'm trying to achieve is:
Load all the user information for the currently logged in user
Load all the favorites that this user has
Create a new field that transforms the created field (a timestamp) into a date
For each favorite, load the associated preset data
This is the code that I use to do that:
// From UsersController.php
public function favorites() {
$userId = $this->Auth->user('id');
$user = $this->Users->find('all')
->where(['id' => $userId])
->contain([
'Favorites' => function($q) {
return $q
->select(['id', 'preset_id', 'user_id', 'created', 'date' => 'DATE(created)'])
->order(['Favorites.created' => 'DESC']);
},
'Favorites.Presets',
])
->first();
$this->set('user', $user);
}
The problem is: when I use the select method as in the code above, the Favorites.Presets association is not loaded, so $user['favorites'][0]['preset'] is always null.
But if I comment out the select method (thus selecting all fields and not retrieving DATE(created), the association is loaded and I can access the information from the presets table.
Could this be a bug or am I doing something wrong?
Thanks for the help!
I think you need to call ->autoFields(true) after select(). This is required if you expect all other fields to be selected. This could probably be seen as a bug, try opening a ticket in github.
Related
I'm relatively new to eloquent, and have problems loading data from a hasMany relation in an app that uses eloquent as the database layer. My (simplified) code is as follows:
use Illuminate\Database\Eloquent\Model;
class Answer extends Model{
protected $table = 'answer';
public $timestamps = false;
public $incrementing = false;
protected $fillable = [
"nerdId",
"invitationid",
"eatingno",
"price",
"haspaid",
"attending",
"noeating",
];
public function topping() {
return $this->hasMany(AnswerTopping::class, 'answerid');
}
}
class AnswerTopping extends Model{
protected $table = 'eating';
public $timestamps = false;
protected $fillable = [
'answerid',
'toppingid',
'add'
];
public function answer() {
return $this->belongsTo(Answer::class);
}
}
The SQL Schema is like below
CREATE TABLE `answer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nerdid` int(11) NOT NULL,
`invitationid` int(11) NOT NULL,
`eatingno` tinytext,
`price` float NOT NULL,
`haspaid` tinyint(4) NOT NULL,
`attending` tinyint(1) NOT NULL DEFAULT '0',
`noeating` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2119 DEFAULT CHARSET=utf8;
CREATE TABLE `eating` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`answerId` int(11) NOT NULL,
`toppingid` int(11) NOT NULL,
`add` tinyint(4) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=280 DEFAULT CHARSET=utf8;
I then do the following query:
$answer = Answer::with('topping')->find(32);
return $answer->toJson());
This results in a json like the following
{"id":32,"nerdid":1,"invitationid":54,"eatingno":"51","price":60,"haspaid":1,"attending":2,"noeating":0,"topping":[]}
Raw SQL query shows me that I do have data in the relation, so it should return more in "topping".
UPDATE
Checking sql queries in mysql (Setting it up for logging), I see that it actually do the expected queries on the database:
select * from `answer` where `answer`.`id` = 32 limit 1;
select * from `eating` where `eating`.`answerid` in (32);
Manually executing the SQL are giving me 2 entries in the eating table. But they are not showing up on the upper "Answer" json.
Found the culprit.. db schema for the "eating" table, had answerId (uppercase I), and the relation in hasMany used answerid (lowercase i), which apparently confused eloquent..
Now I get the expected json..
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();
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.
I am having this same issue. Exactly the same issue accept different class and table names. Does anybody have some insight?
Site:
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function categoryBlocks()
{
return $this->belongsToMany(Category::class, 'blocked_category');
}
Category:
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function sites()
{
return $this->belongsToMany(Site::class, 'blocked_category');
}
The query properly generates when querying from category to site but not from site to category. (I have tried renaming the method on the site Model but it doesn't help at all.)
$catBlocks = $category->sites()->get(); // Query creates a category_id value in the query
$blocks = $site->categoryBlocks()->get(); // Query doesn't create a site_id value in the query
Table: blocked_category
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`site_id` int(10) unsigned NOT NULL,
`category_id` int(10) unsigned NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
Table: sites
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`publisher_id` int(10) unsigned NOT NULL,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`url` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`status_id` int(11) NOT NULL DEFAULT '1',
Table: categories
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
Try passing a third argument to the categoryBlocks relationship of 'category_id':
public function categoryBlocks()
{
return $this->belongsToMany(Category::class, 'blocked_category', 'category_id');
}
Or whatever you have called the category_id within the blocked_category table. If this doesn't work please reply with your database schema.
So I found the issue. The instance of the object being used to query wasn't being fully hydrated which was causing the query builder not to see the relationship properly.
I was getting the instance of the object passed into the method, but for some reason it wasn't a fully hydrated instance, so I just had to manually hydrate the Site object before running the query.
A bit new to yii and have been having trouble trying to do a join query in my gii-generated model.
Summary:
I want to return videos (table 'videos') that have met specific search criteria. To do this, I have my 'videos' table, and I have another table 'searchmaps'. All searchmaps does is associate a video_id to a search_id so that I can keep track of multiple videos that met criteria for a single search scenario..
What I've tried:
I tried following yii docs for relational queries but I guess I'm missing something still... Below is my code. What am I doing wrong??
(Note: I wish to return a model using CActiveDataProvider)
Tables:
CREATE TABLE IF NOT EXISTS `videos` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`directory` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created` varchar(20) COLLATE utf8_unicode_ci NOT NULL,
`title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`description` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`category` int(2) NOT NULL,
`tags` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`filename` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`filetype` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`duration` int(11) NOT NULL,
`status` int(1) NOT NULL,
`error` text COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=17 ;
CREATE TABLE IF NOT EXISTS `searchmaps` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`search_id` int(11) NOT NULL,
`video_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=69 ;
Classes:
Here is the Controller class:
//From VideosController.php
...
public function actionIndex($searchmap_id)
{
$dataProvider = new CActiveDataProvider('SearchVideos', array(
'criteria' => array(
'with' => array('search.video_id','search.search_id'),
'together' => true,
'condition'=>'videos.id = search.video_id AND search.search_id='.$searchmap_id,
)));
$this->render('index',array(
'dataProvider'=>$dataProvider,
));
}
Below is the main model class:
// From Videos.php
...
/**
* #return array relational rules.
*/
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'search'=>array(self::BELONGS_TO, 'Searchmaps', 'video_id'),
);
}
Here is the model class of the related table
// From Searchmaps.php
...
/**
* #return array relational rules.
*/
public function relations()
{
// Each row has a search_id and a video_id relating to a specific video
// Multiple rows may have different videos but share the same search_id
return array(
'video'=>array(self::HAS_ONE, 'Videos', 'video_id'),
);
}
First, I would suggest using InnoDB tables so you can set up proper foreign keys -- if you do this then gii will generate the basic relations for you. If you can convert your tables, then you can add the fk with:
ALTER TABLE `searchmaps`
ADD CONSTRAINT `searchmaps_ibfk_1` FOREIGN KEY (`video_id`) REFERENCES `videos` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
Your relations don't look quite right, seems like they should be:
in Videos model:
return array(
'searchmaps' => array(self::HAS_MANY, 'Searchmaps', 'video_id'),
);
in Searchmaps model:
return array(
'video' => array(self::BELONGS_TO, 'Videos', 'video_id'),
);
then your dataProvider can look something like:
$dataProvider=new CActiveDataProvider('Videos',array(
'criteria'=>array(
'with'=>'searchmaps',
'together' => true,
'condition' => 'searchmaps.search_id='.$search_id,
)
));
to try it you can output a simple grid in your view with something like:
$this->widget('zii.widgets.grid.CGridView', array(
'id'=>'videos-grid',
'dataProvider'=>$dataProvider
));
Again, I would highly recommend using foreign keys in your table and view the relations gii outputs and once you understand what it's doing, it will be much easier to customize. Also, using foreign keys will insure the relationships are maintained. You can use a tool like MysqlWorkbench or similar if you need help creating the foreign keys.