Laravel computing user credit balance from transactions table slow [duplicate] - php

This question already has answers here:
MySQL SUM Query is extremely slow
(3 answers)
Closed last month.
I'm working inside a Laravel 9 project where users can purchase "credits" that are then used each time a user uses the API. However, I need to check the user's remaining quota each time, and right now my query is quite slow.
I have a CreditTransaction model, which stores each transaction in a table with a user_id and a delta column which could be positive or negative depending on whether they purchased credits, or credits were used, then I'm performing a sum of the delta column on my hasMany relationship of credit_transactions, which works, but takes a few seconds to compute when queried.
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable implements MustVerifyEmail
{
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'role',
'first_name',
'last_name',
'email',
'timezone',
'password',
'origin_source',
'origin_source_other',
'origin_campaign',
'last_login_at',
];
/**
* The attributes that should be hidden for serialization.
*
* #var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'two_factor_recovery_codes',
'two_factor_secret',
'origin_campaign',
];
/**
* The attributes that should be cast.
*
* #var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'last_login_at' => 'datetime',
];
/**
* Get the credit transactions for the user
*
* #return int
*/
public function getCreditBalanceAttribute()
{
try {
if (!$this->credit_transactions) {
return 0;
}
$balance = $this->credit_transactions->sum('delta');
if ($balance <= 0) {
return 0;
}
return $balance;
} catch (\Exception $err) { }
return 0;
}
/**
* Get the credit transactions for the user
*
* #return array
*/
public function credit_transactions()
{
return $this->hasMany(CreditTransaction::class);
}
}
I can then perform the following to get my current balance.
Auth::user()->credit_balance
I'm not sure how best to proceed with this, as this is based on a table size of 120k rows which is quite small, equally, I need the credit balance to be accurate and fast, so caching it for 15 minutes isn't an option here.
This is my credit_transactions table:
Schema::create('credit_transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->index();
$table->foreignId('credit_type_id')->default(1)->index();
$table->foreignUuid('message_id')->nullable()->index();
$table->integer('delta')->index();
$table->timestamps();
$table->softDeletes();
});

Update your method to get sum of transactions to:
/**
* Get the credit transactions for the user
*
* #return int
*/
public function getCreditBalanceAttribute()
{
try {
// actually this code retrieves all user's transactions from DB
// and then calculate SUM
//if (!$this->credit_transactions) {
// return 0;
//}
//$balance = $this->credit_transactions->sum('delta');
// instead use this:
// Ask database to calculate SUM of delta
$balance = $this->credit_transactions()->sum('delta');
if ($balance <= 0) {
return 0;
}
return $balance;
} catch (\Exception $err) { }
return 0;
}
See more here - Laravel Eloquent Sum of relation's column
To run sum(delta) faster you need to create a multi-column index on the user_id and delta columns.
CREATE INDEX credit_transactions_user_id_delta_idx ON credit_transactions(user_id, delta);
Or just add it to your Laravel migration:
$table->index(['user_id', 'delta']);
dbfiddle example with 20k transactions per user sum took only 7ms.
Is it possible to calculate user balance faster?
Yes, but with different approach, create a column that stores actual user balance. And after that update balance column atomically, e.g.:
begin;
-- add 500 credits to user balance
UPDATE users SET balance = balance + 500;
-- query to log user transactions
-- INSERT INTO credit_transactions (your_column_list) VALUES (your_value_list);
commit;
P.S. See also Is this SQL safe enough to handle user balance transaction?

Related

Laravel nested resource retrieve model by slug instead of id

I'm working on a Laravel 8 project, it's being used as an API for a frontend. My API contains Brands and Forms, a brand is created by a user and a brand can have a form.
My brand schema contains a slug column, it's this column that's present in my front-end URLs, the slug column is unique, e.g:
/account/brands/my-brand/forms/
/account/brands/my-brand/forms/create/
A form has a brand_id column, this is later used as part of Laralve's hasOne and hasMany relationship for automatic joining since it's easier this way and means I don't have to have an ugly URL.
The trouble I'm having is when I want to show the user a list of their forms for the brand they're on I don't have access to the brand_id in the request, only the slug as this is part of the URL, whereas to retrieve my forms I need the brand_id.
How could I easily (without having another function) obtain this whilst still ensuring my relationships in my model work correctly?
Retrieving forms for a brand that belong to a user
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index($brand)
{
// $brand -> this is the slug, unsure how to utilise this on the joined brand
$forms = Form::where('user_id', Auth::id())->with('brand')->get();
return response()->json([
'forms' => $forms
], 200);
}
My Brand model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Brand extends Model
{
use HasFactory, SoftDeletes;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'brands';
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'uuid',
'brand',
'url',
'telephone',
'link_terms',
'link_privacy',
'seo_description',
'base64_logo',
'base64_favicon',
'text_marketing',
'text_promos',
'text_broker',
'text_footer_1',
'text_footer_2',
'text_credit_disclaimer',
'analytics_internal_affiliate'
];
/**
* The relationships that should always be loaded.
*
* #var array
*/
protected $with = [
'form'
];
/**
* Get the form associated with the user.
*/
public function form()
{
return $this->hasOne(Form::class);
}
/**
* Get the brand that owns the comment.
*/
public function brand()
{
return $this->belongsTo(User::class);
}
}
My Form model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Form extends Model
{
use HasFactory, SoftDeletes;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'forms';
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'name',
'loan_amount',
'loan_min_amount',
'loan_max_amount',
'loan_term',
'loan_min_term',
'loan_max_term'
];
/**
* Get the brand that owns the comment.
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
}
The path of least resistance would be to harness the whereRelationship() method on your query.
I believe that would look like this:
$forms = Form::where('user_id', Auth::id())
->whereRelationship('brand', '=', $brand)
->with('brand')
->get();
https://laravel.com/docs/master/eloquent-relationships
An alternative which would be more work to set up but would likely make your life easier long term would be to use route model binding. https://laravel.com/docs/master/routing#route-model-binding
You can ask Laravel's IoC container to resolve the full record for you by typehinting it and naming the variable correctly in the method signature.
Since you are using a column other than id to identify it, you need to inform Laravel of this. There are a couple of different ways to do this, discussed at https://laravel.com/docs/master/routing#customizing-the-default-key-name
Then, you can ask Laravel's IoC container to build an instance of the model by searching in the database for you right in the method signature. You simply need to typehint the parameter and ensure it's name matches that in the route definition.

Retrieving records with _____________ relationship?

I have 5 tables.
User tabel => This table stores the data of users.
Exam table => This table stores the data of exams.
Feeds table => This table stores the data of feeds.
Exam_User_Pivot table => This table stores the data of users and exams means the user_id and exam_id.
Exam_Feed_Pivot table => This table stores the data of exams and feeds means the exam_id and feed_id.
Now, I want to retrieve the no. of users per each feed ?
I have three models :
1. User Model
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
protected $primaryKey = 'uid';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password', 'phone',
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* #var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function exams()
{
return $this->belongsToMany(exam::class, 'exam_user', 'user_id', 'exam_id')->withTimeStamps();
}
}
2. Exam Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class exam extends Model
{
protected $primaryKey = 'eid';
protected $fillable = [
'exam_name',
];
}
3. Feed Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Feed extends Model
{
protected $primaryKey = 'fid';
protected $fillable = [
'feed_type', 'title', 'description', 'feed_media', 'source',
];
public function feedexams()
{
return $this->belongsToMany(exam::class, 'exam_feed', 'feed_id', 'exam_id')->withTimeStamps();
}
}
Users have many-to-many relationship with Exams
Feeds have many-to-many relationship with Exams
I want to retrieve the no. of users per each feed And I don't know which relationship can be use ?
So first add the following two functions to your Exam Model.
Add to Exam model:
public function feeds() {
return $this->belongsToMany('App\Feed')->withTimeStamps(); //
}
public function users() {
return $this->belongsToMany('App\User')->withTimeStamps(); //
}
I want to retrieve the no. of users per each feed .....
The reasoning is based on the following three points:
get the feed,
then get exams of that feed (one feed has many exams)
then get users in each of these exams (here we will use a loop to ensure we count users of each exam from 2 above)
Loop through all feeds as:
foreach(App\Feed::all() as $feed) {
//here you are accessing all related exams using the function feedexams() that you have created in the feeds model
$exams = $feed->feedexams();
//because each exam will have its own users, we need a way to count the users for each exam, and finally ensure we sum or count all of them. we create a counter called $user_per_feed_count to keep or remember the users count as we loop through all exams.
//Finally we will output this counter which will have the total of all users belonging to exams which belong to the feed.
$users_count = 0; //initialize a users counter with zero
foreach($exams as $exam) {
//now we update our counter by adding the number of users for each exam
$users_count += $exam->users()->count();
}
//HERE IS YOUR no. of users per each feed.
echo "Feed " . $feed->name . "has " . $users_count . " users";
}
Expected output will be as follows ...
Feed myfeed has 10 users
Feed otherfeed has 38 users
Now to test it, just go to your routes/web.php and add the following code
Route::get('getusers', function () {
foreach (App\Feed::all() as $feed) {
$exams = $feed->feedexams();
$users_count = 0; //initialize a users counter with zero
foreach ($exams as $exam) {
//$users_count += $exam->users()->count();
$users_count += $exam->has('users') ? $exam->users()->count() : 0;
}
echo "Feed " . $feed->name . "has " . $users_count . " users";
}
});
then open your browser and type this link:
http://YOUR_PROJECT_NAME_HERE/getusers and press enter:
Note: Replace YOUR_PROJECT_NAME_HERE with your actual project name. Make sure you have some exams,users, and feeds already in your database.
MY RESULTS
/**
* Get all of the user for the feed. define in Feed Model
*/
public function feedUsers()
{
return $this->hasManyThrough(App\Models\Exam::class App\Models\User::class);
}
$feeds = App\Feed::all();
foreach($feeds as $feed){
echo $feed->feedUsers->count();
}

Laravel Pagination / Count - Not Accurate AFTER Filters Applied

When I retrieve the count for my results via laravel's paginator ($var->total() function) OR adding on ->count() to my DB query, it returns the incorrect count. Even when there is ZERO items on the page after a filter is applied, it still shows the incorrect number. This number stays the same, it doesn't change - it's the total # of rows in my parent model. However, again - it shouldn't show the total # of rows in my parent model - it should show the rows that are available after a filter is applied.
Filtered Query in Question:
$orders = list_formstack::with(['order' => function($query) use ($checktv, $dstart, $dend, $thestatus, $pharmacy, $searchid, $searchsubid, $searchrefill, $searchgrams, $filterrep) {
$query->whereNotNull('subid');
if($checktv == "No") {
$query->whereNull('intake');
}
elseif($checktv == "Yes") {
$query->whereNotNull('intake');
}
if($dstart && $dend)
$query->whereBetween('orders.created_at', [$dstart, $dend]);
if($thestatus)
$query->where('orders.status', $thestatus);
if($pharmacy)
$query->where('pharmacy', $pharmacy);
if($searchid)
$query->where('orders.id', $searchid);
if($searchsubid)
$query->where('orders.subid', $searchsubid);
if($searchrefill)
$query->where('refill', $searchrefill);
if($searchgrams)
$query->where('grams', $searchgrams);
if($filterrep)
$query->where('salesrep', $filterrep);
}])->where(function($query) use ($prodgroup, $state, $search, $columns, $searchpatient, $searchcarrier) {
if($state)
$query->where('state', $state);
if($searchpatient)
$query->where('first', 'like', $searchpatient)->orWhere('last', $searchpatient)->orWhere('phone', $searchpatient);
if($searchcarrier)
$query->where('insurancecarrier', 'like', $searchcarrier);
})->orderBy('created_at', 'desc')->paginate(50);
Parent Model:
class list_formstack extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'formstack';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['parentid','first','last','state','insurancecarrier'];
/**
* Get the form for an order
*/
public function order()
{
return $this->hasMany('App\list_orders', 'formid', 'id')->groupBy('id');
}
}
Child / Relationship Model:
class list_orders extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'orders';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['id','status','subid','formid','salesrep','billedamt','copayamt','completed','refillcount','gramsleft','billedamt','copayamt','salesrep','intake','created_at'];
public function formstack()
{
return $this->belongsTo('App\list_formstack', 'orderid');
}
}
So as you can see in my Eloquent query, I filter out results based on whether a user chooses that filter or not. So if I filter out "thestatus" to one specific status, it shows the correct results for only that status HOWEVER the count DOES NOT change.
I even tried to throw the results into a collection:
$count = collect($orders);
$count->count();
No luck - same incorrect number.
Any insight on this? What am I doing wrong? Thanks in advance!
EDIT: I should note that using Laravel's Query Builder (NO Eloquent ORM Models) results in the pagination count working correctly.
Parent Model DB Table
Parent Model Table Structure
Child Model DB Table
Child Model Table Structure

Laravel 5.2: Retrieving list of unique values in a many to many relationship

So I have a pension type:
class Type extends Base
{
use RecordStatusActiveTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'pn_pension_type';
/**
* The actual primary key for the model.
*
* #var string
*/
protected $primaryKey = 'pension_type_id';
// Belongs To -----
public function pensionClass() {
return $this->belongsTo('App\Models\Control\PensionClass', 'pension_class_id');
}
// Belongs To Many -----
public function qualifiers()
{
return $this->belongsToMany('App\Models\Pension\Qualifier', 'pn_pension_type_qualifier', 'type_id', 'qualifier_id');
}
}
And a pension Qualifier:
class Qualifier extends Base
{
use RecordStatusActiveTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'pn_pension_qualifier';
/**
* The attributes that should be casted to native types.
*
* #var array
*/
protected $casts = [
'rules' => 'array',
];
// Belongs To Many -----
public function types()
{
return $this->belongsToMany('App\Models\Pension\Type', 'pn_pension_type_qualifier', 'qualifier_id', 'type_id');
}
}
They have a many to many relationship with a table in between as normal. I would like to grab a set of pension types and find the unique Qualifiers between all of them. I COULD just foreach through each of their qualifiers and end up with a list of unique qualifiers but I was wondering if their was an "Eloquent" way.
Using whereHas will allow you to restrict the results based on the relation. In regards to what you want, you'd want to query from your Qualifiers like so.
Qualifiers::whereHas('pensions', function($query) use ($pensionIds) {
$query->whereIn('id', $pensionIds);
})->distinct()->get();
Adding on ->distinct() will then get you the unique entries.

Laravel Nested Eager Loading with Constraints

I have a Question Eloquent Model, a Course Eloquent Model, a University Eloquent Model. A One to Many relationship exists between the University and the Course. A Many to Many relationship exists between the Question and the Course. The Three models are shown below:
Question Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Question extends Model
{
/**
* The database table that the Model uses
* #var string
*/
protected $table = "questions";
/**
* The fields that are mass assignable
* #var array
*/
protected $fillable = ['title','body','images','quality_score','deactivated','creator_id','reviewer_id'];
/**
* Images is stored as serialized json.
* So we cast it to a PHP array.
* See: http://laravel.com/docs/5.1/eloquent-mutators#attribute-casting
*/
protected $casts = [
'images' => 'array',
];
public function courses(){
return $this->belongsToMany('App\Course');
}
}
Course Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Course extends Model
{
/**
* The database table used by the model
* #var string
*/
protected $table = "courses";
/**
* The fields that can be mass assigned
* #var array
*/
protected $fillable = ['name', 'instructor', 'acronym', 'university_id', 'creator_id', 'reviewer_id'];
/**
* There exists a many to one relationship between the Course and User
* This user is the creator of the course
*
* #method void
*
*/
public function creator(){
return $this->hasOne('App\User','creator_id');
}
/**
* There exists a many to one relationship between the Course and User
* This user is the reviewer of the course
* The reviewer of the Course will always be an admin
* If an Admin is the creator, then the reviewer is also the same admin
*
* #method void
*/
public function reviewer(){
return $this->hasOne('App\User','reviewer_id');
}
/**
* There exists a one to many relationship between the University and the Course
* This university is where the course is held
* Courses may float i.e. not be associated to any university
*
* #method void
*/
public function university(){
return $this->belongsTo('App\University');
}
/**
* This method is an accessor. It automatically changes the acronym to be all capitals
* regardless of how it is stored in the database.
* See: http://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
* #param $value (String from Database)
* #return string (Capitalized String)
*/
public function getAcronymAttribute($value){
return strtoupper($value);
}
}
University Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class University extends Model
{
/**
* The database table used by the model
* #var string
*/
protected $table = "universities";
/**
* The fields that can be mass assigned
* name = Name of the University (Example: University of Illinois at Urbana Champaign)
* acronym = Acronym of the University (Example: UIUC)
* creator_id = Id of User that created the University
* reviewer_id = Id of User that reviewed and approved the University
*
* Universities will not be displayed to users without admin role unless they have been reviewed.
*
* #var array
*/
protected $fillable = ['name','acronym','creator_id','reviewer_id'];
/**
* This method is an accessor. It automatically changes the acronym to be all capitals
* regardless of how it is stored in the database.
* See: http://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
* #param $value (String from Database)
* #return string (Capitalized String)
*/
public function getAcronymAttribute($value){
return strtoupper($value);
}
}
On my home page I am showing a list of questions and allow filters for course and university. The controller method is shown here:
public function getHome(Request $request){
/**
* Eager Load with Course and University
*/
$questions = Question::with('courses.university')->get();
/*
* Filter Questions to remove unwanted entries based on course id
*/
if($request->has('course_id') && $request->input('course_id') != -1){
$questions = $questions->filter(function($question) use ($request){
foreach($question->courses as $course){
if ($course->id == $request->input('course_id')){
return true;
}
}
});
}
/*
* Filter Questions to remove unwanted entries based on university id
*/
if($request->has('university_id') && $request->input('university_id') != -1){
$questions = $questions->filter(function($question) use ($request){
foreach($question->courses as $course){
if ($course->university->id == $request->input('university_id')){
return true;
}
}
});
}
/*
* Return the Welcome View with Pagination on the Questions Displayed
* List of Courses and List of Universities
*/
return view('welcome',[
'questions' => $questions,
'courses' => Course::all(),
'universities' => University::all(),
'selected_university_id' => $request->input('university_id',-1),
'selected_course_id' => $request->input('course_id',-1)
]);
}
What I am doing above is returning all the questions from the database and them combing them through to remove all the ones that don't match the filters. This is obviously quite inefficient. I want to use Nested Eager Loading with constraints except I am having a lot of trouble defining what the constraint would look like. Further, I want to use server side paginate to make the client experience better on lower speed internet connections.
Here is one of my attempts:
$questions = Question::with(['courses.university' => function($query) use ($request){
if($request->has('university_id') && $request->input('university_id') != -1) {
$query->where('id', $request->input('university_id'));
}
if($request->has('course_id') && $request->input('course_id') != -1){
$query->where('courses.id',$request->input('course_id'));
}
}])->paginate(10);
This works fine when I don't have any filters.
When I do have a university_id defined, I get the error: Trying to get property of non-object (View: /var/www/testing.com/resources/views/welcome.blade.php)
When I do have a course_id defined, I get the error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'courses.id' in 'where clause' (SQL: select * from universities where universities.id in (1, 2) and courses.id = 1)
I expected the error when I have course_id defined (because I took a blind stab at the first argument of the $query->where method.
I am looking for help in defining the Nested Eager Loading Constraints.
I found the solution in a medium article. The solution works for later versions of laravel as it uses whereHas.
// If you want to put the constraint on the second relation
$questions = Question::with(['courses' => function($query) use($request){
return $query->whereHas('university', function($inner_query) use($request){
return $inner_query->where('id', $request->input('university_id'));
});
}, 'courses.university'])->paginate(10);
For your case, a simple whereHas ought to do the trick.
$questions = Question::whereHas('courses', function($query) use ($request){
return $query->where('university_id', $request->input('university_id'));
})->with(['courses.university'])->paginate(10);
I would also recommend using when clauses to reduce the amount of code.
$questions = Question::when(($request->has('course_id') && $request->input('course_id') != -1), function ($query) use($request){
return $query->where('course_id', $request->input('course_id'));
})->when($request->has('university_id') && $request->input('university_id') != -1, function ($outer_query) use($request){
return $outer_query->whereHas('courses', function($query) use($request){
return $query->where('university_id', $request->input('university_id'));
})->with(['courses.university']);
})->with(['courses.university'])->paginate(10);

Categories