Beefy Boxes and Bandwidth Generously Provided by pair Networks
good chemistry is complicated,
and a little bit messy -LW
 
PerlMonks  

object method question

by Zarathustra (Beadle)
on Oct 11, 2005 at 02:22 UTC ( [id://499003]=perlquestion: print w/replies, xml ) Need Help??

Zarathustra has asked for the wisdom of the Perl Monks concerning the following question:

Say I have a method like so:
sub _setup_dbh { my $self = shift; $self->{'_dbh'} = DBI->connect( $dsn, $username, $password ); return $self->{'_dbh'}; }
and an accessor like so:
sub get_dbh { my $self = shift; return $self->{'_dbh'}; }
Which means some client code can do the following:
$dbh = $foo->get_dbh; $dbh->prepare( "blah blah whatever" )
That's all fine... HOWEVER.... I want to throw another method into that $dbh object; something like:
sub dbq { my $self = shift; my $sql = shift; my ( $sth ); $sth = $self->{'_dbh'}->prepare( $sql ); $sth->execute; return $sth; }
How do I get that 'dbq' method into that _dbh object? So the client code could do:
$dbh = $foo->get_dbh; $dbh->dbq( "blah blah whatever" );
I of course still want to keep all the other methods that the _dbh object has. Also, the example is just for illustration purposes... basically, I have a class "foo" that instantiates an object of some other completely external class, "bar"; but it's imperative that I stuff that object "bar" with my own method that is defined within my class "foo". Your kindly assistance is most appreciated!

Replies are listed 'Best First'.
Re: object method question
by Tanktalus (Canon) on Oct 11, 2005 at 02:41 UTC

    In general, I think I would subclass from the other object, and add in the functions I want to the subclass. In your specific example with DBI, I think that will be royally difficult to do. You may be better off just telling the users to call $foo->dbq rather than $foo->get_dbh->dbq.

    It is possible in perl to affect other packages' inheritance (that's what base does). It's a bad idea in general unless you're asking someone else to affect your tree (DBI won't be asking you to affect its tree). And with DBI, it's even worse since there aren't any actual DBI objects floating around - DBI->connect is just a factory function for finding the right DBD driver, loading it, and instantiating it with the connection string.

    I'm curious, however, as to why you feel you need to be able to call dbq from the dbh object rather than from the foo object. Currently existing code won't know about the new dbq function they could ask for from the db handle, only new code could be aware of it, so if you just pass in $foo instead of $dbh, then you're all set - call $foo->dbq for your queries, and $foo->get_dbh for anything else.

    The alternate design that may work takes up a fair bit more time, IMO. Not runtime time, but developer time, which is a much more precious commodity most of the time. You could write a wrapper object that HASA $dbh handle (or HASA $foo handle), and an AUTOLOAD which would redirect unknown functions (such as execute or prepare) to the $dbh (or $foo->dbh). Then you could have get_dbh return this object instead, and most users wouldn't be able to tell the difference. e.g.,

    package Foo::DBIWrapper; use strict; sub new { my $class = shift; my $dbh = shift; my $self = { _dbh => $dbh }; bless $self, $class; } sub dbq { my $self = shift; my $sql = shift; my $sth = $self->prepare($sql); # don't use the _dbh! $sth->execute(@_); # added @_ for any binding return $sth; } our $AUTOLOAD; sub AUTOLOAD { my $self = shift; (my $func = $AUTOLOAD) =~ s/.*:://; if (my $coderef = $self->{_dbh}->can($func)) { unshift @_, $self; goto &$coderef; } die "Can't $AUTOLOAD in $self"; } 1;
    Then, instead of returning $self->{_dbh}, you return Foo::DBIWrapper->new($self->{_dbh}). A bit more work, and it does make things just a wee bit more entertaining to debug, and there are lots of AUTOLOAD horror stories to be concerned with (throws UNIVERSAL::can for loops, for example, doesn't inherit very nicely, etc.), but if you're careful, and you know what you're doing, it should work. Perl does make this a bit easier than other languages ;-)

      ...there aren't any actual DBI objects floating around - DBI->connect is just a factory function for finding the right DBD driver, loading it, and instantiating it with the connection string.
      That's not really how it works. You're right thatDBI->connect() is a factory method and that it passes much of the work over to the DBD but connect() also creates DBI driver and database handles which work in conjunction with the DBD's driver ::dr and ::db (and ::st classes for prepare). It's those clases of DBI that you need to override if you want to subclass DBI.
        > ::dr and ::db (and ::st classes for prepare). It's those clases of DBI that you need to override if you want to subclass DBI.

        That just might be what I was missing.

        I had been trying single-mindedly to override DBI itself ( in various creative and ridiculous ways! )

        Many thanks for the heads-up, I'll try the ::db instead.

      Thanks very much for the information. I'll have to take a moment to think about it all; you're absolutely right about the time/work/effort ratio, but you've provided me with plenty of food for thought.

      As far as your question regarding why I need the '$foo->{_dbh}->dbq' behavior rather than the more straight forward '$foo->dbq'. This is all due to some refactoring of old code that's in really ugly shape with some newer stuff that's better designed. A transition phase, we're going to have alot of the old stuff and new stuff forced to work together.

      To keep a very long story short, that '_dbh' needs to get handed around, from class to class - and alot of the old code expects a 'dbq' method attached to it. I specifically would prefer to pass just the '_dbh' object around, rather than the whole '$foo' object ( which'll contain way too much extraneous stuff, unrelated and unwanted by the various recipient class's ).

      Not only that, but ( actually, more importantly ) I've found that I tend to want to do similar things when I'm designing my libraries; and it's something I've simply never been able to implement.

      I do plenty of of base classing and super classing and whatnot, but I'd really like to better understand how to insert methods into existing objects.

      Thanks again!

      sub _setup_dbh { my $self = shift; my $dbh = DBI->connect($foo); *{ref($dbh)."dbq"} = sub { print "I am dbq" }; }
      Not that I'm really advocating such a beast, merely mentioning it =]

        BUU, you're right, of course, that you could do this. And I knew that when I posted. And I deliberately left it out. Figured it was best not to confuse the poor OP with dangerous things like this ;-)

        Oh, and I think you need a "::" in front of "dbq":

        *{ref($dbh)."::dbq"} = sub { print "I am dbq" };
        ;-)

Re: object method question
by pg (Canon) on Oct 11, 2005 at 03:01 UTC
    "How do I get that 'dbq' method into that _dbh object? So the client code could do: $dbh = $foo->get_dbh; $dbh->dbq( "blah blah whatever" );"

    I can feel that you are a little bit confused. The first thing you need to do before you get into the coding, let's get your design clear.

    So you want a class that wraps the basic database operations, which is fine. We got the mission statement, now let's look at the details of the class.

    First we want a property holds the handler (or the connection), so that we can reuse that connection when we need it, and you got that - $self->{'_dbh'}, nice and clean.

    Now you want three methods, 1) to initialize or set up the connection; 2) a getter to return the handler; and 3) a method to execute SQL queriES. Now the third one started to introduce soem sort of confusion to yourself. The way you used the mthod shows that.

    Here is the confusion: on one hand, the dbq method knows the handler as it is part of this class we are working on, and uses the handler; On the other hand, when you call it, you used it in a way as if it is a method belong to a different class - a second class that wraps the handler.

    The correct way of using your methods is (assume that your class is called DB):

    my $db = DB->new(); $db->_setup_dbh($dsn, $user, $passwd); $db->dbq("whateverquery");

    Now you probably want to consider other things like: 1) let the new() method call _setup_dbh; 2) let the dbq method return the query result, not the statement. Otherwise your class doesn't fully wrap the database operations, as the classes that use your class will need to know how to get result from a prepared statement. 3) the getter is not really needed. If all DB operations are wrapped inside, why does the outside world need the knowledge of this handler?

      > I can feel that you are a little bit confused. >

      This exact sort of thing is a breeze in ruby.

      I say that with good humor!

      Unfortunately it's difficult to summarize precisely why I need to do what I'm trying to do here - the best I can do is explain _what_ I need...: It's important for me to be able to inject a custom method ( "dbq" ) into an object of an external class ( "DBI" ) which has been instantiated as an attribute ( "_dbh" ) of a custom base class ( "Foo" ). How do I do so?

      That is what I would like to do - if possible.

      >$db->dbq("whateverquery");

      That's fine for client code that directly instantiates an object of the class in question ( "Foo.pm", "DB.pm", whatever ).

      One may rightly ask: "So why not just pass the '$foo'/'$db' object itself to those legacy modules?"

      Because the "$db"/"$foo" class is much, much more than a simple, specialized/dedicated piece of functionality, such as your example. Passing a large object with a bunch of unrelated methods/attributes to another object that simply wants access to one specific method is serious, ugly overkill which I'm hoping to avoid - the very sort of thing why it's all being refactored/rewritten in the first place. In other words, I'm trying to trick the old code into just using the new stuff without a care or any suspicion that something has changed.

      I'm working with a semi-largish codebase, alot of which relies on receiving an object w/ a particular method, "dbq" in this instance. We're talking some 140+ perl scripts and shoddy modules that all rely on using/recieving this old '$dbh->dbq' thing.

      I've done some major refactoring and have written some decent oop libraries to replace the existing mess with something more sane; however we simply don't have the time to re-write/re-factor every one of those 140+ scripts/modules at the moment.

      Thanks for your time, it's difficult to communicate/discuss the nature of the problem without way too much verbosity and code!

        It's also a breeze in Perl, the other posters are simply questioning whether it is wise to go adding methods to a package that is outside your control.

        If you really want to do this, and you are smart about it (eg. your method only calls the publicly documented DBI methods on itself) then you simply need to define a new method in the right package space.

        For example, if you wanted to have a method that returned the most recent database error in all uppercase (to make sure it is heard loud and clear!), you could include this fragment in your code somewhere:

        { package DBI; sub err_loud { my $self = shift; return uc( $self->err() ); } }
        What the surrounding curly braces do is define a scopeso that the package pragma only applies within that scope. The sub/method err_loud thus becomes DBI::err_loud and will be found when perl tries to resolve the method call $dbh->err_loud()

        Perl trusts you not to shoot yourself in the foot, so you want to be really sure that this is what you want to do. As other posters have said, it would be more conventional to make a subclass.

Re: object method question
by jbrugger (Parson) on Oct 11, 2005 at 08:27 UTC
    We do it like this:
    package DBH; use strict; use DBI (); my $mysql_server = "You fill this in"; my $mysql_user = "You fill this in"; my $mysql_password = "You fill this in"; my $mysql_db = "You fill this in"; sub connect { if (defined $DBH::conn) { my $ret; eval { $ret = DBH->ping; }; if (!$@ && $ret) { return $DBH::conn; } } $DBH::conn = DBI->connect( "DBI:mysql:$mysql_db;$mysql_server",$mysql_user,$mysql_passwor +d, { PrintError => 1, RaiseError => 0, } ) || die $DBI::errstr; #Assume application handles this return $DBH::conn; } sub ping { my $ret = 0; if (time - $DBI::lastPing < 10) { return 1; } eval { local $SIG{__DIE__} = sub { return (0); }; local $SIG{__WARN__} = sub { return (0); }; # adapt the select statement to your database: $ret = $DBH::conn->do('select 1'); $DBI::lastPing = time; #record time in seconds }; $debug && print STDERR "DBH.pm: pinging DB handle: $ret\n"; return ($@) ? 0 : $ret; } 1; # now you can use this in your script: #!/usr/bin/perl -w use strict; use DBH; my $dbh = DBH->connect(); my $sth = $dbh->prepare("your query...");


    "We all agree on the necessity of compromise. We just can't agree on when it's necessary to compromise." - Larry Wall.
Re: object method question
by techcode (Hermit) on Oct 11, 2005 at 11:27 UTC
    I'm also for sub-classing as others have suggested.

    And BTW getting DBH from things like that is needed so that you can set the DBI internals such as $DBH->{AutoCommit} = 0 ... or other things that specific db wrapper isn't taking care of.


    Have you tried freelancing? Check out Scriptlance - I work there.
Re: object method question
by Zarathustra (Beadle) on Oct 11, 2005 at 15:49 UTC

    Big thanks to everyone who provided suggestions, advice - and warnings!

    Special thanks to aufflick and jZed. jZed pointed out that it was DBI::db which I needed to override ( I had been working directly under DBI, which wasn't working ). aufflick showed quite succinctly how to achieve exactly what I was asking.

    In my "Foo.pm" class:

    { package DBI::db; sub dbq { my $self = shift; my $sql = shift; my ( $sth ); $sth = $self->prepare( $sql ); $sth->execute; return $sth; } }

    Works like a charm!

    Now I can hand-off $foo->get_dbh to other classes, which are then able to $dbh->dbq( "blah" ) just as they were before -- no need for me to modify a ton of scripts and modules to subclass and/or change behavior.

    Thanks again everyone,

    Beers!

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://499003]
Approved by Tanktalus
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others chanting in the Monastery: (4)
As of 2024-03-29 10:44 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found