DrGTO has asked for the wisdom of the Perl Monks concerning the following question:
Hi there monks,
this is my first post, so please forgive any foolish behaviour.
I am not a perl newbie but I am trying out some OOP stylish programming.
I have a user class, which is loaded with values from a database.
So, whenever I create a new user object I have one corresponding MySQL query for the user data.
Now, I have the situation, that I would like to get all users in the system.
Using a loop
my @users;
foreach my $id (@userIds) {
push (@users,User->new($id));
}
this will produce one MySQL query per object initialisation. So many queries for a longer user list ...
I am wondering, if there is a way to use the class constructur with a list of ids, running only one query in the new() constructur and then return several user objects at once?
Something like:
my @users=User->new($id1,$id2,$id3,$id4);
Where array @users() contain several user objects afterwards.
My class constructor looks like
sub new {
my $proto = shift(@_);
my $userId=shift(@_);
my $class = ref($proto) || $proto;
my $self = {
"user_id"=> undef,
"user_name" => undef
};
bless ($self, $class);
my $href = GETSQL("SELECT user_name FROM users WHERE user_id = $us
+erId");
@{$self}{keys %{$href}} = values %{$href};
return $self;
All vague solutions I can think of, mean a huge reconstruction of code. E.g. different constructors for single / multiple instances nested within the class....
Any help, comments are appreciated.
Thanks!!
Re: OOP: How to construct multiple instances of a class at once
by kennethk (Abbot) on Nov 14, 2012 at 16:06 UTC
|
The short answer here is that it's your code and you can make any choice you like, though what you propose is certainly not standard OOP. You could naively add a loop to your new subroutine, like:
sub new {
my $proto = shift(@_);
my $class = ref($proto) || $proto;
my @results;
for my $userId (@_) {
my $self = {
"user_id"=> undef,
"user_name" => undef
};
bless ($self, $class);
my $href = GETSQL("SELECT user_name FROM users WHERE user_
+id = $userId");
@{$self}{keys %{$href}} = values %{$href};
push @results, $self;
}
return @results;
}
Except now you will break the code my $user = User->new($id) because you are returning an array in scalar context, and so your $user variable will contain the length of the array. You could address this with wantarray, but any time you reach for that you need to seriously consider if you are being too clever for your own good.
If I were in your situation, I'd probably leave the constructor alone, and use map if I really felt like I needed to get it done in one line:
my @users = map {User->new($_)} @userIds;
If you are having noticeable performance problems and have identified your database interaction as the source (see Devel::NYTProf), then you should optimize your database access. Rather than performing each query as a one-off, you can cache the connector and the query itself -- see prepare (or even prepare_cached) in DBI. Or post that bit of code, and we can critique for you.
#11929 First ask yourself `How would I do this without a computer?' Then have the computer do it the same way.
| [reply] [Watch: Dir/Any] [d/l] [select] |
Re: OOP: How to construct multiple instances of a class at once
by tobyink (Canon) on Nov 14, 2012 at 16:12 UTC
|
As per kennethk's suggestion, something like:
my @users = map { User->new($_) } @user_ids;
... is the best way to do this. However, if you find yourself doing that frequently, then you could bundle that functionality up into a class method in the User package.
{
package User;
...;
sub multi_new {
my $class = shift;
return map { $class->new($_) } @_;
}
}
my @users = User->multi_new(@user_ids);
I wouldn't do it directly in new. While new is just a sub and you can technically do whatever you like with it, there is a strong convention that it should return a single instance of the class. Having it potentially return multiple instances will surprise users of your module... and not in a good way.
Bundling it up into a class method gives you the opportunity to take advantage of "encapsulation" at a later date. People using your class will be calling multi_new to construct multiple instances of your class without worrying about what happens inside multi_new. Perhaps in the future you'll discover a new, more efficient implementation for multi_new; you can change the internals and people using your class will get the more efficient behaviour for free.
perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'
| [reply] [Watch: Dir/Any] [d/l] [select] |
Re: OOP: How to construct multiple instances of a class at once
by sundialsvc4 (Abbot) on Nov 14, 2012 at 16:52 UTC
|
When dealing with situations like this, I like to make my objects “lazy.” They don’t actually fetch information unless and until they have to. One strategy is to send the constructor a hashref of known-good data that you might have obtained from the current row in your query; certain keys are obligatory. The (trusting...) object initializes itself partially with them ... suspiciously checking the values, and using the “setters” if such things exist so that all necessary side-effects take place. But it knows that it hasn't done all of the fetching that it might conceivably need to do ... and, if the need to do so never arises, it never will. “Lazy.” Therefore, efficient.
Your “list of users” is, initially, just a list of blessed-hashes. But they are all smart enough to know when, and if, and what, additional information must be obtained or manipulated to do everything that they are designed by you to do.
| [reply] [Watch: Dir/Any] |
Re: OOP: How to construct multiple instances of a class at once
by davido (Cardinal) on Nov 14, 2012 at 18:13 UTC
|
Could you make the population of the user's attributes lazy? If so, you could keep track of all the ones that haven't yet been populated, and the first time an accessor is used from one of the many newly created user objects, the whole list of those that haven't yet been loaded could be prepared and loaded at once, within your class. This would fix the issue of the database being hit with a bunch of little queries, and would require only a small amount of additional bookkeeping within the class, to track which objects hadn't yet been populated so that they can all be populated together in hopefully a quicker transaction, when an accessor is used. If you're concerned with a big lag hitting from time to time, you could set a limit to how many unpopulated objects may exist, and when the limit is reached, run your query again.
Update: Bah! There seems to be an echo in here. Sorry for parroting previously-provided advice.
| [reply] [Watch: Dir/Any] |
Re: OOP: How to construct multiple instances of a class at once
by tospo (Hermit) on Nov 15, 2012 at 11:31 UTC
|
As others have pointed out, it wouldn't be a good idea to make User return a collection. This is mostly a semantic issue: a User is a single person, so anybody who is unfamiliar with your software will rightly expect it to handle only one such entity and not return a list. This will include you in a couple of months time when you re-use the module and surprise yourself with the behaviour of your User class :-).
So, here is a different approach: why not have a UserList class? Objects of this class could be instantiated with a list of IDs and use a single DB query to get the data, then internally create a list of User objects from them, which it can then return. It would be used like this:
my @userlist = UserList->new( ids => [ 1,2,3 ] );
foreach my $user (@userlist){
print "user's name is: ".$user->user_name;
}
The code for building the UserList object is basically what others have given you already but now you have an appropriately named class with a clear purpose.
Now, having said all of that, you are in danger of creating your own Object-relational mapper. Have you checked out existing solutions like DBIC?
You will see exactly those concepts there with a separation of sets of results and then the actual result, representing one row of data from the DB. | [reply] [Watch: Dir/Any] [d/l] |
Re: OOP: How to construct multiple instances of a class at once
by runrig (Abbot) on Nov 14, 2012 at 23:00 UTC
|
Here is an incomplete example: sub new {
my ($class, @ids) = @_;
my @users;
my $sql = "SELECT user_id, user_name FROM users WHERE user_id IN ("
+. join(",", @ids) . ")";
my $sth = $dbh->prepare($sql);
$sth->execute();
$sth->bind_cols(\my ($id, $name));
while ( $sth->fetch() ) {
push @users, bless({
user_id => $id,
user_name => $name,
}, $class);
};
return wantarray ? @users : $users[0];
}
The @ids should be scrubbed for Bobby Tables issues, but that is also the case in your original code. | [reply] [Watch: Dir/Any] [d/l] |
|
my $sql = 'SELECT user_id, user_name FROM users WHERE user_id IN (' .
+join(',', ('?') x @ids) . ')';
#...
$sth->execute(@ids);
Especially since perl makes this so trivially easy, as compared to, say, C. | [reply] [Watch: Dir/Any] [d/l] |
|
If the ids were strings, I might use map $dbh->quote($_), @ids, but since they're probably integers, validating that they're /^\d+$/ is probably good enough. Either is also easy.
| [reply] [Watch: Dir/Any] [d/l] [select] |
Re: OOP: How to construct multiple instances of a class at once
by space_monk (Chaplain) on Nov 15, 2012 at 09:30 UTC
|
Using the new function in the way you have suggested is not really very OOP, as others have pointed out.
An "OOP approved" way, instead of butchering new() is to have a static class method that returns the list of instances with the supplied identifiers
A Monk aims to give answers to those who have none, and to learn from those who know more.
| [reply] [Watch: Dir/Any] |
Re: OOP: How to construct multiple instances of a class at once
by sundialsvc4 (Abbot) on Nov 14, 2012 at 22:35 UTC
|
Moose, of course, has a “getter/setter subroutine” notion, which is very sugar-y. But you can easily enough do this on your own. I often define an internal-use-only subroutine, named something like _getBlah (the “_” being my nomenclature for “for internal use only”) which determines if this-or-that information needs to be fetched and if necessary fetches it ... then (in any case) returns $self. This allows it to be used as a “pass-thru method” in the getter/setter methods, e.g. return $self->_getUserInfo->{'user_name'};. (Where “$self” is my habitual name for the local variable that refers to “me.”)
| [reply] [Watch: Dir/Any] |
Re: OOP: How to construct multiple instances of a class at once
by DrGTO (Initiate) on Nov 26, 2012 at 20:48 UTC
|
Thanks very much for the comments and inspirations !!!
Makes sense to consider semantics and go with two classes, one for User and one for Users or Userlist.
And yes, to go totally OOP one should think about Moose...
Will be my next adventure then :) | [reply] [Watch: Dir/Any] |
|
|