views:

108

answers:

4

Hi,

I'm having what seems to be a transactional issue in my application. I'm using Java 1.6 and Hibernate 3.2.5.

My application runs a monthly process where it creates billing entries for a every user in the database based on their monthly activity. These billing entries are then used to create Monthly Bill object. The process is:

  1. Get users who have activity in the past month
  2. Create the relevant billing entries for each user
  3. Get the set of billing entries that we've just created
  4. Create a Monthly Bill based on these entries

Everything works fine until Step 3 above. The Billing Entries are correctly created (I can see them in the database if I add a breakpoint after the Billing Entry creation method), but they are not pulled out of the database. As a result, an incorrect Monthly Bill is generated.

If I run the code again (without clearing out the database), new Billing Entries are created and Step 3 pulls out the entries created in the first run (but not the second run). This, to me, is very confusing.

My code looks like the following:

for (User user : usersWithActivities) {

            createBillingEntriesForUser(user.getId());

            userBillingEntries = getLastMonthsBillingEntriesForUser(user.getId());

            createXMLBillForUser(user.getId(), userBillingEntries);
    }

The methods called look like the following:

@Transactional
    public void createBillingEntriesForUser(Long id) {

        UserManager userManager = ManagerFactory.getUserManager();
        User user = userManager.getUser(id);
        List<AccountEvent> events = getLastMonthsAccountEventsForUser(id);
        BillingEntry entry = new BillingEntry();

        if (null != events) {

            for (AccountEvent event : events) {

                if (event.getEventType().equals(EventType.ENABLE)) {
                    Calendar cal = Calendar.getInstance();

                    Date eventDate = event.getTimestamp();
                    cal.setTime(eventDate);

                    double startDate = cal.get(Calendar.DATE);
                    double numOfDaysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
                    double numberOfDaysInUse = numOfDaysInMonth - startDate;

                    double fractionToCharge = numberOfDaysInUse/numOfDaysInMonth;

                    BigDecimal amount = BigDecimal.valueOf(fractionToCharge * Prices.MONTHLY_COST);
                    amount.scale();
                    entry.setAmount(amount);
                    entry.setUser(user);
                    entry.setTimestamp(eventDate);

                    userManager.saveOrUpdate(entry);
                }


            }

        }

    }


@Transactional
    public Collection<BillingEntry> getLastMonthsBillingEntriesForUser(Long id) {

        if (log.isDebugEnabled())
            log.debug("Getting all the billing entries for last month for user with ID " + id);

        //String queryString = "select billingEntry from BillingEntry as billingEntry where billingEntry>=:firstOfLastMonth and billingEntry.timestamp<:firstOfCurrentMonth and billingEntry.user=:user";
        String queryString = "select be from BillingEntry as be join be.user as user where user.id=:id and be.timestamp>=:firstOfLastMonth and be.timestamp<:firstOfCurrentMonth";

        //This parameter will be the start of the last month ie. start of billing cycle
        SearchParameter firstOfLastMonth = new SearchParameter();
        firstOfLastMonth.setTemporalType(TemporalType.DATE);

        //this parameter holds the start of the CURRENT month - ie. end of billing cycle
        SearchParameter firstOfCurrentMonth = new SearchParameter();
        firstOfCurrentMonth.setTemporalType(TemporalType.DATE);

        Query query = super.entityManager.createQuery(queryString);

        query.setParameter("firstOfCurrentMonth", getFirstOfCurrentMonth());        
        query.setParameter("firstOfLastMonth", getFirstOfLastMonth());
        query.setParameter("id", id);

        List<BillingEntry> entries = query.getResultList();

        return entries;
    }

public MonthlyBill createXMLBillForUser(Long id, Collection<BillingEntry> billingEntries) {

        BillingHistoryManager manager = ManagerFactory.getBillingHistoryManager();
        UserManager userManager = ManagerFactory.getUserManager();

        MonthlyBill mb = new MonthlyBill();
        User user  = userManager.getUser(id);

        mb.setUser(user);
        mb.setTimestamp(new Date());

        Set<BillingEntry> entries = new HashSet<BillingEntry>();
        entries.addAll(billingEntries);

        String xml = createXmlForMonthlyBill(user, entries);
        mb.setXmlBill(xml);
        mb.setBillingEntries(entries);
        MonthlyBill bill = (MonthlyBill) manager.saveOrUpdate(mb);
        return bill;

    }

Help with this issue would be greatly appreciated as its been wracking my brain for weeks now!

Thanks in advance, Gearoid.

A: 

Is your top method also transactional ? If yes most of the time i've encountered that kind of problem, it was a flush that was not done at the right time by hibernate.

Try to add a call to session.flush() at the beginning of the getLastMonthsBillingEntriesForUser method, see if it address your problem.

Thierry
Hi Thierry, please see the comment I left in Padmarags question above.Thanks for your input.
Gearóid
For your question about what is causing the problem, in my experience the main cause was the entityManager keeping too long the sql requests in memory without flushing them to db (so that it can batch more inserts or updates at once, for performance purpose).Normally, it should flush the session to the db when you are doing a select request, so that the rdbms give you the right resultset.I have observed sometimes (but rarely) hibernate not doing the flush before the request. We are using hibernate directly, so i've never had the need to work a way around the lack of flush method.
Thierry
A: 

Call session.flush() AND session.close() before getLastMonthsBillingEntriesForUser gets called.

Padmarag
I should have mentioned that we do not use Hibernate explicitly. My code uses the javax.persistence package for objects like Query. We also use javax.persistence.EntityManager.Because of this, we cannot call session.flush() or close(). I have tried calling entityManager.flush() and close() but to no avail. Do you have any other idea what could be causing this error?Thanks for your input btw.
Gearóid
A: 

Please correct my assumptions if they are not correct...

As far as I can tell, the relationship between entry and user is a many to one.

So why is your query doing a "one to many" type join? You should rather make your query:

select be from BillingEntry as be where be.user=:user and be.timestamp >= :firstOfLastMonth and be.timestamp < :firstOfCurrentMonth

And then pass in the User object, not the user id. This query will be a little lighter in that it will not have to fetch the details for the user. i.e. not have to do a select on user.

Unfortunately this is probably not causing your problem, but it's worth fixing nevertheless.

Michael Wiles
A: 

Move the declaration of BillingEntry entry = new BillingEntry(); to within the for loop. That code looks like it's updating one entry over and over again.

I'm guessing here, but what you've coded goes against what I think I know about java persistence and hibernate.

Are you certain that those entries are being persisted properly? In my mind, what is happening is that a new BillingEntry is being created, it is then persisted. At this point the next iteration of the loop simply changes the values of an entry and calls merge. It doesn't look like you're doing anything to create a new BillingEntry after the first time, thus no new id's are generated which is why you can't retrieve them later.

That being said, I'm not convinced the timing of the flush isn't a culprit here either, so I'll wait with bated breathe for the downvotes.

zmf
good eye ! I agree with you that the entry var seems to be at the wrong place
Thierry