【问题标题】:Recently updated db records not reflected in follow-on SELECT最近更新的数据库记录未反映在后续 SELECT 中
【发布时间】:2020-07-04 11:15:36
【问题描述】:

我有一个带有 h2 数据库的 spring mvc 应用程序。 Db 对象使用 JPA 标准和 spring @Transaction 注释进行更新。请参阅底部的 findBySymbol() 和 update()。

update() 完成后,应用调用 findBySymbol() 对更新的记录运行一些逻辑。 findBySymbol() SQL 是正确的。问题是 findBySymbol() 的结果没有被更新。这很令人费解,因为 Hibernate 跟踪日志显示了更新绑定参数、SQL 更新语句和 executeUpdate() 的 int 结果。同样令人困惑的是,我当时可以直接查询数据库并查看记录实际上是否已更新。

我需要做什么才能让应用在更新后立即查询数据库并返回更新的数据?

import com.example.mycompany.beans.AEarnings;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import org.springframework.stereotype.Repository;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.ParameterExpression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.CriteriaUpdate;
import javax.persistence.NoResultException;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class AnnualEarningsDaoImpl implements AnnualEarningsDao {

    @PersistenceContext
    EntityManager entityManager;        

    @Override
    public List<AEarnings> findBySymbol(String s) {

        // CriteriaBuilder 
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();

        // CriteriaQuery
        CriteriaQuery<AEarnings> cQuery = cb.createQuery(AEarnings.class);

        // Root
        Root<AEarnings> ae = cQuery.from(AEarnings.class);

        // Parameters
        ParameterExpression<String> p = cb.parameter(String.class);

        // Db command
        cQuery.select(ae).where(cb.equal(ae.get("symbol"), p));

        // TypedQuery
        TypedQuery<AEarnings> query = entityManager.createQuery(cQuery);

        // Set Parameter
        query.setParameter(p, s);

        // Results
        List<AEarnings> results = query.getResultList();
        
        // Return
        return results;
    }
    
    @Override
    @Transactional
    public void update(
            String a, LocalDate b, String c,
            BigDecimal d, BigDecimal e, BigDecimal f) {
        
        // CriteriaBuilder
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();

        // CriteriaUpdate<AEarnings>
        CriteriaUpdate<AEarnings> update
                = builder.createCriteriaUpdate(AEarnings.class);
        // Root
        Root<AEarnings> root = update.from(AEarnings.class);

        // Holds parameters
        List<Predicate> predicates = new ArrayList<>();

        // Predicates
        predicates.add(builder.equal(root.get("a"), a));
        predicates.add(builder.equal(root.get("b"), b));
        predicates.add(builder.equal(root.get("c"), c));
        
        // Update
        update
            .set(root.get("d"), d)
            .set(root.get("e"), e)
            .set(root.get("f"), f)
            .where(predicates.toArray(new Predicate[]{}));

        // Execute
        int i = entityManager.createQuery(update).executeUpdate();
        System.out.println("Updated: " + i);
    }

}

使用 AEarningsServiceImpl 更新,在这种情况下只调用 dao 方法:

import com.example.mycompany.beans.AEarnings;
import com.example.mycompany.dao.AEarningsDao;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AEarningsServiceImpl implements AEarningsService{

    @Autowired
    AEarningsDao aEarningsDao;

    @Override
    public List<AnnualEarnings> findBySymbol(String sym) {
        return annualEarningsDao.findBySymbol(sym);
    }

    @Override
    @Transactional
    public void update(
            String symbol, LocalDate fiscalEndDate, String fiscalPeriod, 
            BigDecimal prev, BigDecimal diff, BigDecimal diffPercent) {
        annualEarningsDao.update(symbol, fiscalEndDate, fiscalPeriod,
                prev, diff, diffPercent);
    }

    // save()...    
    // delete()...
}

用 pom.xml 更新:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
            https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
  
    <groupId>com.mycompany</groupId>
    <artifactId>appmaker</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <description>Spring Boot Stock App</description>
    <name>app</name>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>

    <dependencies>
       <dependency>
            <groupId>javax.transaction</groupId>
            <artifactId>jta</artifactId>
            <version>1.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-dbcp</artifactId>
            <version>8.0.5</version>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring4</artifactId>
            <version>3.0.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>        
        <dependency>
            <groupId>com.googlecode.json-simple</groupId>
            <artifactId>json-simple</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

更新了调用 AEarningsService 的代码:

package com.example.mycompany.beans.helpers;

import com.example.mycompany.beans.AnnualEarnings;
import com.example.mycompany.beans.Token;
import com.example.mycompany.beans.Url;
import com.example.mycompany.service.AnnualEarningsService;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class AnnualHelper {

    private static final BigDecimal HUNDRED = new BigDecimal(100);
    private static final String EARNINGS_4_TAG = "/earnings/4?period=annual&token=";
    private static final String EARNINGS_1_TAG = "/earnings/1?period=annual&token=";
    private Url url;

    @Autowired
    private AnnualEarningsService annualEarningsService;
    
    // Call class methods, filter for earnings of size > 0 for initial run
    public List<AnnualEarnings> run(String domain, String token, BigDecimal percent) {

        // Holds results
        List<AnnualEarnings> results = new ArrayList<>();

        // Grab symbols
        ...

        // Loop through incoming symbols
        symbols.forEach((symbol) -> {

            // Grab most recent fiscalPeriod for symbol
            List<AnnualEarnings> exists = annualEarningsService.findBySymbol(symbol);

            // Check if symbol is in db
            // If exists, log
            if (!exists.isEmpty()) {
                log(...);

            // Else, process new symbol
            } else {
            
                try {
                    List<AnnualEarnings> earnings = processEarnings(symbol, percent);

                    // Filter for List<AnnualEarnings> greater than zero, add to results
                    if (!earnings.isEmpty()) {

                        // Add to results
                        earnings.forEach(e -> results.add(e));
                    }
                } catch (Exception ex) {
                    logInfo("runAEInitial() - Exception: ", ex);
                }
            }
        });

        return results;
    }

    // For one symbol, parse, save, update initial earnings
    private List<AnnualEarnings> processEarnings(String symbol, BigDecimal percent) {

        // Create Url
        ...

        // Grab api response
        String response = "";
        try {
            response = CommonUtils.callApi(url);
        } catch (IOException ex) {
            log(...);
        }

        // Catch empty json object returned from API
        if (response.length() == 2) {
            return Collections.emptyList();
        }

        // Parse json from api, save to db
        parseAndSaveEarnings(response);

        // Update AnnualEarnings Objects
        updateEarnings(symbol);

        // From db, grab symbol's eearnings for last 4 quarters
        List<AnnualEarnings> updatedEarnings = annualEarningsService.findBySymbol(symbol);

        return filterByDiffPercent(updatedEarnings, percent);
    }
    
    
    // Update previous, diff values of AnnualEarnings objects just created
    private void updateEarnings(String symbol) {

        // Grab all objects from db for symbol
        List<AnnualEarnings> inList = 
                annualEarningsService.findBySymbol(symbol);

        // Update prev & diff values of incoming AnnualEarnings object
        for (int i = 1; i < inList.size(); i++) {

            // Existing object fiscalEndDate
            LocalDate fiscalEndDate = inList.get(i - 1).getFiscalEndDate();

            // Existing object fiscalPeriod
            String fiscalPeriod = inList.get(i - 1).getFiscalPeriod();

            // Get original
            BigDecimal current = inList.get(i - 1).getActualEPS();

            // Get previous
            BigDecimal prev = inList.get(i).getActualEPS();

            // Get diff 
            BigDecimal diff = current.subtract(prev);

            // Get diffPercent
            BigDecimal diffPercent = (diff.divide(prev, 4, RoundingMode.HALF_EVEN)).movePointRight(2);
            
            // Align num signs
            diffPercent = CommonUtils.alignSigns(diff, diffPercent);

            // Update
            annualEarningsService.update(symbol, fiscalEndDate, fiscalPeriod, prev, diff, diffPercent);
        }
    }

    // Parse json string returned from API, save to db
    private void parseAndSaveEarnings(String str) {

        // Create parser
        JSONParser parser = new JSONParser();
        JSONObject fullObj;

        try {
            // Full json object
            fullObj = (JSONObject) parser.parse(str);

            // First layer
            JSONArray earningsArray = (JSONArray) fullObj.get("earnings");

            // Symbol
            String jsonSymbol = fullObj.get("symbol").toString();

            // Loop through earnings for one symbol
            earningsArray.forEach((earningsJson) -> {

                logInfo("", "");

                // Create jsonobject
                JSONObject jsonObject = (JSONObject) earningsJson;

                // Grab values
                String jsonFiscEndDateStr = jsonObject.get("fiscalEndDate").toString();
                
                LocalDate jsonFiscEndDate = CommonUtils.convertStringToLocalDateAnnualEarnings(jsonFiscEndDateStr);
                
                String jsonFiscPer = jsonObject.get("fiscalPeriod").toString();

                // Create update object
                AnnualEarnings e = new AnnualEarnings();

                // Grab data, set properties
                String actEps = jsonObject.get("actualEPS").toString();

                e.setFiscalEndDate(jsonFiscEndDate);
                e.setSymbol(jsonSymbol);
                e.setActualEPS(new BigDecimal(actEps));
                e.setFiscalPeriod(jsonFiscPer);

                // Save 
                annualEarningsService.save(e);
           });
        } catch (ParseException ex) {
            logSevere("parseAndSaveAEarningsInitial()", ex.getMessage());
        }
    }
    
    // Filter objects with earnings difference > 25%
    private List<AnnualEarnings> filterByDiffPercent(List<AnnualEarnings> updatedEarnings, BigDecimal percent) {

        // Hold results
        List<AnnualEarnings> results = new ArrayList<>();

        
        // Loop through objects, filter out objects with diffPercent > 25
        updatedEarnings.forEach((earn) -> {
            
            // THIS IS WHERE I FIND THAT ENTITY OBJECTS RETURNED HAVE NOT BEEN
            //  UPDATED
            logInfo("filterByDiffPercent() - earn: ", earn.toString());

            BigDecimal diffPercent = earn.getDiffPercent();

            if (diffPercent != null && diffPercent.compareTo(percent) == 1) {
                results.add(earn);
            }
        });
        return results;
    }
}

【问题讨论】:

  • EarningsServiceImpl 类 AutowiresincomeDao 并调用其方法。 EarningsServiceImpl 中的 update() 有@Transactional,但类没有。
  • 显示调用这些方法的类(或其相关部分)。您还缺少 @Transactional 方法中的 @Transactional 属性,您需要添加该属性。
  • 我从 AEarningsServiceImpl 中删除了 Transactional,添加了传播,但我得到了相同的结果。
  • 不要同时做两件事。只需从 AEarningsServiceImpl 中删除 Transactional。我建议不要使用 Propagation.REQUIRES_NEW 因为:codete.com/blog/spring-transaction-propagation-modes
  • 你的代码真的很大,我很难理解,伙计。例如。 AnnualHelper 多次调用 findBySymbol 方法。它可以重构很多并且变得更清洁(根据我的说法)。如果可能的话,你可以把所有的代码放到一个公共的 Repository (github/bitbucket) 中分享出来,这样我就可以很容易地在本地设置。

标签: java spring-boot hibernate spring-mvc jpa


【解决方案1】:

为了能够使用注解@Transactional,您需要使用@EnableTransactionManagement 注解或XML 模拟配置您的Spring:&lt;tx:annotation-driven/&gt;。 并正确初始化 TransactionManager bean。

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/EnableTransactionManagement.html

因为 Spring @Transactional 注释通过 Aspects 起作用。

【讨论】:

  • @EnableTransactionManagement 在 Spring boot 中是可选的,前提是在 classpath 中找到 spring-data* 或 spring-tx
  • 因为我没有使用spring-data-*,所以我在我的数据配置类中添加了@EnableTransactionManagement。我将处理 TransactionManager bean
【解决方案2】:

更新:

不要在服务和回购方法中都使用@Transactional,而是只保留在一个地方。

从服务方法中移除@Transactional。在 repo impl 的 findBySymbol 和 update 方法中保留 @Transactional。


显示服务类之前的初始观察:

这是因为 Hibernate 在同一会话中使用缓存。 (默认L1缓存)

为了强制 L1 缓存刷新,您可以使用 Session 的 refresh(Object) 方法

来自 Hibernate 文档,

Re-read the state of the given instance from the underlying database. It is inadvisable to use this to implement long-running sessions that span many business tasks. This method is, however, useful in certain special circumstances. For example

 - where a database trigger alters the object state upon insert or update
 - after executing direct SQL (eg. a mass update) in the same session
 - after inserting a Blob or Clob

【讨论】:

  • 他很可能在使用同一个会话
  • 如果事务边界设置不正确,这可能是问题的根源,但仅使用 refresh() 不是解决方案,因为这是一个设计问题,您需要查看完整代码和所有事务边界以设计正确的代码流。
  • @Kayaman 同意了。提问者需要分享完整代码
  • 使用 AEarningsServiceImpl 更新。
  • @chocalaca 也请先显示您调用更新方法的类/方法,然后是 findBySymbol
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-31
  • 1970-01-01
  • 2017-09-28
相关资源
最近更新 更多