Yii2 update data by excel file long load time - yii2

I'm using Yii2 to create a tool to manage work load of my team. So everyday i need to import large amount of data (larger than 5k) to db using Excel and the loadtime usually take about 20-30 mins. Is there any way to improve the load time?
Please help me with this.
Here's the code I used:
public function actionImportExcel()
{
$inputFile = 'uploads/importexcel/import.csv';
try{
$inputFileType = \PHPExcel_IOFactory::identify($inputFile);
$objReader = \PHPExcel_IOFactory::createReader($inputFileType);
$objPHPExcel = $objReader->load($inputFile);
}catch(Exception $e)
{
die('Error');
}
$sheet = $objPHPExcel->getSheet(0);
$highestRow = $sheet->getHighestRow();
$highestColumn = $sheet->getHighestColumn();
for( $row = 1; $row <= $highestRow; $row++)
{
$rowData = $sheet->rangeToArray('A'.$row.':'.$highestColumn.$row,NULL,TRUE,FALSE);
if($row == 1)
{
continue;
}
$test = $rowData[0][0];
$ext = Sku3d::find()->where(['sku' => $test])->exists();
if($ext){
$one = Sku3d::find()->where(['sku' => $test])->one();
$one->status = $rowData[0][14];
$one->round = $rowData[0][19];
$one->source = $rowData[0][29];
$one->modeler = $rowData[0][30];
if($one->datesubmit == NULL || $one->datesubmit == ""){
$one->save();
}else{
$day = DateTime::createFromFormat('Y-m-d', $one->datesubmit);
$one->monthsubmit=date("Y-m-t", strtotime($one->datesubmit));
$one->save();
}
if($rowData[0][14] == "Approved"){
$one->approvedate = $rowData[0][16];
if($one->approvedate == NULL || $one->approvedate == ""){
$one->save();
}else{
$one->approvemonth=date("Y-m-t", strtotime($one->approvedate));
$one->save();
}
}else{
$one->approvedate = Null;
$one->approvemonth = Null;
}
$one->save();
// print_r($one->getErrors());
// die;
}
else{
}
}
}
Thank you!

You're executing very similar queries when you check if the row already exist and when you are loading it from DB.
If you are expecting more new records then existing one, you can load all skus to the array before the cycle, then check if the sku is among existing.
Before the for cycle:
$existingSkus = Sku3d::find()
->select(['sku'])
->indexBy('sku')
->column();
Then inside your for cycle:
if (array_key_exists($test, $existingsSkus)) {
$one = Sku3d::find()->where(['sku' => $test])->one();
// ...
}
If you are expecting that most of rows in import already exist in DB and you are going to update them, then you can skip the exists() query and load the data directly.
$one = Sku3d::find()->where(['sku' => $test])->one();
if(!empty($one)) {
// ... update existing row loaded in $one
} else {
// ... create new row
}
Another problem in your code is that you call the save multiple times for each updated row.
if($one->datesubmit == NULL || $one->datesubmit == ""){
$one->save(); //first save
} else {
// ...
$one->save(); //first save - else branch
}
// ...
if ($rowData[0][14] == "Approved"){
$one->approvedate = $rowData[0][16];
if ($one->approvedate == NULL || $one->approvedate == ""){
$one->save(); // second save
} else {
$one->approvemonth=date("Y-m-t", strtotime($one->approvedate));
$one->save(); //second save - else branch
}
}else{
$one->approvemonth = Null;
}
$one->save(); //third save when the previous condition is true, second save otherwise
Do you really need to call the save before you are done with all changes? Doing one save at the end will be faster then doing 2 or 3 saves for each row.
Also if there are many new rows in each import, you might want to use batch insert instead of creating and saving new model for each row.

Related

Laravel Query builder returns a row from join instead of multiples row

I am getting fewer results than expected , The data returns 57 rows instead of 70 rows. I need help to get all the rows please, I returned a collection using get(), and i joined it with foreach() to include the rows to existing query, Please any help?
public function getempAttendance(Request $request) {
$id = $request->id;
$department_id = $request->department_id;
if($id !== null){ //return based on type;
$emp = AsEmployee::where('id','=',$id)->orderBy('id','ASC')->get();
} else if($department_id != null){
$emp = AsEmployee::where('department_id','=',$department_id)->orderBy('id','ASC')->get();
} else{ //return all if nothing is given;
$emp = AsEmployee::orderBy('id','DESC')->get();
}
foreach($emp as $emp_data){
$department = AsDepartment::where('id','=',$emp_data->department_id)->get(['department'])->first();
if($department !== NULL){
$emp_data->department_name = $department->department;
}else{
$emp_data->department_name = '';
}
$position = AsEmployeePosition::where('id','=',$emp_data->position_id)->get(['position_name'])->first();
if($position !== NULL){
$emp_data->position_name = $position->position_name;
}else{
$emp_data->position_name = '';
}
$attendances = AsAttendanceLog::select('CHECK_IN_TIME','CHECK_OUT_TIME')
->where('EMPLOYEE_ID','=',$emp_data->employee_id)->get();
if($attendances !== NULL){
foreach($attendances as $attendance){
$emp_data->CHECK_IN_TIME = Carbon::parse($attendance->CHECK_IN_TIME)->toTimeString();
$emp_data->CHECK_OUT_TIME = Carbon::parse($attendance->CHECK_OUT_TIME)->toTimeString();
$emp_data->Date = Carbon::parse($attendance->CHECK_IN_TIME)->format('Y-m-d');
$hours = Carbon::parse($attendance->CHECK_OUT_TIME)->diffInSeconds(Carbon::parse($attendance->CHECK_IN_TIME));
$emp_data->Hours = gmdate('H:i', $hours);
}
}else {
$emp_data->CHECK_IN_TIME = '';
$emp_data->CHECK_OUT_TIME = '';
}
}
return $this->sendResponse($emp);
}
Meanwhile, this works but i need the query builder format to allow me use Carbon and do some operations
$attendanceData = DB::table('as_tbl_employee_master AS emp')
->leftJoin('as_tbl_department AS dept','emp.department_id','=', 'dept.id')
->leftJoin('as_tbl_employee_position AS pos', 'emp.position_id', '=', 'pos.id')
->leftJoin('as_tbl_emp_attendance_daily_log AS att', 'att.EMPLOYEE_ID', '=', 'emp.employee_id')
->select('emp.id','emp.employee_id','emp.english_name','dept.department','pos.position_name','att.CHECK_IN_TIME','att.CHECK_OUT_TIME')
->orderby('att.CHECK_IN_TIME', 'DESC')
->get();
return $this->sendResponse($attendanceData);
if the user's table is "one to many" to as_tbl_emp_attendance_daily_log's table.
You should select('as_tbl_emp_attendance_daily_log') first then left join to user's table.
I assumed that you want to show all as_tbl_emp_attendance_daily_log's row. If that right your Query builder should be like this.
$attendanceData = DB::table('as_tbl_emp_attendance_daily_log AS att')
->leftJoin('as_tbl_employee_master AS emp', 'att.EMPLOYEE_ID', '=', 'emp.employee_id')
->leftJoin('as_tbl_department AS dept','emp.department_id','=', 'dept.id')
->leftJoin('as_tbl_employee_position AS pos', 'emp.position_id', '=', 'pos.id')
->select('emp.id','emp.employee_id','emp.english_name','dept.department','pos.position_name','att.CHECK_IN_TIME','att.CHECK_OUT_TIME')
->orderby('att.CHECK_IN_TIME', 'DESC')
->get();
UPDATE
If you want to add the custom attribute to the model you should define it at the model.
This is for reference:
https://laravel.com/docs/5.1/eloquent-serialization#appending-values-to-json
But you can directly change the attribute if you change the collections to the array first.
I don't have your table, so I cannot test it, I just changed it based on your snippet.
public function getempAttendance(Request $request) {
$id = $request->id;
$department_id = $request->department_id;
if($id !== null){ //return based on type;
$emp = AsEmployee::where('id','=',$id)->orderBy('id','ASC')->toArray();
} else if($department_id != null){
$emp = AsEmployee::where('department_id','=',$department_id)->orderBy('id','ASC')->toArray();
} else{ //return all if nothing is given;
$emp = AsEmployee::orderBy('id','DESC')->toArray();
}
for($i=0;$i < count($emp); $i++){
$department = AsDepartment::where('id','=',$emp[$i]->department_id)->get(['department'])->first();
if($department !== NULL){
$emp[$i]['department_name'] = $department->department;
}else{
$emp[$i]['department_name'] = '';
}
$position = AsEmployeePosition::where('id','=',$emp[$i]['position_id'])->get(['position_name'])->first();
if($position !== NULL){
$emp[$i]['position_name'] = $position->position_name;
}else{
$emp[$i]['position_name'] = '';
}
$attendances = AsAttendanceLog::select('CHECK_IN_TIME','CHECK_OUT_TIME')
->where('EMPLOYEE_ID','=',$emp[$i]['employee_id'])->toArray();
$attendances_data = [];
if($attendances !== NULL){
for($j=0;$j<count($attendances);$j++){
$data = [];
$data['CHECK_IN_TIME'] = Carbon::parse($attendance['i']['CHECK_IN_TIME'])->toTimeString();
$data['CHECK_OUT_TIME'] = Carbon::parse($attendance['i']['CHECK_OUT_TIME'])->toTimeString();
$data['Date'] = Carbon::parse($attendance['i']['CHECK_IN_TIME'])->format('Y-m-d');
$hours = Carbon::parse($attendance['i']['CHECK_OUT_TIME'])->diffInSeconds(Carbon::parse($attendance['i']['CHECK_IN_TIME']));
$data['Hours'] = gmdate('H:i', $hours);
$attendances_data[] = $data
}
$emp[$i]['attendances'] = $attendances_data;
}else {
$emp[$i]['attendances'] = [];
$emp[$i]['attendances'] = [];
}
}
return $this->sendResponse($emp);
}
UPDATE
If you want to multiple row, I think you should try this:
$attendanceData = DB::table('as_tbl_emp_attendance_daily_log AS att')
->leftJoin('as_tbl_employee_master AS emp', 'att.EMPLOYEE_ID', '=', 'emp.employee_id')
->leftJoin('as_tbl_department AS dept','emp.department_id','=', 'dept.id')
->leftJoin('as_tbl_employee_position AS pos', 'emp.position_id', '=', 'pos.id')
->select('emp.id','emp.employee_id','emp.english_name','dept.department','pos.position_name','att.CHECK_IN_TIME','att.CHECK_OUT_TIME')
->orderby('att.CHECK_IN_TIME', 'DESC')
->toArray();
for($i=0;$i<count($attendaceData);$i++)
{
$attendaceData[$i]['CUSTOM VARIABLE'] = 'NEW VALUE CUSTOM VARIABLE AT HERE';
}
return $this->sendResponse($emp);

Yii2 ActiveRecord add a new record with unique text field

I am using Yii2 and ActiveRecord. I have a field called "code" and for each record, it is meant to have a unique value like this: "REC0001", "REC0002", "REC0003" in a sequencial manner.
All works and I can generate a record code as described. However if I refresh my page request fast in a multiple manner (trying to test multiple requests at the same time in a very raw manner hehe), then some of the records end up with the same record code. In other words I found "REC007" a few times.
I generate the code looking at the last code and increase it by one, then I do a while foundFlag == true by checking to see if it already exists in the database.
I am suspecting there is a delay in writing to the database and hence it assumes that it is not there.
Here is a portion of the code:
static function createCode($rec){
if ($rec->code){
return $rec->code;
}
if ($rec->id){ // find it by id if one passed and record exists
$tmpRec = $rec->find()
->where([
'id' => $rec->id,
])
->one();
if ($tmpRec && $tmpRec->code){
return $tmpRec->code;
}
}
$prefix = 'REC';
if (!$prefix){
$prefix = 'REC';
}
$maxDecimals = 12;
$codeLength = $maxDecimals+strlen($prefix);
$query = $rec->find();
$query = $query->where([
'archived' => '0'
]);
// look under an organization if it exists in the model and there is one
if ($rec->hasField('organization_id') && $organization_id){
$query = addQueryWhere($query, [
'organization_id' => $organization_id,
]);
}
$query = addQueryWhere($query, [
'LENGTH(code)' => $codeLength*1,
]);
$query = $query->orderBy('code desc');
$lastRec = $query->one();
$tmpNumber = 0;
if ($lastRec && $lastRec->id){
// check what it returns
$tmpNumber = str_replace($prefix, '', $lastRec->code);
}
$tmpNumber++;
$leftDecimals = $maxDecimals - strlen($tmpNumber.'');
for ($k=0; $k <= $leftDecimals-1 ; $k++){
$tmpNumber = '0'. $tmpNumber;
}
$ret = $prefix . $tmpNumber;
return $ret;
}
public function generateCode($rec){
$foundFlag = true;
$break = 1000; // safe break point - no continuous loop
$cnt = 0;
$code = static::createCode($rec);
while ($foundFlag === true || $cnt < $break){
$tmpRec = $rec->find()
->where([
'code' => $code,
])
->one();
if (!$tmpRec->id){
$foundFlag = false;
break;
}
$time = getCurrentTimestamp();
$code = static::createCode($rec);
$cnt++;
}
$ret = $code;
return $ret;
}
So I simply call: $this->code = $this->generateCode();
Like I said it does work in generating the code, but it creates duplicates when it shouldn't!
Thank you for your assistance.

Auto manage and protect Created\Updated fields with Entity Framework 5

I want so every added\changed record will have a time stamp of creation\change.
But - so it will be easy to embed and easy to manage - automatically.
Overwrite the 'DbContext' class or embed this in the '.tt' file (Codefirst \ DBFirst)
The code assume so you have the fields 'CreatedOn'\'ModifiedOn' inside the POCO.
If you don't have them, or you have only one - the code will work fine.
Be aware! If you use a extension (as this one) so allow you to do batch updates or changes from a stored procedure - this will not work
EDIT:
I found the source of my inspiration - thanks 'Nick' here
public override int SaveChanges()
{
var context = ((IObjectContextAdapter)this).ObjectContext;
var currentTime = DateTime.Now;
var objectStateEntries = from v in context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified)
where v.IsRelationship == false && v.Entity != null
select v;
foreach (var entry in objectStateEntries)
{
var createdOnProp = entry.Entity.GetType().GetProperty("CreatedOn");
if (createdOnProp != null)
{
if (entry.State == EntityState.Added)
{
if (createdOnProp != null)
{
createdOnProp.SetValue(entry.Entity, currentTime);
}
}
else
{
Entry(entry.Entity).Property("CreatedOn").IsModified = false;
}
}
var modifiedOnProp = entry.Entity.GetType().GetProperty("ModifiedOn");
if (modifiedOnProp != null)
{
modifiedOnProp.SetValue(entry.Entity, currentTime);
}
}
return base.SaveChanges();
}

LINQ2SQL - FirstItem

How do you get the first item only? It seems like I have to do the following otherwise i will get an error as if it was a multiple item and i can't get just the first element of it.
My goal is i would like to remove the foreach loop from the code below.
MetaDataPropertyBag propertyBag = new MetaDataPropertyBag();
var dbResultsOfType = db.spi_GetTypesByCaseType(caseType);
foreach (var item in dbResultsOfType)
{
if (item.ASSOC_TYPE_ID == primaryChildTypeID)
{
propertyBag.CaseTypeDesc = item.DESCRIPTION;
propertyBag.Required = item.IS_REQUIRED == 'Y' ? true : false;
propertyBag.Parent = item.PARENT_ID.Value;
propertyBag.Child = item.CHILD_ID.Value;
propertyBag.AssocTypeID = item.ASSOC_TYPE_ID;
propertyBag.CaseTypeID = item.CASE_TYPE_ID;
break; // Only one entry is requested
}
}
FirstorDefault should do it:
MSDN article on firstordefault
Here is one way to do it:
var first = dbResultsOfType.FirstOrDefault(item => item.ASSOC_TYPE_ID == primaryChildTypeID);
if (first != null) {
propertyBag.CaseTypeDesc = first.DESCRIPTION;
propertyBag.Required = first.IS_REQUIRED == 'Y' ? true : false;
propertyBag.Parent = first.PARENT_ID.Value;
propertyBag.Child = first.CHILD_ID.Value;
propertyBag.AssocTypeID = first.ASSOC_TYPE_ID;
propertyBag.CaseTypeID = first.CASE_TYPE_ID;
}

Improving method performance

I wrote the following method that receives a list and updates the database based on certain criteria:
public void UpdateInventoryGoods(List<InventoryGoods> list, int id)
{
int index = 0;
var query = from inventoryGoods in context.InventoryGoods
where inventoryGoods.ParentId == id
select inventoryGoods;
List<InventoryGoods> goodsList = query.ToList();
using (var scope = new TransactionScope())
{
foreach (InventoryGoods i in list)
{
foreach (InventoryGoods e in goodsList)
{
if (index == 30)
{
index = 0;
context.SubmitChanges();
}
if (e.Gid == i.Gid && !getEventId(e.Id).HasValue && !e.ActionOn.HasValue)
{
e.Action = i.Action;
}
else if ((e.Gid == i.Gid && getEventId(e.Id).HasValue) && (e.Action != i.Action || i.ActionOn == DateTime.MinValue))
{
e.Action = i.Action;
e.ActionOn = null;
var allEvents = from invent in context.InventoryGoodsEvents
where invent.InventoryGood == e.Id
select invent;
List<InventoryGoodsEvents> inventoryGoodsEventsList = allEvents.ToList();
var events = from g in context.GoodsEvent
select g;
List<GoodsEvent> goodsEventList = events.ToList();
foreach (InventoryGoodsEvents goodsEvent in inventoryGoodsEventsList)
{
context.InventoryGoodsEvents.DeleteOnSubmit(goodsEvent);
foreach (GoodsEvent ge in goodsEventList)
{
if (ge.Id == goodsEvent.EventId)
{
ge.IsDeleted = true;
ge.DeletedOn = DateTime.Now;
ge.DeletedBy = System.Web.HttpContext.Current.User.Identity.Name;
}
}
}
}
++index;
}
}
context.SubmitChanges();
scope.Complete();
}
}
public int? getEventId(int InventoryGood)
{
var InventoryGoodsEvents = from i in context.InventoryGoodsEvents
where i.InventoryGood == InventoryGood
select i;
List<InventoryGoodsEvents> lst = InventoryGoodsEvents.ToList();
if (lst.Count() > 0)
{
return lst[0].EventId;
}
else
{
return null;
}
}
Though this method works well for about 500 or 1000 objects, it gets too slow or eventually times out when I feed it over 8000 objects or more.
So, where could I improve its performance a little?
Don't call the database in a loop.
Try moving the queries outside the loops like this:
public void UpdateInventoryGoods(List<InventoryGoods> list, int id)
{
int index = 0;
var query = from inventoryGoods in context.InventoryGoods
where inventoryGoods.ParentId == id
select inventoryGoods;
List<InventoryGoods> goodsList = query.ToList();
using (var scope = new TransactionScope())
{
var allEvents = from invent in context.InventoryGoodsEvents
where goodsList.Contains(invent.InventoryGood)
select invent;
List<InventoryGoodsEvents> inventoryGoodsEventsList = allEvents.ToList();
var events = from g in context.GoodsEvent
select g;
List<GoodsEvent> goodsEventList = events.ToList();
foreach (InventoryGoods i in list)
{
foreach (InventoryGoods e in goodsList)
{
if (index == 30)
{
index = 0;
context.SubmitChanges();
}
var eventId = getEventId(e.Id);
if (e.Gid == i.Gid && !eventId.HasValue && !e.ActionOn.HasValue)
{
e.Action = i.Action;
}
else if ((e.Gid == i.Gid && eventId.HasValue) && (e.Action != i.Action || i.ActionOn == DateTime.MinValue))
{
e.Action = i.Action;
e.ActionOn = null;
foreach (InventoryGoodsEvents goodsEvent in inventoryGoodsEventsList)
{
context.InventoryGoodsEvents.DeleteOnSubmit(goodsEvent);
foreach (GoodsEvent ge in goodsEventList)
{
if (ge.Id == goodsEvent.EventId)
{
ge.IsDeleted = true;
ge.DeletedOn = DateTime.Now;
ge.DeletedBy = System.Web.HttpContext.Current.User.Identity.Name;
}
}
}
}
++index;
}
}
context.SubmitChanges();
scope.Complete();
}
}
I'm no Linq expert, but I think you can probably improve getEventId (should be capital first letter btw) with something like
public int? GetEventId(int inventoryGood)
{
var firstInventoryGoodsEvent = context.InventoryGoodsEvents
.Where(i => i.InventoryGood == inventoryGood)
.FirstOrDefault();
// ...etc
}
The use of FirstOrDefault() means you don't process the whole list if you find a matching element.
There are probably other optimisations but it's quite difficult to follow what you're doing. As an example:
foreach (InventoryGoods i in list)
{
foreach (InventoryGoods e in goodsList)
{
}
}
i and e don't confer much meaning here. It might be obvious to you what they mean but they aren't very descriptive to someone who has never seen your code before. Similarly, list is not the best name for a List. List of what? Your variable name should describe it's purpose.
Edit:
I'm not sure about anything else. You seem to be using ToList() in a few places where as far as I can see it's not necessary. I don't know what effect that would have on performance, but someone cleverer than me could probably tell you.
You could also try hoisting a few of your values outside of loops, eg:
foreach (foo)
{
foreach (bar)
{
DeletedOn = DateTime.Now;
DeletedBy = System.Web.HttpContext.Current.User.Identity.Name;
}
}
can be re-written as
var deletedOn = DateTime.Now;
var deletedBy = System.Web.HttpContext.Current.User.Identity.Name;
foreach (foo)
{
foreach (bar)
{
DeletedOn = deletedOn;
DeletedBy = deletedBy;
}
}
Again, I'm not sure how much difference if any that would make, you'll need to test it and see.
It's not going in batches of 30, it's going in batches of 1.
There's a query with no criteria, so it loads the whole table. Is that your intention?
getEventId(e.Id) returns a consistent value. Don't call it twice (per loop).