Select, where JSON Array contains - php

So in Laravel 5 there's the handy thing called JSON Where Clauses using MySQL's new ability to store and fetch JSON stored in a column:
User::where('meta->colors', 'red')->get()
would return all rows, where colors in the column meta would be set to red.
Now let's say colors is not a string, but an array containing multiple colors (colors => ['red', 'blue', 'green']).
What would be an efficient way to retrieve all rows, where colors contains e.g. the value red?

JSON_CONTAINS() does exactly what you're looking for:
JSON_CONTAINS(target, candidate[, path])
Indicates by returning 1 or 0 whether a given candidate JSON document is contained within a target JSON document, or—if a path argument was supplied—whether the candidate is found at a specific path within the target. — 12.16.3 Functions That Search JSON Values
Currently, Laravel's query builder does not provide a corresponding API. There's an open internals proposal for it though.
In the meantime, you can execute a raw query:
\DB::table('users')->whereRaw(
'JSON_CONTAINS(meta->"$.colors", \'["red"]\')'
)->get();
Which would return all users that have "red" in their meta->colors JSON field. Note that the -> operator requires MySQL 5.7.9+.
You can also call the whereRaw() directly on an Eloquent model.
Laravel 5.6
As of the 5.6 release, Laravel's query builder contains a new whereJsonContains method.

I think a way would be using the like operator:
User::where('meta->colors', 'like', '%"red"%')
However, this would only work if the values never contain the character " and the delimiters wouldn't change.

An update for this answer, according to MySQL or MariaDb, the correct syntax must be JSON_CONTAINS(#json, 'red', '$.colors'), and is necessary to use JSON_EXTRACT.
So them, the code inside Laravel (for version 5.5 or less).
Like say #Elwin, meta column must contains the following JSON: { "colors": ["red", "blue", "green"] }
User::whereRaw("JSON_CONTAINS(JSON_EXTRACT(meta, '$.colors'), '\"{$color}\"')")
Remember to use double quotes in value sentence.
JSON_CONTAINS(JSON_EXTRACT(meta, '$.colors'), '"red"')

The whereIn method verifies that a given column's value is contained within the given array.
Try this:
$colorArray = ['red', 'blue', 'green'];
$user = User::whereIn($meta->color, $colorArray)->get();
More about Laravel's whereIn.

Related

Why does Laravel store two different array syntaxes in databases and which one is correct?

I come from a Javascript and Ruby background and this is baffling me. Laravel can store two different array syntaxes in my DB depending on how I handle my array serialization. In my understanding, collect() creates a true Laravel array. Why then is it storing a serialized array? Furthermore, is the {'key':'value'}syntax still an array despite having no square brackets surrounding it? It looks to me like a standard object or a hash, but if I try to do toArray() on it, it recognizes that it's already an array and throws an error. What am I misunderstanding and what is correct here?
Given a form:
edit.blade:
<select class="form-control m-bootstrap-select m_selectpicker" name="temp">
<option value={{ json_encode(array("$key"=>"$cph"), JSON_FORCE_OBJECT) }}>
</select>
The following two controllers syntaxes yield different database insertions.
PageController.php:
$page->cph_default = collect($request->temp);
$page->save();
Laravel stores an array with the following syntax in my database: ["{\"11\":\"1100\"}"]
PageController.php
$page->cph_default = json_decode($request->temp, true);
$page->save();
Laravel stores an array with the following syntax in my database: {"19": "1900"}
A PHP array with the syntax ['key' => 'value'] is called an associative array, and acts like a hash. A JSON-encoded associative array will show up as an object in JSON syntax. Examples and more info on PHP.net
Laravel's collect() function is a convenience wrapper for creating a new Collection. A Collection is not really a "true Laravel array" so much as it is an object wrapper with some convenience methods for modifying the underlying array. Think of it like a scalar object.
In your form when generating the option value, the submitted form value ($request->temp) will be a JSON-encoded string. Literally the string '{"19": "1900"}'.
Calling collect($request->temp) does no modification to that submitted data. It's simply creating a new Collection (array), containing a single string item. If you were to call toArray() on the collection, you'd see something like this:
[
0 => '{"19": "1900"}'
]
Note that this is not an associative array, it is a numeric array with a zero-based index. This array is encoded as a JSON array, not as a hash object. Hence your first result.
Calling json_decode($request->temp) is turning the string back into an associative array (hash) before saving it via Eloquent. Eloquent then calls json_encode() again internally, turning it back into the same JSON as your form's option value.
If you were to decode the form value before creating the collection, the resulting database save would look identical. You'd just have the convenience of the Collection wrapper:
$page->cph_default = collect(json_decode($request->temp, true));
$page->save();
If you're treating the column as a JSON type, you should ensure the data passed to Eloquent is NOT already encoded, or you'll get the double encoding experienced in your first example.
No Matter What is.
First If you are stroring the array into database convert to JSON FORMAT
For eg
$variable = json_encode($request->controlname);
This is the right way to store array
Into database

convert value of a collection into a key in laravel

this my mysql query:
$tmp=almacen::select('nombre_empresa','oferta')->join('users','users.id','=','almacen.emp_id')->where('almacen.event_id','5')->get();
this returns several objects like these:
...
App\almacen {#1948
nombre_empresa: "Aux1",
oferta: "Serv_1234",
},
App\almacen {#1947
nombre_empresa: "Aux2",
oferta: "Serv 12345678",
},
...
i need to convert "nombre_empresa" in a key, for example
$tmp['Aux2']
this return:
"Serv 12345678"
Is it possible to do this in Laravel? or should I do it in another way?
Sure Laravel can handle that, check out the available collections methods. mapWithKeys is probably what you're looking for:
$mapped = $results->mapWithKeys(function ($item) {
return [$item['nombre_empresa'] => $item['oferta']];
});
Edit: mapWithKeys rather than map
https://laravel.com/docs/5.5/collections#method-mapwithkeys
You can use keyBy:
$tmp->keyBy('nombre_empresa');
You can chain it directly at the end of your query, after the get method.
But on this case you are only getting 2 fields from the database you can use pluck directly on the query builder:
$tmp=almacen::select('nombre_empresa','oferta')
->join('users','users.id','=','almacen.emp_id')
->where('almacen.event_id','5')
->pluck('oferta','nombre_empresa');
The second argument of pluck will be use for the key.
If none is given it will just use numeric keys: 0, 1,...
Of course, all solutions are valid, but the way I would do it which I think is also considered to be best practice in Laravel is by using pluck()
Documentation
Example:
$tmp=almacen::select('nombre_empresa','oferta')
->join('users','users.id','=','almacen.emp_id')
->where('almacen.event_id','5')
->get()
->pluck('oferta', 'nombre_empresa')
->toArray();
Do notice that pluck() is a new method introduced in version 5.2 to replace the before used lists()
Notice that I would transform toArray() but it is not a must, I would consider it since with such a simple collection I might not need the extra functionality making this variable take up less space.

Select condition within a hash column using Doctrine mongoDB ODM query builder

I have the following structure within a mongoDB collection:
{
"_id" : ObjectId("5301d337fa46346a048b4567"),
"delivery_attempts" : {
"0" : {
"live_feed_id" : 107,
"remaining_attempts" : 2,
"delivered" : false,
"determined_status" : null,
"date" : 1392628536
}
}
}
// > db.lead.find({}, {delivery_attempts:1}).pretty();
I'm trying to select any data from that collection where remaining_attempts are greater than 0 and a live_feed_id is equal to 107. Note that the "delivery_attempts" field is of a type hash.
I've tried using an addAnd within an elemMatch (not sure if this is the correct way to achieve this).
$qb = $this->dm->createQueryBuilder($this->getDocumentName());
$qb->expr()->field('delivery_attempts')
->elemMatch(
$qb->expr()
->field('remaining_attempts')->gt(0)
->addAnd($qb->expr()->field('live_feed_id')->equals(107))
);
I do appear to be getting the record detailed above. However, changing the greater than
test to 3
->field('remaining_attempts')->gt(3)
still returns the record (which is incorrect). Is there a way to achieve this?
EDIT: I've updated the delivery_attempts field type from a "Hash" to a "Collection". This shows the data being stored as an array rather than an object:
"delivery_attempts" : [
{
"live_feed_id" : 107,
"remaining_attempts" : 2,
"delivered" : false,
"determined_status" : null,
"date" : 1392648433
}
]
However, the original issue still applies.
You can use a dot notation to reference elements within a collection.
$qb->field('delivery_attempts.remaining_attempts')->gt(0)
->field('delivery_attempts.live_feed_id')->equals(107);
It works fine for me if I run the query on mongo.
db.testQ.find({"delivery_attempts.remaining_attempts" : {"$gt" : 0}, "delivery_attempts.live_feed_id" : 107}).pretty()
so it seems something wrong with your PHP query, I suggest running profiler to see which query is actually run against mongo
db.setProfilingLevel(2)
This will log all operation since you enable profiling. Then you can query the log to see which the actual queries
db.system.profile.find().pretty()
This might help you to find the culprit.
It sounds like your solved your first problem, which was using the Hash type mapping (instead for storing BSON objects, or associative arrays in PHP) instead of the Collection mapping (intended for real arrays); however, the query criteria in the answer you submitted still seems incorrect.
$qb->field('delivery_attempts.remaining_attempts')->gt(0)
->field('delivery_attempts.live_feed_id')->equals(107);
You said in your original question:
I'm trying to select any data from that collection where remaining_attempts are greater than 0 and a live_feed_id is equal to 107.
I assume you'd like that criteria to be satisfied by a single element within the delivery_attempts array. If that's correct, the criteria you specified above may match more than you expect, since delivery_attempts.remaining_attempts can refer to any element in the array, as can the live_feed_id criteria. You'll want to use $elemMatch to restrict the field criteria to a single array element.
I see you were using elemMatch() in your original question, but the syntax looked a bit odd. There should be no need to use addAnd() (i.e. an $and operator) unless you were attempting to apply two query operators to the same field name. Simply add extra field() calls to the same query expression you're using for the elemMatch() method. One example of this from ODM's test suite is QueryTest::testElemMatch(). You can also use the debug() method on the query to see the raw MongoDB query object created by ODM's query builder.

return group_concat data as array

I want to return values I retrieve from the db using group_concat as an array of data. Is it possible to do this in the mysql query? Or do I need to explode the data into an array?
GROUP_CONCAT(sh.hold_id) as holds
returns this
[holds] => 3,4
I want it to return:
[holds] => array(3,4)
As I said in my comment: you need to explode the data into an array, using php code like this:
$holds = explode(',', $holds);
because mysql has no concept of array-type for data.
This is possible since MySQL 5.7.22 using the JSON_ARRAYAGG() method
Read more: https://dev.mysql.com/doc/refman/5.7/en/aggregate-functions.html#function_json-arrayagg
Example:
SELECT JSON_ARRAYAGG(category.slug) as categories From categories
If you need to do it on the MySQL level, and then you may probably parse it to an object.
You can do the following
SELECT CONCAT("[", GROUP_CONCAT(category.name), "]") AS categories
From categories
MySQL has no concept of arrays. Therefore it is not able to return an array. It is up to your processing code (here the php scripts) to convert the concatenated notation into a php array.
It is possible to return mysql JSON array like so,
json_array(GROUP_CONCAT(sh.hold_id)) as holds
Refer docs for further info.

Mongodb like statement with array

I am trying to save some db action by compiling a looped bit of code with a single query, Before I was simply adding to the the like statements using a loop before firing off the query but i cant get the same idea going in Mongo, id appreciate any ideas....
I am basically trying to do a like, but with the value as an array
('app', replaces 'mongodb' down to my CI setup )
Here's how I was doing it pre mongofication:
foreach ($workids as $workid):
$this->ci->app->or_like('work',$workid) ;
endforeach;
$query = $this->ci->db->get("who_users");
$results = $query->result();
print_r($results);
and this is how I was hoping I could get it to work, but no joy here, that function is only designed to accept strings
$query = $this->ci->app->like('work',$workids,'.',TRUE,TRUE)->get("who_users");
print_r($query);
If anyone can think of a way any cunning methods I can get my returned array with a single call again it would be great I've not found any documentation on this sort of query, The only way i can think of is to loop over the query and push it into a new results array.... but that is really gonna hurt if my app scales up.
Are you using codeigniter-mongodb-library? Based on the existing or_like() documentation, it looks like CI wraps each match with % wildcards. The equivalent query in Mongo would be a series of regex matches in an $or clause:
db.who_users.find({
$or: [
{ work: /.*workIdA.*/ },
{ work: /.*workIdB.*/ },
...
]});
Unfortunately, this is going to be quite inefficient unless (1) the work field is indexed and (2) your regexes are anchored with some constant value (e.g. /^workId.*/). This is described in more detail in Mongo's regex documentation.
Based on your comments to the OP, it looks like you're storing multiple ID's in the work field as a comma-delimited string. To take advantage of Mongo's schema, you should model this as an array of strings. Thereafter, when you query on the work field, Mongo will consider all values in the array (documented discussed here).
db.who_users.find({
work: "workIdA"
});
This query would match a record whose work value was ["workIdA", "workIdB"]. And if we need to search for one of a set of ID's (taking this back to your OR query), we can extend this example with the $in operator:
db.who_users.find({
work: { $in: ["workIdA", "workIdB", ...] }
});
If that meets your needs, be sure to index the work field as well.

Categories