Laravel transaction saves data into DB even when it's rollbacked - laravel

I send a request, trying to save multiple documents at once.
Here is my code
DB::beginTransaction();
try {
foreach ($request->documents as $index => $documentInfo) {
// Check
if (// some statement) {
$documentExists = Document::where([
// some checks
])
->exists();
if ($documentExists) {
throw new \Exception("Error Processing Request", 1);
}
}
// Assign document properties
$document->save();
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
if ($request->ajax()) {
return response()->json([
'success' => false,
'document' => $index,
'message' => '// message',
]);
}
return redirect()->back()->with('error', '// message');
}
The thing that happens is that I check every document for its own unique values. If two documents with same values try to get saved I want to return an error.
I tried uploading two documents with the same values, the error is returned on the second document and the transaction must fail, but the first document gets saved into the database.
I don't want this, if there is an error I don't want any documents saved into the DB.

Related

Transaction in Laravel rolled back incompletely

I have an array of Organization model. Each Organization contains an array of User model.
There is a many-to-many relationship between them using the OrganizationUser model.
If I have problems inserting an Organization, an User, or a relationship between them, I want to roll everything back.
But I also use "try catch" blocks to see at which stage the error occurred.
To test this, I explicitly made a mistake in the OrganizationUser relationship object. I'm using an User id that can't be in the Organization.
This rolls back the Organization, relationships, and first User, but all other Users are added to the database.
DB::beginTransaction();
$org->save();
foreach ($org->user_list as $user) {
try {
$user->save();
} catch (\Exception $exception) {
DB::rollback();
Log::error('Error user insert : '. $user->inn .' '. $exception->getMessage());
}
try {
$orgs_user = OrganizationUser::create([
'user_id' => 80,
'org_id' => $org->id,
]);
} catch (\Exception $exception) {
DB::rollback();
Log::error('Insert user_org relationship : ' . $exception->getMessage());
}
}
When I didn't use the "try catch" block it worked well
When you call DB::rollBack() (by the way it's rollBack with a capital B), that ends the transaction. The rest of the queries will execute as normal.
The safest way to use transactions is to put the entire thing in a single try catch. The \Exception itself should contain enough information to know what caused it in its message or stack trace.
try {
DB::beginTransaction();
$org->save();
foreach ($org->user_list as $user) {
$user->save();
$orgs_user = OrganizationUser::create([
'user_id' => 80,
'org_id' => $org->id,
]);
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
}
It is a bit verbose, but you can also use the DB::transaction() method.
DB::transaction(function () use ($org) {
$org->save();
foreach ($org->user_list as $user) {
$user->save();
$orgs_user = OrganizationUser::create([
'user_id' => 80,
'org_id' => $org->id,
]);
}
});
DB::rollback does not stop the execution of the code.
If you are in a catch block the transaction will be rolled back but the next block (as well as the rest of the iteration) will continue.
Use a break; after the rollback to break out of the loop completely like below:
DB::beginTransaction();
$org->save();
foreach ($org->user_list as $user) {
try {
$user->save();
} catch (\Exception $exception) {
DB::rollback();
Log::error('Error user insert : '. $user->inn .' '. $exception->getMessage());
break;
}
try {
$orgs_user = OrganizationUser::create([
'user_id' => 80,
'org_id' => $org->id,
]);
} catch (\Exception $exception) {
DB::rollback();
Log::error('Insert user_org relationship : ' . $exception->getMessage());
break;
}
}
You can also return; or just exit() depending on how you want to handle the error

display message for database insert operation

Is there any mechanism that we can know that the data sent from app is successfully inserted to database, as in stored procedure, we use output parameter.
How can we be sure that the data is inserted into database and in case if the data insertion operation is failed, how to catch the error cause and display in a user friendly way?
// controller code
public function store(Request $request)
{
$validatedInput = $request -> validate([
'NBookName' => 'required|string|min:3|max:100',
'NBookId' => 'required|string|min:2|max:10|unique:books,BookID', // unique:table_name,column_name
'NBookUnitPrice' => 'required|max:5|'
]);
$book = new Book;
$book -> BookName = $request->input('NBookName');
$book -> BookID = $request->input('NBookId');
$book -> BookUnitPrice = $request->input('NBookUnitPrice');
$book->save();
return view('pages.about');
}
The Eloquent method save returns either true or false, thus checking the return of the save will let you know if the operation was successful.
You can alternatively use the method saveOrFail which will also return true / false, but may also give a Throwable
From the documentation: https://laravel.com/api/5.3/Illuminate/Database/Eloquent/Model.html#method_save
Examples:
if( $book->save() ){
return response()->json($book, 200);
} else {
return response()->json(['error' => 'Could not save'], 400);
}
or
try {
$book->saveOrFail();
return response()->json($book, 200);
} catch( \Exception $e ){
return response()->json(['error' => $e->getMessage()], 400);
}
save() will return a boolean, saved or not saved. So you can either do Like:
$saved = $book->save();
if(!$saved)
{
App::abort(500, 'Error');//Or return any message to view
}
For more details:check-if-laravel-model-got-saved-or-query-got-executed
I'd start off by separating the layer, I don't like watching validations, Eloquent queries and view related output in the same place.
Before reading the answer below, take a look at
https://laravel.com/docs/5.6/validation#form-request-validation
I'd make middleware to authenticate and authorize users, then use formrequests to validate the input data, and by the time it reaches the controller, the user is authenticated, authorized, data validated and all it's needed is call someone that is responsible for the insert operation and return something to the UI.
"How can we be sure that the data is inserted into database and in
case if the data insertion operation is failed"
Generally, a try catch will do:
public function store(Request $request)
{
try {
$validatedInput = $request -> validate([
'NBookName' => 'required|string|min:3|max:100',
'NBookId' => 'required|string|min:2|max:10|unique:books,BookID', // unique:table_name,column_name
'NBookUnitPrice' => 'required|max:5|'
]);
$book = new Book;
$book -> BookName = $request->input('NBookName');
$book -> BookID = $request->input('NBookId');
$book -> BookUnitPrice = $request->input('NBookUnitPrice');
$book->save();
} catch (\Exception $e) { 
// return your pretty response
// If the operation saving to the database fails, you'll get the information here on whatever it is about,
// as the generic exception catchs it. It should catch the stored procedure output aswell
}
return view('pages.about');
}
If you plan on having a series of insertions and updates/deletes and you don't want them to break logic (insert some and stop at inserting when 1 is wrong), you can use DB; and use DB::beginTransaction(), DB::commit() and DB::rollback() on your code
https://laravel.com/docs/5.6/database

How to handle native exception in Laravel?

For example, I use:
return User::findOrFail($id);
When row does not exist with $id I get exception.
How I can return this exception in Json response? It returns HTML Laravel page now.
I need something like as:
{"error", "No query results for model"}
From their documentation:
Sometimes you may wish to throw an exception if a model is not found. This is particularly useful in routes or controllers. The findOrFail and firstOrFail methods will retrieve the first result of the query. However, if no result is found, a Illuminate\Database\Eloquent\ModelNotFoundException will be thrown.
So, you can either catch that exception, or go with the simple Find method. It will return false if not found, so you can handle it accordingly.
return User::find($id);
UPDATE:
Option 1:
try {
return User::findOrFail($id);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
return json_encode(['error' => 'No query results for model']);
}
Option 2:
$user = User::find($id);
if($user) {
return $user;
}
return json_encode(['error' => 'No query results for model']);
You can handle various types of exceptions, that exception can be handle with a ModelNotFoundException in this case
try{
$user = User::findOrFail($id);
}catch(ModelNotFoundException $e){
return response()->json(['error' => 'User not found'], 400);
}
And there's another way to catch various types of exceptions in the Handler.php located on app/Exceptions/Handler.php there you can catch the exceptions and return whatever you want inside the render function.
For example insede that function you can add this before the return parent::render($request, $e):
if($e instanceof ModelNotFoundException)
{
return new Response(['message' => 'We haven\'t find any data'], 204);
}
You should look in render method of Handler file. You can test exception class here and depending on it return different response in Json format

Return Date To Client From Within DB::Transaction Closure()

I am executing several database saves within my db transaction closure:
DB::transaction(function() {
...
});
However what i want to do now is when a transaction fails instead of throwing an exception i want to return a custom JSON object straight t the client, if the transactions succeeds i want to do the same thing.
This is my object:
return [
'code' => '',
'message' => '',
'data' => []
];
How would i return the above to the client from within the closure?
Instead of DB::transaction Closure you can use DB::beginTransaction, DB::commit and DB::rollback methods in order to have more control in code, you can wrap your DB actions like this :
DB::beginTransaction();
try {
DB::insert(...);
DB::insert(...);
DB::insert(...);
//If everything is ok we commit
DB::commit();
return response()->json(["status" => "success"])
} catch (Exception $e) {
//something goes wrong, we rollback
DB::rollback();
return response()->json(["error" => "Some error"]);
}
You can research more in database transaction docs

Validating an API request in Laravel 5.1

I am creating a simple api using laravel 5.1 to create a row in a table and return the id of the newly created row.
I am able to do so, but when it comes to validating I am not sure what to do.
e.g.
transaction_request table
id|order_id|customer_id|amount
validation rules for order_id and customer_id is just required
my uri to request this is http://localhost:8000/api/v1/transactionRequests?order_id=123&customer_id=&amount=3300
note that customer_id is not defined. user is returned with following json.
{
"error": {
"code": "GEN-WRONG-ARGS",
"http_code": 400,
"message": "{\"customer_id\":[\"The customer_id field is required.\"]}"
}
}
See the message: with those '\', How do I fix that. I know, it is because, I use following validation method in my controller that throws the exception
public function validateRequestOrFail($request, array $rules, $messages = [], $customAttributes = [])
{
$validator = $this->getValidationFactory()->make($request->all(), $rules, $messages, $customAttributes);
if ($validator->fails())
{
throw new Exception($validator->messages());
}
}
and I use catch to deal with it as following
try {
if(sizeof(TransactionRequest::$rules) > 0)
$this->validateRequestOrFail($request, TransactionRequest::$rules);
} catch (Exception $e) {
return $this->response->errorWrongArgs($e->getMessage());
}
errorWrongArgs is defined as following
public function errorWrongArgs($message = 'Wrong Arguments')
{
return $this->setStatusCode(400)->withError($message, self::CODE_WRONG_ARGS);
}
public function withError($message, $errorCode)
{
return $this->withArray([
'error' => [
'code' => $errorCode,
'http_code' => $this->statusCode,
'message' => $message
]
]);
}
I want the response to be clean as following (btw i am using ellipsesynergie/api-response library and not the default response class, because I am using chrisbjr/api-guard)
{
"error": {
"code": "GEN-WRONG-ARGS",
"http_code": 400,
"message": {
"customer_id": "The customer_id field is required."
}
}
}
Yes, the problem was in my understanding of what $validator->messages() returns, it returns a MessageBag object which is more like a Json object. Where as Exception() expects a String message. Thus it treats the Json as a String by escaping quote character as \". Solution to this is I pass the Json string to Exception method but after I catch it I convert the Json String to an array using json_decode, which is then taken by my response class.
throw new Exception($validator->messages());
return $this->response->errorWrongArgs(json_decode($e->getMessage(),true));

Resources