?

Log in

No account? Create an account

C# and Java - Silnith’s Lair

Jun. 8th, 2018

01:02 am - C# and Java

Previous Entry Share

My new job is entirely working with C# on the Azure platform. After my brief stint at Microsoft, I deliberately avoided adding C# to my resume because I did not want to work in it again. A couple co-workers from my previous job convinced me to join them at Starbucks, and since I like them I decided to go for it and give C# another try.

The new codebase is wildly better than anything I had to deal with at Microsoft. It uses standard design patterns, is modular, has dependency injection and normal logging and unit tests. Overall it is a significantly better experience than the prior one.

However, even though I've been able to find C# analogues for most of the things I've come to rely on in the Java ecosystem, there is a constant stream of little frustrations and gotchas that make me appreciate all the things that Java provided for me.

Here is an example. Today I tried to figure out how to inject a database connection into a handler in a standard C#-idiomatic way. I was looking for something akin to JNDI in an application container, where I set up the database connection in external configuration and the code merely accepts and uses a generic connection interface.

After a few hours of searching online, I managed to cobble together enough tiny clues and analyze enough bad code samples to produce roughly this code (edited to remove company-specific details).

using System.Data;
using System.Data.Common;

namespace Org.Silnith
{
    public class DatabaseToucher
    {
        private readonly DbProviderFactory _dbProviderFactory;
        private readonly string _connectionString;
        
        public DatabaseToucher(DbProviderFactory dbProviderFactory, string connectionString)
        {
            _dbProviderFactory = dbProviderFactory;
            _connectionString = connectionString;
        }

        public async Task<int> Update(string transactionId)
        {
            using (var connection = _dbProviderFactory.CreateConnection())
            {
                if (connection == null)
                {
                    throw new Exception();
                }
                connection.ConnectionString = _connectionString;

                await connection.OpenAsync();
                try
                {
                    using (var command = connection.CreateCommand())
                    {
                        const string transactionIdParameterName = "@transactionId";
                        const string requestSentParameterName = "@requestSent";
                        var insertStatement = $"insert into transactions (transaction_id, request_sent) values ({transactionIdParameterName}, {requestSentParameterName})";

                        var transactionIdParameter = command.CreateParameter();
                        transactionIdParameter.ParameterName = transactionIdParameterName;

                        var requestSentParameter = command.CreateParameter();
                        requestSentParameter.ParameterName = requestSentParameterName;

                        command.CommandText = insertStatement;
                        command.Parameters.Add(transactionIdParameter);
                        command.Parameters.Add(requestSentParameter);

                        command.Prepare();

                        transactionIdParameter.Value = transactionId;
                        requestSentParameter.Value = false;

                        using (var transaction = connection.BeginTransaction(IsolationLevel.Serializable))
                        {
                            command.Transaction = transaction;

                            var rowsUpdated = await command.ExecuteNonQueryAsync();

                            transaction.Commit();

                            return rowsUpdated;
                        }
                    }
                }
                finally
                {
                    connection.Close();
                }
            }
        }

        public async Task<bool> Query(string transactionId)
        {
            using (var connection = _dbProviderFactory.CreateConnection())
            {
                if (connection == null)
                {
                    throw new Exception();
                }
                connection.ConnectionString = _connectionString;

                await connection.OpenAsync();
                try
                {
                    using (var command = connection.CreateCommand())
                    {
                        const string transactionIdParameterName = "@transactionId";
                        const string requestSentParameterName = "@requestSent";
                        var selectStatement = $"select count(*) from transactions where transaction_id = {transactionIdParameterName}";

                        var transactionIdParameter = command.CreateParameter();
                        transactionIdParameter.ParameterName = transactionIdParameterName;

                        command.CommandText = selectStatement;
                        command.Parameters.Add(transactionIdParameter);

                        command.Prepare();

                        transactionIdParameter.Value = transactionId;

                        using (var transaction = connection.BeginTransaction(IsolationLevel.Serializable))
                        {
                            command.Transaction = transaction;

                            var count = (int) await command.ExecuteScalarAsync();

                            transaction.Commit();

                            return count > 0;
                        }
                    }
                }
                finally
                {
                    connection.Close();
                }
            }
        }
    }
}

When my co-worker asked me what about the .NET database connection API was making me grumble, I took a deep breath and produced a rant a good thirty seconds long, cut off only because I started a coughing fit and could not continue.

For reference, this is the Java code that I wrote tonight that provides the same functionality.

package org.silnith.temp;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.inject.Inject;
import javax.sql.DataSource;

public class DatabaseToucher {
    
    private final DataSource dataSource;
    
    @Inject
    public DatabaseToucher(final DataSource dataSource) {
        super();
        this.dataSource = dataSource;
    }

    public int insert(final String transactionId) throws SQLException {
        try (final Connection connection = dataSource.getConnection()) {
            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            connection.setAutoCommit(false);
            try (final PreparedStatement insertStatement = connection.prepareStatement("insert into transactions (transaction_id, request_sent) values (?, ?)")) {
                insertStatement.setString(1, transactionId);
                insertStatement.setBoolean(2, false);
                
                final int rowsUpdated = insertStatement.executeUpdate();
                
                connection.commit();
                
                return rowsUpdated;
            }
        }
    }
    
    public boolean query(final String transactionId) throws SQLException {
        try (final Connection connection = dataSource.getConnection()) {
            try (final PreparedStatement selectStatement = connection.prepareStatement("select count(*) from transactions where transaction_id = ?")) {
                selectStatement.setString(1, transactionId);
                
                try (final ResultSet resultSet = selectStatement.executeQuery()) {
                    while (resultSet.next()) {
                        int count = resultSet.getInt(1);
                        
                        return count > 0;
                    }
                    throw new IllegalStateException("Cannot reach this point.");
                }
            }
        }
    }
    
}

Note that I added extra lines to the Java version in an attempt to make it as functionally-equivalent to the C# version as possible. The original was actually shorter.

So why does the C# version bother me so much? Let me count the ways.

  1. The DbProviderFactory object only provides a mechanism for instantiating database drivers. It does not encapsulate all the information needed to create a connection to a specific database. Therefore, you also need to inject the database connection string to every location where you want to use a connection. Why does every object need to know what the database connection string is? That's breaking the abstraction in a horrible way, and allowing a significant source of errors.
  2. The DbProviderFactory.CreateConnection method can return null. So inside of a using block, you also need to check the variable to see if it is null and handle the failure.
  3. I already created a connection, now I also have to set the connection string and then open the connection, too? I think of a database connection as a functional object, not a class that provides the functionality for connecting manually. I said give me a connection, not give me the means to build a connection. In Java, when I get a connection it is an open connection to the database, ready to execute queries and updates.
  4. The idea of having named parameters always sounds nice in theory, but once you try it in practice you quickly discover that you have to repeat yourself constantly. I can either type the parameter names twice, once when specifying them and again when using them in the SQL statement, or I can use string interpolation to re-use a constant with the names in them. Reality does not come out as clean as theory predicted.
  5. I create objects to represent the parameters in the statement. It sounds reasonable, but in practice it does not really add any additional descriptive power or improve the type safety at all. And why do I need to attach the parameters back onto the command from which I created them in the first place? I said, Have this command create a parameter, it is perfectly reasonable to expect the parameter object to be associated with the command when I receive it.
  6. On a similar note, why do I have to use the connection to create a command, use the same connection to create a transaction, and then associate the two myself? A connection is inherently stateful, the whole point of having a persistent connection is so it can maintain state. If I can create a command and not associate it with the transaction, does that mean I can run multiple transactions on a single connection? That truly defies logic, you would be maintaining multiple states for a single stateful object (the connection). So if it makes no sense to have a command not associated with the open transaction, then why are they not pre-associated? It is additional work for the programmer and an opportunity for errors.
  7. Oh, yay. Asynchronous method calls. So instead of the thread blocking and the operating system doing a context switch, instead the programming language runtime can emulate a context switch. You still need to save and restore the stack, and you still incur the same cache and buffer change costs. But now there are two context switching mechanisms instead of one, and the new mechanism breaks a lot of the assumptions that the operating system was built around, as well as making useless any specialized hardware the machine offers for context switching.
  8. Oh, and since we had to manually open the connection instead of having the open implicit in acquiring it, we are also responsible for closing it as well, and we cannot make use of a using block for it since the open was a simple method call, not an object retrieval.

Now contrast with the Java version.

  1. The DataSource is an abstraction for the database itself, not a piece of code used to communicate with the database. Acquiring a connection gets a functional connection, ready to be used. Any failure throws an exception, so the returned object is guaranteed to never be null.
  2. Creation of the SQL prepared statement parameters is implicit in creating the statement abstraction object itself. Parameters are positional, so no need to keep names in multiple locations synchronized.
  3. In Java there is no such thing as disposing of an object. If an object maintains resources that must be released, it is closed, which is what the try-with-resources block does. There is only one abstraction for releasing resources, not two. (Distinct close versus dispose.)
  4. Transactions are part of the state of a connection, not a distinct entity. It is not possible to have a statement outside of a transaction, or to have multiple open transactions associated with a connection. A transaction begins when the previous one ends.

The Java version is just so much cleaner, conceptually, than the C# version that it boggles my mind anybody could see the C# code as superior in any way to the Java code. I know many people do, and many of them are very smart and experienced people. I just cannot see the world in the way that they do. I really do not mean to belittle or denigrate those people. I am simply befuddled.

Tags: , ,