Rob, The transactions are not global, the transactions are local. Connections are global and there's no way around it with the constraints of a traditional RDBMs. Your idea of making the global connection unavailable for code that's in the context of a transaction would prevent the errors I'm trying to prevent but you would still required to pass the connection around, which in a webapp, means that the whole chain of function calls, pretty much everything, would have a database connection. That is ugly in some cases, impossible in others.
A piece of code that would be impossible is to enclose each test in a transaction using clojure.test: http://stackoverflow.com/questions/31735423/how-to-pass-a-value-from-a-fixture-to-a-test-with-clojure-test Furthermore, I don't think this solution gains you any purity, you still have a global estate and you are hiding it away when starting a transaction. My proposal was to make it work instead of hiding it. They are rather equivalent from the complexity point of view. On 1 August 2015 at 18:29, Rob Lally <rob.la...@gmail.com> wrote: > Hey Pablo, > > I could be wrong, but it seems that the key problem here is the existence > of the global transaction. If the global transaction didn’t exist then any > time you failed to pass in a transaction the code would fail: immediately > and loudly. > > I appreciate what you’re trying to do but it seems like you’re on a path > to solve the problems caused by one shared, implicit, global variable by > creating more shared, implicit, slightly less global, variables and that > doesn’t seem like it is going to end well. Implicit connections in a > binding will fail in, perhaps mysterious ways if you ever include any sort > of concurrency: even things as simple as asynchronous logging can start > logging wrong values or missing values. > > Can you somehow render the global connection inoperable in some way? > Perhaps redefine it, point it at a data source that doesn’t exist or… by > some other hook-or-crook have it fail in a loud, grotesque manner if it is > touched? > > > R. > > > > > On 31 Jul 2015, at 01:54, J. Pablo Fernández <pup...@pupeno.com> wrote: > > Hello James, > > Thanks for your answer. I do understand your point. Pure functions are > easier to reason about and my use of dynamic here breaks that purity. I'm > not doing it lightly. It already happened to me, that one of those > functions that was running inside the transaction, was not passed the > transaction connection and instead got the global one and the failure was > silent and very hard to debug, and this was with a project that has less > than 200 lines of code. I'm trying to find patterns that will work when > this project has 200k lines of code. > > For me, the thing is, I have a traditional relational database here, this > is already far from pure. For example, calling (db/create-user " > pup...@pupeno.com") twice will not only not return the same thing the > second time, it'll actually raise an exception the second time. Also, the > database connection is *global state* unless each function creates its own > connection, which would be terrible. So, this global state also breaks > functional purity. > > The problem with the second aspect of breaking purity as far as I can see > is this: at some point, this global state has to be picked up and used, so > at some point a function will *not* get a database connection passed to it > but *will* access the database by using this global connection. I haven't > advanced this project enough to say this with 100% certainty, but, I think > there's going to be more than one function like that and at some point I'll > need to have one inside the other so I need them to be composable. Let me > show you a naive example: > > db/create-user is the low level database function that creates a record in > the user table > user/create is the function used to create a user, it takes care of, for > example, encrypting the password. > account/register is the function to register a new user, it takes care of > creating a user but also validation, sending a welcome email and so on. > > So each function calls the predecessor there and would pass the database > connection, account/register, being the entry point, would grab it from the > global state so it doesn't get a connection passed to it. So far, a lot of > it looks like pure functions (let's ignore the fact that a database breaks > that purity). The problem arises when I get another function, > account/invite, that is used to register a bunch of people one after the > other, so that account/invite would call account/register many times. The > problem is that account/invite *can't* start a transaction and have > account/register and all its inner functions use that transaction when that > makes a lot of sense. > > To make account/register composable it needs to accept an optional > database connection and use that one if it's present, or the global one if > it's not. Every time a function does that there's a high risk of picking > the wrong database and account/invite and account/register shouldn't be > dealing with database connection management. That feels to me like lower > level details leaked into higher level abstractions. > > Now, I know this is a naive example and you could push the grabbing of the > global connection higher and higher, as long as the example is naive and > simple like this, but it does represent what in my experience is the > reality of web application development at least in another languages and I > haven't seen anything to make me think Clojure will be radically different > here (at least when using a patterns such as compojure). > > So yes, it's not purely function but with a database that's already > impossible and if I wanted purely functional I would probably be using > Haskell instead of Clojure. What I like about Clojure is this: > > Clojure is a practical language that recognizes the occasional need to >> maintain a persistent reference to a changing value and provides 4 distinct >> mechanisms for doing so in a controlled manner - Vars, Refs, Agents and >> Atoms. > > > I'm just trying to be practical here. But I'm new and I'm not sure if an > atom that is a dynamic var has some hidden issues that I'm not seeing > (other than the fact of it being state that changes and that I have to > manage explicitly because the language is not protecting me from shooting > myself in the foot with it). > > Does it make sense? > > > > On 31 July 2015 at 03:16, James Reeves <ja...@booleanknot.com> wrote: > >> On 31 July 2015 at 01:44, J. Pablo Fernández <pup...@pupeno.com> wrote: >>> >>> I found passing around the database connection to each function that >>> uses it very error prone when you are using transactions as passing the >>> wrong one could mean a query runs outside the transaction when in the >>> source code it is inside the with-db-transaction function. So I ended up >>> defining the db namespace like this: >>> >>> (ns db) >>> >>> (defonce ^:dynamic conn (atom nil)) >>> >>> (defn connect! >>> (reset conn (generate-new-connection))) >>> >>> (defn run-query >>> [query] (run-query query @conn) >>> [query conn] (run-the-query-in-connection query conn)) >>> >> >> This style of code is generally considered to be unidiomatic in Clojure. >> The reason for this is that it significantly increases complexity, and >> Clojure is about reducing complexity where possible. >> >> Consider a function like: >> >> (defn find-by-id [conn id] >> (sql/query conn ["SELECT * FROM foo WHERE id = ?" id])) >> >> The output of this function is affected by its arguments (and by the >> state of the database the connection is associated with), which is passed >> by its caller. >> >> Now consider a function like: >> >> (defn find-by-id [id] >> (sql/query @conn ["SELECT * FROM foo WHERE id = ?" id])) >> >> The output of this function is affected by its arguments... and by >> anything that touches the global conn var, which could literally be >> anything in your program, in any namespace, in any function, in any thread. >> >> The more ways in which a function has, the more "complex" it is. This is >> why Clojure prefers immutable data over mutable data, and why function >> arguments are generally preferred over dynamic vars. >> >> The problem of accidentally calling a database connection directly inside >> a transaction is a difficult one, but I don't think the solution is to add >> more complexity. An alternative solution would be to take the original >> database connection out of scope, by moving your transaction code to a >> separate function: >> >> (defn do-things* [tx] >> (do-foo tx) >> (do bar tx) >> (do baz tx)) >> >> (defn do-things [db-spec] >> (sql/with-db-transaction [tx db-spec] >> (do-things* tx))) >> >> If this is still too prone to error, you could also automate this pattern >> with a function: >> >> (defn wrap-transaction [f] >> (fn [db-spec & args] >> (sql/with-db-transaction [tx db-spec] >> (apply f tx args)))) >> >> (def do-things >> (wrap-transaction do-things*)) >> >> - James >> >> -- >> You received this message because you are subscribed to the Google >> Groups "Clojure" group. >> To post to this group, send email to clojure@googlegroups.com >> Note that posts from new members are moderated - please be patient with >> your first post. >> To unsubscribe from this group, send email to >> clojure+unsubscr...@googlegroups.com >> For more options, visit this group at >> http://groups.google.com/group/clojure?hl=en >> --- >> You received this message because you are subscribed to a topic in the >> Google Groups "Clojure" group. >> To unsubscribe from this topic, visit >> https://groups.google.com/d/topic/clojure/fRi554wbPSk/unsubscribe. >> To unsubscribe from this group and all its topics, send an email to >> clojure+unsubscr...@googlegroups.com. >> For more options, visit https://groups.google.com/d/optout. >> > > > > -- > J. Pablo Fernández <pup...@pupeno.com> (http://pupeno.com) > > -- > You received this message because you are subscribed to the Google > Groups "Clojure" group. > To post to this group, send email to clojure@googlegroups.com > Note that posts from new members are moderated - please be patient with > your first post. > To unsubscribe from this group, send email to > clojure+unsubscr...@googlegroups.com > For more options, visit this group at > http://groups.google.com/group/clojure?hl=en > --- > You received this message because you are subscribed to the Google Groups > "Clojure" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to clojure+unsubscr...@googlegroups.com. > For more options, visit https://groups.google.com/d/optout. > > > -- > You received this message because you are subscribed to the Google > Groups "Clojure" group. > To post to this group, send email to clojure@googlegroups.com > Note that posts from new members are moderated - please be patient with > your first post. > To unsubscribe from this group, send email to > clojure+unsubscr...@googlegroups.com > For more options, visit this group at > http://groups.google.com/group/clojure?hl=en > --- > You received this message because you are subscribed to a topic in the > Google Groups "Clojure" group. > To unsubscribe from this topic, visit > https://groups.google.com/d/topic/clojure/fRi554wbPSk/unsubscribe. > To unsubscribe from this group and all its topics, send an email to > clojure+unsubscr...@googlegroups.com. > For more options, visit https://groups.google.com/d/optout. > -- J. Pablo Fernández <pup...@pupeno.com> (http://pupeno.com) -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en --- You received this message because you are subscribed to the Google Groups "Clojure" group. To unsubscribe from this group and stop receiving emails from it, send an email to clojure+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.