← Back to blog

Frappe Query Builder: Simplifying ERPNext Customization

Frappe Query Builder: Simplifying ERPNext Customization

Introduction to Frappe's Query Builder

For developers working with Frappe and its flagship product, ERPNext, crafting custom queries is a frequent necessity. Whether it's for generating complex reports, creating custom data manipulations, or integrating with external systems, understanding how to query the underlying database effectively is crucial. While raw SQL offers ultimate flexibility, it can also lead to verbose, less readable, and potentially error-prone code, especially when dealing with dynamic conditions or complex joins. Fortunately, the Frappe framework provides a powerful and elegant abstraction layer: the Query Builder.

This article delves into the Frappe Query Builder, exploring its features, benefits, and practical applications within the ERPNext ecosystem. By mastering this tool, you can significantly streamline your development process, write more maintainable code, and harness the power of your ERPNext data with greater ease.

Why Use the Frappe Query Builder?

Before diving into the specifics, let's understand the advantages of using the Query Builder over writing raw SQL directly.

  1. Readability and Maintainability: The Query Builder uses a fluent, object-oriented approach. This makes the code more intuitive and easier to understand for other developers (or your future self). It abstracts away much of the SQL syntax's complexity.

  2. Security: By constructing queries programmatically, the Query Builder helps mitigate common SQL injection vulnerabilities. It handles parameterization and escaping automatically, which is a significant security benefit.

  3. Database Agnosticism: While Frappe typically uses MariaDB/MySQL, the Query Builder aims for a degree of database independence. This means your query logic might be more portable if you ever need to switch database backends, though extensive testing would be required.

  4. Abstraction and Simplification: It provides a higher-level interface for common database operations like SELECT, INSERT, UPDATE, DELETE, JOIN, WHERE, ORDER BY, and GROUP BY. This simplifies complex query construction.

  5. Integration with Frappe's ORM: The Query Builder works seamlessly with Frappe's DocType and ORM (Object-Relational Mapper) layer. You can easily query DocTypes as if they were database tables, leveraging Frappe's metadata and relationships.

Core Components of the Frappe Query Builder

The Frappe Query Builder is essentially an object that represents a database query. You instantiate it and then chain methods to build your query step by step.

Instantiating the Query Builder

The most common way to start building a query is by specifying the table (or DocType) you want to query. You can do this using frappe.qb.from_('TableName').

from frappe.query_builder import Query
 
# Querying a specific DocType (e.g., 'Customer')
query = Query('Customer')

Selecting Columns (SELECT)

To specify which columns you want to retrieve, you use the .select() method. You can select specific columns or use * to select all.

# Select specific fields
query = Query('Customer').select('name', 'customer_name', 'email_id')
 
# Select all fields
query = Query('Customer').select('*')

You can also select columns from related tables using their respective DocType names.

# Select fields from Customer and its related Sales Partner
query = Query('Customer').select('name', 'customer_name', 'sales_partner.name', 'sales_partner.customer_name')

Filtering Data (WHERE)

The .where() method is used to apply filtering conditions. It accepts a condition object created using frappe.qb.Field.

from frappe.query_builder import Query, Field
 
customers_in_india = Query('Customer').select('name', 'customer_name').where(Field('country') == 'India')

Complex WHERE Clauses

You can combine multiple conditions using logical operators like & (AND) and | (OR).

from frappe.query_builder import Query, Field
 
# Customers in India AND created after a specific date
date_threshold = '2023-01-01'
 
query = (
    Query('Customer')
    .select('name', 'customer_name', 'creation')
    .where(
        (Field('country') == 'India') &
        (Field('creation') > date_threshold)
    )
)

Other WHERE Operators

The Field object supports various operators: ==, !=, >, <, >=, <=, like, not_like, in, not_in, is_null, is_not_null.

# Customers whose name starts with 'A' and are not disabled
query = (
    Query('Customer')
    .select('name', 'customer_name')
    .where(
        (Field('customer_name').like('A%')) &
        (Field('is_disabled') != 1)
    )
)

Joining Tables (JOIN)

Joining tables is a common requirement. The .join() method allows you to perform various types of joins (INNER, LEFT, RIGHT).

from frappe.query_builder import Query, Field
 
# Select customer name and the linked sales invoice number
query = (
    Query('Customer')
    .select('name', 'customer_name', 'sales_invoice.name') # Assuming Sales Invoice is linked
    .join('Sales Invoice', 'Customer.name', 'Sales Invoice.customer')
)

By default, .join() performs an INNER JOIN. You can specify the join type:

# Left join customers with their sales orders
query = (
    Query('Customer')
    .select('name', 'customer_name', 'sales_order.name')
    .left_join('Sales Order', 'Customer.name', 'Sales Order.customer')
)

Ordering Results (ORDER BY)

Use .order_by() to sort your results. You can specify ascending or descending order.

# Get the 5 most recently created customers
query = (
    Query('Customer')
    .select('name', 'customer_name', 'creation')
    .order_by('creation', order='DESC')
    .limit(5)
)

Grouping Results (GROUP BY)

The .groupby() method is used for aggregation.

# Count of customers per country
query = (
    Query('Customer')
    .select('country', Field('count(name)').as_('customer_count'))
    .groupby('country')
    .where(Field('country').is_not_null())
)

Executing the Query

Once your query is built, you need to execute it. The .get_list() method executes the query and returns a list of dictionaries.

# Execute the query and get results
results = query.get_list()
 
for row in results:
    print(row)

If you need to perform an INSERT, UPDATE, or DELETE, you'll use methods like .insert(), .update(), .delete().

# Example: Updating a customer's email
frappe.qb.update('Customer').set('email_id', 'new.email@example.com').where(Field('name') == 'CUST-00001').run()

Advanced Use Cases

Subqueries

The Query Builder supports subqueries, allowing you to nest queries for more complex data retrieval.

# Find customers who have placed more than 5 orders
from frappe.query_builder import Query, Field
 
sub_query = (
    Query('Sales Order')
    .select('customer')
    .group_by('customer')
    .having(Field('count(name)') > 5)
)
 
main_query = (
    Query('Customer')
    .select('name', 'customer_name')
    .where(Field('name').`in`(sub_query))
)
 
# Note: The `in` operator needs to be correctly mapped or use a specific subquery syntax if available.
# This example illustrates the concept; actual implementation might vary slightly based on Frappe version.
# A more direct way might be using the .build()` method to get the SQL string and then embedding.

Using Aliases

Aliases are very useful when dealing with joins or complex aggregations to make the query more readable and to avoid column name conflicts.

from frappe.query_builder import Query, Field
 
qb = Query().from_('Customer').as_('c')
 
query = (
    qb.select('c.name', 'c.customer_name', 's.name', 's.status')
    .join('Sales Order', 's', 'c.name', 's.customer')
    .where(Field('c.country') == 'USA')
)

Raw SQL Execution

While the Query Builder abstracts SQL, there are times when you might need to execute raw SQL. Frappe provides a safe way to do this using frappe.db.sql().

# Execute a raw SQL query (use with caution!)
raw_results = frappe.db.sql(
    "SELECT name, customer_name FROM `tabCustomer` WHERE country = %s",
    ('India',),
    as_dict=1 # Returns results as a list of dictionaries
)
 
for row in raw_results:
    print(row)

It's always recommended to use the Query Builder whenever possible for its security and readability benefits.

Conclusion

The Frappe Query Builder is an indispensable tool for any developer working with ERPNext or other Frappe-based applications. It empowers you to write cleaner, more secure, and more maintainable database queries, abstracting away the complexities of raw SQL. By integrating this tool into your development workflow, you can boost your productivity and build more robust, efficient custom solutions within the Frappe ecosystem.

Start incorporating the Query Builder into your next custom module or report, and experience the difference it makes in your development process.

Get new articles in your inbox

Occasional writing on AI, ERP and data analytics — no spam, unsubscribe any time.