简体   繁体   English

如何避免 jdbc postgres 的多个连接

[英]How to avoid multiple connections for jdbc postgres

I need to query a postgresql database based upon the result of the previuos query.我需要根据之前的查询结果查询一个postgresql database Right now I have for each query a new DriverManager connection and this seems not like the best practise to me (I'm new to Java).现在我为每个查询都有一个新的DriverManager连接,这对我来说似乎不是最佳实践(我是 Java 新手)。

public static Integer item(DataModel params) {

    Psqlcon psql = new Psqlcon();
    
    try (Connection con = DriverManager.getConnection(psql.url, psql.user, psql.password);
         PreparedStatement pst = con.prepareStatement("SELECT * FROM table1 WHERE var=? "
        pst.setObject(1, params.getID());
       
        ResultSet find_id = pst.executeQuery();{

            while (find_id.next()) {

                id = find_id.getInt(1);
            }
            pst.close();
        }

    } catch (SQLException ex) {

        Logger lgr = Logger.getLogger(something.class.getName());
        lgr.log(Level.SEVERE, ex.getMessage(), ex);
    }

    try (Connection con = DriverManager.getConnection(psql.url, psql.user, psql.password);
             PreparedStatement pst = con.prepareStatement("SELECT something FROM table2 WHERE id=? "
            
        pst.setObject(1, id);
       
        ResultSet find_next = pst.executeQuery();{

            while (find_next.next()) {

                id2 = find_next.getInt(1);
            }
            pst.close();
        }
   
        ....on so on

This is an oversimplified example.这是一个过于简单的例子。 The queries are actually a bit more complex.查询实际上要复杂一些。 Wouldn't it be better to have one connection and close only the prepareStatement ?有一个connection并只关闭prepareStatement不是更好吗? How do I accomplish to have only one connection?如何实现只有一个连接?

Your question boils down to state tracking.您的问题归结为 state 跟踪。 Tons of your code across the entire breath of the project will want to do stuff with the DB, and DBs are fundamentally session oriented concepts.整个项目中的大量代码都需要使用 DB,而 DB基本上是面向 session 的概念。 Thus, you will need to change your code flow – any code anywhere that needs to do anything with the DB can't just go: "Eh, fine, I'll go make a connection then".因此,您将需要更改您的代码流——任何需要对 DB 执行任何操作的代码不能只是 go:“嗯,好吧,我将 go 建立连接”。 It needs to get an existing connection provided to it, because 'just make new connections every time' doesn't work.它需要获得提供给它的现有连接,因为“每次都建立新连接”是行不通的。 Not just because that is quite slow, but primarily because transactions are a thing.不仅因为这很慢,而且主要是因为交易是一回事。

This is where injection frameworks come in: They are all about having state that needs to exist in loads of places in the project.这就是注入框架的用武之地:它们都是为了让 state 存在于项目的大量位置。 But, you can also do it 'by hand', so to speak: Pass a Connection object around as method parameter.但是,您也可以“手动”完成,可以这么说:传递一个Connection object 作为方法参数。

You have a few code smells in this code.这段代码中有一些代码异味。 These issues are leading you to this question.这些问题正在引导您提出这个问题。

Exception handling异常处理

'when an exception occurs, print some stuff to a log and just keeps going' is what your code does now. “发生异常时,将一些内容打印到日志并继续运行”是您的代码现在所做的。 This is, hopefully now obvious given that I put it that way, a really bad idea.鉴于我是这样说的,希望现在很明显,这是一个非常糟糕的主意。 A method that fundamentally does DB things should be declared to throws SQLException .应该声明一个从根本上做数据库事情的方法throws SQLException The entire 'code flow' (every method that ends up being called to perform a particular job) needs to be methods that ALL are declared as throws SQLException , at which point you can get rid of all your try/catch blocks.整个“代码流”(最终被调用以执行特定作业的每个方法)都需要是ALL声明为throws SQLException的方法,此时您可以摆脱所有 try/catch 块。 You'll be reducing your code LOC by quite a bit whilst you make the code better!在使代码变得更好的同时,您将大大减少代码 LOC! Yay!耶!

You want 'an exception' to do many things:您希望“例外”做很多事情:

  • Stop execution.停止执行。 You don't want code to continue when you're in a state you don't understand.当您在您不理解的 state 中时,您不希望代码继续运行。 SQLException has a ton of reasons for it, hence, you have no idea what happened. SQLException 有很多原因,因此,您不知道发生了什么。 Carrying on is bad.继续下去是不好的。 One easy fix is to have the catch block rethrow something ( throw new RuntimeException("uncaught", e) - you should update your IDE templates, this is the correct default catch block content), but even better is to just let the SQLException happen.一个简单的解决方法是让 catch 块重新抛出一些东西( throw new RuntimeException("uncaught", e) - 你应该更新你的 IDE 模板,这是正确的默认 catch 块内容),但更好的是让 SQLException 发生.
  • You want to save the relevant info as much as possible: The stack trace, the causal chain, the exact exception type, the message, and any special info that the exception object has - SQLException has lots of this, such as the postgres error state number which is very important, and is not always included in the message.您希望尽可能多地保存相关信息:堆栈跟踪、因果链、确切的异常类型、消息以及异常 object 具有的任何特殊信息 - SQLException 有很多这样的信息,例如 postgres 错误 state非常重要的数字,并不总是包含在消息中。

Just letting everything throw SQLException does all that - there will be very few places (all the top level calls) where you can write code that deals with it, instead of writing 83951 times lgr.log(Level.SEVERE) .只是让所有东西都抛出 SQLException 就可以完成所有这些 - 很少有地方(所有顶级调用)可以编写处理它的代码,而不是编写 83951 次lgr.log(Level.SEVERE)

Connections are created every time每次都会创建连接

You don't want to do that - you want the connection to be passed along.您不想这样做 - 您希望传递连接。 In general you want a more robust setup to track transactions.通常,您需要更强大的设置来跟踪交易。 All DB everythings are always in transactional form.所有数据库的一切都始终处于事务形式。 Even if you have autocommit true (that's why it is called 'AutoCommit' and not 'transactionless'): That just means the DB assumes you meant to db.commit() after every query.即使你有 autocommit true (这就是为什么它被称为'AutoCommit'而不是'transactionless'):这只是意味着数据库假设你打算在每次查询后执行db.commit()

Even read queries are transactional.甚至读取查询也是事务性的。 Imagine a bank that needs to hand out a bank guarantee for buyout by a partnership.想象一家银行需要为合伙企业的收购提供银行担保。 To do so, it needs to sum up the balance of every member of the partnership.为此,它需要总结合伙企业中每个成员的余额。 You could simply do:你可以简单地做:

int sum = 0;
for (Partner p : partners) {
  try (var pst = con.prepareStatement("SELECT balance FROM table2 WHERE id=?")) {
    ... 
    sum += rs.getInteger(1);
  }
}

return sum > requiredGuarantee;

However, the above code is subtly broken .但是,上面的代码被巧妙地破坏了 Imagine that right in the middle of this code, one of the partners transfers a large sum to another partner.想象一下,就在这段代码的中间,一个合伙人将一大笔钱转移给了另一个合伙人。 You so happen to read the partner transfering the cash out before the transfer, but read the balance of the receiving partner after it.您碰巧在转账之前读取了将现金转出的合作伙伴,但在其之后读取了接收合作伙伴的余额。 Your code now has the wrong sum.您的代码现在有错误的总和。 It things the total amount in the accounts of all the partners is much higher than it really is, The bank gives the guarantee, the deal falls through.事情是所有合伙人账户中的总金额远高于实际金额,银行提供担保,交易失败。 and the bank just lost 10 million dollars.银行刚刚损失了1000万美元。 Oops, Quite the bug?哎呀,真是个bug? isn't it?不是吗?

The solution is transactions.解决方案是交易。 And the right transactionlevel.以及正确的事务级别。 At the right level (SERIALIZABLE is the only level that can do this correctly), if you do all these SELECTs in a single transaction, the above situation cannot occur .在正确的级别(SERIALIZABLE 是唯一可以正确执行此操作的级别),如果您在单个事务中执行所有这些 SELECT,则不会发生上述情况。 Postgres will let you query and sum up as normal, and you may indeed get the wrong sum, but the .commit() call (and note that in this transaction we have not written anything at all yet!) will fail . Postgres 会让你像往常一样查询和总结,你可能确实得到了错误的总和,但是.commit()调用(请注意,在这个事务中我们还没有写任何东西!)会失败 That's your cue to start over.这是你重新开始的提示。

Connections are at the same level as the transaction: If you want to run 2 transactions simultaneously, you must have 2 connections.连接与事务处于同一级别:如果要同时运行 2 个事务,则必须有 2 个连接。 A single connection is 'in' a transaction.单个连接在事务中。 Hence, passing the Connection object around is the same as passing the transaction around, and you must have transactions as the above code shows (imagine that the bank guarantee ends up being represented by INSERTing a new row in the 'guarantees' table. You want it all in a single transaction, so that after all that, you .commit() and postgres will throw you an exception (and that row you wrote never happened) if the summing wasn't consistent due to some other transaction having occurred in between that changed the balance of one or more partners.因此,传递连接 object传递事务相同,并且您必须具有如上面代码所示的事务(想象银行担保最终由在“担保”表中插入新行来表示。你想要这一切都在一个事务中,所以毕竟,如果由于在两者之间发生了一些其他事务而求和不一致,那么你.commit()和 postgres 会抛出一个异常(你写的那一行从未发生过)这改变了一个或多个合作伙伴的平衡。

That's the only correct way to write a 'consistent' data model.这是写入“一致”数据 model 的唯一正确方法。

For certain projects, the cost of consistency is too high.对于某些项目,一致性成本太高。 However, unless your name is literally google, you won't ever get there, and if you must have this, it's way more complicated than just picking a lower transaction isolation level.然而,除非你的名字是真正的 google,否则你永远不会到达那里,如果你必须拥有这个,它比仅仅选择较低的事务隔离级别要复杂得多。 So, go for the consistency.所以,为了一致性,go。

It's up to you if you want Connection con to be the first parameter of all your db-based methods, or if you want to create an object and have a field of type 'con' (but remember, they HAVE to be closed, so either the code that creates such an object has to use try-with-resources to close that connection, or your class that has a field Connection con needs to be AutoClosable , and has to be created with try-with-resources.) - or if you want to use an injection framework like spring or dagger or guice.如果您希望Connection con成为所有基于 db 的方法的第一个参数,或者您想创建一个 object 并拥有一个类型为“con”的字段,这取决于您(但请记住,它们必须被关闭,所以创建此类 object 的代码必须使用 try-with-resources 来关闭该连接,或者您的 class 具有字段Connection con需要为AutoClosable ,并且必须使用 try-with-resources 创建。) - 或如果你想使用像 spring 或 dagger 或 guice 这样的注入框架。

Use an abstraction使用抽象

JDBC is primary designed to expose every feature that every relational DB system could possibly have, and to be a consistent 'bottom layer'. JDBC 主要设计用于公开每个关系数据库系统可能具有的每个功能,并成为一致的“底层”。 This means that as an API to directly program an app on, it's horrible .这意味着作为一个 API 直接在其上编写应用程序,这太可怕了 It's not meant for you.它不适合你。

Use a library that was meant for such a purpose.使用为此目的设计的库。 It'll be built on top of JDBC.它将建立JDBC 之上。 You're doing the equivalent of writing direct machine code, effectively.您正在有效地编写直接机器代码。 That's rarely a good idea.这很少是一个好主意。

You can use JPA (Object Persistence Frameworks) like Hibernate, but that's a whole new can of worms.您可以使用 JPA(对象持久性框架),如 Hibernate,但这是一个全新的蠕虫罐。 If you like SQL, the tools you are looking for are JOOQ or JDBI .如果您喜欢 SQL,您正在寻找的工具是JOOQJDBI These mean your complicated:这些意味着你的复杂:

pst.setObject(1, id);
       
        ResultSet find_next = pst.executeQuery();{

            while (find_next.next()) {

                id2 = find_next.getInt(1);
            }
            pst.close();

all turn into simple one-liners.都变成了简单的单行。

connection pooling连接池

Creating a connection object is really expensive.创建连接 object 真的很昂贵。 It's an inevitable cost you must pay if you want multiple simultaneous transactions, but you don't really want to pay it if you have multiple sequential ones.如果您想要多个同时交易,这是您必须支付的不可避免的成本,但如果您有多个连续交易,您真的不想支付它。 It depends a bit on the DB engine and JDBC driver (some even have a baked-in pooling system, some don't).这在一定程度上取决于 DB 引擎和 JDBC 驱动程序(有些甚至有内置的池系统,有些则没有)。 But one easy solution is to add a layer of abstraction: Instead of asking the DriverManager to get you a connection, you can ask a connection pooler.但一个简单的解决方案是添加一个抽象层:您可以请求连接池,而不是要求 DriverManager 为您获取连接。 It gives you a virtualized connection object that passes through all calls, except for close() , which doesn't actually close the connection, but simply invalidates the virtual object and passes the actual connection back into 'the pool'.它为您提供了一个虚拟化连接 object ,它通过所有调用,除了close() ,它实际上并没有关闭连接,而只是使虚拟 object 无效并将实际连接传递回“池”。

This is not necessary until you get quite a bit of traffic, but if you want to be fully ready, it's something you can look at.在您获得大量流量之前,这不是必需的,但是如果您想做好充分的准备,那么您可以看看它。 Libraries like HikariCP make this easy.HikariCP这样的库使这变得容易。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM