Perl: DBIx::Class Beginner - Subsetting Relationships and Prefetching - sql

Okay, I'm new to DBIx::Class. I have a one-to-many relationship set up, like so:
User -> has_many -> Addresses
Okay, good. I can do a query, and call it prefetching JOINed tables, like so:
Foo::DBIC->storage->debug(1); # output SQL to STDOUT
my $user = Foo::DBIC->resultset('Users')->search({}, {
prefetch => [ 'addresses' ],
join => [ 'addresses' ],
rows => 1
})->single;
for my $address ($user->addresses->all) {
say $address->zip_code;
}
Two tables, one SQL query (verified via debug). All is well.
Now, however, let's say I want to write an overload method or two in Foo::DBIC::Result::Users that returns a subset of addresses, based on certain criteria. Here's what I've added to the Users class:
sub home_addresses {
my $self = shift;
return $self->search_related('addresses', { address_type => 'home' });
}
sub business_addresses {
my $self = shift;
return $self->search_related('addresses', { address_type => 'business' });
}
I can call these overloads like so, and they work:
for my $address ($user->home_addresses->all) {
say $address->zip_code;
}
However, this ignores the fact that I've prefetched my join, and it performs ADDITIONAL QUERIES (as if I've not prefetched and joined anything).
So, my question is this: how do I define an overload method that returns a subset of a related table, but uses the already prefetched join? (just appending a WHERE clause to the prefetch)...
My problem is that if I have a lot of the overloaded methods returning related table subsets, my query count can blow up; especially if I'm calling them from within a loop.
I have reasons for doing this that are, of course, ugly. My real life schema is a lot, lot, lot messier than Users and Addresses, and I'm trying to abstract away ugly as best I can.
Thanks!

something like this for home_addresses might work:
sub home_addresses {
my $self = shift;
my $addresses = $self->addresses;
my $home_addresses;
while (my $row = $addresses->next()) {
push #$home_addresses, $row if $row->address_type() eq 'home';
}
my $home_rs = $addresses->result_source->resultset;
$home_rs->set_cache( $home_addresses );
$home_rs;
}
Alternatively, if there a lot of address types something like this:
sub addresses_by_type {
my $self = shift;
my $addresses = $self->addresses;
my $type;
my $rs_type;
while (my $row = $addresses->next()) {
push #{$type->{"".$row->address_type}},
$row;
}
for (keys %$type) {
my $new_rs = $addresses->result_source->resultset;
$new_rs->set_cache( $type->{$_} );
$rs_type->{$_} = $new_rs
}
return $rs_type
}
which you could access the 'home' addresses from like this:
while (my $r = $user->next) {
use Data::Dumper;
local $Data::Dumper::Maxdepth = 2;
print $r->username,"\n";
my $d = $r->addresses_by_type();
my $a = $d->{home};
while (defined $a and my $ar = $a->next) {
print $ar->address,"\n";
}
}

Could you try something like this:
sub home_addresses {
my $self = shift;
my $return = [];
my #addresses = $self->addresses->all();
foreach my $row (#addresses) {
push #$return, $row if $row->address_type() eq 'home';
}
return $return;
}

Related

Is there a way to wait until a function is finished in React Native?

I'm trying to get information (true/false) from AsyncStorage in a function and create a string which is importent to fetch data in the next step. My problem is, the function is not finished until the string is required.
I tried many solutions from the internet like async function and await getItem or .done() or .then(), but none worked out for me.
//_getFetchData()
AsyncStorage.getAllKeys().then((result) => { //get all stored Keys
valuelength = result.length;
if (valuelength !== 0) {
for (let i = 0; i < valuelength; i++) {
if (result[i].includes("not") == false) { //get Keys without not
AsyncStorage.getItem(result[i]).then((resultvalue) => {
if (resultvalue === 'true') {
if (this.state.firstValue) {
this.state.channels = this.state.channels + "channel_id" + result[i];
console.log("channel: " + this.state.channels);
}
else {
this.state.channels = this.state.channels + "channel" + result[i];
}
}
});
}
return this.state.channels;
_fetchData() {
var channel = this._getFetchData();
console.log("channel required: " + channel);
}
The current behaviour is that the console displays first "channel required: " than "channel: channel_id0".
Aspects in your question are unclear:
You don't say when this.state.firstValue is set, and how that relates to what you are trying to accomplish.
You have a for-loop where you could be setting the same value multiple times.
You mutate the state rather than set it. This is not good, see this SO question for more on that.
There are somethings we can do to make your code easier to understand. Below I will show a possible refactor. Explaining what I am doing at each step. I am using async/await because it can lead to much tidier and easier to read code, rather than using promises where you can get lost in callbacks.
Get all the keys from AsyncStorage
Make sure that there is a value for all the keys.
Filter the keys so that we only include the ones that do not contain the string 'not'.
Use a Promise.all, this part is important as it basically gets all the values for each of the keys that we just found and puts them into an array called items
Each object in the items array has a key and a value property.
We then filter the items so that only the ones with a item.value === 'true' remain.
We then filter the items so that only the ones with a item.value !== 'true' remain. (this may be optional it is really dependent on what you want to do)
What do we return? You need to add that part.
Here is the refactor:
_getFetchData = async () => {
let allKeys = await AsyncStorage.getAllKeys(); // 1
if (allKeys.length) { // 2
let filteredKeys = allKeys.filter(key => !key.includes('not')); // 3
let items = await Promise.all(filteredKeys.map(async key => { // 4
let value = await AsyncStorage.getItem(key);
return { key, value }; // 5
}))
let filteredTrueItems = items.filter(item => items.value === 'true'); // 6
let filteredFalseItems = items.filter(item => items.value !== 'true'); // 7
// now you have two arrays one with the items that have the true values
// and one with the items that have the false values
// at this points you can decide what to return as it is not
// that clear from your question
// return the value that your want // 8
} else {
// return your default value if there are no keys // 8
}
}
You would call this function as follows:
_fetchData = async () => {
let channel = await this._getFetchData();
console.log("channel required: " + channel);
}
Although the above will work, it will not currently return a value as you haven't made it clear which value you wish to return. I would suggest you build upon the code that I have written here and update it so that it returns the values that you want.
Further reading
For further reading I would suggest these awesome articles by Michael Chan that discuss state
https://medium.learnreact.com/setstate-is-asynchronous-52ead919a3f0
https://medium.learnreact.com/setstate-takes-a-callback-1f71ad5d2296
https://medium.learnreact.com/setstate-takes-a-function-56eb940f84b6
I would also suggest taking some time to read up about async/await and promises
https://medium.com/#bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8
And finally this article and SO question on Promise.all are quite good
https://www.taniarascia.com/promise-all-with-async-await/
Using async/await with a forEach loop
Try this instead. Async functions and Promises can be tricky to get right and can be difficult to debug but you're on the right track.
async _getFetchData() {
let channels = "";
let results = await AsyncStorage.getAllKeys();
results.forEach((result) => {
if (result.includes("not") === false) {
let item = await AsyncStorage.getItem(result);
if (item === 'true') {
console.log(`channel: ${result}`)
channels = `channel_id ${result}`;
}
}
});
return channels;
}
_fetchData() {
this._getFetchData().then((channels) => {
console.log(`channel required: ${channel}`);
});
}
what if you wrap the _getFetchData() in a Promise? This would enable you to use
var channel = this._getFetchData().then(console.log("channel required: " + channel));
Otherwise the console.log won't wait for the execution of the _getFetchData().
This is what the console.log is telling you. it just logs the string. the variable is added after the async operation is done.
UPDATE
I would try this:
//_getFetchData()
AsyncStorage.getAllKeys().then((result) => { //get all stored Keys
valuelength = result.length;
if (valuelength !== 0) {
for (let i = 0; i < valuelength; i++) {
if (result[i].includes("not") == false) { //get Keys without not
AsyncStorage.getItem(result[i]).then((resultvalue) => {
if (resultvalue === 'true') {
if (this.state.firstValue) {
this.state.channels = this.state.channels + "channel_id" + result[i];
console.log("channel: " + this.state.channels);
}
else {
this.state.channels = this.state.channels + "channel" + result[i];
}
}
});
}
return new Promise((resolve, reject) => {
this.state.channels !=== undefined ? resolve(this.state.channels) : reject(Error('error '));
}
_fetchData() {
var channel = this._getFetchData().then(console.log("channel required: " + channel));
}
maybe you must change the this.state.channels !=== undefined to an expression that's matches the default value of this.state.channels.

Call to a member function on non object in CodeIgniter

I'm trying to validate an username and check if this already exists in the system. Now the problem's that I've the user list in the table: ea_user
and the user settings in the table: ea_user_settings. Now:
ea_user > contain the id of the user that isn't remove from the system, the field 'data'
ea_user_settings > contain the username
So my function is this:
public function validate_username($username, $user_id)
{
$num_rows = $this->db->join('ea_user_settings', 'ea_user_settings.id = ea_user.id', 'inner')
->get_where('ea_user_settings',
array('username' => $username, 'id_users <> ' => $user_id, 'ea_user.data' => 0))->num_rows();
return ($num_rows > 0) ? FALSE : TRUE;
}
The problem's I get this error:
Fatal error: Call to a member function num_rows() on a non-object
Try this
public function validate_username($username, $user_id)
{
$this->db->select(*);
$this->db->from('table_name');
$this->db->join('ea_user_settings', 'ea_user_settings.id = ea_user.id', 'inner')
$this->db->get_where('ea_user_settings', array('username' => $username, 'id_users <> ' => $user_id, 'ea_user.data' => 0));
$query = $this->db->get();
$result = $query->result_array(); # add this
$count = count($result);
if (!empty($count)) {
return true;
}
else{
return false;
}
}
}
It isn't documented, but any of the get() functions (get_where() in your case) can return FALSE. (Learned this the hard way.) That would seem to be your problem. FALSE->num_rows() has no meaning as FALSE is not a object.
This usually happens because the query does not build correctly.
The compiled query can be examined by doing this.
public function validate_username($username, $user_id)
{
$this->db->select(*)
->from('ea_user_settings')
->join('ea_user_settings', 'ea_user_settings.id = ea_user.id', 'inner')
->where(array('username' => $username, 'id_users <> ' => $user_id, 'ea_user.data' => 0));
$compiled_select = $this->db->get_compiled_select(NULL, FALSE);
//inspect or var_dump($compiled_select) to see what query statement was built.
After setting up all the join, where and other pieces of a query I have taken to checking the truthiness of get() before using its return. In your case something like this. Code picks up after (or instead of) the line $compiled_select = $this->...
if($query = $this->db->get())
{
return ($query->num_rows() > 0) ? FALSE : TRUE;
}
else
{
throw new Exception("get call returned FALSE");
}
}
For me, the Exception is useful mostly during development. But you could put your call to the validate_username() in a try catch block if it would be useful.

Pass extra data to finder auth

My finder from Auth has conditions that I need to access $this->request but I don't have access for that on UsersTable.
AppController::initialize
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'finder' => 'auth',
]
]
]);
UsersTable
public function findAuth(Query $query, array $options)
{
$query
->select([
'Users.id',
'Users.name',
'Users.username',
'Users.password',
])
->where(['Users.is_active' => true]); // If I had access to extra data passed I would use here.
return $query;
}
I need pass extra data from AppController to finder auth since I don't have access to $this->request->data on UsersTable.
Update
People are saying on comments that is a bad design so I will explain exactly what I need.
I have a table users but each user belongs to a gym.
The username(email) is unique only to a particular gym so I can have a example#domain.comfrom gym_id 1 and another example#domain.com from gym_id 2.
On login page I have the gym_slug to tell to auth finder which gym the user username that I provided belongs.
To my knowledge, there is no way to do this by passing it into the configuration in 3.1. This might be a good idea submit on the cakephp git hub as a feature request.
There are ways to do it by creating a new authentication object that extends base authenticate and then override _findUser and _query. Something like this:
class GymFormAuthenticate extends BaseAuthenticate
{
/**
* Checks the fields to ensure they are supplied.
*
* #param \Cake\Network\Request $request The request that contains login information.
* #param array $fields The fields to be checked.
* #return bool False if the fields have not been supplied. True if they exist.
*/
protected function _checkFields(Request $request, array $fields)
{
foreach ([$fields['username'], $fields['password'], $fields['gym']] as $field) {
$value = $request->data($field);
if (empty($value) || !is_string($value)) {
return false;
}
}
return true;
}
/**
* Authenticates the identity contained in a request. Will use the `config.userModel`, and `config.fields`
* to find POST data that is used to find a matching record in the `config.userModel`. Will return false if
* there is no post data, either username or password is missing, or if the scope conditions have not been met.
*
* #param \Cake\Network\Request $request The request that contains login information.
* #param \Cake\Network\Response $response Unused response object.
* #return mixed False on login failure. An array of User data on success.
*/
public function authenticate(Request $request, Response $response)
{
$fields = $this->_config['fields'];
if (!$this->_checkFields($request, $fields)) {
return false;
}
return $this->_findUser(
$request->data[$fields['username']],
$request->data[$fields['password']],
$request->data[$fields['gym']],
);
}
/**
* Find a user record using the username,password,gym provided.
*
* Input passwords will be hashed even when a user doesn't exist. This
* helps mitigate timing attacks that are attempting to find valid usernames.
*
* #param string $username The username/identifier.
* #param string|null $password The password, if not provided password checking is skipped
* and result of find is returned.
* #return bool|array Either false on failure, or an array of user data.
*/
protected function _findUser($username, $password = null, $gym = null)
{
$result = $this->_query($username, $gym)->first();
if (empty($result)) {
return false;
}
if ($password !== null) {
$hasher = $this->passwordHasher();
$hashedPassword = $result->get($this->_config['fields']['password']);
if (!$hasher->check($password, $hashedPassword)) {
return false;
}
$this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword);
$result->unsetProperty($this->_config['fields']['password']);
}
return $result->toArray();
}
/**
* Get query object for fetching user from database.
*
* #param string $username The username/identifier.
* #return \Cake\ORM\Query
*/
protected function _query($username, $gym)
{
$config = $this->_config;
$table = TableRegistryget($config['userModel']);
$options = [
'conditions' => [$table->aliasField($config['fields']['username']) => $username, 'gym' => $gym]
];
if (!empty($config['scope'])) {
$options['conditions'] = array_merge($options['conditions'], $config['scope']);
}
if (!empty($config['contain'])) {
$options['contain'] = $config['contain'];
}
$query = $table->find($config['finder'], $options);
return $query;
}
}
For more information see this: Creating Custom Authentication Objects
I know this is an old question but I thought I would post the finder I am using in one of our SaaS apps built on Cakephp 3. Does it follow DRY etc probably not. To say everything can be done X or Y way ..... you always have to bend the rules. In this case depending on the URL (xdomain.com or ydomain.com) our app figures out who the customer is and changes layouts etc. Also the user based is tied to Email & site_id much like yours
public function findAuth(\Cake\ORM\Query $query, array $options) {
$query
->select([
'Users.id',
'Users.email',
'Users.password',
'Users.site_id',
'Users.firstname',
'Users.lastname'])
->where([
'Users.active' => 1,
'Users.site_id'=> \Cake\Core\Configure::read('site_id')
]);
return $query;
}
Anyway hope it helps someone

How to delete multiple users from a group

Not sure why facebook refered me here but anyhow, let me ask the question. I have a group on facebook with over 4000 members. I want to delete old members that are not active on the group anymore. Is there a way to select multiple users for deletion?
How to get a list of ID's of your facebook group to avoid removal of active users, it's used to reduce as well a group from 10.000 to 5000 members as well as removal of not active members or old members "You will risk removing some few viewers of the group" "remember to open all comments while you browse down the page":
You will need to have Notepad++ for this process:
After you save the HTML. Remove all information before of document:
"div id=contentArea" to
"div id=bottomContent"
to avoid using messenger ID's,
somehow script will run problems if you have ID's by blocked users.
As well as a different example of how to parse as well as text and code out of HTML. And a range of numbers if they are with 2 digits up to 30.
You can try this to purge the list of member_id= and with them along with numbers from 2 to up to 30 digits long. Making sure only numbers and whole "member_id=12456" or "member_id=12" is written to file. Later you can replace out the member_id= with blanking it out. Then copy the whole list to a duplicate scanner or remove duplicates. And have all unique ID's. And then use it in the Java code below.
"This is used to purge all Facebook user ID's by a group out of a single HTML file after you saved it scrolling down the group"
Find: (member_id=\d{2,30})|.
Replace: $1
You should use the "Regular Expression" and ". matches newline" on above code.
Second use the Extended Mode on this mode:
Find: member_id=
Replace: \n
That will make new lines and with an easy way to remove all Fx0 in all lines to manually remove all the extra characters that come in buggy Notepad++
Then you can easily as well then remove all duplicates. Connect all lines into one single space between. The option was to use this tool which aligns the whole text with one space between each ID: https://www.tracemyip.org/tools/remove-duplicate-words-in-text/
As well then again "use Normal option in Notepad++":
Find: "ONE SPACE"
Replace ','
Remember to add ' to beginning and end
Then you can copy the whole line into your java edit and then remove all members who are not active. If you though use a whole scrolled down HTML of a page. ['21','234','124234'] <-- remember right characters from beginning. Extra secure would be to add your ID's to the beginning.
You put your code into this line:
var excludedFbIds = ['1234','11223344']; // make sure each id is a string!
The facebook group removal java code is on the user that as well posted to this solution.
var deleteAllGroupMembers = (function () {
var deleteAllGroupMembers = {};
// the facebook ids of the users that will not be removed.
// IMPORTANT: bobby.leopold.5,LukeBryannNuttTx!
var excludedFbIds = ['1234','11223344']; // make sure each id is a string!
var usersToDeleteQueue = [];
var scriptEnabled = false;
var processing = false;
deleteAllGroupMembers.start = function() {
scriptEnabled = true;
deleteAll();
};
deleteAllGroupMembers.stop = function() {
scriptEnabled = false;
};
function deleteAll() {
if (scriptEnabled) {
queueMembersToDelete();
processQueue();
}
}
function queueMembersToDelete() {
var adminActions = document.getElementsByClassName('adminActions');
console.log(excludedFbIds);
for(var i=0; i<adminActions.length; i++) {
var gearWheelIconDiv = adminActions[i];
var hyperlinksInAdminDialog = gearWheelIconDiv.getElementsByTagName('a');
var fbMemberId = gearWheelIconDiv.parentNode.parentNode.id.replace('member_','');
var fbMemberName = getTextFromElement(gearWheelIconDiv.parentNode.parentNode.getElementsByClassName('fcb')[0]);
if (excludedFbIds.indexOf(fbMemberId) != -1) {
console.log("SKIPPING "+fbMemberName+' ('+fbMemberId+')');
continue;
} else {
usersToDeleteQueue.push({'memberId': fbMemberId, 'gearWheelIconDiv': gearWheelIconDiv});
}
}
}
function processQueue() {
if (!scriptEnabled) {
return;
}
if (usersToDeleteQueue.length > 0) {
removeNext();
setTimeout(function(){
processQueue();
},1000);
} else {
getMore();
}
}
function removeNext() {
if (!scriptEnabled) {
return;
}
if (usersToDeleteQueue.length > 0) {
var nextElement = usersToDeleteQueue.pop();
removeMember(nextElement.memberId, nextElement.gearWheelIconDiv);
}
}
function removeMember(memberId, gearWheelIconDiv) {
if (processing) {
return;
}
var gearWheelHref = gearWheelIconDiv.getElementsByTagName('a')[0];
gearWheelHref.click();
processing = true;
setTimeout(function(){
var popupRef = gearWheelHref.id;
var popupDiv = getElementByAttribute('data-ownerid',popupRef);
var popupLinks = popupDiv.getElementsByTagName('a');
for(var j=0; j<popupLinks.length; j++) {
if (popupLinks[j].getAttribute('href').indexOf('remove.php') !== -1) {
// this is the remove link
popupLinks[j].click();
setTimeout(function(){
var confirmButton = document.getElementsByClassName('layerConfirm uiOverlayButton selected')[0];
var errorDialog = getElementByAttribute('data-reactid','.4.0');
if (confirmButton != null) {
if (canClick(confirmButton)) {
confirmButton.click();
} else {
console.log('This should not happen memberid: '+memberId);
5/0;
console.log(gearWheelIconDiv);
}
}
if (errorDialog != null) {
console.log("Error while removing member "+memberId);
errorDialog.getElementsByClassName('selected layerCancel autofocus')[0].click();
}
processing = false;
},700);
continue;
}
}
},500);
}
function canClick(el) {
return (typeof el != 'undefined') && (typeof el.click != 'undefined');
}
function getMore() {
processing = true;
more = document.getElementsByClassName("pam uiBoxLightblue uiMorePagerPrimary");
if (typeof more != 'undefined' && canClick(more[0])) {
more[0].click();
setTimeout(function(){
deleteAll();
processing = false;
}, 2000);
} else {
deleteAllGroupMembers.stop();
}
}
function getTextFromElement(element) {
var text = element.textContent;
return text;
}
function getElementByAttribute(attr, value, root) {
root = root || document.body;
if(root.hasAttribute(attr) && root.getAttribute(attr) == value) {
return root;
}
var children = root.children,
element;
for(var i = children.length; i--; ) {
element = getElementByAttribute(attr, value, children[i]);
if(element) {
return element;
}
}
return null;
}
return deleteAllGroupMembers;
})();
deleteAllGroupMembers.start();
// stop the script by entering this in the console: deleteAllGroupMembers.stop();
Use this in Chrome or Firefox Javascript control panel.

codeigniter pagination for a query

So far found plenty of help to get the pagination working for a get(table) command.
What I need is to pick only few of the entries from a couple of linked tables based on a sql where statement.
I guess the query command is the one to use but in this case how do I do the pagination since that command does not take extra parameters such $config['per_page']
Thanks for the help
Without any more info to go on I think that what you're looking for is something like the following.
public function pagination_example($account_id)
{
$params = $this->uri->ruri_to_assoc(3, array('page'));
$where = array(
'account_id' => $account_id,
'active' => 1
);
$limit = array(
'limit' => 10,
'offset' => (!empty($params['page'])) ? $params['page'] : 0
);
$this->load->model('pagination_model');
$data['my_data'] = $this->pagination_model->get_my_data($where, $limit);
foreach($this->uri->segment_array() as $key => $segment)
{
if($segment == 'page')
{
$segment_id = $key + 1;
}
}
if(isset($segment_id))
{
$config['uri_segment'] = $segment_id;
}
else
{
$config['uri_segment'] = 0;
}
$config['base_url'] = 'http://'.$_SERVER['HTTP_HOST'].'/controller_name/method_name/whatever_your_other_parameters_are/page/';
$config['total_rows'] = $this->pagination_model->get_num_total_rows();// Make a method that will figure out the total number
$config['per_page'] = '10';
$this->load->library('pagination');
$this->pagination->initialize($config);
$data['pagination'] = $this->pagination->create_links();
$this->load->view('pagination_example_view', $data);
}
// pagination_model
public function get_my_data($where = array(), $limit = array())
{
$this->db
->select('whatever')
->from('wherever')
->where($where)
->limit($limit['limit'], $limit['offset']);
$query = $this->db->get();
if($query->num_rows() > 0)
{
$data = $query->result_array();
return $data;
}
return FALSE;
}
This should at least get you on the right track
If this isn't what you're asking I'd happy to help more if you can be a little more specific. How about some of your code.
The only other options that I can think of would be to either code a count in your select statement or not limit the query and use array_slice to select a portion of the returned array.