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);
});
})
]
];
}
Related
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();
Can not find the desired Id
I am joining three SQL tables in CodeIgniter. I can able to retrieve the data from three tables. But I face a problem in finding the proper id.
I put my three tables below:
books table:
CREATE TABLE `books` (
`id` int(11) NOT NULL,
`book_name` varchar(200) NOT NULL,
`description` text NOT NULL,
`author` varchar(200) NOT NULL,
`publisher` varchar(200) NOT NULL,
`price` varchar(200) NOT NULL,
`quantity` int(11) NOT NULL,
`categoryId` int(11) NOT NULL,
`book_image` varchar(200) NOT NULL,
`create_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`userId` int(11) NOT NULL,
`status` enum('1','0') NOT NULL DEFAULT '0' COMMENT '1 = published | 0 = unpublished'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
category table:
CREATE TABLE `category` (
`id` int(11) NOT NULL,
`category` varchar(100) NOT NULL,
`description` text NOT NULL,
`tag` varchar(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
users table:
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`name` varchar(200) NOT NULL,
`contact` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`password` varchar(255) NOT NULL,
`address` varchar(500) NOT NULL,
`city` varchar(50) NOT NULL,
`type` varchar(20) NOT NULL DEFAULT 'U',
`createdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
My Model:
public function get_books($limit, $offset)
{
/*=== SQL join ===*/
$this->db->select('*');
$this->db->from('category');
$this->db->join('books', 'books.categoryId = category.id');
$this->db->join('users', 'books.userId = users.id'); #...Join three sql table
$this->db->order_by('books.id', 'DESC');
$this->db->where('books.status', '1');
$this->db->limit($limit, $offset);
$query = $this->db->get();
return $query->result();
}
Now I get the id of users in my joined table. But I want books id as the main id. How I could solve that problem?
As the previous answer, you should not use the '*' this. You should write the select query manually, select whatever column you want from the tables.
To get the book's id as your desired id, you should change your model code as below.
Model code
public function get_books($limit, $offset)
{
$this->db->select('books.id, books.book_name, books.description, books.author, books.book_image, books.otherCol, category.category, users.name'); // You can add many more if you want.
$this->db->from('books');
$this->db->join('category', 'books.categoryId = category.id'); //2nd join
$this->db->join('users', 'books.userId = users.id'); //3rd join
$this->db->order_by('books.id', 'DESC');
$this->db->where('books.status', '1');
$this->db->limit($limit, $offset);
$query = $this->db->get();
return $query->result();
}
I think now you will get your desired id from the books table.
Don't use select * in your query,
Signify which columns you want by their table names or table aliases, since you have 3 tables joined and all have an id column it doesn't know which id column you want from your query result,
$this->db->select('books.id, books.status, users.someData, etc.');
below, i am just selecting all columns from books table. I have also made books the main table instead of the category table
public function get_books($limit, $offset)
{
/*=== SQL join ===*/
$this->db->select('books.*');
$this->db->from('books');
$this->db->join('category', 'books.categoryId = category.id');
$this->db->join('users', 'books.userId = users.id'); #...Join three sql table
$this->db->order_by('books.id', 'DESC');
$this->db->where('books.status', '1');
$this->db->limit($limit, $offset);
$query = $this->db->get();
return $query->result();
}
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 have two tables:
Table 1:
CREATE TABLE `users` (
`user_id` int(255) NOT NULL,
`user_name` varchar(100) NOT NULL,
`user_email` varchar(100) NOT NULL,
`user_password` varchar(200) NOT NULL,
`user_phone` varchar(20) NOT NULL,
`building_units` int(200) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` enum('0','1') NOT NULL DEFAULT '1' COMMENT '1 = Active(default), 0 = Inactive',
`user_role` enum('0','1','2','3') NOT NULL DEFAULT '2' COMMENT '0=Manager,1=Admin,2=User,3=Tenant',
`user_ip` int(39) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Table 2:
CREATE TABLE `building_admins_tbl` (
`id` int(255) NOT NULL,
`building_id` int(255) NOT NULL COMMENT 'FK ',
`building_admin_id` int(255) DEFAULT NULL COMMENT 'FK',
`user_id` int(255) DEFAULT NULL,
`tenant_id` int(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
And I have written the following query:
$select = 'users.user_id,users.user_name,users.user_role,users.status,building_admins_tbl.building_admin_id';
$join_str = 'users.user_id != building_admins_tbl.building_admin_id';
$where = ['users.user_role'=>'1','users.status'=>'1'];
$q =$this->db
->select($select)
->where($where)
->from('users')
->join('building_admins_tbl',$join_str)
->get();
$admin_list =$q->result_array();
In the above query the join string is important. I want to get only those rows where(users.user_id != building_admins_tbl.building_admin_id) with the where condition as well.
But I am not getting the expecting result. How can I fix this?
I'm not certain what you should be joining, because you haven't properly set up your foreign keys, and if you did you didn't include that information, but I'll give it a shot because the real answer is that you want to do the join, but use a different WHERE clause.
You must actually join the other table, and you can't use !=:
// This looks like you are trying to join on the admin ID, which is probably wrong
$join_str = 'users.user_id = building_admins_tbl.building_admin_id';
// If you are trying to get tenants, it's probably supposed to be:
$join_str = 'users.user_id = building_admins_tbl.tenant_id';
// If you are trying to join with the users table, it might be:
$join_str = 'users.user_id = building_admins_tbl.user_id';
But then make sure you aren't including admins
$where = [
'users.user_role !=' => '1', // Do not include user role #1 (admins)
'users.status' => '1'
];
after inserting just load your view page
and add these line in your view page
$this->output->enable_profiler(TRUE); //within php tag
these will give you raw query in your view just copy the query and execute it in phpmyadmin
I found the answer.
$q = $this->db
->from('users')
->where(['user_role'=>'1'])
->where(['status'=>'1'])
->where("
NOT EXISTS (
select building_admin_id from building_admins_tbl where users.user_id = building_admins_tbl.building_admin_id)")
->get();
$data['admins_list'] = $q->result_array();
I have an issue with LEFT JOIN. I do not want to use eloquent relations because I want to keep my models folder clean. I have an appointments application in which I am using "labels" and "statuses". I want to be able to filter my view based on the labels and statuses. The issue with the LEFT JOIN is that when I want to click on my edit link, it uses the "id" field from my "appointments_statuses" table, instead of the "appointments" table. Below is the relevant code:
My controller:
$appointments = $query->orderBy('appointment', 'asc')
->leftJoin('appointments_labels','appointments_labels.id','=','appointments.label_id')
->leftJoin('appointments_statuses','appointments_statuses.id','=','appointments.status_id')
->get();
My view:
#foreach($appointments as $appointment)
{{ $appointment->id }} // Problem here, it uses the "status_id" field from the "appointments" table instead of the "id" field
#endforeach
My database tables:
CREATE TABLE IF NOT EXISTS `appointments` (
`id` int(11) NOT NULL,
`appointment` varchar(255) NOT NULL,
`location` varchar(255) NOT NULL,
`description` text NOT NULL,
`start` datetime NOT NULL,
`end` datetime NOT NULL,
`label_id` int(11) NOT NULL,
`status_id` int(11) NOT NULL,
`contact` int(11) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
)
CREATE TABLE IF NOT EXISTS `appointments_labels` (
`id` int(11) NOT NULL,
`label` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
)
CREATE TABLE IF NOT EXISTS `appointments_statuses` (
`id` int(11) NOT NULL,
`status` varchar(255) NOT NULL,
`flag` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
)
Well, that's because your query collects ALL the fields of the 3 tables, so the columns with the same name get overwritten.
Simply use a select() on what fields you want (which is a good practice, anyway):
$appointments = $query->orderBy('appointment', 'asc')
->leftJoin('appointments_labels','appointments_labels.id','=','appointments.label_id')
->leftJoin('appointments_statuses','appointments_statuses.id','=','appointments.status_id')
->select('appointments.id', 'appointments.name', '........', 'appointments_statuses.name', 'appointments_labels.name')
->get();
NB: I'm guessing the fields you want from the main and the joined tables, but you get the idea :)
NB2: You can also pass an array of values to the select() method:
->select(['appointments.id', 'appointments.name', ....])