I'm working on somehow messy project where structure of db is:
Object
id, title, description, url ... [and some more columns]
ObjectMeta
object_id, variable, value
So let's give you:
Object: 1, 'Cool Book', 'This cool book is just for you!', ...
Object Meta:
1, 'author_name', 'John Doe'
1, 'released', 2014
1, 'price', 23.22
1, 'url', 'http://amazon.com/coolbook'
1, 'genre', 3
So I need to perform query which will:
Pull all Objects that has genre 3, then sort those Objects by their released date.
$objs = static::Active()
->where('object', '=', $object)
->where('category','=',$category)
->whereIn('site_id', array(self::$site_id, 0))
->leftJoin('object_meta', function($join)
{
$join->on('object.id','=', 'object_meta.content_id');
})
->where('object_meta.variable', 'genre')
->where('object_meta.value', 3);
->where('object_meta.variable', 'released');
->orderBy('object_meta.value', 'desc');
->groupBy('object.id')
->paginate(20);
This query has 0 results even if there are plenty of those books out there. I know that the second object_meta.variable where is guilty here. How can I write this query to make it work?
Really appreciating your help.
-- Edit
I've created workaround but it's really, really bad workaround (funny thing is that I swear the query above was working for month or so).
$objs = static::Active()
->where('object', '=', $object)
->where('category','=',$category)
->whereIn('site_id', array(self::$site_id, 0))
->leftJoin('object_meta', function($join)
{
$join->on('object.id','=', 'object_meta.content_id');
})
->where('object_meta.variable', 'genre')
->where('object_meta.value', 3);
->groupBy('object.id')
->get();
foreach($objs as $obj)
{
$obj->id = $obj->object_id;
$objs_meta = ObjectMeta::where('object_id',$obj->object_id)->get();
foreach($objs_meta as $obj_meta)
{
$variable = $obj_meta->var;
$obj->$variable = $obj_meta->value;
}
}
$objs = $objs->sortBy(function($role){
return $role->released;
});
$objs = Paginator::make($objs->toArray(), sizeof($objs), $limit);
You need inner join (join()) not leftJoin(), and you need it twice:
$objs = static::Active()
->where('object', '=', $object)
->where('category','=',$category)
->whereIn('site_id', array(self::$site_id, 0))
->join('object_meta as genre', function ($join) {
$join->on('object.id', '=', 'genre.content_id')
->where('genre.variable', '=', 'genre')
->where('genre.value', '=', 3);
})
->join('object_meta as released', function ($join) {
$join->on('object.id', '=', 'released.content_id')
->where('released.variable', '=', 'released');
})
->orderBy('released.value', 'desc');
->select('object.*', 'released.value as released')
->distinct()
// or group by, doesn't matter in your case
// ->groupBy('object.id')
->paginate(20);
Then your objects will have additional property released with appropriate value.
According to the comments:
If you want to dynamically add the joins, then I suggest scopes like below:
public function scopeGenre($query, $value)
{
$query->join('object_meta as genre', function ($join) use ($value) {
$join->on('object.id', '=', 'genre.content_id')
->where('genre.variable', '=', 'genre')
->where('genre.value', '=', $value);
});
}
then:
$query->where(..)->genre(3)->...
Related
I have a model called Business, a business can have several services they provide. I have another model called Payments. Payment holds a record of what service people pay for. A service can have many Payments. I intend to fetch top 10 and worst 10 businesses based on payments received. The code below works fine, but it is very inefficient. I have to loop through the whole data to retrieve the information I need. Any more efficient way of achieving this?
$businesses = Business::with(['services'])->get();
foreach($businesses as $business){
$id = $business->id;
$name = $business->display_name;
$services = $business->services;
$businessRevenue = 0;
if(count($services)>0){
foreach($services as $service){
$serviceId = $service->id;
$totalAmount = PaymentTransaction::whereHas('invoice', function($query) use ($serviceId){
$query->where('product_code_id', $serviceId);
})->where('amount_paid', ">", 0)->sum('amount_paid');
$businessRevenue= $businessRevenue + $totalAmount;
}
}
$businessArray = (object) array('id'=> $id, 'name'=> $name, 'revenue'=> $businessRevenue);
array_push($transformedBusiness, $businessArray);
}
$topBusiness = $bottomBusiness = $transformedBusiness;
usort($bottomBusiness, function($a, $b) {return strcmp($a->revenue, $b->revenue);});
usort($topBusiness, function($a, $b) {return strcmp($b->revenue, $a->revenue);});
$topBusiness = array_slice($topBusiness, 0, 10);
$bottomBusiness = array_slice($bottomBusiness, 0, 10);
return view('report.department_performance', compact('topBusiness', 'bottomBusiness'));
I guess you could use a join query to get top and lowest 10 businesses directly from database instead of looping all business records and manually calculate their revenue
For top 10 business you can use inner joins for rest of the related tables
$topBusinesses = DB::query()
->select('b.id', 'b.display_name', DB::raw('sum(p.amount_paid) as revenue')
->from('business as b')
->join('service as s', 'b.id', '=', 's.business_id')
->join('invoice as i', 's.id', '=', 'i.product_code_id')
->join('payment_transaction as p', function ($join) {
$join->on('p.id', '=', 'i.payment_transaction')
->where('p.amount_paid', '>', 0);
})
->groupBy('b.id', 'b.display_name')
->orderByDesc('revenue')
->limit(10)
->get();
For lowest 10 business use left joins for invoice and payment_transaction so that if there are no records in these table for a business you will still get these business records
$lowestBusinesses = DB::query()
->select('b.id', 'b.display_name', DB::raw('coalesce(sum(p.amount_paid),0) as revenue')
->from('business as b')
->join('service as s', 'b.id', '=', 's.business_id')
->leftJoin('invoice as i', 's.id', '=', 'i.product_code_id')
->leftJoin('payment_transaction as p', function ($join) {
$join->on('p.id', '=', 'i.payment_transaction')
->where('p.amount_paid', '>', 0);
})
->groupBy('b.id', 'b.display_name')
->orderBy('revenue')
->limit(10)
->get();
I have used MySQL coalesce function to show 0 value in case sum() returns null, If you are using any other database you can use an alternate function.
I have this data
$result = DB::table('zone_regions')
->where('zone_regions.id', '=', $request->input('zone'))
->join('areas', function ($join) use($area) {
$join->on('zone_regions.id', '=', 'areas.zone_id')
->where('areas.id', '=', $area);
})
->join('hthree_regions', function ($join) use($city) {
$join->on('areas.id', '=', 'hthree_regions.area_id')
->where('hthree_regions.id', '=', $city);
})
->join('segments', function ($join) use($segment) {
$join->on('hthree_regions.id', '=', 'segments.hthree_id')
->where('segments.id', '=', $segment);
})
->join('links', function ($join) use($link) {
$join->on('segments.id', '=', 'links.segment_id')
->where('links.id', '=', $link);
})
->join('titik_closurs', function ($join) use($closure) {
$join->on('links.id', '=', 'titik_closurs.link_id')
->where('titik_closurs.id', '=', $closure);
})
// ->leftjoin('core_histories', 'titik_closurs.id', '=', 'core_histories.titik_id')
// ->select('titik_closurs.*', 'core_histories.*')
->groupBy('titik_closurs.id')
->get();
I want to add titik_closurs histories (core_histories) as an array into my returned data.
Sample data
Current data
This is what I have currently
data: [{...}]
0: {...}
id: 3
latitude: "6.2088000"
longitude: "106.8456000"
site_name: "98987454-54741115"
user_id: 1
zone_id: 2
And this is what I want to have
data: [{...}]
0: {...}
id: 3
latitude: "6.2088000"
longitude: "106.8456000"
site_name: "98987454-54741115"
user_id: 1
zone_id: 2
histories: [{......}]
histories: [{......}] array added to the results.
Note
This data (current data) are a bit messed up as is include data of all joined tables in my function, what I really need is just data of table titik_closurs and child of it table core_histories the rest aren't really useful for me in this case.
Sample of final data I wish to have:
data: [{...}]
0: {...}
// just data of `titik_closurs` exclude data of `zone_regions, areas, hthree_regions` etc.
id: 3
latitude: "6.2088000"
longitude: "106.8456000"
site_name: "98987454-54741115"
histories: [{....}]
Any idea how to do that?
Update
If I change my query (last part) to something like this:
->join('titik_closurs', function ($join) use($closure) {
$join->on('links.id', '=', 'titik_closurs.link_id')
->where('titik_closurs.id', '=', $closure)
->leftjoin('core_histories', function ($joinn) {
$joinn->on('titik_closurs.id', '=', 'core_histories.titik_id');
});
})
->groupBy('titik_closurs.id')
->get();
It does join core_histories to titik_closurs BUT,
It doesn't show them as child array
It only gets first core_histories not all.
Solved
I was playing with codes and came up with union solution then it led me to array_merge which fixed my issue.
So what I did basically was getting all histories in separate variable and push it into my default array.
//get histories
$histories = DB::table('core_histories')->where('titik_id', $closure)->get();
// default function
$result = DB::table('zone_regions')......
//merge them into one
$res = array_merge($result, ['histories' => $histories]);
Result
data: {
0: {...} // data
histories: [ // histories
0: {...}
1: {...}
]
}
Hope it help others as well.
For this particular situation you seem to have 3 (or 4?) related models. It's far easier (and much more readable) to define them as relationships and then do a nested retrieval with aggregation say:
$zone = Zone::with('region.segment.link.titik')
->where('id', $request->input('zone');
$titik = $zone->region->flatMap(function ($region) { return $region->segment })
->flatMap(function ($segment) { return $segment->link })
->flatMap(function ($link) { return $segment->titik });
What could also work (with yout current approach):
$result = DB::table('zone_regions')
->where('zone_regions.id', '=', $request->input('zone'))
->join('areas', function ($join) use($area) {
$join->on('zone_regions.id', '=', 'areas.zone_id')
->where('areas.id', '=', $area);
})
->join('hthree_regions', function ($join) use($city) {
$join->on('areas.id', '=', 'hthree_regions.area_id')
->where('hthree_regions.id', '=', $city);
})
->join('segments', function ($join) use($segment) {
$join->on('hthree_regions.id', '=', 'segments.hthree_id')
->where('segments.id', '=', $segment);
})
->join('links', function ($join) use($link) {
$join->on('segments.id', '=', 'links.segment_id')
->where('links.id', '=', $link);
})
->join('titik_closurs', function ($join) use($closure) {
$join->on('links.id', '=', 'titik_closurs.link_id')
->where('titik_closurs.id', '=', $closure);
})
// ->leftjoin('core_histories', 'titik_closurs.id', '=', 'core_histories.titik_id')
// ->select('titik_closurs.*', 'core_histories.*')
->get()
->groupBy('titik_closurs.id'); // Note the order change
This will use the collection's groupBy rather than the SQL GROUP BY (which does not provide any groups to the result)
You can do this with nested relationship. If you have model for each table its easier for you. see below code:
ZoneRegion Model:
public function area() {
return $this->belongsTo(Area::class);
}
in Area model:
public function hthree() {
return $this->belongsTo(HthreeRegions::class);
}
and so on...
when you want to use and append array as you want you can do:
ZoneRegion::where('your condition')->with('area.hthree.segments.etc')->get();
Maybe my relationship is wrong because I don't know exactly what your relationship is like, and if you give an example or tell the relationship, maybe I can answer better.
But in the end, this method simply eliminates your need.
Hope it help
I want to filter ONLY nested values in dealTransactions.
In plain English, Merchant wants only dealTransactions have provided dates with Deals.
I tried something like below but it does not work.
dates = ['2019-01-01', '2019-01-02', '2019-01-03', '2019-01-04', '2019-01-05'];
$merchant = Merchant::with(['deals.dealTransactions'])->where('slug', $slug)
->whereHas('deals.dealTransactions', function($query) use ($dates) {
foreach($dates as $date) {
$query->where('date', '=', $date);
}
})
->first();
deal_transactions table
id, deal_id, date
deals table
id, merchant_id,
merchants table
id, many columns for merchant
Thank you
You should be able to do this with a eager load constraint on the nested relationship:
$merchant = Merchant::where('slug', $slug)
->with(['deals.dealTransactions' => function ($query) use ($dates) {
$query->whereIn('date', $dates);
}])->first();
If I understood correctly your schema, this might help:
// Here you define the scope that will be used to select & eager load.
$transactionsScope = function ($q) use ($dates) {
return $q->whereIn('date', $dates);
};
// All merchant of slug in/with that transaction's scope.
$merchant = Merchant::where('slug', $slug)
->whereHas('deals', function ($q) use ($transactionsScope) {
return $q->whereHas('dealTransactions', $transactionsScope);
})
->with(['deals' => function ($q) use ($transactionsScope) {
return $q->with('dealTransactions', $transactionsScope);
}])
->firstOrFail();
I use the select2 jquery plugin but I can't search for the data that I want to output.
I've been trying to search the name of a book in my database which should be available and with the company branch, I am currently in. I'm dealing with 2 tables for this.
books
book_type_id [1, 2]
status ['Available', 'Available']
book_type
id [1, 2] name ['Book 1', 'Book 2']
and
public function get_available_book_type(Request $request){
$employee = employee::where('id', Auth::user()->emp_id)->first();
$bt = books::with('book_type')->where('branch_id', $employee->branch_id)
->where('status', 'Available')->where('id', 'LIKE', '%'.$request->name.'%')->groupBy('book_type_id')->get();
$book_type = $bt->where('book_type.name', 'like', '%'.$request->name.'%');
$array = [];
foreach ($book_type as $key => $value){
$array[] = [
'id' => $value['id'],
'text' => $value['book_type']['name']
];
}
return json_encode(['results' => $array]);
}
This results in a "no results found". However, when I use
$book_type = $bt->where('book_type.name', $request->name);
It returns a data so I believe my initial query is correct and not empty but this is not what I want because I use this for searching and I don't expect my user to type the whole word to output it to select2.
The $book_type like query works if I don't use relationship.
You can use whereHas method to filter through the relations:
$book_type = books::with('book_type')
->where('branch_id', $employee->branch_id)
->where('status', 'Available')
->where('id', 'LIKE', '%'.$request->name.'%')
->whereHas('book_type', function($query) use ($request) {
$query->where('book_type.name', 'LIKE', '%'.$request->name.'%');
}
->groupBy('book_type_id')->get();
I have two tables tbl_law_master & tbl_law_sub_master
on both these tables there is a column named as assigned_to, this column stores user's id.
my task is to get sublaw from tbl_law_sub_master for particular user's id by joining law_id of tbl_law_master.
this is my code,
$law_id = $_GET['law_id'];
$sublaw = DB::table('tbl_law_sub_master')
->select('tbl_law_sub_master.*', 'tbl_law_master.lm_id', 'tbl_law_master.law_name', 'tbl_law_master.id as lawId')
->leftJoin('tbl_law_master', 'tbl_law_master.id', '=', 'tbl_law_sub_master.lm_id')
->where('tbl_law_sub_master.lm_id', $law_id)
->orderBy('type_of_event', 'asc')
->orderBy('section', 'asc')
->orderBy('rules', 'asc')
->orderBy('notification', 'asc')
->orderBy('circular', 'asc')->get();
if (!in_array(Auth::user()->role, [1, 7]))
{
$sublaw = $sublaw->where('tbl_law_master.assigned_to', Auth::user()->id);
}
it shows me error as
Call to a member function where() on array
The problem is that you already called get() of your query then called where() again.
You should use where clause function like this
$sublaw = DB::table('tbl_law_sub_master')
->select('tbl_law_sub_master.*', 'tbl_law_master.lm_id', 'tbl_law_master.law_name', 'tbl_law_master.id as lawId')
->leftJoin('tbl_law_master', 'tbl_law_master.id', '=', 'tbl_law_sub_master.lm_id')
->where(function($query) use ($law_id) {
$query->where('tbl_law_sub_master.lm_id', $law_id);
if (!in_array(Auth::user()->role, [1, 7]))
{
$query->where('tbl_law_master.assigned_to', Auth::user()->id);
}
})
->orderBy('type_of_event', 'asc')
->orderBy('section', 'asc')
->orderBy('rules', 'asc')
->orderBy('notification', 'asc')
->orderBy('circular', 'asc')->get();
I think you should call get after the last where statement
$sublaw = DB::table('tbl_law_sub_master')
->select('tbl_law_sub_master.*', 'tbl_law_master.lm_id', 'tbl_law_master.law_name', 'tbl_law_master.id as lawId')
->leftJoin('tbl_law_master', 'tbl_law_master.id', '=', 'tbl_law_sub_master.lm_id')
->where('tbl_law_sub_master.lm_id', $law_id)
->orderBy('type_of_event', 'asc')
->orderBy('section', 'asc')
->orderBy('rules', 'asc')
->orderBy('notification', 'asc')
->orderBy('circular', 'asc');
if (!in_array(Auth::user()->role, [1, 7]))
{
$sublaw = $sublaw->where('tbl_law_master.assigned_to', Auth::user()->id)->get();
}