Subclassing interfaces and JDBI

1,441 views
Skip to first unread message

Ankur Chauhan

unread,
Oct 18, 2012, 2:02:02 AM10/18/12
to jd...@googlegroups.com
I am working on a project and thought of using JDBI but although jdbi seems to work wonderfully with simple DAOs, I seem to have hit a roadblock.

consider the following interfaces

public interface ChannelRepository<T extends Channel>{
    public T get(Long id);
    public Long save(T entity);
    public void delete(Long id);
    public void update(T entity);
}

@RegisterMapper(YoutubeChannelMapper.class)
public interface YoutubeChannelRepository extends ChannelRepository<YoutubeChannel> {

    @Override
    @SqlQuery("select * from channel where id = :id")
    public YoutubeChannel get(@Bind("id") Long id);

    @Override
    @GetGeneratedKeys
    @SqlUpdate("insert into remote_media (publisherId, created, lastUpdated, name, category, oauthToken, oauthTokenSecret) values(:publisherId, :created, :lastUpdated, :name, :category, :oauthToken, :oauthTokenSecret)")
    public Long save(@BindBean YoutubeChannel entity);

    @Override
    @SqlUpdate("delete from remote_media where id = :id")
    public void delete(@Bind("id") Long id);

    @Override
    @SqlUpdate("update channel set lastUpdated = :lastUpdated, name = :name, oauthToken = :oauthToken, oauthTokenSecret = :oauthTokenSecret where id = :id")
    public void update(@BindBean YoutubeChannel entity);
}

so we have a ChannelRepository and multiple subclasses of this interface such as YoutubeChannelRepository and VimeoChannelRepository. Now, in my resource if I create it as 

@Path("/dummy")
@Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public class DummyResource {
    private static final Log LOG = Log.forThisClass();

    ChannelRepository channelRepository;
    RemoteMediaRepository remoteMediaRepository;

    public DummyResource(RemoteMediaRepository remoteMediaRepository, ChannelRepository channelRepository) {
        this.remoteMediaRepository = remoteMediaRepository;
        this.channelRepository = channelRepository;
    }

    @GET @Path("/medias/{id}")
    public RemoteMedia getRemoteMedia(@PathParam("id") Long id) {
        RemoteMedia remoteMedia = remoteMediaRepository.get(id);
        if(remoteMedia == null) {
            LOG.debug("Unable to find remoteMedia: {}", id);
            throw new NotFoundException();
        }
        LOG.debug("Found remoteMedia: {}", remoteMedia);
        return remoteMedia;
    }

    @GET @Path("/channels/youtube/{id}")
    public YoutubeChannel getChannel(@PathParam("id") Long id) {
        AbstractEntity channel = channelRepository.get(id);
        if(channel == null) {
            LOG.debug("Unable to find channel: {}" ,id);
            throw new NotFoundException();
        }
        LOG.debug("Found Channel: {}", channel);
        return (YoutubeChannel) channel;
    }
}

I get the following stack trace 
ERROR [2012-10-18 05:34:31,979] com.yammer.dropwizard.jersey.LoggingExceptionMapper: Error handling a request: 705a19866397ff71
! java.lang.NullPointerException: null
! at org.skife.jdbi.v2.sqlobject.SqlObject.invoke(SqlObject.java:145)
! at org.skife.jdbi.v2.sqlobject.SqlObject$1.intercept(SqlObject.java:58)
! at org.skife.jdbi.v2.sqlobject.CloseInternalDoNotUseThisClass$$EnhancerByCGLIB$$2fe777e7.get(<generated>)
! at com.brightcove.yeti.resources.DummyResource.getChannel(DummyResource.java:43)
! at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
! at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
! at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
! at java.lang.reflect.Method.invoke(Method.java:597)
! at com.sun.jersey.spi.container.JavaMethodInvokerFactory$1.invoke(JavaMethodInvokerFactory.java:60)
! at com.sun.jersey.server.impl.model.method.dispatch.AbstractResourceMethodDispatchProvider$TypeOutInvoker._dispatch(AbstractResourceMethodDispatchProvider.java:185)
! at com.sun.jersey.server.impl.model.method.dispatch.ResourceJavaMethodDispatcher.dispatch(ResourceJavaMethodDispatcher.java:75)
! at com.sun.jersey.server.impl.uri.rules.HttpMethodRule.accept(HttpMethodRule.java:302)
! at com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147)
! at com.sun.jersey.server.impl.uri.rules.ResourceObjectRule.accept(ResourceObjectRule.java:100)
! at com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147)
! at com.sun.jersey.server.impl.uri.rules.RootResourceClassesRule.accept(RootResourceClassesRule.java:84)
! at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1480)
! at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1411)
! at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1360)
! at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1350)
! at com.sun.jersey.spi.container.servlet.WebComponent.service(WebComponent.java:416)
! at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:538)
! at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:716)
! at javax.servlet.http.HttpServlet.service(HttpServlet.java:848)
! at com.yammer.dropwizard.jetty.NonblockingServletHolder.handle(NonblockingServletHolder.java:47)
! at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1331)
! at com.yammer.dropwizard.servlets.ThreadNameFilter.doFilter(ThreadNameFilter.java:29)
! at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1302)
! at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:448)
! at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1067)
! at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:377)
! at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1001)
! at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)
! at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:111)
! at com.yammer.metrics.jetty.InstrumentedHandler.handle(InstrumentedHandler.java:200)
! at org.eclipse.jetty.server.handler.GzipHandler.handle(GzipHandler.java:233)
! at com.yammer.dropwizard.jetty.BiDiGzipHandler.handle(BiDiGzipHandler.java:123)
! at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:149)
! at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:111)
! at org.eclipse.jetty.server.Server.handle(Server.java:360)
! at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:454)
! at org.eclipse.jetty.server.BlockingHttpConnection.handleRequest(BlockingHttpConnection.java:47)
! at org.eclipse.jetty.server.AbstractHttpConnection.headerComplete(AbstractHttpConnection.java:890)
! at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.headerComplete(AbstractHttpConnection.java:944)
! at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:630)
! at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:230)
! at org.eclipse.jetty.server.BlockingHttpConnection.handle(BlockingHttpConnection.java:66)
! at org.eclipse.jetty.server.nio.BlockingChannelConnector$BlockingChannelEndPoint.run(BlockingChannelConnector.java:293)
! at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:603)
! at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:538)
! at java.lang.Thread.run(Thread.java:680)

Upon replacing all instances of ChannelRepository with YoutubeChannelRepository, everything works just fine. 

What is the problem here? Am I doing something wrong or is this just not supported by jdbi?

Oliver Schrenk

unread,
Oct 18, 2012, 6:56:09 AM10/18/12
to jd...@googlegroups.com
This is intended. I had a similar problem in January:

On Mon, Jan 9, 2012 at 7:41 AM, Oliver Schrenk <oliver....@gmail.com> wrote:
> Following scenario:
>
> I have the following JDBI Interface
>
>        @RegisterMapper(AttributeMapper.class)
>        public interface AttributeDaoJdbi extends AttributeDao {
>
>                @Override
>                @SqlQuery("sql/base/attribute/select-all")
>                public Set<Attribute> all();
>
>        }
>
> with `AttributeDao` being
>
>        public interface AttributeDao {
>
>                public abstract Collection<Attribute> all() throws SQLException;
>
>        }
The sql object API works against interfaces specifically so that you
can just annotate the interface. In this case, I would suggest not
creating a sub-interface, but just annotating your AttributeDao.
Before you lynch me for suggesting something so impure, keep in mind
that annotations are *optional* at runtime - so if you are using a
different implementation of AttributeDao and don't have jdbi on the
classpath, that is no big deal.
In fact, the sql object api came into existence because of the
tendency to havve:
interface FooDao { ... }
class JdbiFooDao { ... }
Where the JdbiFooDao has exactly one sql query, and usually exactly
one expression (a chained return
createQuery(...).bind(...).bind(...).map(...).list(); In fact, in
almost every case there were exactly two implementations of the Dao
interface -- the first was the concrete jdbi using implementation, the
second was with a mock object framework for testing.
This seemed like annoying boilerplate, and could be made more
expressive and clear by making it declarative through annotations --
and the sql object API was born.
So, unless you have an overriding reason not to, I suggest just
annotating AttributeDao.
>
> Now of course someone else in my team might extends the `AttributeDao`
> for his purposes without me noticing during compile time, as I only
> extend the interface.
>
> During runtime though jDBI emits the exception
>
>        java.lang.UnsupportedOperationException: Not Yet Implemented!
>                at
> org.skife.jdbi.v2.sqlobject.SqlObject.buildHandlersFor(SqlObject.java:
> 69)
>                at
> org.skife.jdbi.v2.sqlobject.SqlObject.buildSqlObject(SqlObject.java:
> 33)
>                at
> org.skife.jdbi.v2.sqlobject.SqlObjectBuilder.attach(SqlObjectBuilder.java:
> 40)
>                at org.skife.jdbi.v2.BasicHandle.attach(BasicHandle.java:399)
>
> because the other method has no handler attached to it.
>
>
> Do you guys have any experience on how to deal with this?
We need a better error message here, when you have a sub-interface like this.
If you do need to use this style of interface inheritance, and need
compile time checks, it will probably require a heuristic-based
annotation processor (look for interfaces which which extend other
interfaces where at least one method has an annotation in the
org.skife.jdbi.v2 namespace, and so on) which fails your build if it
finds anything. Failing a build based on heuristics can be annoying
(ie, future version of jdbi adds a new mixin interface type which your
checked doersn't know about, so thinks it picked up on this class of
error).

Brian McCallister

unread,
Oct 18, 2012, 11:56:03 AM10/18/12
to jd...@googlegroups.com
Okay, so if I understand, the problem comes in that "overriding"
save() and friends doesn't pick up the annotations?

I can imagine why this happens, as the proxy looks at the method
invoked, which is actually different then the one annotated, it just
resolves to the same concrete method. As Oliver pointed out, resolving
this would require a search heuristic I think. Ugh.

I am not against making the changes to support this, but I don't
actually consider not supporting it a bug. A bloody annoyance, though,
certainly. I'm happy to add it to my list of things to tackle (behind
rejiggering result creation to support arbitrary containers better),
but am not sure when I will get it wrapped. I'd love to see a pull
request!

-Brian
> --
>
>

Ankur Chauhan

unread,
Oct 18, 2012, 12:47:26 PM10/18/12
to jd...@googlegroups.com
I would be happy to do the patching if you can tell me where this goes or a code walkthrough to the place that takes this decision.
> --
>
>

Brian McCallister

unread,
Oct 18, 2012, 2:08:02 PM10/18/12
to jd...@googlegroups.com
On Thu, Oct 18, 2012 at 10:47 AM, Ankur Chauhan <an...@malloc64.com> wrote:
> I would be happy to do the patching if you can tell me where this goes or a code walkthrough to the place that takes this decision.

It is done from a number of places, starting with

https://github.com/brianm/jdbi/blob/master/src/main/java/org/skife/jdbi/v2/sqlobject/SqlObject.java

Various things instantiated therein also look for annotations.

I suspect if you want a search pattern for annotations, we'll need to
factor out annotation lookup into some common helper code, which will
probably need to be aware of the concrete sql object class as well.
Possibly an AnnotationFinder which is created per sql object and given
the actual sql object type, a la…

new AnnotationFinder(sqlObjectType);

which then supports searching for annotations in a context (type,
method, etc). The api details will depend on what is needed.

-Brian

Ankur Chauhan

unread,
Oct 18, 2012, 4:38:00 PM10/18/12
to jd...@googlegroups.com
Hi Brian,

I have a change that gets the job done and its a pretty simple change. I will send a pull request as soon as github comes back online. :|

- Ankur
> --
>
>

Ankur Chauhan

unread,
Oct 18, 2012, 5:01:15 PM10/18/12
to jd...@googlegroups.com
I just submitted a pull request for this:

Reply all
Reply to author
Forward
0 new messages