Fork me on GitHub
#beginners
<
2021-09-25
>
eyalch12:09:12

I'm trying to implement the repository pattern in Clojure but not sure how. Could someone please shed some light?

delaguardo12:09:04

Clojure's hash-map together with get, assoc, dissoc and update functions can be used directly to mimic repository pattern. Do you need some specific logic that is not possible to express using it?

eyalch12:09:54

The repository pattern I'm referring to is the one which has to do with having an interface with all the possible persistence operations for a given entity. In Golang for example:

type UserRepository interface {
    Create(user User)
    Get(id int)
    Update(id int, user User)
    Delete(id int)
}

eyalch12:09:20

I might have 2 different implementations for the same repository interface, e.g. one which stores users in memory and another which uses a relational database.

delaguardo12:09:06

Then look at defprotocol and it's companion functions

☝️ 3
eyalch12:09:30

Will do. Thanks!

emccue13:09:02

so for me, I would start with

emccue13:09:52

(ns proj.persistence.user)

(defn create [db user]
  ...)

(defn get [db id]
  ...)

(defn update [db id user]
  ...)

(defn delete [db id]
  ...)

πŸ‘ 3
emccue13:09:19

just a namespace that directly has the logic for performing this stuff given a relational database

emccue13:09:57

and then from there if you want to truly have a part of the code where you don't want to pass the db connection

emccue13:09:52

(defprotocol UserRespository
  (create [_ user]
    ...)
  (get [_ id]
    ...)
  (update [_ id user]
    ...)
  (delete [_ id]
    ...))

emccue13:09:23

you can make a protocol and then an instance by using partial on the namespace functions

emccue13:09:29

(defn from-db [db]
  (reify UserRespository
    (create [_ user]
      ...)
    (get [_ id]
      ...)
    (update [_ id user]
      ...)
    (delete [_ id]
      ...)))

emccue13:09:56

so thats a start for thinking about it

emccue13:09:06

...but we can do better

emccue13:09:17

start with this namespace

emccue13:09:19

(ns proj.persistence.user)

(defn create [db user]
  ...)

(defn get [db id]
  ...)

(defn update [db id user]
  ...)

(defn delete [db id]
  ...)

emccue13:09:34

"start simple"

emccue13:09:48

then later when you are writing your tests, put your protocol

emccue13:09:51

(ns proj.persistence.user)

(defprotocol UserRespository
  (create* [_ user]
    ...)
  (get* [_ id]
    ...)
  (update* [_ id user]
    ...)
  (delete* [_ id]
    ...))

(defn create [db user]
  (create* db user))

(defn get [db id]
  (get* db id))

(defn update [db id user]
  (update* db id user))

(defn delete [db id]
  (delete* db id))

emccue13:09:33

and then extend it to datasource to provide your actual implementation

emccue13:09:20

(ns proj.persistence.user)

(defprotocol UserRespository
  (create* [_ user])
  (get* [_ id])
  (update* [_ id user])
  (delete* [_ id]))

(defn create [db user]
  (create* db user))

(defn get [db id]
  (get* db id))

(defn update [db id user]
  (update* db id user))

(defn delete [db id]
  (delete* db id))

(extend-protocol UserRepository
   Datasource
   ...)

emccue13:09:55

maybe skipping the * functions if you want the protocol to be the public thing

emccue13:09:37

but the important thing, at least to me, is that you don't need to start making it extensible. Your public interface doesn't need to change (`(proj.persistence.user/get db 123)`) when you have that requirement

βž• 1
emccue13:09:44

does that make sense?

emccue13:09:05

it might have been too dense an explanation

eyalch14:09:42

@U3JH98J4R Thanks you very much for the detailed explanation! I'll try to start simple without a protocol and see how it goes from there

πŸ‘ 1
didibus23:09:54

Screw all that, just write normal functions inside a namespace. You don't need anything more.

SK16:09:20

hi! when I have a map with namespaced keys, is there some function that will change it into keys without namespace?

Fredrik16:09:51

The newest alpha of Clojure has a function update-keys (also easy to write one yourself), which combined with (comp keyword name) will do what you want

Fredrik16:09:08

But there's no such function in the core library.

Fredrik16:09:28

(there's qualified-keyword? if your map has keys that are not all keywords and you want to check before trying to change those)

SK16:09:43

I see! Thanks a lot!

didibus03:09:55

(into
 {}
 (map (fn [[k v]] [(-> k name keyword) v]))
 #:user{:name "John" :age 17})

didibus03:09:53

or:

(reduce-kv
 (fn [m k v] (assoc m (-> k name keyword) v))
 {}
 #:user{:name "John" :age 17})

popeye16:09:02

I have written a function with if which has 200 lines of code a year ago , As part of the enhancement now I have to put i if else condition in multiple lines and many in future, code is becoming ugly and not able to maintain uniformity in it, How can I handle such multiple if condition better in clojure ? any suggestion ? Any design pattern suggestions ?

emccue16:09:51

Name things

emccue16:09:09

if you have an if with 200 lines, can you group any of those lines by a "name"?

βž• 1
emccue16:09:27

thats a really vague suggestion, but also its a vague problem

popeye16:09:33

after adding new if else condition , need to write 10 lines in both the if and else block!

popeye16:09:43

initially though of adding defmulti , but thought of posting!

dpsutton18:09:59

Cond is made for this

SK19:09:26

is there a trim-to-nil function in the core libs? (if string has only whitespace, then return nil otherwise trim and return the contents)

Fredrik19:09:15

No. But you can check the result of clojure.string/trim with clojure.string/blank?

πŸ‘ 1
phronmophobic19:09:45

I'm curious if other languages have a trim-to-nil and how it's used.

SK19:09:55

thanks!

SK19:09:15

Java has it, handy when taking strings from user input

πŸ‘ 1
SK19:09:27

trimtonull, etc.

Fredrik19:09:06

You can define a function like

(let [s (str/trim ...)]
  (when-not (str/blank? s)
    s))

πŸ‘ 1
Ben Sless04:09:48

Won't blank check already have you covered there? Also, on older jdks strings have a isBlank method

Fredrik17:09:22

You're right, I didn't remember that str/blank? also returns true if a string contains only whitespace. Then you save potentially trimming a blank string by checking str/blank? before trimming

πŸ‘ 1
Ben Sless18:09:44

But again, on jdk >=11 you have String/isBlank

John Bradens19:09:54

Does anyone have any ideas on how to help me with this 😬 Posting here & on stack overflow to maximize potential solutions https://stackoverflow.com/questions/69328970/why-isnt-my-luminus-app-working-on-heroku

Apple20:09:17

I have the same book but not there at chap9 yet. Do you have your database config defined anywhere for production build?

Apple20:09:15

Top of page 342 says this "To actually run the app in production, all you need to do is replace dev-config.edn with the appropriate filename. By convention, prod-config.edn is usually used."

Apple20:09:26

The [dev-|test-|prod-]config.edn are not in git by default but you need to create them.

Apple20:09:48

If you create even an empty file prod-config.edn with just "{}" in it the error message will probably be different.

John Bradens21:09:00

Wow thanks. I had no idea that [dev-|test-|prod-]config.edn are not in git, so I was really confused why those files weren't doing anything or being read. I'll try creating prod-config.edn now and update

John Bradens21:09:02

Unfortunately I get the same error, strangely enough... @UP82LQR9N . I created prod-config.edn, then did git add ., git commit, git push and everything went okay on that end, but heroku run lein run migrate gives the same error saying it can't find the config.edn file, and I see the same errors show up in the webpage :thinking_face:

Apple21:09:59

config.edn is it? does the error msg spell out this file name?

Apple21:09:10

perhaps file name needs to be that

Apple21:09:26

somehow i dont see JVM_OPTS setting any config file

Apple21:09:42

if you check project.clj there's JVM_OPTS for dev and test but nothing for prod

Apple21:09:25

on page 132 and 343 both the native and docker execution JVM_OPTS is set

Apple21:09:06

the docker setup actually has this line: COPY prod-config.edn /guestbook/config.edn

Apple21:09:03

Procfile doesn't have any JVM_OPTS.

John Bradens21:09:47

Ok do you think I should try adding JVM_OPTS to the Procfile or to project.clj?

John Bradens21:09:05

I notice it is in the book that there is JVM_OPTS in the procfile for Heroku so I will try that?

Apple21:09:50

page 342 Now you can find the connection settings for the database on your Heroku dashboard. Heroku provides configuration via environment variables. It provides PORT and DATABASE_URL by default, and you can add any additional configuration to the environment via the Heroku dashboard. We're now ready to push our application to Heroku. perhaps you can set JVM_OPTS there?

Apple21:09:11

changing profile should work. my guess.

John Bradens21:09:43

Ok I changed project.clj to include :project/prod {:jvm-opts ["-Dconf=prod-config.edn" ] and I added $JVM_OPTS to the Procfile

Apple21:09:02

Procfile makes more sense than project.clj

John Bradens21:09:19

Unfortunately I don't understand the Heroku dashboard quite yet but I can definitely look into that too

Apple21:09:16

Essentially the JVM_OPTS it's telling the app which file to load db config from.

John Bradens21:09:33

Ok thanks. So in the book with Heroku deployment on p. 340 it has this for the procfile

web: java $JVM_OPTS -cp target/guestbook.jar \
     clojure.main -m guestbook.core
I will try that. Right now I have the one that was automatically generated
web: java $JVM_OPTS -Dclojure.main.report=stderr -cp target/uberjar/guestbook.jar clojure.main -m guestbook.core

Apple21:09:35

same here and i have 0 knowledge in heroku

John Bradens21:09:01

Ok thanks for helping me I really appreciate it

John Bradens21:09:34

So for my Procfile, do you think it's good enough to have the $JVM_OPTS as it's written in the book on p. 340? Or should I add something about prod-config.edn?

Apple21:09:57

book page 340 or pdf page 340?

Apple21:09:20

are you reading final P1.0 pdf? or the betas?

John Bradens21:09:23

Book page 340 in the section Heroku Deployment

John Bradens21:09:48

Book version: B17.0β€”March 12, 2021

John Bradens21:09:02

That might be my issue, is I don't have the final version... I forgot about that

John Bradens21:09:10

This is what it says for the Procfile there

Apple21:09:36

ah ok. yeah so JVM_OPTS has to be set somewhere.

Apple21:09:05

perhaps you can hardcoded it procfile for now just to get it working.

Apple21:09:43

like -Dconf=config.edn or -Dconf=prod-config.edn behind java

Apple21:09:13

perhaps the heroku panel is the best place i have no idea.

John Bradens21:09:50

Ok I'll try this for my procfile. The other stuff was already there, except for -Dconf=prod-config.edn

web: java -Dconf=prod-config.edn -Dclojure.main.report=stderr -cp target/uberjar/guestbook.jar clojure.main -m guestbook.core

πŸ™Œ 1
John Bradens22:09:34

Same errors πŸ˜•

John Bradens22:09:20

I wonder what else to try. Or maybe just change the name to config.edn instead of prod-config.edn? I'm not sure

Apple22:09:12

what's the error now heroku run lein run migrate? what's in prod-config.edn?

John Bradens22:09:34

I just found this and I'm going to read it really quick https://github.com/luminus-framework/luminus/issues/231

John Bradens22:09:03

Here is the error:

Syntax error compiling at (/tmp/form-init6003546018927796534.clj:1:73).
could not find a non empty configuration file to load. looked in the classpath (as a "resource") and on a file system via "conf" system property

John Bradens22:09:26

Here is my prod-config.edn:

{:prod true
 :port (System/getenv "PORT")
 :database-url (System/getenv "JDBC_DATABASE_URL")
 }

John Bradens22:09:01

In the luminus docs it says it can just be {:prod true} but then someone yesterday here on slack suggested what I have now. But I can change back to {:prod true} now and try that again...

John Bradens22:09:41

So the person in the github issue said they changed a file in the env/prod/resources/config.edn, and now what they changed it to is the default. So I don't think that will help me. Unless maybe I should try removing that folder from the .gitignore so that it gets read?

Apple22:09:38

foreman start is there such program installed locally?

Apple22:09:00

Once the repository is created, we can test the application by running foreman start in the project's root directory. If the application starts up fine, then we're ready to deploy it to the cloud by running the following command:

John Bradens22:09:26

When I run foreman start I get this

15:19:47 web.1  | started with pid 85167
15:19:48 web.1  | Error: Could not find or load main class clojure.main
15:19:48 web.1  | Caused by: java.lang.ClassNotFoundException: clojure.main
15:19:49 web.1  | exited with code 1
15:19:49 system | sending SIGTERM to all processes

John Bradens22:09:41

Maybe that is the root issue that it's not loading or finding the main class

Apple22:09:17

did you rebuild uberjar?

John Bradens22:09:37

I'm rebuilding right now!

John Bradens22:09:48

I thought I did but better make sure

Apple22:09:15

see if the config files make their way into the final .jar file

Apple22:09:26

is the location now target/uberjar/guestbook.jar instead of target/guestbook.jar?

John Bradens22:09:52

When I do git push heroku main I see the resulting jar as target/uberjar/guestbook.jar but when I open up the target folder, I don't see any uberjar folder...

John Bradens22:09:24

I don't know where it is or how to check the contents. Sorry I'm such a newbie I feel like this stuff is way over my head even if it's really simple.

John Bradens22:09:38

I just saw this from the github link I posted where people were having the same issue. I'm going to try it, even though it looks like I might have more errors regardless...

it seems that

heroku run lein run migrate
from the documentation is not going to work.

Migration could be done running the generated jar file:

heroku run java -cp target/uberjar/<app name>.jar clojure.main -m guestbook.core migrate

John Bradens22:09:33

Wow ok so the migrations just worked

John Bradens22:09:46

I'm going to try looking at the app in heroku now to see if it works

John Bradens22:09:26

Holy smokes it works now!!! @UP82LQR9N thank you SO MUCH for all your help. I really, really appreciate it. I think it was changing the heroku migration command that worked for me, but all the other things might have contributed too, because I did run it with the updated Procfile and everything with the JVM_OPTS.

Apple22:09:45

in the other github issue you linked i think the author didnt use heroku a lot and keep it up to date.

John Bradens22:09:36

Ok yea that makes sense. I added the solution to my stack overflow post, so hopefully anyone else who tries will find the solution from Google. Or, maybe most people find that comment in the github post faster than I did πŸ™‚