sql

All things SQL and JDBC...
athomasoriginal 2021-03-06T16:27:30.119800Z

Hello. I ran into a situation where my tests were failing because rollback was not happening. Digging into the code, I realized I was nesting transactions which, according to my readings, is not a great idea. The code roughly looks like this:

;; the application code
(defn create-product!
  [db ,,,]
  (jdbc/with-transaction [tx dbs]
   (do-thing-1 tx)
   (do-thing-2 tx)
   (do-thing-3 tx)))

;; the test
(deftest test-product! 
  (testing "create product"
    (jdbc/with-transaction [tx testdbs {:rollback-only true}]
      ;; generate mock data 
      (create-product! tx))))
      ;; assertions
What are some patterns that others use for dealing with this?

seancorfield 2021-03-06T17:42:00.121300Z

There's a dynamic var you can bind to either disallow or ignore nested TX. In your case you want it to ignore nested TX.

1
athomasoriginal 2021-03-06T17:55:03.123Z

In general, what is the advise on where to use these transactions? I added mine at what I would call the “application” layer, but maybe the transactions are better in general in the handler? Perhaps this is an architecture question though. :thinking_face:

seancorfield 2021-03-06T18:05:51.124Z

@tkjone I almost never use transactions. I use them only if I have a specific sequence of mutations that I need to either all succeed or all fail. In practice, that is pretty rare.

athomasoriginal 2021-03-06T18:21:10.124100Z

hmmmm. Okay, I will take note of this. In my use case, it’s being used after a successful sale where a order is generated, cart cleared etc. I wouldn’t want one to succeed while the others fail. would you say this is a valid use case? (apologies for the limited information)

seancorfield 2021-03-06T18:23:56.124300Z

Yeah, e-commerce is sometimes a good case for a transaction. I tend to point folks to https://ieeexplore.ieee.org/document/1407829

seancorfield 2021-03-06T18:24:41.124600Z

Transactions work well "in the small" but they don't scale well (and can't be nested in JDBC)

seancorfield 2021-03-06T18:25:30.124800Z

clojure.java.jdbc slightly ignored nested TX (so your test code would likely have worked with c.j.j) but it also meant that accidental nesting were not detectable.

seancorfield 2021-03-06T18:26:15.125Z

next.jdbc takes the "do what I say" approach and that's why it specifically documents use named and unnamed save points on connections instead of simply relying on transactions.

seancorfield 2021-03-06T18:26:52.125200Z

And it's also why you can tell it to throw an exception if a nested TX is attempted or to simply ignore nested TX, just like c.j.j silently did.

seancorfield 2021-03-06T18:28:39.125500Z

In general, even in e-commerce, I tend to explicitly control mutations, with try/`catch` and manually undo things if part of a "transaction" fails -- that way I can control all the logging (as in, to the database) separate from the actual e-commerce actions.

seancorfield 2021-03-06T18:29:18.125700Z

It's more work, but it gives you more control, and you can see a "paper trail" in the (logging tables) of what was attempted and what failed.

seancorfield 2021-03-06T18:30:20.125900Z

After all, what happens if you charge the customer and then fail to insert the records in your local DB? Or you've performed some remote actions and then others fail? Or your e-commerce process is a mixture of several remote interactions and several local database ones?

seancorfield 2021-03-06T18:30:49.126100Z

None of those scenarios can be handled just be JDBC transactions.

seancorfield 2021-03-06T18:32:11.126300Z

It's why, at work, we have processes that reconcile remote e-commerce logs (PayPal, Braintree, etc) with what ends up in our DB, so we can detect "orphan" transactions (in the financial sense) caused by partial failures during the overall process.

mg 2021-03-06T22:47:14.128600Z

Just want to say, I used c.j.jdbc for a long time, but started switching to next.jdbc in recent projects, and absolutely loving it. Reducing over plan is super powerful. I haven't dived into the nav/datafy parts yet but very excited about it

mg 2021-03-06T22:51:35.131300Z

(So far the one thing that I haven't liked is the way connection/component works. Having to call the resulting component as a function to get the datasource is a bit awkward, as it means callers need to be aware of the difference between using that or using something that has value semantics. I worked around this with my own version:

(defn new-hikari-connection-pool
  "Returns new hikari connection pool component from db spec."
  [db-spec]
  (let [;; Hikari wants :username key rather than :user
        db-spec (set/rename-keys db-spec {:user :username})]
    (with-meta {}
      {`component/start
       (fn [_]
         (let [pool (connection/->pool HikariDataSource db-spec)]
           (reify
             component/Lifecycle
             (start [this] this)
             (stop [this]
               (.close pool)
               (new-hikari-connection-pool db-spec))

             p/Sourceable
             (get-datasource [_] pool))))})))

mg 2021-03-06T22:58:36.133100Z

At the same time probably this reflects even better on the core design - the fact that central next.jdbc protocols made this so easy

seancorfield 2021-03-06T23:30:09.135100Z

@michael.gaare Glad you're liking it. I would note that a connection pool is a DataSource already so perhaps implementing p/Connectable would be more natural here? The function call idiom is something we've been using extensively at work where Component-compatible things are callable to return the thing they wrap and we really like that.

lukasz 2021-03-07T18:00:37.159500Z

@seancorfield this sounds really interesting - do you have some sample code you could share?

seancorfield 2021-03-07T18:02:42.159700Z

@lukaszkorecki Beyond what's in next.jdbc you mean?

lukasz 2021-03-07T19:59:16.159900Z

@seancorfield yes, just the idea of making "where Component-compatible things are callable to return the thing they wrap and we really like that."

seancorfield 2021-03-07T20:17:00.160100Z

OK, some examples from our code base at work: 1. our configuration Component: reads and merges EDN files at startup; function invocation acts like key access into the config: (cfg :messaging :datasource) 2. our JWT Component: reads a key file at startup and generates a secure seed for our tokens; function invocation returns the underlying seed 3. our Redis pooling library: creates and starts a Jedis Pool at startup; function invocation returns the pool 4. our HTTP server Component: starts the web server and compiles the routes; function invocation decodes the route and runs the associated handler ... and many, many more

lukasz 2021-03-07T20:22:54.160300Z

Gotcha - this is really cool, it fixes the issue we have in components, where you have to know the internal structure to dig out the wrapped object/connection/etc. Than you for the details!

seancorfield 2021-03-07T20:25:48.160500Z

And it's nice that a function can carry metadata so your component start can return a fn with metadata for stop.

seancorfield 2021-03-07T20:26:30.160700Z

(which is specifically what next.jdbc does for Component for the pooled datasource)

lukasz 2021-03-08T15:28:51.160900Z

Indeed :thumbsup: I'll study the component function in next.jdbc a bit more, it does open some interesting possibilities

mg 2021-03-06T23:36:42.141300Z

Having a component that implements p/Sourceable works well in my case because I inject into other service-level components. Those components then use it directly (jdbc/plan datasource ...) or (jdbc/execute-one! datasource ...) or whatever. For that pattern it's nice to be able to just inject in a db-spec map as a datasource for dev/testing, and have that work exactly the same as the connection pool

seancorfield 2021-03-06T23:37:36.141800Z

Fair enough. We use connection pools in dev so the same code works everywhere for us.

mg 2021-03-06T23:40:07.146900Z

I can see the advantages of the unwrap-with-function pattern for components more broadly though. Not everything has a nice set of protocols to hook into

seancorfield 2021-03-06T23:45:18.152300Z

I just announced com.github.seancorfield/next.jdbc {:mvn/version "1.1.643"} (in #announcements) and I wanted to call out the GraalVM compatibility change here in particular: I've copied next.jdbc.prepare/execute-batch! to next.jdbc/execute-batch! and would encourage anyone using execute-batch! to switch over to the next.jdbc version. The original next.jdbc.prepare version ended up depending on next.jdbc.result-set (which already depended on next.jdbc.prepare) and to break that circular dependency, I used requiring-resolve at runtime. Clearly, execute-batch! should never have been added to next.jdbc.prepare in the first place -- a bad decision on my part -- but in order to avoid breaking changes, I have modified next.jdbc.prepare/execute-batch! to rely on a volatile! Var that is set up in next.jdbc.result-set -- this is a workaround suggested by @borkdude as a way to get GraalVM compatibility when you would otherwise have a dynamic runtime dependency. It shouldn't be a breaking change, since you would normally require next.jdbc to call prepare to get a PreparedStatement object which is what execute-batch! requires, and next.jdbc requires next.jdbc.result-set so the new Var should always be set up. However, any code that somehow managed to not cause next.jdbc.result-set to be required and yet was requiring next.jdbc.prepare and calling execute-batch! may now fail. Like I say, I don't think any real world code could call execute-batch! without somehow causing next.jdbc.result-set to be required but I wanted to post this caution, just in case you update to 1.1.643 and your code breaks.

athomasoriginal 2021-03-06T23:48:06.154800Z

Thanks, Sean! That was a great response and I see the approach you take makes more sense in many ways :man-bowing:

seancorfield 2021-03-06T23:49:37.156100Z

In addition, I'm double-publishing all my libraries to both the existing seancorfield group and the new com.github.seancorfield group so that I can switch them all over to verified group names per the recently-announced Clojars policy. depstar and clj-new already started doing that last week (and the depstar README now explains how I do it). I plan to keep the double-publishing going until download stats on Clojars indicate dropping the non-verified group will cause very little inconvenience -- but, obviously, I'd like folks to switch over to using the new group whenever they next update their dependencies. Any new libraries I publish will be under com.github.seancorfield.

seancorfield 2021-03-06T23:52:27.156200Z

It's tricky because whenever we learn about SQL, RDBMS, and JDBC, it is always emphasized how important transactions are to data integrity -- but the real world has grown a lot more complicated and a simple, in-database transaction just isn't sufficient a lot of the time these days (which in turn also makes our lives so much more complicated too!).

➕ 1
athomasoriginal 2021-03-06T23:57:12.156500Z

You mentioned “logging tables”. How do you mean?

athomasoriginal 2021-03-06T23:57:54.156700Z

And in your model, if something fails, and you have the logs, at what point do you course correct?