For example, in ActiveRecord (the Ruby library that this looks very much influenced by), given:
posts = Post.where(user_id: 1)
...you can now do things like: recent_posts = posts.order(created_at: :desc).limit(100)
or tagged = recent_posts.where('tag in ?', ['hacker', 'news'])
and then you can extract data: tagged.group_by('tag')
or do mutate it: tagged.update_attributes({author_id: 2})
or tagged.destroy_all
...And so on. By encapsulating a result set as something that can be augmented with new query clauses (where, limit, select) and so on, you can incrementally build queries, pass them to other functions, store them as member variables so that they be reused as "scopes" across multiple calls, and so on.If the query were just a string, this sort of thing becomes awkward, verbose, brittle, and generally not type-safe.
A "proper" solution to this problem requires language changes (as Microsoft did with C# 3.0).
For example, ActiveRecord doesn't have support for window functions, but its underlying AST (a library called ARel) allows you to extend it, which at one point I did, allowing me to write something like this:
things = Arel::Table.new('things')
things.project(Arel::Nodes::NamedFunction.new(:sum, [things.count]).over('my_window')).
window('my_window').
order(things[:name]).
range(Arel::Nodes::Preceding.new)
which generates something like: select sum(count(*)) over my_window
from things
window my_window as (order by name rows unbounded preceding)
(It's been a while since I looked at this particular code, which is no longer in production use, so it might not be correct.)In other words, I could continue to express whatever I wanted of SQL, at the language level — no type mismatches.
The SQL builder code isn't pretty, to be sure, but it's something you typically encapsulate into a lower-level module rather than writing in application code. And as it's highly analogous to the SQL code it generates, it's not like it's that much more complicated. (ARel isn't a shining example of how to do it right, either. I expect Diesel implements the AST aspect more cleanly.)
Edit: I see what you're saying about typed record types. I was thinking more about the grammar, not the shape of the data.
I'd like to point out that strong static-typing does not preclude powerful metaprogramming! Nim's macro system is extremely powerful because of it's great interplay between it and the type system.
Look at what Microsoft did with LINQ. You can't pull that off in Rust no matter how much you want to.
The main reasons I like this approach are:
* it makes way easier (and safer) to reuse query parts compared to string composition/interpolation
* the ORM takes care of differences between the backends, so it is easier if I have to port the application to another RDBMS.
1. http://www.sandimetz.com/blog/2016/1/20/the-wrong-abstractio...
Additionally, SQL is extremely hard to compose. In my applications, I very rarely have some specific one off query that I want to run, I have multiple predicates that I want to be able to mix and match easily. String munging is fragile, while this actually lets me build reasonable domain abstractions.
To your final point "Unless this has some huge performance gain over rust-postgres"
Diesel performs 33% faster than rust-postgres, and will be competitive with if not faster than ideomatic C.
I've seen almost everyone who doesn't use SQL directly say this but I always feel it's the exact opposite. SQL is easy to write once you do it enough. I've gotten to the point where I compose zero SQL in any application's code and, instead, do it all inside of stored procedures (of which they do not generate dynamic SQL). Then my code just calls stored procedures, my database users for the web application never have more access than executing a specific subset of stored procedures, and if there is a bug found in a stored procedure I can deploy them separately from the web application.
I'm guessing it's just different philosophies but almost every time I've used something that abstracts an existing interface to another, completely different interface, I always end up running into major issues.
Don't get me wrong I see the simplicity in it from the ORM side I've just never had a good experience with ORM tools. Having worked directly against many RDMS and key value stores at least for me I don't see much value in them.
But anything that helps get a product out the door more power to ya, in my opinion.
I just implemented this recently in my job, but it was even tougher than that; I was joining dynamically created tables, and the number of joins depended on criteria. I ended up building my own AST to represent filters, and compiling to SQL. Every extra table join hurts performance - typically filters are implented as joins against a filtered sorted limited subquery returning ids, so fewer different table joins means less data to pull in when evaluating filters even when all the data is needed for the results page.
Point being, I had to compose complex dynamic queries and SQL was not a particularly pleasant target language to target directly. Abstractions (like asts) help with composition.
-- Parameter :filter
--
AND CASE WHEN :filter IS NOT NULL THEN
table.column = :filter
ELSE TRUE END
This has zero cost in the case where :filter is unset.Sorting is easy. The stored procedure just accepts a sorting flag, add it into your query and your done. Filtering though, I could think of a dozen types of things you may want to filter on and I still may not hit your use case so it's hard to say how to do it based on what data is stored in the database, how it's stored and which RDMS you're using.
I've primarily done this type of work in MS SQL where I've built some pretty complex queries but I was always able to replace dynamically generated SQL with SQL that could generate a cacheable query plan.
Not saying there won't be edge cases and I don't know how your filtering worked so I don't have any type of satisfying answer for you. I would just be surprised if it wasn't doable somehow (I've seen some crazy shit in SQL queries :D)
Sorting has a couple of wrinkles. When Excel sorts rows, it changes the in-memory order; its sort is also stable. That means if you sort by column C, then B, then A, you're effectively doing "order by a, b, c". Seems simple enough. But that's not the only wrinkle in this problem.
Not all sortable columns are in the same table. If you do something naive like "select x.* , y.* from x join y [...] where [...] order by x.a, y.b limit 10 offset 20000", you're doing too much work - you're sorting a wide result set. It's normally better to do your pagination in a subquery and join against it, like this:
select x.*, y.* from x join y [...] join (
select x.id from x join y [...] where [...]
order by x.a, y.b limit 10 offset 20000
) f on f.id = x.id
order by x.a, y.b
Depending on the body of the where clause, you want to join the minimum set of tables to include just enough data to evaluate the page of ids - every join has a cost.The body of the where clause is another story, it's pretty much entirely arbitrary. The user may choose to filter on any column, just like Excel autofilter. Creating good indexes up front isn't feasible - you don't know how many columns to include in any one index, they take time to create and they're not free in disk storage either. And the filtered columns span multiple tables; the final result set may contain several hundred columns across all these tables.
So what I did is take the criteria - the filter clause, which might look something like this:
(and (or (eq (field 'x.f1') "foo")
(eq (field 'y.f2') "bar"))
(between (field 'm.timestamp') "date1" "date2"))
And turn it into a where clause. I analyze the filter tree to determine the minimum set of joins required to access all the fields needed by the filter and sorting criteria. I can then put that into a subquery to join against.There are more wrinkles. To give a good experience, we need to populate a dropdown of potential filter values, like Excel autofilter. Potential filter values need to be filtered themselves - we only display filter values for that are potentially visible given the current filter. They also need to be searchable, because there may be millions of them. And they may include joins, like when you want to filter by tags or some other 1:N style relation, like row:tags. There are a bunch of different shapes to the different queries, but there are common composable underlyers to them.
All the above runs on mostly unindexed tables - we're relying on pure query compute performance within the designed data limits (less than 3 million rows). Sub-millisecond query response times aren't the goal here; we're supporting the user doing reasonably complex data manipulation operations with worst case performance measured in single-digit seconds.
(This is all in MySQL, because reasons that include business model constraints.)
That's where I usually get fed up with stored procedures, as pseudo-ADA ain't a nice language for list and string manipulation. I'd rather do that in a language better suited, and that quickly leads to query builders…
Ultimately an ORM can help you to build more maintainable and resilient (to changing market conditions) software than direct SQL. Diesel in particular can catch mistakes that SQL strings cannot.
I do understand why a lot of people have reservations about ORMs. I share a lot of them. I've maintained Active Record for about 2 years now, and learned a lot from doing so (note: This does not mean that I like AR or want this to be anything like it).
TL;DR: Your points aren't wrong. Diesel is different. Maybe give it a shot. ;)
I couldn't disagree more. Creating the separation between SQL DB and web application allows you to maintain, deploy, fix and upgrade things independently and it's easy (maybe not quite as easy as an ORM would make it but not really harder either; you're going to have to know how SQL works anyway if you want to effectively optimize and secure your data).
Not only that but you can provide far better security controls because you can limit certain users access to certain procedures, never direct access to tables and various other things and if you don't do any dynamic SQL in the procedures itself you not only never have to worry about SQL injection but unless there is a critical vulnerability in the SQL system itself it's going to be impossible to conduct an injection attack.
Yeah you can separate your code to make it easier to deploy separately to get a similar benefit and yeah your code can prevent SQL injections too but it's so much more work, more possible points of failure and the differences between databases can really bite you in the ass when you do everything generically. SQL is fun :)
> TL;DR: Your points aren't wrong. Diesel is different. Maybe give it a shot. ;)
Yeah I'm not saying NEVER use ORMs; they're great for creating things quickly that don't need to scale. But after my experience with them I'm highly skeptical using them outside of that type of use case. Of course you can scale ORMs but I've never seen it done well.
I'm curious what you mean by scale. Are you referring to performance, or code size? If you're referring to performance, you'll probably find it interesting that we out-perform or are competitive with ideomatic C.
Don't get me wrong ORMs can certainly be performant. I just haven't seen or know anyone who used it in a manner that scaled to thousands of concurrent users and, at the same time, didn't have a lot of pain trying to do that with the ORM.
A lot of systems lately are being built in a different way, that is, only one application connects to the database. This application presents an API (ReST, protobufs, thrift) for other apps that need that data. It handles access control and business logic, using the DB as a backing store. That means you don't have to worry about dababase user access control, because you don't have more than one user.
I don't have a strong opinion about either of these, but the latter way of working is how we do things at my current job (reasonably large payments software company), and it does work quite well. One advantage I appreciate is that there isn't arbitrary business logic sitting in the database where it can be changed on the fly - I wouldn't consider that a feature.
At least in Postgres they act as an optimization fence. In practice this means you can use them as an abstraction layer only in very limited ways where they happen to fit into a decent execution plan.
In my experience outside of extension code the need for composable SQL is very limited. Most uses cases can be satisfied by Rails 2 scopes.
Now, if composability was free - sure it's great. But it's not free.
So, on one hand:
* it would be nice to have infinitely composable SQL
On the other hand:
* it would be nice to just have the actual SQL with all the features, well understood syntax and semantics that cost decades and billions of dollars to develop
* and no wrapper-on-top-of-SQL limitations and translation bugs, and need for training
With these language-on-top-of-SQL you don't get both. In fact you get severe compromises in the second group.
Columns may be sorted and filtered individually or in combination. Filters may be based on value set membership or range criteria, comparisons with fields in the data or via a join to other tables, or based on the presence of an entry in a join table (think: tags or labels, and you want to filter by presence or absence of tag).
(This is what I spent the past couple of months implementing. The simplest way of composing SQL is via union / except / intersect, but it's also the worst performing; doesn't work great past a few hundred thousand rows. And you still need to compose together the filter conditions into a where clause.)
I'd love to see a solution with tradeoffs made the other way:
* syntax and semantics 99% like underlying SQL
* support for 99% of underlying DB functionality
* traditional areas where ORMs add value:
* deserialization into convenient data structures
* casting, quoting and escaping on the way in and out
* using stored association metadata for concise joins in the query Diesel performs 33% faster than rust-postgres, and will be
competitive with if not faster than ideomatic C.
Interesting. What makes it faster than rust-postgress?Long version: http://bikeshed.fm/49
Longer version: Planning on doing a really in depth write up for the website soon. I'll also be going into more detail during my talk at the SF rust meetup, which there's a link to the live stream towards the bottom of the thread
As far as I saw, Diesel is essentially shipping with its own PostgreSQL driver.
Please include in your larger talk!
(Sorry for the double post, I think I replied to my own comment and not yours!)
I don't think you're ragging on rust-postgress, although I'd probably attempt to change rust-postgres first :P
rusqlite has the same issues. libsqlite doesn't since it's just C bindings. Same as pq_sys. Ironically, rusqlite makes the same design choices as rust-postgres, but it's actually wrapping a heap allocated object anyway, so it gains nothing by doing so...
> I'd probably attempt to change rust-postgres first
I did, too. It turned out to be a pretty major rewrite.
Why is
Post::belonging_to(usre).load(&connection)
an improvement over SELECT * FROM posts WHERE usre_id = 1;
?... to which the answer is probably more obvious.
On the contrary, the purpose of an ORM is to let your code be a schema for your database. This is my personal use case for an ORM; I want persistence but am not overly concerned with the database. There are plenty of use cases [1] where a schema update from outside is unlikely or impossible. In these cases, there is a very low cost for (by way of an ORM) tightly coupling your code to your schema.
Also, an ORM lets somebody else worry about sanitisation, and database specific-code. It can abstract away the choice of database, another very valuable feature. I think that the example you quote is actually a poor demonstration of the value of an ORM. The other ones on the linked page are far better :)
[1] A program running on only one machine, like an Android app or a music player on Linux desktops etc
Whereas as an applications programmer I'd rather deal with structures that make sense in my domain of interest, and let the marshalling code from SQL results be handled for me, and with compile-time checking for correctness. When that's coupled with a database schema reverse engineering tool (not sure if this project provides one), that way whenever the database changes, compile errors will result, which errors allow me to quickly hunt down places in my code that are affected and need changing.
Example:
var posts = connection.Query<Post>("select * from posts where user_id = @UserId", param: new { UserId = 1 });Are you sure? Why do you say that?
If you use this, it nails the database schema into your Rust code, because this needs to know the table formats. If someone adds a new field to a row, you have to rebuild all your Rust programs that use the database. Now your deployment process just got much more complicated.
It's a neat hack, but seems an overly complex way to do something that's not that complicated. Unless this has some huge performance gain over rust-postgres[1], it's probably too much.
If rust-postgres is too slow, it would be worth having an optional generic for rust-postgres to help with unmarshalling returned rows. Rust-postgres returns row objects from which fields can be extracted. Converting that to a Rust struct is the only operation which needs to go fast.
[1] https://github.com/sfackler/rust-postgres