SQLServerCentral Article

Using SQL Server Stored Procedures with the Django ORM

,

There comes a time when the built-in ORM just isn't enough. You might have complex queries or performance-critical operations that benefit from the power and efficiency of stored procedures. Stored procedures are precompiled SQL code that lives on your database server. They can encapsulate complex logic, reduce network traffic, and often outperform dynamically generated ORM queries.

In this article, we're going to take a friendly, step-by-step look at how to integrate SQL Server stored procedures into your Django application. We'll build up from simple calls to more advanced examples, adding code snippets as we go and explaining each step along the way.

A Simple Stored Procedure Call

Let's start with the basics. Imagine you have a stored procedure called usp_GetEmployeeData that returns employee details based on a given department. In Django, you can use the django.db.connection to open a cursor and execute this stored procedure. Here's a simple example:

from django.db import connection
def get_employee_data(department):
    with connection.cursor() as cursor:
        # Execute the stored procedure with a parameter
        cursor.execute("EXEC usp_GetEmployeeData @department = %s", [department])
        rows = cursor.fetchall()
        return rows

Usage example:

data = get_employee_data("Sales")
for row in data:
    print(row)

Let's break down what's happening here line by line. First, we import Django's database connection. This connection is what Django uses to talk to your SQL Server database. Next, we create a function that takes a department name. Inside this function, we open what's called a "cursor" - think of it like a pointer that will run commands on your database and fetch results.

The most important line is where we call the stored procedure:

cursor.execute("EXEC usp_GetEmployeeData @department = %s", [department])

This line has two parts separated by a comma. The first part is the SQL command as a string. It's saying "run the stored procedure named usp_GetEmployeeData and pass it a parameter named @department." The %s is a special placeholder that says "put a value here."

The second part, [department], is a list containing the actual value we want to use. When Django runs this code, it takes the value in the department variable (like "Sales") and puts it in place of the %s. So if department equals "Sales", the final command becomes something like EXEC usp_GetEmployeeData @department = 'Sales'.

After running the stored procedure, we use cursor.fetchall() to get all rows of data it returned. This gives us a list of tuples, where each tuple is one row of data. We then return this data from our function.

In the usage example, we call our function with "Sales" as the department, and then loop through each row of data, printing it out.

Adding More Parameters

Often, stored procedures need more than one parameter. Suppose you have a stored procedure usp_GetSalesReportthat takes two dates—a start date and an end date—to generate a sales report. The code to call this procedure becomes a bit more involved:

from django.db import connection
def get_sales_report(start_date, end_date):
    with connection.cursor() as cursor:
        # Call the stored procedure with two parameters
        cursor.execute(
            "EXEC usp_GetSalesReport @start_date = %s, @end_date = %s",
            [start_date, end_date]
        )
        report = cursor.fetchall()
        return report

Usage example:

report_data = get_sales_report("2023-01-01", "2023-03-31")
for record in report_data:
    print(record)

This builds on our first example but now we're passing two parameters to our stored procedure. Let's examine the key differences.

Our SQL command now has two placeholders (%s), one for each parameter we need to pass. Notice how each parameter in the stored procedure has a name: @start_date and @end_date. These names match the parameters defined in your stored procedure on the SQL Server side.

The second argument to cursor.execute() is now a list with two values: [start_date, end_date]. The order here is very important - the first value in this list (start_date) will replace the first %s in your SQL string, and the second value (end_date) will replace the second %s.

So if start_date is "2023-01-01" and end_date is "2023-03-31", the final command Django runs would be something like: EXEC usp_GetSalesReport @start_date = '2023-01-01', @end_date = '2023-03-31'.

Just like before, we use cursor.fetchall() to get all the rows returned by the stored procedure. Each row might contain information like the date, product sold, quantity, and revenue.

In the usage example, we call our function with two date strings and then loop through the results. In a real application, you might format these results into a nice report for your users.

Can I Use Anything After the % as a Token?

While we've been using %s as our parameter token, you might wonder: can I use anything after the %? The short answer is no. Django (and the underlying database adapter) specifically expects %s as the placeholder. Unlike some other systems, you can't use %d for integers or %f for floats - it's %s for all types of parameters.

Django will automatically convert your Python values to the appropriate SQL types. This includes:

  • Strings to VARCHAR/NVARCHAR
  • Integers to INT
  • Floats to FLOAT or DECIMAL
  • Dates to DATETIME
  • Booleans to BIT

This means you don't need to worry about formatting your data before passing it to a stored procedure. If you have a Python date object, Django will convert it properly. If you have a number, Django will make sure it's passed as a number to SQL Server. This automatic conversion saves you time and helps prevent errors.

Handling Output Parameters

In some cases, stored procedures return output parameters in addition to result sets. Handling output parameters in Django requires a slightly different approach since the database adapter doesn't directly support them in the same way. One common workaround is to have your stored procedure return the output parameter as part of the result set.

For example, if your stored procedure usp_GetEmployeeCount returns a count as a single row, you can handle it like this:

from django.db import connection
def get_employee_count(department):
    with connection.cursor() as cursor:
        # Suppose the stored procedure returns a single row with the count
        cursor.execute("EXEC usp_GetEmployeeCount @department = %s", [department])
        count = cursor.fetchone()[0]
        return count

Usage example:

employee_count = get_employee_count("Marketing")
print(f"Employee count for Marketing: {employee_count}")

This shows how to deal with a stored procedure that returns a single value - in this case, the count of employees in a department. Let's see exactly what's happening.

We execute the stored procedure just like before, passing the department name as a parameter. But instead of using fetchall() to get all rows, we use fetchone() which returns just the first row as a tuple. Then, we add [0] after fetchone() to get just the first value from that tuple. For example, if fetchone() returns (42,)(a tuple with one value), then fetchone()[0] gives us just 42.

This approach is useful when your stored procedure calculates a single result value. In a real application, this might be a count, a sum, an average, or any other single value calculation. The usage example shows how you might use this function - calling it with "Marketing" and then printing out the result. This gives a clean, simple way to get a count without having to write SQL directly in your Django code.

Using Transactions with Stored Procedures

Sometimes, you need to ensure that your stored procedure executes as part of a transaction, especially when your stored procedure performs multiple operations. Django's transaction management can be combined with stored procedure calls. Here's how:

from django.db import connection, transaction
def update_employee_status(employee_id, new_status):
    try:
        with transaction.atomic():
            with connection.cursor() as cursor:
                cursor.execute(
                    "EXEC usp_UpdateEmployeeStatus @employee_id = %s, @new_status = %s",
                    [employee_id, new_status]
                )
                # Optionally, fetch any return values if needed.
                result = cursor.fetchone()
                return result
    except Exception as e:
        # Handle exceptions or log the error as needed.
        print(f"An error occurred: {e}")
        return None

Usage example:

status_update = update_employee_status(101, "Active")
print(f"Status update result: {status_update}")

This example introduces the concept of transactions - a way to make sure that multiple database operations either all succeed or all fail together. Let's see how it works.

First, we import both connection and transaction from Django's database module. Then we define a function that takes an employee ID and a new status. The key difference here is the with transaction.atomic(): line. This starts a database transaction. Everything inside this block will be part of the same transaction. If anything goes wrong inside the transaction block, all changes are automatically undone (rolled back). This keeps your database in a consistent state. For example, if your stored procedure updates multiple tables and one update fails, you don't want to be left with partial changes.

We've also added error handling with the try/except block. If an error occurs, we catch it, print out a message, and return None. In a real application, you might log the error or show a user-friendly message.

After executing the stored procedure, we call fetchone() to get any result it might return. This could be a success message or a status code.

The usage example shows calling this function with an employee ID of 101 and a new status of "Active", then printing out the result. This pattern gives you a safe way to make changes to your database, especially when those changes involve multiple steps.

Capturing Multiple Result Sets

There are times when a stored procedure returns more than one result set - perhaps employee details followed by department information. You can handle this using the cursor's nextset() method, which moves to the next set of results:

from django.db import connection
def get_multi_result_data():
    with connection.cursor() as cursor:
        cursor.execute("EXEC usp_GetMultiResultData")
        first_result = cursor.fetchall()
        
        # Move to the next result set
        if cursor.nextset():
            second_result = cursor.fetchall()
        else:
            second_result = None
            
        return first_result, second_result

Usage example:

result1, result2 = get_multi_result_data()
print("First result set:")
for row in result1:
    print(row)
    
if result2:
    print("Second result set:")
    for row in result2:
        print(row)

Here we show how to handle a stored procedure that returns multiple sets of data. Think of a stored procedure that might return both employee data and department data in a single call. Let's break it down step by step.

First, we execute the stored procedure as usual. Then we call fetchall() to get the first set of results and store it in the variable first_result.

The new part is the cursor.nextset() method. This tells Django to move to the next set of results from the stored procedure. It returns True if there is another set of results available, or False if there are no more results.

We use an if statement to check if there are more results. If there are (if nextset() returns True), we fetch those results with another call to fetchall() and store them in second_result. If there are no more results, we set second_result to None.

Finally, we return both sets of results from our function.

The usage example shows how to handle these two result sets. We call our function and get back two variables: result1 and result2. Then we print out each row in the first result set. If the second result set exists (if it's not None), we print out each row in that set too.

This approach is useful when you need to get related data in a single database call, which can be more efficient than making multiple separate calls.

Encapsulating Stored Procedure Calls in a Django Manager

To keep your code organized, you might want to encapsulate your stored procedure calls within a custom model manager. This creates a clean interface between your Django models and the database's stored procedures:

from django.db import models, connection
class EmployeeManager(models.Manager):
    def get_by_department(self, department):
        with connection.cursor() as cursor:
            cursor.execute("EXEC usp_GetEmployeeData @department = %s", [department])
            rows = cursor.fetchall()
            # Transform rows into model instances or dictionaries as needed.
            return rows
class Employee(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    department = models.CharField(max_length=100)
    # Other fields...
    
    objects = EmployeeManager()

Usage example:

employees = Employee.objects.get_by_department("IT")
for emp in employees:
    print(emp)

This example shows how to integrate stored procedure calls with Django's model system. Instead of having standalone functions, we're putting our stored procedure calls inside a special class called a "manager." Let's see how it works.

First, we create a custom manager class called EmployeeManager that extends Django's models.Manager. Inside this class, we define a method called get_by_department that takes a department name. The body of this method is similar to our first example - it opens a cursor, executes a stored procedure, and returns the results. In a real application, you might add code to convert the raw database rows into Django model instances or dictionaries.

Next, we define an Employee model with fields for first name, last name, and department. At the end, we set objects = EmployeeManager(). This replaces Django's default manager with our custom one, so our new methods become available on the model. The usage example shows how clean this makes your code. Instead of calling a separate function, you can use a method on your model's objects manager: Employee.objects.get_by_department("IT"). This looks and feels like a regular Django query method.

This approach makes your code more organized and maintainable. All database access logic is contained within the model, and the rest of your application doesn't need to know the details of how the data is retrieved.

Limitations and Best Practices

When working with stored procedures in Django, keep these points in mind:

The %s token is always used regardless of the parameter type. Django will handle the proper conversion based on the Python type.

Parameter order matters. The first %s corresponds to the first value in your parameter list, the second %s to the second value, and so on.

For complex data structures, you might need to convert them to a format SQL Server understands. For example, to pass a list of values, you might need to convert it to a comma-separated string.

Always use parameterized queries as shown in these examples rather than string concatenation to build your SQL statements. This prevents SQL injection attacks.

Consider adding error handling with specific exception types to make debugging easier. The database might throw different exceptions for syntax errors versus constraint violations.

Wrapping Up

We've covered how to call stored procedures with single and multiple parameters, how the %s token works as a placeholder that gets replaced with your actual parameter values, and how Django handles the conversion from Python types to SQL types. We've also explored more advanced scenarios like handling output parameters, working with transactions, and processing multiple result sets.

I encourage you to experiment with these techniques in your own projects. Whether you're handling complex reporting, updating data with multiple validations, or simply aiming for better performance, stored procedures provide a robust tool that complements Django's capabilities.

Happy coding, and I hope you enjoy the journey of making your database interactions as efficient and secure as possible!

 

Rate

3 (2)

You rated this post out of 5. Change rating

Share

Share

Rate

3 (2)

You rated this post out of 5. Change rating