本指南介绍如何使用 Power SDK 设置 Azure SQL 数据库并将其连接到 Power Apps 代码应用。
注释
预览功能不适合生产使用且功能可能受限。 这些功能在正式发布之前可用,以便客户能够提前访问并提供反馈。
本指南涵盖:
- 预配 Azure SQL Server 和数据库
- 创建 SQL 表和存储过程
- 使用 Power SDK 连接 Power Apps 代码应用
先决条件
- Azure 订阅服务
- 访问 Azure 门户
- 启用了代码应用的 Power Platform 环境
- Visual Studio Code
- Node.js (LTS 版本)
- 适用于 VS Code 的 Power Platform 工具
- SQL Server (mssql) VS Code 扩展
设置 Azure SQL Server 和数据库
- 导航到 “选择 SQL 部署”选项 - Microsoft Azure
- 选择 SQL 数据库 -> 资源类型: 单一数据库 ->Create
- 填写:
- 资源组:选择 “新建 ”并输入资源组名称,例如
rg-codeapps-dev - 数据库名称:
sqldb-codeapps-dev - 服务器:选择“ 新建 ”并填写:
- 服务器名称:
sql-codeapps-dev - 位置:选择离 Power Platform 环境最近的区域。
- 身份验证方法: 使用仅限 Microsoft Entra 的身份验证
- 设置Microsoft Entra 管理员:选择 “设置管理员”,然后选择 自己的用户。
- 服务器名称:
- 选择 “确定”
- 资源组:选择 “新建 ”并输入资源组名称,例如
- 计算 + 存储: 常规用途 - 无服务器
- 选择“下一步:网络”
- 填写:
- 连接方法: 公共终结点
- 允许 Azure 服务和资源访问此服务器: 是
- 添加当前客户端 IP 地址: 是
- 选择 “审阅 + 创建 ->创建”
- 等待部署完成,然后选择“转到资源”
部署示例数据
在 Visual Studio Code 中,选择 “扩展 ”(Ctrl + Shift + X)
在活动栏上找到 SQL Server (mssql) 扩展并打开它,或使用 Ctrl + Alt + D
在 “连接”下,选择“ + 添加连接”
在“ 连接到数据库 ”对话框中,选择 “浏览 Azure”,选择订阅、资源组(例如:
rg-codeapps-dev)、服务器(例如:sql-codeapps-dev)和数据库(例如sqldb-codeapps-dev)在 “身份验证类型”下,选择 Microsoft Entra ID - 支持 MFA 的通用
确保已在浏览器中打开 Azure 门户,然后选择 “登录”。 应提示你登录,然后查看:
选择“连接”
在 SQL SERVER 面板中,右键单击数据库并选择“ 新建查询”
在新查询窗口中,粘贴以下 SQL:
-- Drop existing objects if they exist IF OBJECT_ID('dbo.Projects', 'U') IS NOT NULL DROP TABLE dbo.Projects; -- ============================================= -- CREATE TABLES -- ============================================= -- Projects Table CREATE TABLE [dbo].[Projects]( [ProjectId] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](255) NOT NULL, [Description] [nvarchar](max) NULL, [StartDate] [date] NULL, [EndDate] [date] NULL, [Status] [nvarchar](50) NOT NULL DEFAULT ('Planning'), [Priority] [nvarchar](20) NOT NULL DEFAULT ('Medium'), [Budget] [decimal](18, 2) NULL, [ProjectManagerEmail] [nvarchar](255) NOT NULL, [CreatedBy] [nvarchar](255) NOT NULL, [CreatedDate] [datetime2](7) NOT NULL DEFAULT (getutcdate()), [IsActive] [bit] NOT NULL DEFAULT (1), CONSTRAINT [PK_Projects] PRIMARY KEY ([ProjectId]) ); GO -- ============================================= -- ADD CONSTRAINTS -- ============================================= -- Project Status Check ALTER TABLE [dbo].[Projects] ADD CONSTRAINT [CK_Projects_Status] CHECK ([Status] IN ('Planning', 'Active', 'On Hold', 'Completed', 'Cancelled')); -- Project Priority Check ALTER TABLE [dbo].[Projects] ADD CONSTRAINT [CK_Projects_Priority] CHECK ([Priority] IN ('Low', 'Medium', 'High', 'Critical')); GO -- ============================================= -- STORED PROCEDURES -- ============================================= -- Get All Projects IF OBJECT_ID('dbo.GetAllProjects', 'P') IS NOT NULL DROP PROCEDURE dbo.GetAllProjects; GO CREATE PROCEDURE [dbo].[GetAllProjects] AS BEGIN SET NOCOUNT ON; SELECT [ProjectId], [Name], [Description], [StartDate], [EndDate], [Status], [Priority], [Budget], [ProjectManagerEmail], [CreatedBy], [CreatedDate], [IsActive] FROM [dbo].[Projects] WHERE [IsActive] = 1 ORDER BY [CreatedDate] DESC; END GO -- Create Project IF OBJECT_ID('dbo.CreateProject', 'P') IS NOT NULL DROP PROCEDURE dbo.CreateProject; GO CREATE PROCEDURE [dbo].[CreateProject] @Name NVARCHAR(255), @Description NVARCHAR(MAX) = NULL, @StartDate DATE = NULL, @EndDate DATE = NULL, @Status NVARCHAR(50) = 'Planning', @Priority NVARCHAR(20) = 'Medium', @Budget DECIMAL(18,2) = NULL, @ProjectManagerEmail NVARCHAR(255), @CreatedBy NVARCHAR(255) AS BEGIN SET NOCOUNT ON; INSERT INTO [dbo].[Projects] ( [Name], [Description], [StartDate], [EndDate], [Status], [Priority], [Budget], [ProjectManagerEmail], [CreatedBy] ) VALUES ( @Name, @Description, @StartDate, @EndDate, @Status, @Priority, @Budget, @ProjectManagerEmail, @CreatedBy ); SELECT SCOPE_IDENTITY() as ProjectId; END GO -- Update Project IF OBJECT_ID('dbo.UpdateProject', 'P') IS NOT NULL DROP PROCEDURE dbo.UpdateProject; GO CREATE PROCEDURE [dbo].[UpdateProject] @ProjectId INT, @Name NVARCHAR(255) = NULL, @Description NVARCHAR(MAX) = NULL, @StartDate DATE = NULL, @EndDate DATE = NULL, @Status NVARCHAR(50) = NULL, @Priority NVARCHAR(20) = NULL, @Budget DECIMAL(18,2) = NULL, @ProjectManagerEmail NVARCHAR(255) = NULL AS BEGIN SET NOCOUNT ON; UPDATE [dbo].[Projects] SET [Name] = ISNULL(@Name, [Name]), [Description] = ISNULL(@Description, [Description]), [StartDate] = ISNULL(@StartDate, [StartDate]), [EndDate] = ISNULL(@EndDate, [EndDate]), [Status] = ISNULL(@Status, [Status]), [Priority] = ISNULL(@Priority, [Priority]), [Budget] = ISNULL(@Budget, [Budget]), [ProjectManagerEmail] = ISNULL(@ProjectManagerEmail, [ProjectManagerEmail]) WHERE [ProjectId] = @ProjectId AND [IsActive] = 1; SELECT @@ROWCOUNT as RowsAffected; END GO -- Delete Project (Soft Delete) IF OBJECT_ID('dbo.DeleteProject', 'P') IS NOT NULL DROP PROCEDURE dbo.DeleteProject; GO CREATE PROCEDURE [dbo].[DeleteProject] @ProjectId INT AS BEGIN SET NOCOUNT ON; UPDATE [dbo].[Projects] SET [IsActive] = 0 WHERE [ProjectId] = @ProjectId AND [IsActive] = 1; SELECT @@ROWCOUNT as RowsAffected; END GO -- ============================================= -- SAMPLE DATA -- ============================================= -- Insert Sample Projects INSERT INTO [dbo].[Projects] ([Name], [Description], [StartDate], [EndDate], [Status], [Priority], [Budget], [ProjectManagerEmail], [CreatedBy]) VALUES ('Website Redesign', 'Complete redesign of company website with modern UI/UX', '2025-06-01', '2025-08-31', 'Active', 'High', 75000.00, 'sarah.johnson@company.com', 'admin@company.com'), ('Mobile App Development', 'Develop iOS and Android mobile application for customer portal', '2025-07-01', '2025-12-31', 'Planning', 'Critical', 150000.00, 'mike.chen@company.com', 'admin@company.com'), ('Database Migration', 'Migrate legacy database to cloud infrastructure', '2025-05-15', '2025-09-30', 'Active', 'Medium', 50000.00, 'lisa.williams@company.com', 'admin@company.com'); GO PRINT 'Projects-only database schema created successfully with sample data!';选择绿色播放图标(Ctrl-Shift-E) 以执行查询。
查询结果输出中不应显示任何错误。
初始化您的代码应用程序
如果尚未创建和/或初始化您的应用程序,请按照此处的说明进行操作:从头开始创建应用。
在 Power Platform 中创建 SQL Server 连接
打开 Power Apps
选择你的 环境
导航到 “连接”。 它可能位于 ...更多菜单。
选择 “+ 新建连接”
选择 SQL Server
选择身份验证类型: Microsoft Entra ID 集成
在弹出窗口身份验证提示中选择 “创建 并登录”
将 SQL 表连接添加到应用
列出环境中的可用连接。 应会看到已创建的连接:
pac connection list应会看到类似于以下内容的列表:
若要将项目表添加到项目,请复制连接 ID(第一列),并使用以下命令:
pac code add-data-source -a "shared_sql" -c "[CONNECTION ID]" -d "[SQL SERVER NAME].database.windows.net,[DATA BASE NAME]" -sp "dbo.GetAllProjects"例如:
pac code add-data-source -a "shared_sql" -c "aaaa0000bb11222233cc444444dddddd" -d "sql-codeapps-dev.database.windows.net,sqldb-codeapps-dev" -sp "dbo.GetAllProjects"打开
Services和Models文件夹,观察新生成的代码。
添加项目表
我们使用 Fluent UI 来显示项目表格,因此需要将 React 降级到 18 版本,并使用以下指令进行安装:
npm install react@^18.0.0 react-dom@^18.0.0 @types/react@^18.0.0 @types/react-dom@^18.0.0 npm install @fluentui/react-components在名为
src的文件夹中添加一个名为ProjectsTable.tsx的新文件,并使用以下代码:/** * ProjectsTable Component - Displays project data from Power Platform in a sortable DataGrid */ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { DataGrid, DataGridHeader, DataGridRow, DataGridHeaderCell, DataGridCell, DataGridBody, TableColumnDefinition, TableRowId, Spinner, MessageBar, Badge, makeStyles, tokens, } from '@fluentui/react-components'; import { GetAllProjectsService } from './Services/GetAllProjectsService'; // String formatting utility for localizable messages const formatMessage = (template: string, params: Record<string, string | number> = {}): string => { return template.replace(/\{(\w+)\}/g, (match, key) => { const value = params[key]; return value !== undefined ? String(value) : match; }); }; // Common UI messages const MESSAGE_STRINGS = { LOADING: 'Loading data...', NO_DATA: 'No data found.', GENERIC_ERROR: 'An unexpected error occurred', LOAD_ERROR: 'Failed to load data. Please try again.', PROJECT_COUNTER_SINGLE: 'Showing {count} project', PROJECT_COUNTER_PLURAL: 'Showing {count} projects', COLUMN_PROJECT_NAME: 'Project Name', COLUMN_DESCRIPTION: 'Description', COLUMN_START_DATE: 'Start Date', COLUMN_END_DATE: 'End Date', COLUMN_STATUS: 'Status', COLUMN_PRIORITY: 'Priority', ARIA_LABEL_DATA_GRID: 'Projects data grid', } as const; // Project data type type ProjectItem = { ProjectId?: number; Name?: string; Description?: string; StartDate?: string; EndDate?: string; Status?: string; Priority?: string; Budget?: number; ProjectManagerEmail?: string; CreatedBy?: string; CreatedDate?: string; IsActive?: boolean; }; // DataGrid columns const COLUMNS: TableColumnDefinition<ProjectItem>[] = [ { columnId: 'name', compare: (a, b) => (a.Name || '').localeCompare(b.Name || ''), renderHeaderCell: () => MESSAGE_STRINGS.COLUMN_PROJECT_NAME, renderCell: (item) => item.Name || '', }, { columnId: 'description', compare: (a, b) => (a.Description || '').localeCompare(b.Description || ''), renderHeaderCell: () => MESSAGE_STRINGS.COLUMN_DESCRIPTION, renderCell: (item) => item.Description || '', }, { columnId: 'startDate', compare: (a, b) => new Date(a.StartDate || '').getTime() - new Date(b.StartDate || '').getTime(), renderHeaderCell: () => MESSAGE_STRINGS.COLUMN_START_DATE, renderCell: (item) => item.StartDate ? new Date(item.StartDate).toLocaleDateString() : '', }, { columnId: 'endDate', compare: (a, b) => new Date(a.EndDate || '').getTime() - new Date(b.EndDate || '').getTime(), renderHeaderCell: () => MESSAGE_STRINGS.COLUMN_END_DATE, renderCell: (item) => item.EndDate ? new Date(item.EndDate).toLocaleDateString() : '', }, { columnId: 'status', compare: (a, b) => (a.Status || '').localeCompare(b.Status || ''), renderHeaderCell: () => MESSAGE_STRINGS.COLUMN_STATUS, renderCell: (item) => <StatusBadge status={item.Status || ''} />, }, { columnId: 'priority', compare: (a, b) => (a.Priority || '').localeCompare(b.Priority || ''), renderHeaderCell: () => MESSAGE_STRINGS.COLUMN_PRIORITY, renderCell: (item) => <PriorityBadge priority={item.Priority || ''} />, }, ]; // Row ID generator const getRowId = (item: ProjectItem): TableRowId => item.ProjectId?.toString() || `temp-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; // Extracts a user-friendly error message from various error types const extractErrorMessage = ( error: unknown, fallbackMessage = MESSAGE_STRINGS.GENERIC_ERROR ): string => { if (error instanceof Error) { return error.message; } if (typeof error === 'string') { return error; } return fallbackMessage; }; // Badge component for Priority const PriorityBadge: React.FC<{ priority: string }> = React.memo(({ priority }) => { const styles = useStyles(); const badgeProps = useMemo(() => { const getPriorityAppearance = (priority: string) => { switch (priority?.toLowerCase()) { case 'critical': return { appearance: 'filled' as const, color: 'danger' as const }; case 'high': return { appearance: 'filled' as const, color: 'important' as const }; case 'medium': return { appearance: 'filled' as const, color: 'warning' as const }; case 'low': return { appearance: 'filled' as const, color: 'success' as const }; default: return { appearance: 'outline' as const, color: 'subtle' as const }; } }; return getPriorityAppearance(priority); }, [priority]); return ( <Badge {...badgeProps} className={styles.badge}> {priority || 'Unknown'} </Badge> ); }); PriorityBadge.displayName = 'PriorityBadge'; // Badge component for Status const StatusBadge: React.FC<{ status: string }> = React.memo(({ status }) => { const styles = useStyles(); const badgeProps = useMemo(() => { const getStatusAppearance = (status: string) => { switch (status?.toLowerCase()) { case 'completed': return { appearance: 'filled' as const, color: 'success' as const }; case 'active': return { appearance: 'filled' as const, color: 'brand' as const }; case 'planning': return { appearance: 'filled' as const, color: 'informative' as const }; case 'on hold': return { appearance: 'filled' as const, color: 'warning' as const }; case 'cancelled': return { appearance: 'filled' as const, color: 'danger' as const }; default: return { appearance: 'outline' as const, color: 'subtle' as const }; } }; return getStatusAppearance(status); }, [status]); return ( <Badge {...badgeProps} className={styles.badge}> {status || 'Unknown'} </Badge> ); }); StatusBadge.displayName = 'StatusBadge'; // Styles const useStyles = makeStyles({ container: { padding: tokens.spacingVerticalXXL, }, loadingContainer: { display: 'flex', alignItems: 'center', gap: tokens.spacingHorizontalS, padding: tokens.spacingVerticalXXL, }, messageBar: { marginBottom: tokens.spacingVerticalXL, }, projectCounter: { marginBottom: tokens.spacingVerticalM, fontSize: tokens.fontSizeBase200, color: tokens.colorNeutralForeground2, }, dataGrid: { width: '100%', }, badge: { fontSize: tokens.fontSizeBase200, fontWeight: tokens.fontWeightMedium, textTransform: 'capitalize', }, }); // Custom hook to fetch and manage project data const useProjectsData = (): { projects: ProjectItem[]; loading: boolean; error: string | null; refetch: () => Promise<void>; } => { const [projects, setProjects] = useState<ProjectItem[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const fetchProjects = useCallback(async () => { try { setLoading(true); setError(null); const result = await GetAllProjectsService.GetAllProjects(); if (result.success && result.data?.ResultSets?.Table1) { const projectsData = Array.isArray(result.data.ResultSets.Table1) ? result.data.ResultSets.Table1 as ProjectItem[] : [result.data.ResultSets.Table1] as ProjectItem[]; setProjects(projectsData); } else { const errorMsg = result.error instanceof Error ? result.error.message : result.error || MESSAGE_STRINGS.LOAD_ERROR; setError(errorMsg); console.error('Failed to fetch projects:', result.error); } } catch (error) { const errorMessage = extractErrorMessage(error, MESSAGE_STRINGS.GENERIC_ERROR); setError(errorMessage); console.error('Error fetching projects:', error); } finally { setLoading(false); } }, []); useEffect(() => { fetchProjects(); }, [fetchProjects]); return { projects, loading, error, refetch: fetchProjects }; }; // UI Components const LoadingSpinner: React.FC = () => { const styles = useStyles(); return ( <div className={styles.loadingContainer}> <Spinner size="small" /> <span>{MESSAGE_STRINGS.LOADING}</span> </div> ); }; const ErrorMessage: React.FC<{ error: string }> = ({ error }) => { const styles = useStyles(); return ( <MessageBar intent="error" className={styles.messageBar}> {error} </MessageBar> ); }; const EmptyState: React.FC = () => { const styles = useStyles(); return ( <MessageBar intent="info" className={styles.messageBar} style={{ textAlign: 'center' }}> {MESSAGE_STRINGS.NO_DATA} </MessageBar> ); }; const ProjectCounter: React.FC<{ count: number }> = ({ count }) => { const styles = useStyles(); const counterMessage = useMemo(() => { return count === 1 ? formatMessage(MESSAGE_STRINGS.PROJECT_COUNTER_SINGLE, { count }) : formatMessage(MESSAGE_STRINGS.PROJECT_COUNTER_PLURAL, { count }); }, [count]); return ( <div className={styles.projectCounter}> {counterMessage} </div> ); }; // Main component const ProjectsTable: React.FC = () => { const styles = useStyles(); const { projects, loading, error } = useProjectsData(); const projectCount = useMemo(() => projects.length, [projects.length]); const memoizedProjects = useMemo(() => projects, [projects]); const dataGridProps = useMemo(() => ({ items: memoizedProjects, columns: COLUMNS, sortable: true, getRowId, focusMode: "cell" as const, className: styles.dataGrid, "aria-label": MESSAGE_STRINGS.ARIA_LABEL_DATA_GRID, }), [memoizedProjects, styles.dataGrid]); if (loading) { return ( <div className={styles.container}> <LoadingSpinner /> </div> ); } if (error) { return ( <div className={styles.container}> <ErrorMessage error={error} /> </div> ); } if (projectCount === 0) { return ( <div className={styles.container}> <EmptyState /> </div> ); } return ( <div className={styles.container}> <ProjectCounter count={projectCount} /> <DataGrid {...dataGridProps}> <DataGridHeader> <DataGridRow> {({ renderHeaderCell }) => ( <DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell> )} </DataGridRow> </DataGridHeader> <DataGridBody<ProjectItem>> {({ item, rowId }) => ( <DataGridRow<ProjectItem> key={rowId}> {({ renderCell }) => ( <DataGridCell>{renderCell(item)}</DataGridCell> )} </DataGridRow> )} </DataGridBody> </DataGrid> </div> ); }; export default React.memo(ProjectsTable);将
FluentProvider和ProjectsTable添加到maint.tsx:import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import PowerProvider from './PowerProvider.tsx' import { FluentProvider, webLightTheme } from '@fluentui/react-components' import ProjectsTable from './ProjectsTable.tsx' createRoot(document.getElementById('root')!).render( <StrictMode> <PowerProvider> <FluentProvider theme={webLightTheme}> <ProjectsTable /> </FluentProvider> </PowerProvider> </StrictMode>, )运行您的应用程序使用:
npm run dev在打开的命令窗口中,打开提供的应用链接:
当应用打开时,应会看到同意对话框,选择 “允许”。
您应该看到项目的数据表格。
将应用发布到 Power Apps
应用准备好发布和共享后,请确保 Vite 服务器已停止使用 Ctrl + C,然后使用以下 PowerShell:
npm run build pac code push使用提供的链接打开应用以测试应用!
Troubleshooting
本部分介绍使用 Azure SQL 数据库设置 Power Apps 代码应用时可能会遇到的常见问题。
Azure SQL 数据库问题
使用 Azure SQL 数据库时可能会遇到这些问题。
无法连接到 Azure SQL 数据库
症状:
- 连接超时错误
- 从 VS Code SQL 扩展进行连接时身份验证失败
解决方案:
检查防火墙设置:
- 在 Azure 门户中,导航到您的 SQL Server
- 转到 安全 → 网络
- 确保“允许 Azure 服务和资源访问此服务器”设置为“是”
- 将当前客户端 IP 地址添加到防火墙规则
验证身份验证方法:
- 确保在 VS Code 中使用支持 MFA 的 Microsoft Entra ID - Universal
- 请确保在 Azure 门户和 VS Code 中登录到同一 Azure 帐户
- 尝试注销并重新登录以刷新身份验证令牌
检查网络连接:
# Test connectivity to SQL Server Test-NetConnection -ComputerName "your-sql-server.database.windows.net" -Port 1433
SQL 查询执行错误
症状:
- 执行 SQL 脚本时权限被拒错误
- 对象已存在错误
解决方案:
权限问题:
- 确保您的用户帐户设置为 SQL Server 的 Microsoft Entra 管理员
- 验证你是否对数据库具有
db_owner或适当的权限
对象存在错误:
- SQL 脚本包含
DROP语句 - 这些语句可以安全多次运行 - 如果您遇到约束错误,请先逐一运行 drop 语句
- SQL 脚本包含
Node.js 和 npm 问题
使用 Node.js 和 npm 时,可能会遇到这些问题。
端口 3000 已在使用中
症状:
- “EADDRINUSE:地址已在使用:::3000”
- Vite 服务器未启动
解决方案:
终止现有进程:
# Find process using port 3000 netstat -ano | findstr :3000 # Kill the process (replace PID with actual process ID) taskkill /PID [PID] /F使用备用端口:
- 若要使用其他端口,请更新
vite.config.ts - 相应地更新 Power SDK 配置
- 若要使用其他端口,请更新
包安装失败
症状:
- npm 安装错误
- 模块未找到错误
解决方案:
清除 npm 缓存:
npm cache clean --force npm install删除node_modules并重新安装:
Remove-Item -Recurse -Force node_modules Remove-Item package-lock.json npm install节点版本问题:
# Check Node version node --version # Should be LTS version (18.x or 20.x)
运行时连接错误
症状:
- React 应用中的“无法加载数据”
- 连接被拒绝错误
解决方案:
检查 Power Platform 连接:
- 验证 SQL Server 连接是否在 Power Platform 中正常工作
- 在 Power Apps 中测试连接
同意问题:
- 请在应用首次加载时确保授予必要的权限。
- 清除浏览器缓存,然后重试
环境不匹配:
- 验证是否在创建连接所在的同一环境中运行应用
- 检查浏览器配置文件是否与 Power Platform 帐户匹配