<p>Hi !
I'm now making a web project. It uses standard Go library, julienschmidt/httprouter, gorilla/schema, go-sql-driver/mysql and pkg/errors. It creates simple templated server-rendered pages and currently all interaction with the server is performed with ordinary HTTP requests and forms though some AJAX is on the way.
I use one model struct to parse HTML forms data to and to interact with the database. It looks like this:</p>
<p>HTML forms -> gorilla/schema -> model struct -> Create/Update/Delete -> go-sql-driver/mysql</p>
<p>I use no ORM and write SQL queries myself. Model struct fields map to database fields and sometimes differ (if i always use some tables together i create embedded structs or create fields that are pointers to other models).</p>
<p>And all this worked perfectly until i decided to introduce natural composite primary keys for some models (for those who don't know it's when you use a combination of several database columns which is unique as primary key if it's the essential unique property of a table row).
For example: i have a <em>relations</em> table (and respective Relation struct in Go) which have <em>pupil_id</em>, <em>adult_id</em> and <em>relation</em> (varchar) columns. There is no need for surrogate <em>id</em> column - the combination of <em>pupil_id</em> and <em>adult_id</em> defines the exact row in the table. And this approach can sometimes save you so many JOINs (when you select from multiple tables in one request as your data is spread) that even including varchar columns in primary key can be justified (actually it doesn't involve as much overhead as someone could think - B-trees are very bushy - you can hardly reach 5-6 levels of depth).</p>
<p>Previously i had the id of table row to edit in the URL to which a POST request with new data was made. Now i have several variables which together indicates the table row. And in some cases they can change too. So i need either to create additional fields for every model struct (for old and new values of each field participating in composite primary key) or to pass them another way. I have chosen the latter. And between passing these variables in URL and in an HTML form i have chosen HTML form (for what reasons - see below).</p>
<p>Previously the server received a POST request to a specific URL (with id of table row) and passed them to a universal function UpdateFromForm along with a setIDfunc function to set ID of the model (as it was parsed with Atoi in UpdateFromForm and used later in it) because Go doesn't have classes, only interfaces. Now i have several variables, in some cases they are int, in some - string, in some - even time.Time. I don't want to parse them myself one-by-one. Thankfully gorilla/schema decoder can do it.</p>
<p>BUT</p>
<p>But i need to unfold this parameter struct later to pass it to sql.Exec. I don't know how to do it without reflection. I need your advice about how to do it better.</p>
<p>Some code snippets:</p>
<p>What i had previously: <a href="https://play.golang.org/p/HIWEOGPSl4" rel="nofollow">https://play.golang.org/p/HIWEOGPSl4</a></p>
<p>What i have now (incomplete): <a href="https://play.golang.org/p/D288kNKVBv" rel="nofollow">https://play.golang.org/p/D288kNKVBv</a></p>
<hr/>**评论:**<br/><br/>everdev: <pre><p>I've gone down this rabbit hole before as well as the approach you're describing is much easier in dynamic languages, where I started developing. However, with static typed languages, you'll have less headaches being explicit for each unique use case. Sending every request through a single function that processes a SQL statement looks like premature optimization and you'll run into these issues where one size does not fit all. In my web apps, I typically have the following functions:</p>
<pre><code>FindUserHandler() <- service layer, called by the router, transform form data into Go data types
FindUserLogic(id int) <- business layer, models/schema/logic, sometimes not much here other than calling the data layer
FindUserData(id int) <- data layer, queries the DB
</code></pre>
<p>It does mean re-creating this structure for each method on a model, but in my experience you're more likely to want to add something to a specific method than add something to every method.</p>
<p>As a bonus, I typically have Postgres return a JSON response, so if there's no error, I can just serve the JSON back up the chain as my API response.</p></pre>lion_rouge: <pre><p>I don't send every request through a single function. Update requests have one universal function, Get requests another, Create and Delete - another one.
It has nothing to do with optimization - i'm just trying not to repeat myself. And it worked perfectly well until recently as i onlt generalize things that i have already repeated about 5 times and i think it's a very natural approach.</p>
<p>P.S. If using Go implies violating DRY principle heavily, then i quit.</p></pre>everdev: <pre><p><a href="http://go-proverbs.github.io/" rel="nofollow">http://go-proverbs.github.io/</a> might help.</p>
<p>Go is not anti-DRY, but with static typing, a little copying is sometimes better than trying to combine features that aren't easily combined.</p>
<p>In other words, if your function is general enough that you need to use reflect to discover what type of data you're working with, it's probably not idiomatic Go.</p></pre>lion_rouge: <pre><p>I realise that. I faced such issues before and always managed to refactor things so that it was normal Go code (no reflection and interface{}) and not repetitive. In this case i just can't figure it out.</p></pre>shovelpost: <pre><blockquote>
<p>Hi ! I'm now making a web project.</p>
<p>Model struct fields map to database fields and sometimes differ (if i always use some tables together i create embedded structs or create fields that are pointers to other models).</p>
<p>And all this worked perfectly until i decided to introduce natural composite primary keys for some models...</p>
</blockquote>
<p>I do not understand why you need this abstraction. Is this some kind of mini ORM-ish library that is going to be used by many other projects of yours? I am assuming no since you are clearly stating that you are making a web project. </p>
<p>I recommend to keep it simple. Use one of the techniques described in <a href="http://www.alexedwards.net/blog/organising-database-access" rel="nofollow">this article</a> (preferably the technique <a href="/u/everdev" rel="nofollow">/u/everdev</a> described), write your database functions along with their tests and move on. If you want some help to reduce the boilerplate you could use a package like <a href="https://github.com/jmoiron/sqlx" rel="nofollow">sqlx</a>. </p>
<p>Sure you are going to have to write boring code but there's nothing fundamentally interesting about this problem anyways. By the time you have finished and moved to a more interesting problem or even another project, you'll be wondering why you even bothered with the abstraction in the first place.</p></pre>lion_rouge: <pre><p>Why i need an abstraction? To be able to refactor at any stage without facing O(n) complexity (sometimes it's even worse). Compare changing 1 function and 200 functions.</p></pre>lion_rouge: <pre><p>Sqlx is useless as it only supports embedded structs and doesn't understand pointers to structs at all.</p></pre>lion_rouge: <pre><p>Thanks for the article, it's a good read but my problem is different: how to pass multiple variables from request body to db-specific code without writing redundant code for Atoi, error handling, etc. Gorilla/schema can solve this if those variables are organized as struct fields. But sql.Exec need those variables one-by-one, comma-separated.</p></pre>lion_rouge: <pre><p>OK, i have figured it out. As always the decision is in using <strong>closures</strong> (really, i did this trick so often that i start to think Go interfaces are pretty useless (unless for decoupling packages) ). Anyway interfaces are gradually disappearing from my project.</p>
<p>If someone cares, the new code:</p>
<p><a href="https://play.golang.org/p/HSMO_XdCN3" rel="nofollow">https://play.golang.org/p/HSMO_XdCN3</a></p></pre>
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码`
- 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传