Laravel lockForUpdate and invoice number generation - mysql

I have a multi tenanted app where each user can generate invoices / credits.
I would like to generate an auto incrementing number by each user invoice or user credit while still keeping the id column used in Laravel relationships.
I want to auto increment the number column based on user_id and type.
My Invoices table:
id number biller_id type ...
1 1 1 invoice
2 2 1 invoice
3 1 1 credit
4 1 2 invoice
So I end up with:
Biller 1 will have invoice numbers 1, 2, 3... and credit numbers 1, 2, 3...
And same for each user.
I have not been successful in locking the table for each creation event so that other transactions do not access their last invoice number, so I end up with repeated invoice / credit numbers for each user. I can see this from running my seed.
I have tried the following without success:
In the boot method of my Invoice class:
/**
* Boot Method of class
*/
protected static function boot()
{
/**
* When creating the invoice, create its "number"
*/
static::creating(function ($obj) {
$lastTransaction = Invoice::where('biller_id', $obj->biller_id)
->where('type', $obj->biller_id)
->orderBy('created_at', 'DESC')
->lockForUpdate()
->first();
$nextId = 1;
if ($lastTransaction) {
$nextId = $lastTransaction->number + 1;
}
$obj->number = $nextId;
});
parent::boot();
}
Also as a static createWithlock method (and I remove the lockForUpdate from the creating method):
public static function createWithLock($invoiceData = null)
{
if (! $invoiceData) {
return [
'type' => 'error',
'value' => 'No invoice data supplied!',
];
}
DB::beginTransaction();
try {
// Lock Invoices table to ensure correct creation of invoice number
DB::select(DB::raw('LOCK TABLES invoices WRITE'));
self::create($invoiceData);
DB::select(DB::raw('UNLOCK TABLES'));
} catch (\Exception $e) {
DB::rollBack();
return [
'type' => 'error',
'value' => $e->getMessage(),
];
}
DB::commit();
return [
'type' => 'success',
'value' => 'Invoice created successfully.',
];
}
I get repeated number values for the combinations. Any suggestions appreciated on how to lock the table during creation process to stop duplicates.

Your "for update" lock must be run inside a transaction. I cannot see from your code, that it is. So both the ->lockForUpdate() and the creation of the new invoice must be inside the same transaction.
I often find that Atomic Locks are a lot easier to use, and will take care of more actions (including if you calculate an ID in your code that is parsed back to the database layer after some small delay).
Atomic Locks are also easily added as a middleware (that can later easily be added to more endpoints, that locks the same object).

Related

Laravel: resolving the n + 1 problem that has raw query or custom function

I am working on a Laravel application. I am having a bit of a problem with querying the data.
This is my database schema.
I have a users table with the following fields.
users
- id
- name
- email
- password
Then I have the transactions table with the following fields.
transactions
- id
- amount
- user_id
- type
- created_at
As you can see transactions table has the user_id as a foreign key to the users table.
This is the User model.
class User extends Model
{
public function transactions()
{
return $this->hasMany(Transaction::class, 'user_id');
}
}
This is the Transaction model
class Transaction extends Model
{
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
Now, I am trying to get a user's transactions with additional column and conditional checking. Normally, if I wanted to get a user's transactions, I would do something like this.
$user->transactions()->get()
But I want to get an extra column for each row, for each row, I would like to get the sum of the amount of transactions until the created_at of transactions of the current row.
If I have to write a function for the single record, it would be something like this.
function totalAmountUntil($user, $date)
{
return $user->transactions->where('transactions.created_at', '<=', $date)->sum('transactions.amount');
}
I need to apply that to each row in the query based on the created_at of the transactions also resolving the n+1 problem. How can I do that?
You can use withCount for this :
User::withCount([
'transactions as transactions_amount_sum' => function($query) use ($date){
$query->where('created_at', '<=', $date)
->select(DB::raw('SUM(amount)'));
}
])->get();

Accessing to the `latest` record stored in DB table right after recording it

In Laravel After recording last row to a DB table, can I safely access same recorded data right after recording it by calling latest() queries? Because transactions by other users may occur at the same time, and it may not really be the last record anymore?
Edit:
For example:
Public function StoreNotif($data){
auth()->user()->Notif()->create(store $data here..)
}
Public function SendNotif(){
$data="123";
$this->StoreNotif($data)
event(new Notification(stored Notif instance?));
}
No, you cannot rely on the database to return the record from your current script.
The ->latest() method will always sort the records with the most recent created_at date first.
https://laravel.com/docs/6.x/queries#ordering-grouping-limit-and-offset
But you haven't provided any code or explanation as to why this is a concern. If you just created a new record, why do you need to query it again? You should already have access to an instance of the model.
EDIT: I've made a few edits to demonstrate how you would pass the model from a controller to an event as referenced in the comments. Please post your code if you want more specific help.
SomeController.php
function store()
{
$model = Model::create([
'some_data' => 1
]);
// fire an event with the newly created model
event(new SomeEvent($model));
dd($model);
}
------------------------
Model {
// ...
attributes: [
'id' => 101,
'some_data' => 1
'created_at' => '2019-10-06 12:48:01',
'updated_at' => '2019-10-06 12:48:01',
]
// ...
}
SomeEvent.php
<?php
namespace App\Events;
use App\Model;
use Illuminate\Queue\SerializesModels;
class SomeEvent
{
use SerializesModels;
public $model;
public function __construct(Model $model)
{
$this->model = $model;
// ...
}
}
EDIT: Per your newly added code, you just need to pass the new model back to the original method. You could do something like this.
Public function StoreNotif($data)
{
// add a return statement
return auth()->user()->Notif()->create(store $data here..);
}
Public function SendNotif()
{
$data="123";
// store the returned data to a variable
$model = $this->StoreNotif($data);
// call the event with the model instance
event(new Notification(model));
}
I'm not sure what 'latest' is but I do know that MySQL uses SELECT LAST_INSERT_ID as the query to get the 'per-connection' id of the last inserted item. Under the covers it's using mysql_insert_id so if you are in a language that supports it, you could use that too.

Relationships between tables in laravel using backpack package

I am using backpack CRUD package to create my website project in laravel 5.2
I want to establish a relationship between two tables. First table is called customer and second table is called transaction. Each customer has many transaction(1:N relationship).
Customer table record:
ID Name
123456 xyz
Transaction table record:
ID CustomerID
101010 123456
I know that I have to specify the relation in the customer model. But, how can I display the result of the relationship in CRUD ?
You should have relationships on both the Transaction and the Customer models, so you can do $customer->transactions and $transaction->customer:
class Customer extends Model
{
/**
* Get the comments for the blog post.
*/
public function transactions()
{
return $this->hasMany('App\Transactions', 'CustomerID', 'ID');
}
}
and
class Transaction extends Model
{
/**
* Get the comments for the blog post.
*/
public function customer()
{
return $this->belongsTo('App\Customer', 'CustomerID', 'ID');
}
}
Spend some time in the Eloquent Relationships Documentation. It's really important to understand them if you want to be a Laravel developer.
In order to display the relationship in the CRUD, you can then use Backpack's select column type to display it in the table view and select or select2 field types to display it in the add/edit views. Read the CRUD Example Entity to better understand how that works.
First of all when you are creating migrations for both tables, table which contain Foreign Key (FK) must have field like this:
public function up(){
$table->increments('id');
$table->integer('customerID')->unsigned();
}
After that you are need to call next command into console
php artisan migrate
Next is going next commands:
php arisan backpack:crud customers
php arisan backpack:crud transactions
After that you need to define functions in models which returns values from other tables. Customer models need to have next function
public function transactions(){
return $this->hasMany('Transaction');
}
Transaction model must have next function
public function customer() {
return $this->belongsTo('Customer');
}
Next you must add CRUD field in Customer controller to display
transactions in select box.
$this->crud->addField([
'label' => 'Transactions', // Label for HTML form field
'type' => 'select2', // HTML element which displaying transactions
'name' => 'customerID', // Table column which is FK for Customer table
'entity'=> 'customer', // Function (method) in Customer model which return transactions
'attribute' => 'ID', // Column which user see in select box
'model' => 'Transaction' // Model which contain FK
]);
Hope this helps :)
After you built onetomany relationship with transaction, you can get the results.
$customer=Customer::where(['id'=>'123456'])->with('transaction')
->first();
print_r($customer->Name); // gives the customer name
foreach($customer->transaction as $cid)
{
print_r($cid->CustomerID); // gives the customer id
}
Laravel Relationships Documentation is always helpful. Go through it.

How to update a pivot table using Eloquent in laravel 5

I am new to laravel. I am working on a laravel 5 app and I am stuck here. I have 2 models as such:
class Message extends Eloquent{
public function user()
{
return $this->belongsTo('App\User', 'from');
}
public function users()
{
return $this->belongsToMany('App\User')->withPivot('status');
}
}
class User extends Eloquent {
public function messages()
{
return $this->hasMany('App\Message', 'from');
}
public function receive_messages() {
return $this->belongsToMany('App\Message')->withPivot('status');
}
}
There exist a many-to-many relationship between Message and User giving me a pivot table as such:
Table Name: message_user
Colums:
message_id
user_id
status
I have an SQL query as such:
update message_user
set status = 1
where user_id = 4 and message_id in (select id from messages where message_id = 123)
How can I translate this query to the laravel equivalent?
The code below solved my problem:
$messages = Message::where('message_id', $id)->get();
foreach($messages as $message)
$message->users()->updateExistingPivot($user, array('status' => 1), false);
You may use one of these two functions, sync() attach() and the difference in a nutshell is that Sync will get array as its first argument and sync it with pivot table (remove and add the passed keys in your array) which means if you got 3,2,1 as valued within your junction table, and passed sync with values of, 3,4,2, sync automatically will remove value 1 and add the value 4 for you. where Attach will take single ID value
The GIST: if you want to add extra values to your junction table, pass it as the second argument to sync() like so:
$message = Messages::find(123);
$user = User::find(4);
// using attach() for single message
$user->message()->attach($message->id, [
'status' => 1
]);
$message2 = Messages::find(456); // for testing
// using sync() for multiple messages
$user->message()->sync([
$message->id => [
'status' => 1
],
$message2->id => [
'status' => 1
],
]);
Here is a small example of how to update the pivot table column
$query = Classes::query();
$query = $query->with('trainees')
->where('user_id', Auth::id())
->find($input['classId']);
foreach ($query->trainees as $trainee) {
$trainee->pivot->status = 1 //your column;
$trainee->pivot->save();
}
Note: make sure your relation data must in an array
Hope its help you :)
happy coding
Laravel 5.8
First, allow your pivot columns to be searchable by chaining the withPivot method to your belongsToMany
Copied from my own code to save time
// I have 3 columns in my Pivot table which I use in a many-to-many and one-to-many-through scenarios
$task = $user->goalobjectives()->where(['goal_objective_id'=>$goal_objective_id,'goal_obj_add_id'=>$goal_obj_add_id])->first(); //get the first record
$task->pivot->goal_objective_id = $new; //change your col to a new value
$task->pivot->save(); //save
The caveat is that your pivot table needs to have a primary 'id' key.
If you don't want that then you can try the following:
$tasks=$user->posts()->where(['posts_id'=>$posts_id,'expires'=>true])->get()->pluck('id'); // get a collection of your pivot table data tied to this user
$key=join(",",array_keys($tasks->toArray(),$valueYouWantToRemove));
$tasks->splice($key,1,$newValueYouWantToInsert);
$c = array_fill(0,$tasks->count(),['expires'=>true]); //make an array containing your pivot data
$newArray=$tasks->combine($c) //combine the 2 arrays as keys and values
$user->posts()->sync($newArray); //your pivot table now contains only the values you want
4th July Update Update to above snippet.
//Ideally, you should do a check see if this user is new
//and if he already has data saved in the junction table
//or are we working with a brand new user
$count = $user->goalobjectives->where('pivot.goal_obj_add_id',$request->record)->count();
//if true, we retrieve all the ids in the junction table
//where the additional pivot column matches that which we want to update
if($count) {
$ids = $user->goalobjectives->where('pivot.goal_obj_add_id',$request->record)->pluck('id');
//convert to array
$exists = $ids->toArray();
//if user exists and both saved and input data are exactly the same
//there is no need
//to update and we redirect user back
if(array_sum($inputArray) == array_sum($exists)) {
//redirect user back
}
//else we update junction table with a private function
//called 'attachToUser'
$res = $this->attachToUser($user, $inputArray, $ids, $request->record);
}//end if
elseif(!$count) {
//we are working with a new user
//we build an array. The third pivot column must have equal rows as
//user input array
$fill = array_fill(0,count($inputArray),['goal_obj_add_id'=>$request->record]);
//combine third pivot column with user input
$new = array_combine($inputArray,$fill);
//junction table updated with 'user_id','goal_objective_id','goal_obj_add_id'
$res = $user->goalobjectives()->attach($new);
//redirect user if success
}
//our private function which takes care of updating the pivot table
private function attachToUser(User $user, $userData, $storedData, $record) {
//find the saved data which must not be deleted using intersect method
$intersect = $storedData->intersect($userData);
if($intersect->count()) {
//we reject any data from the user input that already exists in the database
$extra = collect($userData)->reject(function($value,$key)use($intersect){
return in_array($value,$intersect->toArray());
});
//merge the old and new data
$merge = $intersect->merge($extra);
//same as above we build a new input array
$recArray = array_fill(0,$merge->count(),['goal_obj_add_id'=>$record]);
//same as above, combine them and form a new array
$new = $merge->combine($recArray);
//our new array now contains old data that was originally saved
//so we must remove old data linked to this user
// and the pivot record to prevent duplicates
$storedArray = $storedData->toArray();
$user->goalobjectives()->wherePivot('goal_obj_add_id',$record)->detach($storedArray);
//this will save the new array without detaching
//other data previously saved by this user
$res = $user->goalobjectives()->wherePivot('goal_obj_add_id',$record)->syncWithoutDetaching($new);
}//end if
//we are not working with a new user
//but input array is totally different from saved data
//meaning its new data
elseif(!$intersect->count()) {
$recArray = array_fill(0,count($userData),['goal_obj_add_id'=>$record]);
$new = $storedData->combine($recArray);
$res = $user->goalobjectives()->wherePivot('goal_obj_add_id',$record)->syncWithoutDetaching($new);
}
//none of the above we return false
return !!$res;
}//end attachToUser function
This will work for pivot table which doesn't have a primary auto increment id. without a auto increment id, user cannot update,insert,delete any row in the pivot table by accessing it directly.
For Updating your pivot table you can use updateExistingPivot method.

cakephp, validation checking a value in a separate table

I'm trying to create a site where, when we send an invoice, the relationship between sender and receiver must be active. This is done in a separate table and requires a boolean value of 1 in the other table.
The invoice will be saved in the table invoices with the following structure:
id
sender
receiver
amount
date due
the relationship table has the following structure:
id
partyone
partytwo
active
partyone and partytwo reference the users table.
The users table has the following structure:
id
username
password
What I'm trying to see is, if the relationships table contains partyone and partytwo AND the boolean is equal to one.
This is the function I have in my invoice table to add a new invoice. It can't be added to the database before a user is in an active relationship with that user.
public function addinvoice(){
if($this->request->is('post')) {
$this->Invoice->create();
if (0 === $this->relationship->find('count',array('conditions'=>array('activate'=>1,'relationship'=> $relationship)))) {
$this->Session->setFlash('Sorry, you do not have an active relationship.');
$this->redirect($this->referer());
}
if ($this->Invoice->save($this->request->data)){
$this->Session->setFlash('The invoice has been saved');
}
} else {
$this->Session->setFlash('The invoice could not be saved. Please, try again.');
}
}
This is the simple answer to your question.
Modify your find like this:
$relationship = $this->relationship->find('count', array(
'conditions' => array(
'partyone' => $user_one_id,
'partytwo' => $user_two_id,
),
));
if (!$relationship) {
//Error here
} else {
//Save here
}