2013. augusztus 9., péntek

scala - slick - left join

I'm going to switch to English for the sake of this post.

While playing with Play Framework, Slick and Scala recently, I came across this problem when doing a left join:
[error] SlickException: Read NULL value for column ORDERS.GUEST_ID (TypeMapper.scala:158) [error] models.BarTables$$anonfun$currentTableStateFull$1.apply(BarTable.scala:108)
Let's see what happens here:

        for{
            (guests,orders) <- Guests leftJoin Orders on (_.id === _.guestId)
        } yield (guests,orders).list

So this is how you do a left join with Slick. Here's the two tables used in the query:

object Orders extends Table[Order]("ORDERS") with CRUDModel[Order] with Logger{

  def id = column[Long]("ID", O.PrimaryKey, O AutoInc) // This is the primary key column
  def guestId = column[Long]("GUEST_ID")
  def created = column[DateTime]("CREATED")
  def delivered = column[Option[DateTime]]("DELIVERED")
  def paid = column[Option[DateTime]]("PAID")

  def * = id.? ~ guestId ~ created ~ delivered ~ paid <> (Order.apply _, Order.unapply _)

...
 
object Guests extends Table[User]("GUESTS") with CRUDModel[User]{

  def id = column[Long]("ID", O.PrimaryKey, O AutoInc) // This is the primary key column
  def name = column[String]("NAME")
  def email = column[String]("EMAIL")

  def * = id.? ~ name ~ email <> (User.apply _, User.unapply _)

...

That looks quite normal I guess.

But then the problem arises when there's no orders belonging to the guests, and so all the columns selected from the Orders table are null. And guestId cannot be null, it's not defined as an Option (obviously).

What am I doing wrong?

The problem is that the default projection * is used when yielding the result:
    
    yield (guests,orders).list

We have to provide a projection that doesn't use the default one. So after some googling and stackoverflowing, I came across this post, and then this one and the result looks like this:

  def maybe = id.? ~ guestId.?  ~ created.?  ~ delivered  ~ paid  <> (
      tupleToOrder _,
      (order: Option[Order]) => None
  )
  
  def tupleToOrder(orderTuple: (Option[Long],Option[Long],Option[DateTime],Option[DateTime],Option[DateTime])) : Option[Order]= orderTuple match {
  case (Some(id),Some(guestId),Some(created),delivered,paid) => Some(Order(Some(id),guestId,created,delivered,paid))
  case (None,_,_,_,_) => None
  }

So instead of using the values as they would be not-nulls, we provide a way to pass nulls, and then get an Option[Order] instead of an Order. We only have to match for Some(id) really, since if that's a null, all the other columns are null as well.

And so the original query looks like this:
    for{
        (guests,orders) <- guests(tableId) leftJoin Orders on (_.id === _.guestId)
    } yield (guests,orders.maybe)

So finally this mystery is resolved now!

2013. június 28., péntek

CDI event + JMS - szinkron és aszinkron események


Kétféle eseményt különböztetünk meg aszerint, milyen időben (pontosabb Threadben) futhat le:
  • szinkron (a hívó threadben fut),
  • aszinkron (nem a hívó threadben fut).
A CDI eseménykezelése segítségével valósítjuk meg mindkét fajtát.

Szinkron esemény

CDI event fire -> CDI event handle with @Observe
class Producer{
   
    @Inject @InForeground
    Event<MyEvent> myEventToBeFiredInSync;

...
    void myMethod(){
        ...
        //do the business
        myEventToBeFiredInSync.fire(new MyEvent());
        ...
    }
}

class Consumer{

...
    void consumeAndHandleEvent(@Observe @InForeground MyEvent myEvent){
        ...
        //do the business
        workWith(myEvent);
        ...
    }
}
Például:
TransactionEvent -> automatikus history és üzenetkezelés az ügylethez

Aszinkron esemény

Ez JMS messaging segítségével van megvalósítva. A keletkezés helyén azonban lecsatoljuk a JMS üzenetet generáló logikát az üzleti részről, és CDI esemény segítségével pukkan el az aszinkron üzenet is.
CDI event fire -> JMS Producer @Observe and send message -> JMS consumes message and CDI event fire -> CDI event handle with @Observe
class Producer{

    @Inject @InBackground
    Event<MyEvent> myEventToBeFiredAsync;

...
    void myMethod(){
        ...
        //do the business
        myEventToBeFiredInSync.fire(new MyEvent());
        ...
    }
}

class Consumer{

...
    void consumeAndHandleEvent(@Observe @InForeground MyEvent myEvent){
        ...
        //do the business
        workWith(myEvent);
        ...
    }
}
A Consumer nem tudja megenni az eseményt, hiszen az @InForeground eventet vár. Itt jön a JMS:
class JMSMessageProducer{

...
    @Inject
    private QueueSession session;

    @Inject
    private Queue queue;

    public void produceJMSMessage(@Observes @InBackground MyEvent myEvent) {

        try {
            QueueSender sender = session.createSender(queue);
            Message message = session.createMessage();
            message.setObjectProperty("myEvent", myEvent);
            sender.send(message);
        } catch (JMSException e) {
            logger.error("Error sending message to queue", e);
        }
    }
}

@FancyAnnotations
class ConsumerMDB{

...
    @Inject @InForeground
    Event<MyEvent> myEventToBeFiredInSync;
    

    @Override
    public void onMessage(Message rcvMessage) {
        try {
            MyEvent myEvent = (MyEvent)rcvMessage.getObjectProperty("myEvent");
            myEventToBeFiredInSync.fire(myEvent);
        } catch (JMSException e) {
            logger.error("Incorrect message format.", e);
        }
    }
}
Tehát gyakorlatilag visszatranszformálja a JMS üzenetet CDI eseménnyé.
Az előny, hogy az üzleti kódban kizárólag CDI eseményeket találunk, és a szinkron-aszinkron események között egy annotáció lecserélésével tudunk váltani, akár programmatikus módon.
Például:
A kérelem benyújtása után aszinkron módon készítjük fel az ügyletet az alapítvány oldali befogadásra.

JMS erőforrások megadása CDI segítségével

package hu.avhga.business.common.resource;

import javax.annotation.Resource;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;
import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSession;
import javax.jms.Session;

/**
 * JMS erőforrások előállítására és lezárására használt osztály
 *
 * @author bnemeth
 *
 */
public class JMSResources {

    @Resource(mappedName = "java:/JmsXA")
    private QueueConnectionFactory connectionFactory;

    @Produces @AdmissionQueue @Resource(mappedName = "java:jboss/exported/jms/queue/transactionAdmissionMDBQueue")
    private Queue admissionQueue;

    @Produces @AssessmentQueue
    @Resource(mappedName = "java:jboss/exported/jms/queue/transactionAssessmentMDBQueue")
    private Queue assessmentQueue;

    @Produces @AssessmentQueue
    public QueueSession createAssessmentSession(@AssessmentQueue QueueConnection conn) throws JMSException {
        return conn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
    }

    @Produces @AssessmentQueue
    public QueueConnection createAssessmentConnection() throws JMSException {
        return connectionFactory.createQueueConnection();
    }

    public void closeAssessmentSession(@Disposes @AssessmentQueue QueueConnection conn) throws JMSException {
        conn.close();
    }

    public void closeAssessmentSession(@Disposes @AssessmentQueue QueueSession session) throws JMSException {
        session.close();
    }

    @Produces @AdmissionQueue
    public QueueConnection createOrderConnection() throws JMSException {
        return connectionFactory.createQueueConnection();
    }

    @Produces @AdmissionQueue
    public QueueSession createOrderSession(@AdmissionQueue QueueConnection conn) throws JMSException {
        return conn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
    }

    public void closeOrderSession(@Disposes @AdmissionQueue QueueConnection conn) throws JMSException {
        conn.close();
    }

    public void closeOrderSession(@Disposes @AdmissionQueue QueueSession session) throws JMSException {
        session.close();
    }
}

A fenti osztály lehetővé teszi megadott Queue és QueueSession injektálását, továbbá nem kell explicit módon lezárni sem az erőforrásokat, erre való a @Disposes annotáció.

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.

2013. április 25., csütörtök

JBoss 7 remote EJB lookup

Felírom, mert már legalább másodszor szívom meg.

Ha ezt írja ki a JBoss 7 a modul deployolása után:

java:global/cdi_events_poc_ear/cdi_events_poc/TransactionServiceBean!hu.avhga.service.TransactionServiceR
java:app/cdi_events_poc/TransactionServiceBean!hu.avhga.service.TransactionServiceR
java:module/TransactionServiceBean!hu.avhga.service.TransactionServiceR
java:jboss/exported/cdi_events_poc_ear/cdi_events_poc/TransactionServiceBean!hu.avhga.service.TransactionServiceR
java:global/cdi_events_poc_ear/cdi_events_poc/TransactionServiceBean!hu.avhga.service.TransactionService
java:app/cdi_events_poc/TransactionServiceBean!hu.avhga.service.TransactionService
java:module/TransactionServiceBean!hu.avhga.service.TransactionService

Akkor próbálhatunk lookupolni pl. a legfelsőre, az eredmény:

java.lang.IllegalStateException: No EJB receiver available for handling [appName:java:global,modulename:cdi_events_poc_ear,distinctname:cdi_events_poc] combination for invocation context org.jboss.ejb.client.EJBClientInvocationContext@374d2f77
at org.jboss.ejb.client.EJBClientContext.requireEJBReceiver(EJBClientContext.java:584)
at org.jboss.ejb.client.ReceiverInterceptor.handleInvocation(ReceiverInterceptor.java:119)
at org.jboss.ejb.client.EJBClientInvocationContext.sendRequest(EJBClientInvocationContext.java:181)
at org.jboss.ejb.client.EJBInvocationHandler.doInvoke(EJBInvocationHandler.java:136)
at org.jboss.ejb.client.EJBInvocationHandler.doInvoke(EJBInvocationHandler.java:121)
at org.jboss.ejb.client.EJBInvocationHandler.invoke(EJBInvocationHandler.java:104)
at $Proxy4.submit(Unknown Source)
at hu.avhga.TestHistory.aaaa(TestHistory.java:52)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:46)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:43)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:270)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:62)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:52)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:307)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

Az Exception egyébként el is árulja, de amúgy utólag okos az ember:
[appName:java:global,modulename:cdi_events_poc_ear,distinctname:cdi_events_poc]

Hát persze, hogy nem java:global az appName! Tehát ha azt a prefixet levesszük, rendesen megtalálja a keresett szolgáltatást a lookup. Nem az mondom, hogy ha java:globallal kezdődik a String, akkor dobjon külön exceptiont, hogy "nézz már oda mivel kezdted, Géza!", de elég nehezen észrevehető hiba.

Emellett még ugye kell a jboss-ejb-client.properties, a client jar, és kábé ennyi, amit fél évente elfelejtek, és nem 10 perc összekalapálni a remote tesztet.