Nested Eager Loading with Joins in Laravel - eloquent

I have the following Tables and their relationships
products Table:
id product_name product_price
1 Product A 2 USD
2 Product B 3 USD
components Table
id component_name component_price
1 Component A 5 USD
2 Component B 3 USD
product_component Pivot Table
id component_id product_id
1 1 1
2 1 2
3 2 2
orders Table
id order_date
1 "2015-05-06"
order_items TABLE
id order_id component_id quantity
1 1 1 1
2 1 2 2
The Order Model
class Order extends Model {
public function items()
{
return $this->hasMany('OrderItem');
}
}
The OrderItem Model:
class OrderItem extends Model {
public function orders()
{
return $this->belongsTo('Order');
}
}
Product Model
class Product extends Model {
public function components()
{
return $this->belongToMany('Component');
}
}
Component Model
class Component extends Model {
public function products()
{
return $this->hasOne('Product');
}
}
Product Component Model
class ProductComponent extends Model {
public function products()
{
return $this->belongsTo('Product')->withPivot();
}
public function components()
{
return $this->belongsTo('Component')->withPivot();
}
}
View
<h3>Order Id : {{ $order->id }} </h3>
<h3>Order Date : {{ $order->order_date }} </h3>
#foreach($order->items as $item)
<tr>
<td>{{ $item->component_name }}</td>
<td>{{ $item->component_price }}</td>
</tr>
#endforeach
My Controller:
public function show($id)
{
$order = Order::with(array('items' => function($query)
{
$query->join('components AS c', 'c.id', '=', 'order_items.component_id');
}))
->find($id);
return view('orders', compact('order'));
}
I am only able to produce the following report with the above code
Order No : 1
Order Date : 2015-05-06
Component A 5 USD
Component B 3 USD
However, I need the Order Report in the following format with Product details for every Product Component.
Order No : 1
Order Date : 2015-05-06
Component A
- Product A 2 USD
- Product B 3 USD
Total 5 X 1 = 5 USD
Component B 3 USD
- Product B 3 USD
Total 3 X 2 = 6 USD
I think I am in the correct direction, but need guidance to generate the desired report.

You can eager-load nested relations as follows:
Order::with('items.products')->get();

I was able to solve this using the following:
class OrderItem extends Model {
public function orders()
{
return $this->belongsTo('Order');
}
public function products()
{
return $this->component->belongsToMany('App\Models\Product\Product');
}
public function component()
{
return $this->hasOne('App\Models\Product\Component');
}
}
Controller:
$order = Order::with(array('items' => function($query)
{
$query->with('products')
->join('product_components AS c', 'c.id', '=', 'product_order_items.component_id')
;
}))
->find($id);
View:
<h3>Order Id : {{ $order->id }} </h3>
<h3>Order Date : {{ $order->order_date }} </h3>
#foreach($order->items as $item)
<tr>
<td>{{ $item->component_name }}</td>
<td>{{ $item->component_price }}
#foreach($item->products AS $product)
<li>{{ $product->name }}</li>
#endforeach
</td>
</tr>
#endforeach

Related

Linq / EF in ASP.NET Blazor

I am using Blazor.
I am trying to get a specific result from dbset.
I have two conditions, salary is 620 and deduction is for p2.
The difficult thing is range, btw 600~700 for salary. Then how I move to specific column to get the result for p2.
var result = await _db.TaxTable.......blah...blah...
In this case, I have to find the result 70
I have tried some code..but I failed everytime.
Could you share the exact knowledge?
Thank you in adv.
You have to locate the row. Then use a switch statement to get the appropriate value.
double GetDeduction(double salary, string discount)
{
var row = someData.SingleOrDefault(a => salary >= a.start && salary < a.end);
if(row == null) throw new SomeRowNotFoundException();
return discount switch
{
"p1" => row.p1,
"p2" => row.p2,
"p3" => row.p3,
_ => throw new SomeDiscountNotValidException();
}
}
You have to include one of the boundaries and exclude the other to avoid overlap. I arbitrarily chose to include start the alternative would be
someData.SingleOrDefault(a => salary > a.start && salary <= a.end)
Your query into your DbSet should look something like this to get the specific value:
using var dbContext = _factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var value = await dbContext.Set<TRecord>()
.SingleOrDefault(item => item.start <= this.salary && item.end >= this.salary)?.p2 ?? 0;
Here's a demo page.
#page "/"
<PageTitle>Index</PageTitle>
<input class="form-control" #bind-value=this.salary />
<div class="text-end m-2">
<button class="btn btn-primary" #onclick=Calc>Calculate</button>
<button class="btn btn-secondary" #onclick=Calc2>Calculate 2</button>
</div>
<div class="bg-dark text-white m-2 p-2">
P2 = #P2
</div>
#code {
private int salary;
private int P2;
private void Calc()
{
var record = deductions.SingleOrDefault(item => item.start <= this.salary && item.end >= this.salary);
P2 = record?.p2 ?? 0;
}
private void Calc2()
{
P2 = deductions.SingleOrDefault(item => item.start <= this.salary && item.end >= this.salary)?.p2 ?? 0;
}
private List<Deduction> deductions = new List<Deduction>
{
new Deduction(1,0,199,10,20,30),
new Deduction(2,200,299,20,30,40),
new Deduction(3,300,399,30,40,50),
new Deduction(4,400,499,40,50,60),
};
public record Deduction(int Id, int start, int end, int p1, int p2, int p3 );
}
Note: there's a problem in your dataset your ranges overlap!

Show xml data in Frontend

Is it possible fetching data from a cached xml file and then showing them on front end?
I was thinking doing it in a TYPO3 extension and with its domain model (and getter/setter) but without a database table. And then filling in data with SimpleXML just to "store" them in memory. At least display the data from domain model with fluid on front end. But I don't know is this approach right or is there a better way to do that? In particular setting up the persistence layer I don't understand.
For any help I thank you very much for your effort in advance.
I found an "acceptable" solution. My approach for that was:
Get all items from xml file
Add a slug field
Sort the items
Display sorted items on the front end
Create unique pretty url
1. Get all items from xml file
Controller: listAction, detailAction
public function listAction() {
$jobs = $this->xmlDataRepository->findAll();
$jobsArray = $this->simpleXmlObjToArr($jobs);
$jobsArraySorted = $this->sortJobsByTitle($jobsArray);
$this->view->assign('jobs', $jobsArraySorted);
}
public function detailAction($slugid) {
$job = $this->xmlDataRepository->findBySlugWithId($slugid);
$this->view->assign('job', $job[0]);
}
Repository: findAll, findBySlugWithId
public function findAll() {
$objectStorage = new ObjectStorage();
$dataFolder = ConfigurationService::setDataFolder();
$xmlFile = glob($dataFolder . '*.xml')[0];
$xmlData = simplexml_load_file($xmlFile,'SimpleXMLElement',LIBXML_NOWARNING);
// error handling
if ($xmlData === false) {
...
}
foreach($xmlData->children() as $job) {
$objectStorage->attach($job);
}
return $objectStorage;
}
public function findBySlugWithId($slugid) {
// get id from slugid
$id = substr($slugid,strrpos($slugid,'-',-1)+1);
$objectStorage = new ObjectStorage();
$dataFolder = ConfigurationService::setDataFolder();
$xmlFile = glob($dataFolder . '*.xml')[0];
$xmlData = simplexml_load_file($xmlFile,'SimpleXMLElement',LIBXML_NOWARNING);
// error handling
if ($xmlData === false) {
...
}
$jobfound = false;
foreach($xmlData->children() as $job) {
if ($job->JobId == $id) {
$objectStorage->attach($job);
$jobfound = true;
}
}
// throw 404-error
if (!$jobfound) {
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$GLOBALS['TYPO3_REQUEST'],
'Ihre angeforderte Seite wurde nicht gefunden',
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
);
throw new ImmediateResponseException($response, 9000006460);
}
return $objectStorage;
}
2. Add a slug field (controller)
protected function simpleXmlObjToArr($obj) {
// 2-dimensional array
$array = [];
foreach($obj as $item){
$row = [];
foreach($item as $key => $val){
$row[(string)$key] = (string)$val;
}
//add slug field, build it with Title
$row['Slug'] = $this->convertToPathSegment($row['Titel']);
// add $row to $array
array_push($array,$row);
}
return $array;
}
3. Sort the items (controller)
protected function sortJobsByTitle(array $jobs) {
$title = array();
$id = array();
foreach ($jobs as $key => $job) {
$title[$key] = $job['Titel'];
$id[$key] = $job['JobId'];
}
// sort jobs array according to title, uid (uid because if there are courses with the same title!)
array_multisort($title,SORT_ASC, $id,SORT_ASC, $jobs,SORT_STRING);
return $jobs;
}
4. Display sorted items on the front end (templates)
List.html:
...
<ul>
<f:for each="{jobs}" as="job">
<li>
<f:comment>
<f:link.action class="" pageUid="2" action="show" arguments="{id: job.JobId, slug: job.Slug}">{job.Titel}</f:link.action> ({job.JobId})<br>
<f:link.action class="" pageUid="2" action="detail" arguments="{xml: job}">NEW {job.Titel}</f:link.action> ({job.JobId})
</f:comment>
<f:variable name="slugid" value="{job.Slug}-{job.JobId}"/>
<f:link.action class="" pageUid="2" action="detail" arguments="{slugid: slugid}"><f:format.raw>{job.Titel}</f:format.raw></f:link.action> ({job.JobId})
</li>
</f:for>
</ul>
...
Detail.html:
...
<f:image src="{job.Grafik}" width="500" alt="Detailstellenbild" />
<p><strong><f:format.raw>{job.Titel}</f:format.raw></strong> ({job.JobId})</p>
<p>Region: {job.Region}</p>
<f:format.html>{job.Beschreibung}</f:format.html>
...
5. Create unique pretty url
...
routeEnhancers:
XmlJobDetail:
type: Extbase
limitToPages:
- 2
extension: Wtdisplayxmldata
plugin: Displayxmldata
routes:
-
routePath: '/{job-slugid}'
_controller: 'XmlData::detail'
_arguments:
job-slugid: slugid
defaultController: 'XmlData::list'
aspects:
job-slugid:
type: XmlDetailMapper
Routing/Aspect/XmlDetailMapper.php:
use TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
class XmlDetailMapper implements StaticMappableAspectInterface {
/**
* {#inheritdoc}
*/
public function generate(string $value): ?string
{
return $value !== false ? (string)$value : null;
}
/**
* {#inheritdoc}
*/
public function resolve(string $value): ?string
{
return isset($value) ? (string)$value : null;
}
}

Eloquent, how to select rows if a value is present in any of two tables?

I have tables that have the following structure, one product can have many skus:
product skus
id product_id
sku_prin sku
other fields other fields
If a search value is present in either sku_prin of products table or sku of skus table the row should be selected.
$search_value = "ramdom_value";
$query = product::query();
$result = $query->with(['skus' => function($q) use($search_value){
// this code won't work the orWhere cannot change the context of skus table
$q->where('sku', $search_value)->orWhere('products.sku_prin', $search_value)
}])->paginate(50);
Above is my failed attempt. How can accomplish what I want?
An approach is to use whereHas function of Eloquent
Consider you have the product and skus model like this
class Product extends Model
{
protected $table = 'product';
public function sku()
{
return $this->belongsTo('App\Skus', 'product_id', 'id');
}
}
class Skus extends Model
{
protected $table = 'skus';
public function products()
{
return $this->hasMany('App\Product', 'id', 'product_id');
}
}
You can obtain your data with Eloquent like this
$keyword = 'some keyword'
Product::where('sku_prin', '=', $keyword) // for product table
->orWhereHas('sku', function($query) use ($keyword) {
$query->where('sku', '=', $keyword); // for sku table
});
Or even more, do the fuzzy whereLike query with exploding keywords from a single string
$keywordString = 'keyword1 keyword2';
$keywords = explode(' ', $keywordString);
Product::where(function($query) use ($keywords) { // for product table
foreach ($keywords as $keyword)
{
$query->orWhere('sku_prin', 'like', "%$keyword%");
}
})
->orWhereHas('sku', function($query) use ($keywords) { // for sku table
foreach ($keywords as $keyword)
{
$query->orWhere('sku', '=', $keyword);
}
});

Angular 5 Angular Material 2 - autocomplete with minLength

I added autocomplete field on my form in order to select a patient.
The patient data come from database. The problem is there are 40.000 patients
So i would like to load data after user has entered 3 characters minimum.
But i don't know how to check that and how to pass the input to the function (filter argument).
This is what i have done. for the moment the data are loaded when i click on the input field :
HTML :
<mat-form-field class="index-full-width">
<input
matInput
type="text"
[(ngModel)]="patientChoice"
placeholder="Patient"
aria-label="Patient"
[matAutocomplete]="autoPatient"
[formControl]="myControl"
(click)="getPatients()">
<mat-autocomplete (optionSelected)="selectPat()" #autoPatient="matAutocomplete" [displayWith]="displayFnPat">
<mat-option *ngFor="let patient of filteredPatients | async" [value]="patient">
<span>{{ patient.lastName }}</span>
<small>{{patient.firstName}}</small> |
<span>né(e) le {{ patient.dateNaissance }}</span> |
<small>IPP: {{patient.ipp}}</small>
</mat-option>
</mat-autocomplete>
</mat-form-field>
TS :
getPatients() {
let searchTerm = '*';
let success: any = {};
this.klinckServices.getPatients(searchTerm)
.then((webScriptdata) => {
success = webScriptdata;
this.listPatients = success.data.items ;
console.log(this.listPatients);
},
msg => {
alert(msg);
});
}
ngOnInit() {
this.filteredPatients = this.myControl.valueChanges.pipe(
startWith<string | Patient>(''),
map(patient => typeof patient === 'string' ? patient : patient.name),
map(name => name ? this.filterPatient(name) : this.listPatients.slice())
);
}
displayFnPat(patient: Patient): string | undefined {
return patient ? patient.name : undefined;
}
filterPatient(name: string) {
return this.listPatients.filter(patient =>
patient.name.toLowerCase().includes(name.toLowerCase()));
}
There's another way to do it and it is recommended here.
It's basically checking the length on your filter method.
filterPatient(name: string) {
if (name.length < 2) {
return [];
}
return this.listPatients.filter(patient =>
patient.name.toLowerCase().includes(name.toLowerCase()));
}
OK, solved by adding in HTML (keyup)="getPatients($event)" :
<input
matInput
type="text"
[(ngModel)]="patientChoice"
placeholder="Patient"
aria-label="Patient"
[matAutocomplete]="autoPatient"
[formControl]="myControl"
(keyup)="getPatients($event)"
>
And in TS file :
getPatients(event: any) {
let searchTerm = '';
searchTerm += event.target.value;
console.log(searchTerm);
if (searchTerm.length === 2) {
let success: any = {};
this.klinckServices.getPatients(searchTerm)
.then((webScriptdata) => {
success = webScriptdata;
this.listPatients = success.data.items;
console.log(this.listPatients);
},
msg => {
alert(msg);
});
}
}

Entity Framework is not providing me IDs when calling back

I'm using a ViewModel (RoleVM) with a collection of ViewModels (RolePermissionVM) for this particular edit view. The view displays the RoleVM fields, and a checkbox list of RolePermissionVM. Each row in the checkbox list has a hiddenFor for the ID of the RolePermission.
When I save the form, my controller correctly writes the data to the database, adding or updating records. However, I would like the user to remain on the page, so I call the View again, but trying to get an updated model so that I have the IDs for any newly created RolePermissionVM objects. I am not getting the new IDs into the HiddenFor fields.
Here's my class:
public class RolePermissionVM
{
public int? RolePermissionId { get; set; }
public int RoleId { get; set; }
public int PermissionId { get; set; }
public string PermissionName { get; set; }
public bool IsActive { get; set; }
}
My controller code:
private RoleVM GetRoleVm(int id)
{
var thisRoleVm = (from r in db.Role
where r.RoleId == id
select new RoleVM
{
RoleId = r.RoleId,
RoleName = r.RoleName,
RoleDescription = r.RoleDescription,
OwnerId = r.OwnerId,
IsActive = r.IsActive
}).FirstOrDefault();
thisRoleVm.RolePermission = (from p in db.Permission
join rPerm in
(from rp in db.RolePermission
where rp.RoleId == id
select rp)
on p.PermissionId equals rPerm.PermissionId into pp
from rps in pp.DefaultIfEmpty()
select new RolePermissionVM
{
RolePermissionId = (int?)rps.RolePermissionId,
RoleId = id,
PermissionId = p.PermissionId,
PermissionName = p.PermissionName,
IsActive = (rps.IsActive == null ? false : rps.IsActive)
})
.OrderBy(p => p.PermissionName).ToList();
return thisRoleVm;
}
[HttpPost, ActionName("_roleedit")]
[ValidateAntiForgeryToken]
public ActionResult _RoleEdit(RoleVM editedRole)
{
//...
if (ModelState.IsValid)
{
var dbRole = db.Role.Find(editedRole.RoleId);
dbRole.RoleName = editedRole.RoleName;
dbRole.RoleDescription = editedRole.RoleDescription;
dbRole.OwnerId = editedRole.OwnerId;
foreach (var thisPerm in editedRole.RolePermission) // RolePermission here is the ViewModel, not the actual model
{
if (thisPerm.RolePermissionId != null && thisPerm.RolePermissionId > 0)
{
// We have a record for this, let's just update it
var thisRolePerm =
dbRole.RolePermission.FirstOrDefault(rp => rp.RolePermissionId == thisPerm.RolePermissionId);
thisRolePerm.IsActive = thisPerm.IsActive;
db.Entry(thisRolePerm).State = EntityState.Modified;
}
else
{
if (thisPerm.IsActive)
{
// New and active, so we add it
dbRole.RolePermission.Add(new RolePermission
{
RoleId = editedRole.RoleId,
PermissionId = thisPerm.PermissionId,
IsActive = true
});
}
}
}
db.Entry(dbRole).State = EntityState.Modified;
db.SaveChanges(User.ProfileId);
var newEditedRole = GetRoleVm(editedRole.RoleId); // We don't get the new IDs here, but I would like to
newEditedRole.ResponseMessage = "Saved Successfully";
return View(newEditedRole); // This should have the new RolePermissionId values, but it doesn't.
}
editedRole.ResponseMessage = "Error Saving";
return View(editedRole);
}
The partial view used for each row of the CheckBox list:
#using PublicationSystem.Tools
#model PublicationSystem.Areas.Admin.Models.RolePermissionVM
<li class="editorRow ui-state-default removable-row">
#using (Html.BeginCollectionItem("RolePermission"))
{
<div class="row">
#Html.HiddenFor(model => model.RolePermissionId)
#Html.HiddenFor(model => model.RoleId)
#Html.HiddenFor(model => model.PermissionId)
#Html.HiddenFor(model => model.PermissionName)
<div class="col-md-7">
#Html.DisplayFor(model => model.PermissionName, new {htmlAttributes = new {#class = "form-control"}})
</div>
<div class="col-md-3">
#Html.CheckBoxFor(model => model.IsActive, new { htmlAttributes = new { #class = "form-control" } })
</div>
</div>
}
</li>
var newEditedRole = GetRoleVm(editedRole.RoleId); should be calling the database to get the updated IDs, but it does not. I think the issue is the DBContext is using a cached copy.
So, why do the new database generated IDs not get pulled back? How can I fix that? Is there a more efficient way to do this?
You have to call return RedirectToAction("ViewName") instead of return View(newEditedRole);
Another way is removing the value from the ModelState, so it will be updated on view:
ModelState.Remove("RoleId")
model.RoleId = dbRole.RoleId
I think return RedirectToAction("ViewName") is better/more reliable choice.